Further Agent updates

This commit is contained in:
2026-08-18 15:51:13 -04:00
parent a94de5be1d
commit 2ad8ae1666
22 changed files with 3043 additions and 60 deletions
@@ -156,7 +156,17 @@ test('agent-tools exposes agent ops tools', async (t) => {
'wait_for',
'undo_last_edit',
'git_diff',
'history_search'
'history_search',
'git_log',
'git_show',
'git_blame',
'copy_path',
'diff_files',
'find_symbol',
'create_skill',
'remember',
'rewind_session',
'export_session'
]) {
t.ok(
TOOLS.includes("name: '" + toolName + "'"),
@@ -220,6 +230,10 @@ test('agent CLI exposes production flags and inspect subcommands', async (t) =>
t.ok(CLI.includes("sub === 'undo'"))
t.ok(CLI.includes("sub === 'hooks'"))
t.ok(CLI.includes("sub === 'history'"))
t.ok(CLI.includes("sub === 'rewind'"))
t.ok(CLI.includes("sub === 'export'"))
t.ok(CLI.includes("sub === 'remember'"))
t.ok(CLI.includes("sub === 'recap'"))
t.ok(TUI.includes('planMode'))
t.ok(TUI.includes('modelOverride'))
t.ok(TUI.includes('extraSystemFile'))
@@ -611,6 +611,182 @@ test('gitignore, fuzzy, undo, history search', async (t) => {
t.ok(waited.matched)
})
test('rewind, export, unified diff, symbol regex', async (t) => {
const s = loadPort()
const hist = [
{ role: 'user', content: 'first' },
{ role: 'assistant', content: 'ok1' },
{ role: 'user', content: 'second' },
{ role: 'assistant', content: 'ok2' }
]
const points = s.bareAgentRewindPoints(hist)
t.is(points.length, 2)
const rewound = s.bareAgentRewindHistory(hist, { steps: 1 })
t.ok(rewound.ok)
t.is(rewound.dropped, 2)
t.is(rewound.messages.length, 2)
t.is(rewound.messages[0].content, 'first')
const md = s.bareAgentExportTranscript(hist)
t.ok(md.includes('# Agent session export'))
t.ok(md.includes('first'))
const diff = s.bareAgentUnifiedDiff('a\nb\nc\n', 'a\nB\nc\n', { from: '/a', to: '/b' })
t.ok(diff.ok)
t.absent(diff.identical)
t.ok(String(diff.text).includes('-b'))
t.ok(String(diff.text).includes('+B'))
const re = new RegExp(s.bareAgentSymbolRegex('bareAgentCopyPath'))
t.ok(re.test('async function bareAgentCopyPath(ctx, from, to) {'))
t.absent(re.test('const x = bareAgentCopyPath'))
})
test('copy, tree, find_symbol, create_skill, rewind dispatch', async (t) => {
const d = loadDispatch()
const { vfs, files, b4a } = makeVfs({
'/home/guest/src/lib.js': 'async function helloWorld() {\n return 1\n}\n',
'/home/guest/src/a.txt': 'alpha\n',
'/home/guest/src/b.txt': 'beta\n',
'/home/guest/.agent/history.json': JSON.stringify(
[
{ role: 'user', content: 'one' },
{ role: 'assistant', content: 'ok' },
{ role: 'user', content: 'two' },
{ role: 'assistant', content: 'ok2' }
],
null,
2
)
})
await vfs.mkdir('/home/guest/src')
await vfs.mkdir('/home/guest/.agent')
const ctx = { vfs, b4a }
const paths = {
dir: '/home/guest/.agent',
history: '/home/guest/.agent/history.json',
workspace: '/home/guest/.agent/workspace',
workspaceSkills: '/home/guest/.agent/workspace/skills',
workspaceMemory: '/home/guest/.agent/workspace/memory'
}
const copied = await dispatch(d, {
ctx,
paths,
toolName: 'copy_path',
args: { from_path: '/home/guest/src/a.txt', to_path: '/home/guest/src/a.copy.txt' },
home: '/home/guest'
})
t.ok(copied.ok)
t.is(files.get('/home/guest/src/a.copy.txt'), 'alpha\n')
const tree = await dispatch(d, {
ctx,
paths,
toolName: 'list_directory',
args: { path: '/home/guest/src', tree: true },
home: '/home/guest'
})
t.ok(tree.ok)
t.ok(String(tree.tree || '').includes('lib.js'))
const found = await dispatch(d, {
ctx,
paths,
toolName: 'find_symbol',
args: { name: 'helloWorld', root: '/home/guest/src' },
home: '/home/guest'
})
t.ok(found.ok)
t.ok(found.count >= 1)
const counted = await dispatch(d, {
ctx,
paths,
toolName: 'grep',
args: { pattern: 'hello', root: '/home/guest/src', output_mode: 'count' },
home: '/home/guest'
})
t.ok(counted.ok)
t.is(counted.output_mode, 'count')
t.ok(counted.count >= 1)
const skill = await dispatch(d, {
ctx,
paths,
toolName: 'create_skill',
args: { id: 'demo-skill', description: 'Demo', body: '# Demo\n\nDo the thing.\n' },
home: '/home/guest'
})
t.ok(skill.ok)
t.ok(String(files.get('/home/guest/.agent/workspace/skills/demo-skill/SKILL.md') || '').includes('name: demo-skill'))
const rewound = await dispatch(d, {
ctx,
paths,
toolName: 'rewind_session',
args: { steps: 1 },
home: '/home/guest'
})
t.ok(rewound.ok)
t.is(rewound.dropped, 2)
const hist = JSON.parse(files.get('/home/guest/.agent/history.json') || '[]')
t.is(hist.length, 2)
const remembered = await dispatch(d, {
ctx,
paths,
toolName: 'remember',
args: { text: 'holesail keys live in ~/.holesail' },
home: '/home/guest'
})
t.ok(remembered.ok)
t.is(remembered.kind, 'FACT')
t.ok(String(files.get('/home/guest/.agent/workspace/MEMORY.md') || '').includes('holesail keys'))
const exported = await dispatch(d, {
ctx,
paths,
toolName: 'export_session',
args: { path: '/home/guest/.agent/export.md' },
home: '/home/guest'
})
t.ok(exported.ok)
t.ok(String(files.get('/home/guest/.agent/export.md') || '').includes('# Agent session export'))
})
test('copy from read-only system path and project skill roots', async (t) => {
const d = loadDispatch()
const { vfs, files, b4a } = makeVfs({
'/bin/sh': 'readonly-shell\n',
'/home/guest/proj/.grok/skills/demo/SKILL.md':
'---\nname: demo\ndescription: Project skill\n---\n\n# Demo\n'
})
await vfs.mkdir('/bin')
await vfs.mkdir('/home/guest')
const ctx = { vfs, b4a, env: { PWD: '/home/guest/proj', HOME: '/home/guest' } }
const copied = await dispatch(d, {
ctx,
paths: { dir: '/home/guest/.agent', workspace: '/home/guest/.agent/workspace' },
toolName: 'copy_path',
args: { from_path: '/bin/sh', to_path: '/home/guest/sh.copy' },
home: '/home/guest'
})
t.ok(copied.ok)
t.is(files.get('/home/guest/sh.copy'), 'readonly-shell\n')
const blocked = await dispatch(d, {
ctx,
paths: { dir: '/home/guest/.agent' },
toolName: 'copy_path',
args: { from_path: '/home/guest/sh.copy', to_path: '/bin/sh.hijack' },
home: '/home/guest'
})
t.absent(blocked.ok)
t.is(blocked.error, 'path_not_allowed')
const s = loadPort()
const roots = await s.bareAgentDiscoverProjectSkillRoots(ctx, '/home/guest/proj/src')
t.ok(roots.some((r) => String(r.path || '').endsWith('/.grok/skills')))
})
test('search result parser extracts DDG-style topics', async (t) => {
const s = loadPort()
const rows = s.bareAgentParseSearchResults(