sync: update from private repo (2ef0fa6)
CI / build-and-test (push) Has been cancelled

This commit is contained in:
oss-sync
2026-06-17 05:23:28 +00:00
parent 517142c61d
commit 1602d52510
42 changed files with 3387 additions and 64 deletions
+2
View File
@@ -28,6 +28,8 @@ const DOCS_DIR = path.join(REPO_ROOT, 'docs', 'tools');
// 関連ツールが同じ doc を参照できるようエイリアスを定義
// キー・値ともに小文字
const TOOL_DOC_ALIASES: Record<string, string> = {
// updateuseragents.md にまとめる
readuseragents: 'updateuseragents',
// checklist.md にまとめる
createchecklist: 'checklist',
checkitem: 'checklist',
+3 -1
View File
@@ -378,10 +378,12 @@ export async function getToolDefs(
// - MissionUpdate: タスクの目標 / 進捗のピン止めメモを更新 (会話が長くなって
// 最初の要件を見失わないため。常時上書き可能、未指定フィールドは保持)
// - ListUserAssets / RunUserScript: ユーザーフォルダのスクリプト探索・実行
// - ReadUserAgents / UpdateUserAgents: per-user AGENTS.md (常時指示書) の読み書き
// (UpdateUserMemory が事実断片、こちらは振る舞い方針そのもの)
// - Brainstorm: 着手前 or 行き詰まり時の多アプローチ比較 (issue #247)
// - ReadAppDoc / ListAppDocs / GetMyOrchestratorState: Help アシスタント用 (#help piece)
// ただし他の piece からも参照できるようにメタ扱い
const META_TOOLS = ['ReadToolDoc', 'CreateChecklist', 'CheckItem', 'GetChecklist', 'MissionUpdate', 'ListUserAssets', 'RunUserScript', 'UpdateUserMemory', 'ReadUserMemory', 'ReadUserTemplate', 'RenderUserTemplate', 'WriteUserScript', 'WriteUserTemplate', 'Brainstorm', 'ReadAppDoc', 'ListAppDocs', 'GetMyOrchestratorState', 'ReadSkill', 'ListSkills', 'InstallSkill'];
const META_TOOLS = ['ReadToolDoc', 'CreateChecklist', 'CheckItem', 'GetChecklist', 'MissionUpdate', 'ListUserAssets', 'RunUserScript', 'UpdateUserMemory', 'ReadUserMemory', 'ReadUserAgents', 'UpdateUserAgents', 'ReadUserTemplate', 'RenderUserTemplate', 'WriteUserScript', 'WriteUserTemplate', 'Brainstorm', 'ReadAppDoc', 'ListAppDocs', 'GetMyOrchestratorState', 'ReadSkill', 'ListSkills', 'InstallSkill'];
const effectiveAllowed = [...allowedTools];
for (const meta of META_TOOLS) {
if (!effectiveAllowed.includes(meta) && meta in allDefs) {
+119
View File
@@ -4,6 +4,8 @@ import {
rmSync,
mkdirSync,
writeFileSync,
readFileSync,
readdirSync,
} from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
@@ -769,3 +771,120 @@ describe('RunUserScript: audit log hook', () => {
expect(auditCalls.some(c => c.action === 'user_script_denied')).toBe(true);
});
});
// ── ReadUserAgents / UpdateUserAgents ─────────────────────────────────────────
describe('ReadUserAgents / UpdateUserAgents', () => {
const agentsPath = () => join(userFolderRoot, TEST_USER, 'AGENTS.md');
it('ReadUserAgents returns a friendly note when AGENTS.md does not exist', async () => {
const ctx = buildCtx({ userId: TEST_USER });
const r = await executeTool('ReadUserAgents', {}, ctx);
expect(r?.isError).toBe(false);
expect(r?.output).toContain('does not exist yet');
});
it('append creates AGENTS.md when empty', async () => {
const ctx = buildCtx({ userId: TEST_USER });
const r = await executeTool('UpdateUserAgents', { mode: 'append', new_text: '# 方針\n常に日本語で答える' }, ctx);
expect(r?.isError).toBe(false);
expect(readFileSync(agentsPath(), 'utf-8')).toBe('# 方針\n常に日本語で答える');
});
it('append adds a blank line between existing content and new text', async () => {
writeFileSync(agentsPath(), '# 既存\nルール1');
const ctx = buildCtx({ userId: TEST_USER });
await executeTool('UpdateUserAgents', { mode: 'append', new_text: 'ルール2' }, ctx);
expect(readFileSync(agentsPath(), 'utf-8')).toBe('# 既存\nルール1\n\nルール2');
});
it('ReadUserAgents returns the full current content', async () => {
writeFileSync(agentsPath(), 'line A\nline B');
const ctx = buildCtx({ userId: TEST_USER });
const r = await executeTool('ReadUserAgents', {}, ctx);
expect(r?.output).toBe('line A\nline B');
});
it('replace swaps an exact unique match', async () => {
writeFileSync(agentsPath(), '# 方針\n古い行\nおわり');
const ctx = buildCtx({ userId: TEST_USER });
const r = await executeTool('UpdateUserAgents', { mode: 'replace', old_text: '古い行', new_text: '新しい行' }, ctx);
expect(r?.isError).toBe(false);
expect(readFileSync(agentsPath(), 'utf-8')).toBe('# 方針\n新しい行\nおわり');
});
it('replace does not interpret $-patterns in new_text', async () => {
writeFileSync(agentsPath(), 'price: PLACEHOLDER yen');
const ctx = buildCtx({ userId: TEST_USER });
await executeTool('UpdateUserAgents', { mode: 'replace', old_text: 'PLACEHOLDER', new_text: '$100 & $&' }, ctx);
expect(readFileSync(agentsPath(), 'utf-8')).toBe('price: $100 & $& yen');
});
it('replace errors when old_text is not found', async () => {
writeFileSync(agentsPath(), 'something');
const ctx = buildCtx({ userId: TEST_USER });
const r = await executeTool('UpdateUserAgents', { mode: 'replace', old_text: 'missing', new_text: 'x' }, ctx);
expect(r?.isError).toBe(true);
expect(r?.output).toContain('not found');
});
it('replace errors when old_text matches more than once', async () => {
writeFileSync(agentsPath(), 'dup\ndup');
const ctx = buildCtx({ userId: TEST_USER });
const r = await executeTool('UpdateUserAgents', { mode: 'replace', old_text: 'dup', new_text: 'x' }, ctx);
expect(r?.isError).toBe(true);
expect(r?.output).toContain('matches 2 times');
});
it('replace on an empty file is rejected with a hint to append', async () => {
const ctx = buildCtx({ userId: TEST_USER });
const r = await executeTool('UpdateUserAgents', { mode: 'replace', old_text: 'x', new_text: 'y' }, ctx);
expect(r?.isError).toBe(true);
expect(r?.output).toContain('append');
});
it('snapshots the prior version to trash/agents-history before overwriting', async () => {
writeFileSync(agentsPath(), 'original content');
const ctx = buildCtx({ userId: TEST_USER });
await executeTool('UpdateUserAgents', { mode: 'append', new_text: 'more' }, ctx);
const histDir = join(userFolderRoot, TEST_USER, 'trash', 'agents-history');
const backups = readdirSync(histDir).filter((f) => f.startsWith('AGENTS.md.') && f.endsWith('.bak'));
expect(backups.length).toBe(1);
expect(readFileSync(join(histDir, backups[0]!), 'utf-8')).toBe('original content');
});
it('rejects when result would exceed the 64KB cap', async () => {
const ctx = buildCtx({ userId: TEST_USER });
const big = 'x'.repeat(64 * 1024 + 1);
const r = await executeTool('UpdateUserAgents', { mode: 'append', new_text: big }, ctx);
expect(r?.isError).toBe(true);
expect(r?.output).toContain('exceeds');
});
it('emits a user_agents_updated audit event', async () => {
const auditCalls: Array<{ action: string; detail: object }> = [];
setUserFolderToolDeps({
sessRepo: null as never,
masterKeyPath: '',
userFolderRoot,
auditLog: (action, detail) => auditCalls.push({ action, detail }),
});
const ctx = buildCtx({ userId: TEST_USER });
await executeTool('UpdateUserAgents', { mode: 'append', new_text: 'hello' }, ctx);
expect(auditCalls.some((c) => c.action === 'user_agents_updated')).toBe(true);
});
it('both tools require an authenticated user', async () => {
const ctx = buildCtx({ userId: undefined });
const r1 = await executeTool('ReadUserAgents', {}, ctx);
const r2 = await executeTool('UpdateUserAgents', { mode: 'append', new_text: 'x' }, ctx);
expect(r1?.isError).toBe(true);
expect(r2?.isError).toBe(true);
});
it('registers both tools in TOOL_DEFS', () => {
expect(TOOL_DEFS['ReadUserAgents']).toBeDefined();
expect(TOOL_DEFS['UpdateUserAgents']).toBeDefined();
});
});
+152
View File
@@ -20,6 +20,9 @@ import {
userRoot,
assertOwnerAccess,
resolveUserSubdir,
readUserAgentsMdRaw,
writeUserAgentsMd,
snapshotUserAgentsMd,
} from '../../user-folder/paths.js';
import {
upsertMemoryEntry,
@@ -124,6 +127,48 @@ export const TOOL_DEFS: Record<string, ToolDef> = {
},
},
ReadUserAgents: {
type: 'function',
function: {
name: 'ReadUserAgents',
description:
'Returns the caller\'s full AGENTS.md (per-user standing instructions, auto-injected into every task). Read it before editing. Details via ReadToolDoc({ name: "UpdateUserAgents" }).',
parameters: {
type: 'object',
properties: {},
required: [],
},
},
},
UpdateUserAgents: {
type: 'function',
function: {
name: 'UpdateUserAgents',
description:
'Edits the caller\'s AGENTS.md via "replace" (old_text→new_text, must match exactly once) or "append". Snapshots the prior version first. Details via ReadToolDoc({ name: "UpdateUserAgents" }).',
parameters: {
type: 'object',
properties: {
mode: {
type: 'string',
enum: ['replace', 'append'],
description: '"replace" swaps old_text for new_text; "append" adds new_text to the end (also creates the file if empty).',
},
old_text: {
type: 'string',
description: 'Exact existing text to replace. Required for "replace"; must appear exactly once (add surrounding context to disambiguate).',
},
new_text: {
type: 'string',
description: 'For "replace", the replacement text; for "append", the text to add. Required.',
},
},
required: ['mode', 'new_text'],
},
},
},
ListUserAssets: {
type: 'function',
function: {
@@ -309,6 +354,111 @@ async function executeReadUserMemory(
return { output, isError: false };
}
// ── ReadUserAgents implementation ─────────────────────────────────────────────
async function executeReadUserAgents(
_input: Record<string, unknown>,
ctx: ToolContext,
): Promise<ToolResult> {
if (!ctx.userId) {
return { output: 'ReadUserAgents requires an authenticated user', isError: true };
}
const content = readUserAgentsMdRaw(getUserFolderRoot(), ctx.userId);
if (content === null || content.length === 0) {
return {
output:
'AGENTS.md does not exist yet (empty). Use UpdateUserAgents with mode="append" to create it.',
isError: false,
};
}
return { output: content, isError: false };
}
// ── UpdateUserAgents implementation ───────────────────────────────────────────
async function executeUpdateUserAgents(
input: Record<string, unknown>,
ctx: ToolContext,
): Promise<ToolResult> {
if (!ctx.userId) {
return { output: 'UpdateUserAgents requires an authenticated user', isError: true };
}
const mode = input['mode'];
if (mode !== 'replace' && mode !== 'append') {
return { output: 'UpdateUserAgents: "mode" must be "replace" or "append"', isError: true };
}
const newText = input['new_text'];
if (typeof newText !== 'string') {
return { output: 'UpdateUserAgents: "new_text" is required', isError: true };
}
const folderRoot = getUserFolderRoot();
const current = readUserAgentsMdRaw(folderRoot, ctx.userId) ?? '';
let next: string;
if (mode === 'replace') {
const oldText = input['old_text'];
if (typeof oldText !== 'string' || oldText.length === 0) {
return { output: 'UpdateUserAgents: "old_text" is required for mode "replace"', isError: true };
}
if (current.length === 0) {
return {
output: 'UpdateUserAgents: AGENTS.md is empty — use mode "append" to add content',
isError: true,
};
}
const count = current.split(oldText).length - 1;
if (count === 0) {
return {
output:
'UpdateUserAgents: "old_text" not found in AGENTS.md. Call ReadUserAgents to see the exact current content.',
isError: true,
};
}
if (count > 1) {
return {
output: `UpdateUserAgents: "old_text" matches ${count} times — include more surrounding context to make it unique.`,
isError: true,
};
}
// Function replacement avoids `$&`/`$1` being interpreted in new_text.
next = current.replace(oldText, () => newText);
} else {
// append
if (newText.length === 0) {
return { output: 'UpdateUserAgents: "new_text" must be non-empty for mode "append"', isError: true };
}
next = current.length === 0 ? newText : `${current.replace(/\n*$/, '')}\n\n${newText}`;
}
// Snapshot the prior version before overwriting (best-effort; missing/empty → null).
let backup: string | null = null;
try {
backup = snapshotUserAgentsMd(folderRoot, ctx.userId);
} catch {
/* snapshot is best-effort; proceed with the write */
}
try {
writeUserAgentsMd(folderRoot, ctx.userId, next);
} catch (err) {
return { output: `UpdateUserAgents: ${(err as Error).message}`, isError: true };
}
_deps?.auditLog?.(
'user_agents_updated',
{ userId: ctx.userId, mode, bytes: Buffer.byteLength(next, 'utf-8'), snapshotted: backup !== null },
ctx.taskId ?? null,
);
return {
output: `AGENTS.md updated (mode=${mode}, now ${Buffer.byteLength(next, 'utf-8')} bytes)${backup ? ' — previous version snapshotted to trash/agents-history/' : ''}`,
isError: false,
};
}
// ── ListUserAssets implementation ─────────────────────────────────────────────
async function executeListUserAssets(
@@ -626,6 +776,8 @@ export async function executeTool(
): Promise<ToolResult | null> {
if (name === 'UpdateUserMemory') return executeUpdateUserMemory(input, ctx);
if (name === 'ReadUserMemory') return executeReadUserMemory(input, ctx);
if (name === 'ReadUserAgents') return executeReadUserAgents(input, ctx);
if (name === 'UpdateUserAgents') return executeUpdateUserAgents(input, ctx);
if (name === 'ListUserAssets') return executeListUserAssets(input, ctx);
if (name === 'RunUserScript') return executeRunUserScript(input, ctx);
if (name === 'WriteUserScript') return executeWriteUserScript(input, ctx);