Update Agent Shape
This commit is contained in:
@@ -9,6 +9,7 @@ var BARE_AGENT_PLAN_READONLY_TOOLS = Object.freeze({
|
||||
list_dir: 1,
|
||||
file_stat: 1,
|
||||
search_files: 1,
|
||||
grep: 1,
|
||||
glob_files: 1,
|
||||
glob: 1,
|
||||
get_system_info: 1,
|
||||
@@ -19,6 +20,8 @@ var BARE_AGENT_PLAN_READONLY_TOOLS = Object.freeze({
|
||||
get_swarm_peers: 1,
|
||||
get_resource_limits: 1,
|
||||
web_fetch: 1,
|
||||
web_search: 1,
|
||||
git_status: 1,
|
||||
read_skill: 1,
|
||||
list_services: 1,
|
||||
service_status: 1,
|
||||
@@ -51,7 +54,8 @@ var BARE_AGENT_PLAN_READONLY_TOOLS = Object.freeze({
|
||||
var BARE_AGENT_PLAN_WRITE_TOOLS = Object.freeze({
|
||||
write_file: 1,
|
||||
edit_file: 1,
|
||||
search_replace: 1
|
||||
search_replace: 1,
|
||||
apply_patch: 1
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -757,3 +761,455 @@ function bareAgentToolPathArg(args) {
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
*/
|
||||
function bareAgentLooksBinaryText(text) {
|
||||
const s = String(text || '')
|
||||
if (!s) return false
|
||||
if (s.indexOf('\0') !== -1) return true
|
||||
let bad = 0
|
||||
const n = Math.min(s.length, 800)
|
||||
for (let i = 0; i < n; i++) {
|
||||
const c = s.charCodeAt(i)
|
||||
if (c === 9 || c === 10 || c === 13) continue
|
||||
if (c < 32) bad++
|
||||
}
|
||||
return bad > 8
|
||||
}
|
||||
|
||||
/**
|
||||
* Guest-native grep (Grok-style). VFS walk + JS regex — no host rg/node.
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {{
|
||||
* pattern: string,
|
||||
* root?: string,
|
||||
* glob?: string,
|
||||
* ignore_case?: boolean,
|
||||
* before?: number,
|
||||
* after?: number,
|
||||
* context?: number,
|
||||
* max_matches?: number,
|
||||
* files_with_matches?: boolean
|
||||
* }} opts
|
||||
*/
|
||||
async function bareAgentGrepFiles(ctx, opts) {
|
||||
const o = opts && typeof opts === 'object' ? opts : {}
|
||||
const pattern = String(o.pattern || '')
|
||||
if (!pattern) return { ok: false, error: 'pattern_required' }
|
||||
let re
|
||||
try {
|
||||
re = new RegExp(pattern, o.ignore_case ? 'i' : '')
|
||||
} catch (e) {
|
||||
const msg =
|
||||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||||
return { ok: false, error: 'bad_regex', detail: msg }
|
||||
}
|
||||
const root = String(o.root || '/').replace(/\/+$/, '') || '/'
|
||||
const before = Math.min(
|
||||
8,
|
||||
Math.max(0, Math.floor(Number(o.before != null ? o.before : o.context) || 0))
|
||||
)
|
||||
const after = Math.min(
|
||||
8,
|
||||
Math.max(0, Math.floor(Number(o.after != null ? o.after : o.context) || 0))
|
||||
)
|
||||
const maxMatches = Math.min(
|
||||
200,
|
||||
Math.max(1, Math.floor(Number(o.max_matches) || 50))
|
||||
)
|
||||
const filesOnly = Boolean(o.files_with_matches)
|
||||
const glob = typeof o.glob === 'string' && o.glob.trim() ? o.glob.trim() : ''
|
||||
const files = glob
|
||||
? await bareAgentGlobFiles(ctx, root, glob, { maxFiles: 800, maxDepth: 12 })
|
||||
: await bareAgentVfsWalkFiles(ctx, root, { maxFiles: 800, maxDepth: 12 })
|
||||
/** @type {{ path: string, line?: number, text?: string }[]} */
|
||||
const matches = []
|
||||
/** @type {string[]} */
|
||||
const filesHit = []
|
||||
let truncated = false
|
||||
for (let f = 0; f < files.length; f++) {
|
||||
const path = files[f]
|
||||
const text = await bareAgentReadTextFile(ctx, path)
|
||||
if (!text || bareAgentLooksBinaryText(text)) continue
|
||||
const lines = text.split('\n')
|
||||
if (lines.length && lines[lines.length - 1] === '') lines.pop()
|
||||
let fileHit = false
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
re.lastIndex = 0
|
||||
if (!re.test(lines[i])) continue
|
||||
fileHit = true
|
||||
if (filesOnly) break
|
||||
const start = Math.max(0, i - before)
|
||||
const end = Math.min(lines.length, i + 1 + after)
|
||||
const slice = lines.slice(start, end)
|
||||
const body =
|
||||
before || after
|
||||
? slice
|
||||
.map(function (ln, j) {
|
||||
const n = start + j + 1
|
||||
const mark = n === i + 1 ? ':' : '-'
|
||||
return String(n) + mark + ln
|
||||
})
|
||||
.join('\n')
|
||||
: String(i + 1) + ':' + lines[i]
|
||||
matches.push({ path, line: i + 1, text: body })
|
||||
if (matches.length >= maxMatches) {
|
||||
truncated = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (fileHit && filesOnly) {
|
||||
filesHit.push(path)
|
||||
if (filesHit.length >= maxMatches) {
|
||||
truncated = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (truncated) break
|
||||
}
|
||||
if (filesOnly) {
|
||||
return {
|
||||
ok: true,
|
||||
pattern,
|
||||
root,
|
||||
glob: glob || null,
|
||||
files_with_matches: filesHit,
|
||||
count: filesHit.length,
|
||||
truncated
|
||||
}
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
pattern,
|
||||
root,
|
||||
glob: glob || null,
|
||||
matches,
|
||||
count: matches.length,
|
||||
truncated
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Codex / Grok apply_patch parser (Begin Patch … End Patch).
|
||||
* @param {string} text
|
||||
* @returns {{ ok: boolean, error?: string, ops?: object[] }}
|
||||
*/
|
||||
function bareAgentParseApplyPatch(text) {
|
||||
const raw = String(text || '').replace(/\r\n/g, '\n')
|
||||
if (!raw.trim()) return { ok: false, error: 'empty_patch' }
|
||||
const all = raw.split('\n')
|
||||
let start = 0
|
||||
let end = all.length
|
||||
for (let i = 0; i < all.length; i++) {
|
||||
if (/^\s*\*\*\*\s*Begin Patch\s*$/i.test(all[i])) {
|
||||
start = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
for (let i = all.length - 1; i >= 0; i--) {
|
||||
if (/^\s*\*\*\*\s*End Patch\s*$/i.test(all[i])) {
|
||||
end = i
|
||||
break
|
||||
}
|
||||
}
|
||||
const body = all.slice(start, end)
|
||||
/** @type {object[]} */
|
||||
const ops = []
|
||||
/** @type {Record<string, unknown> | null} */
|
||||
let cur = null
|
||||
/** @type {{ context: string, old_lines: string[], new_lines: string[], is_end_of_file?: boolean } | null} */
|
||||
let chunk = null
|
||||
|
||||
function flushChunk() {
|
||||
if (cur && cur.type === 'update' && chunk) {
|
||||
const chunks = Array.isArray(cur.chunks) ? cur.chunks : []
|
||||
chunks.push(chunk)
|
||||
cur.chunks = chunks
|
||||
chunk = null
|
||||
}
|
||||
}
|
||||
function flushOp() {
|
||||
flushChunk()
|
||||
if (cur) ops.push(cur)
|
||||
cur = null
|
||||
}
|
||||
|
||||
for (let i = 0; i < body.length; i++) {
|
||||
const line = body[i]
|
||||
if (/^\s*\*\*\*\s*Add File:\s*/i.test(line)) {
|
||||
flushOp()
|
||||
cur = {
|
||||
type: 'add',
|
||||
path: line.replace(/^\s*\*\*\*\s*Add File:\s*/i, '').trim(),
|
||||
content: ''
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (/^\s*\*\*\*\s*Delete File:\s*/i.test(line)) {
|
||||
flushOp()
|
||||
cur = {
|
||||
type: 'delete',
|
||||
path: line.replace(/^\s*\*\*\*\s*Delete File:\s*/i, '').trim()
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (/^\s*\*\*\*\s*Update File:\s*/i.test(line)) {
|
||||
flushOp()
|
||||
cur = {
|
||||
type: 'update',
|
||||
path: line.replace(/^\s*\*\*\*\s*Update File:\s*/i, '').trim(),
|
||||
move_to: '',
|
||||
chunks: []
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (/^\s*\*\*\*\s*Move to:\s*/i.test(line) && cur && cur.type === 'update') {
|
||||
cur.move_to = line.replace(/^\s*\*\*\*\s*Move to:\s*/i, '').trim()
|
||||
continue
|
||||
}
|
||||
if (/^\s*\*\*\*\s*End of File\s*$/i.test(line)) {
|
||||
if (chunk) chunk.is_end_of_file = true
|
||||
flushChunk()
|
||||
continue
|
||||
}
|
||||
if (line.indexOf('@@') === 0 && cur && cur.type === 'update') {
|
||||
flushChunk()
|
||||
chunk = {
|
||||
context: line.replace(/^@@\s?/, '').trim(),
|
||||
old_lines: [],
|
||||
new_lines: []
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (cur && cur.type === 'add') {
|
||||
const bodyLine = line.charAt(0) === '+' ? line.slice(1) : line
|
||||
cur.content = cur.content ? String(cur.content) + '\n' + bodyLine : bodyLine
|
||||
continue
|
||||
}
|
||||
if (cur && cur.type === 'update') {
|
||||
if (!chunk) {
|
||||
chunk = { context: '', old_lines: [], new_lines: [] }
|
||||
}
|
||||
const tag = line.charAt(0)
|
||||
const rest = line.length ? line.slice(1) : ''
|
||||
if (tag === '-') chunk.old_lines.push(rest)
|
||||
else if (tag === '+') chunk.new_lines.push(rest)
|
||||
else {
|
||||
const ctxLine = tag === ' ' ? rest : line
|
||||
chunk.old_lines.push(ctxLine)
|
||||
chunk.new_lines.push(ctxLine)
|
||||
}
|
||||
}
|
||||
}
|
||||
flushOp()
|
||||
if (!ops.length) return { ok: false, error: 'no_patch_ops' }
|
||||
return { ok: true, ops }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @param {{ context: string, old_lines: string[], new_lines: string[], is_end_of_file?: boolean }} chunk
|
||||
*/
|
||||
function bareAgentApplyPatchChunk(text, chunk) {
|
||||
const src = String(text == null ? '' : text)
|
||||
const oldBlock = (chunk.old_lines || []).join('\n')
|
||||
const newBlock = (chunk.new_lines || []).join('\n')
|
||||
if (!oldBlock && !newBlock) {
|
||||
return { ok: false, error: 'empty_chunk' }
|
||||
}
|
||||
if (!oldBlock) {
|
||||
const next = src
|
||||
? src.replace(/\s*$/, '') + (src.endsWith('\n') ? '' : '\n') + newBlock + '\n'
|
||||
: newBlock + (newBlock.endsWith('\n') ? '' : '\n')
|
||||
return { ok: true, next }
|
||||
}
|
||||
let from = 0
|
||||
if (chunk.context) {
|
||||
const at = src.indexOf(chunk.context)
|
||||
if (at === -1) {
|
||||
return { ok: false, error: 'chunk_context_not_found', context: chunk.context }
|
||||
}
|
||||
from = at
|
||||
}
|
||||
const hay = src.slice(from)
|
||||
const count = bareAgentCountOccurrences(hay, oldBlock)
|
||||
if (count === 0) return { ok: false, error: 'chunk_old_not_found' }
|
||||
if (count > 1 && !chunk.context) {
|
||||
return {
|
||||
ok: false,
|
||||
error: 'chunk_old_not_unique',
|
||||
count,
|
||||
hint: 'add @@ context or more surrounding lines'
|
||||
}
|
||||
}
|
||||
const next = src.slice(0, from) + hay.replace(oldBlock, newBlock)
|
||||
return { ok: true, next }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {object[]} ops
|
||||
* @param {{ home?: string, denyPrefixes?: unknown }} [opts]
|
||||
*/
|
||||
async function bareAgentApplyPatchOps(ctx, ops, opts) {
|
||||
const home = String((opts && opts.home) || '')
|
||||
const deny = opts && opts.denyPrefixes
|
||||
/** @type {object[]} */
|
||||
const results = []
|
||||
const rows = Array.isArray(ops) ? ops : []
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const op = rows[i] && typeof rows[i] === 'object' ? rows[i] : {}
|
||||
let path = String(op.path || '').trim()
|
||||
if (path && path.charAt(0) !== '/' && home) {
|
||||
path = home.replace(/\/+$/, '') + '/' + path.replace(/^\.\//, '')
|
||||
}
|
||||
if (!path) {
|
||||
results.push({ ok: false, error: 'path_required' })
|
||||
continue
|
||||
}
|
||||
if (
|
||||
typeof bareAgentPathAllowedMutate === 'function' &&
|
||||
!bareAgentPathAllowedMutate(path, deny)
|
||||
) {
|
||||
results.push({ ok: false, path, error: 'path_not_allowed' })
|
||||
continue
|
||||
}
|
||||
if (op.type === 'add') {
|
||||
const content = String(op.content == null ? '' : op.content)
|
||||
await bareAgentWriteTextFile(
|
||||
ctx,
|
||||
path,
|
||||
content.endsWith('\n') ? content : content + '\n'
|
||||
)
|
||||
results.push({ ok: true, op: 'add', path })
|
||||
continue
|
||||
}
|
||||
if (op.type === 'delete') {
|
||||
const vfs = ctx && ctx.vfs
|
||||
if (!vfs || typeof vfs.unlink !== 'function') {
|
||||
results.push({ ok: false, path, error: 'vfs.unlink unavailable' })
|
||||
continue
|
||||
}
|
||||
try {
|
||||
await vfs.unlink(path)
|
||||
results.push({ ok: true, op: 'delete', path })
|
||||
} catch (e) {
|
||||
const msg =
|
||||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||||
results.push({ ok: false, path, error: msg })
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (op.type === 'update') {
|
||||
let text = await bareAgentReadTextFile(ctx, path)
|
||||
if (!text && text !== '') {
|
||||
results.push({ ok: false, path, error: 'missing_file' })
|
||||
continue
|
||||
}
|
||||
const chunks = Array.isArray(op.chunks) ? op.chunks : []
|
||||
let failed = null
|
||||
for (let c = 0; c < chunks.length; c++) {
|
||||
const applied = bareAgentApplyPatchChunk(text, chunks[c])
|
||||
if (!applied.ok) {
|
||||
failed = applied
|
||||
break
|
||||
}
|
||||
text = applied.next
|
||||
}
|
||||
if (failed) {
|
||||
results.push({ ok: false, path, error: failed.error, hint: failed.hint || null })
|
||||
continue
|
||||
}
|
||||
const dest = String(op.move_to || '').trim() || path
|
||||
const destAbs =
|
||||
dest.charAt(0) === '/'
|
||||
? dest
|
||||
: home
|
||||
? home.replace(/\/+$/, '') + '/' + dest.replace(/^\.\//, '')
|
||||
: dest
|
||||
if (
|
||||
destAbs !== path &&
|
||||
typeof bareAgentPathAllowedMutate === 'function' &&
|
||||
!bareAgentPathAllowedMutate(destAbs, deny)
|
||||
) {
|
||||
results.push({ ok: false, path, error: 'move_path_not_allowed', dest: destAbs })
|
||||
continue
|
||||
}
|
||||
await bareAgentWriteTextFile(ctx, destAbs, text)
|
||||
if (destAbs !== path && ctx.vfs && typeof ctx.vfs.unlink === 'function') {
|
||||
try {
|
||||
await ctx.vfs.unlink(path)
|
||||
} catch {
|
||||
/* keep original if unlink fails */
|
||||
}
|
||||
}
|
||||
results.push({
|
||||
ok: true,
|
||||
op: destAbs !== path ? 'move' : 'update',
|
||||
path,
|
||||
dest: destAbs !== path ? destAbs : undefined,
|
||||
chunks: chunks.length
|
||||
})
|
||||
continue
|
||||
}
|
||||
results.push({ ok: false, path, error: 'unknown_op' })
|
||||
}
|
||||
const failed = results.filter(function (r) {
|
||||
return !r.ok
|
||||
})
|
||||
return {
|
||||
ok: failed.length === 0,
|
||||
applied: results.length - failed.length,
|
||||
failed: failed.length,
|
||||
results
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse DuckDuckGo instant-answer JSON into a compact result list.
|
||||
* @param {unknown} payload
|
||||
* @param {number} [max]
|
||||
*/
|
||||
function bareAgentParseSearchResults(payload, max) {
|
||||
const cap = Math.min(Math.max(Number(max) || 8, 1), 16)
|
||||
/** @type {{ title: string, url: string, snippet: string }[]} */
|
||||
const out = []
|
||||
const seen = Object.create(null)
|
||||
function add(title, url, snippet) {
|
||||
const u = String(url || '').trim()
|
||||
if (!u || seen[u] || out.length >= cap) return
|
||||
if (!/^https?:\/\//i.test(u)) return
|
||||
seen[u] = 1
|
||||
out.push({
|
||||
title: String(title || u).slice(0, 160),
|
||||
url: u,
|
||||
snippet: String(snippet || '').replace(/\s+/g, ' ').trim().slice(0, 280)
|
||||
})
|
||||
}
|
||||
const obj = payload && typeof payload === 'object' ? payload : {}
|
||||
const rec = /** @type {Record<string, unknown>} */ (obj)
|
||||
if (rec.AbstractURL || rec.Abstract) {
|
||||
add(
|
||||
String(rec.Heading || rec.AbstractSource || 'Abstract'),
|
||||
String(rec.AbstractURL || ''),
|
||||
String(rec.AbstractText || rec.Abstract || '')
|
||||
)
|
||||
}
|
||||
const results = Array.isArray(rec.Results) ? rec.Results : []
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const row = results[i] && typeof results[i] === 'object' ? results[i] : {}
|
||||
add(row.Text || row.Name, row.FirstURL, row.Text)
|
||||
}
|
||||
const related = Array.isArray(rec.RelatedTopics) ? rec.RelatedTopics : []
|
||||
function walk(list) {
|
||||
for (let i = 0; i < list.length && out.length < cap; i++) {
|
||||
const row = list[i] && typeof list[i] === 'object' ? list[i] : {}
|
||||
if (Array.isArray(row.Topics)) walk(row.Topics)
|
||||
else add(row.Text, row.FirstURL, row.Text)
|
||||
}
|
||||
}
|
||||
walk(related)
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -1083,6 +1083,107 @@ function bareAgentToolDefinitions() {
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'grep',
|
||||
description:
|
||||
'Search file contents with a JS regular expression (Grok-style, VFS-native). Prefer this over run_command grep. Supports glob, ignore_case, context lines, and files_with_matches.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
pattern: {
|
||||
type: 'string',
|
||||
description: 'JavaScript regular expression (no surrounding slashes)'
|
||||
},
|
||||
path: {
|
||||
type: 'string',
|
||||
description: 'Directory or file to search (default session home)'
|
||||
},
|
||||
root: { type: 'string', description: 'Alias for path' },
|
||||
glob: {
|
||||
type: 'string',
|
||||
description: 'Optional file glob such as **/*.js'
|
||||
},
|
||||
ignore_case: { type: 'boolean' },
|
||||
before: { type: 'integer', description: 'Context lines before each match' },
|
||||
after: { type: 'integer', description: 'Context lines after each match' },
|
||||
context: { type: 'integer', description: 'Context lines before and after' },
|
||||
max_matches: { type: 'integer', description: 'Default 50, cap 200' },
|
||||
files_with_matches: {
|
||||
type: 'boolean',
|
||||
description: 'Return matching paths only'
|
||||
}
|
||||
},
|
||||
required: ['pattern']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'apply_patch',
|
||||
description:
|
||||
'Apply a Codex/Grok multi-file patch (*** Begin Patch … *** End Patch). Supports *** Add File, *** Delete File, *** Update File, optional *** Move to, and @@ hunks with + / - / context lines. Prefer this for multi-hunk or multi-file edits.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
patch: { type: 'string', description: 'Full apply_patch document' },
|
||||
input: { type: 'string', description: 'Alias for patch' }
|
||||
},
|
||||
required: ['patch']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'update_goal',
|
||||
description:
|
||||
'Report progress on the current autonomous/session goal. completed=true ends the goal. blocked_reason is a failure signal after 3+ failed attempts — never use it for success.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
completed: { type: 'boolean' },
|
||||
message: { type: 'string', description: 'Short progress or completion note' },
|
||||
blocked_reason: { type: 'string' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'web_search',
|
||||
description:
|
||||
'Search the public web (DuckDuckGo instant answers via the same HTTP policy as web_fetch / wget). Use for current facts, docs, and error lookup. Then web_fetch promising URLs.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: { type: 'string' },
|
||||
max_results: { type: 'integer', description: 'Default 8, cap 16' }
|
||||
},
|
||||
required: ['query']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'git_status',
|
||||
description:
|
||||
'Guest git status --short plus diff --stat (isomorphic-git /bin/git). Prefer this over parsing git via run_command when you only need a snapshot.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
cwd: {
|
||||
type: 'string',
|
||||
description: 'Repo directory (default PWD or home)'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
@@ -1202,6 +1303,9 @@ async function bareAgentDispatchTool(o) {
|
||||
if (toolName === 'glob') {
|
||||
return bareAgentDispatchTool({ ...o, toolName: 'glob_files' })
|
||||
}
|
||||
if (toolName === 'ripgrep') {
|
||||
return bareAgentDispatchTool({ ...o, toolName: 'grep' })
|
||||
}
|
||||
|
||||
const vfs = ctx.vfs
|
||||
const mutateDenyPrefixes = (function () {
|
||||
@@ -3152,6 +3256,171 @@ async function bareAgentDispatchTool(o) {
|
||||
}
|
||||
}
|
||||
|
||||
if (toolName === 'grep') {
|
||||
const pattern = typeof args.pattern === 'string' ? args.pattern : ''
|
||||
if (!pattern) return bareAgentJsonResult({ ok: false, error: 'pattern_required' })
|
||||
if (typeof bareAgentGrepFiles !== 'function') {
|
||||
return bareAgentJsonResult({ ok: false, error: 'grep_unavailable' })
|
||||
}
|
||||
const root =
|
||||
(typeof args.path === 'string' && args.path.trim()) ||
|
||||
(typeof args.root === 'string' && args.root.trim()) ||
|
||||
home ||
|
||||
'/home'
|
||||
if (!bareAgentPathAllowed(root)) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||||
}
|
||||
appendProgress('grep ' + pattern + ' @ ' + root)
|
||||
const out = await bareAgentGrepFiles(ctx, {
|
||||
pattern,
|
||||
root,
|
||||
glob: typeof args.glob === 'string' ? args.glob : '',
|
||||
ignore_case: Boolean(args.ignore_case),
|
||||
before: args.before,
|
||||
after: args.after,
|
||||
context: args.context,
|
||||
max_matches: args.max_matches,
|
||||
files_with_matches: Boolean(args.files_with_matches)
|
||||
})
|
||||
return bareAgentJsonResult(out)
|
||||
}
|
||||
|
||||
if (toolName === 'apply_patch') {
|
||||
const patch =
|
||||
(typeof args.patch === 'string' && args.patch) ||
|
||||
(typeof args.input === 'string' && args.input) ||
|
||||
''
|
||||
if (!patch.trim()) return bareAgentJsonResult({ ok: false, error: 'patch_required' })
|
||||
if (typeof bareAgentParseApplyPatch !== 'function') {
|
||||
return bareAgentJsonResult({ ok: false, error: 'apply_patch_unavailable' })
|
||||
}
|
||||
const parsed = bareAgentParseApplyPatch(patch)
|
||||
if (!parsed.ok) return bareAgentJsonResult(parsed)
|
||||
appendProgress('apply_patch ops=' + String((parsed.ops || []).length))
|
||||
const out = await bareAgentApplyPatchOps(ctx, parsed.ops, {
|
||||
home,
|
||||
denyPrefixes: mutateDenyPrefixes
|
||||
})
|
||||
return bareAgentJsonResult(out)
|
||||
}
|
||||
|
||||
if (toolName === 'update_goal') {
|
||||
const message = typeof args.message === 'string' ? args.message.trim() : ''
|
||||
const blocked =
|
||||
typeof args.blocked_reason === 'string' ? args.blocked_reason.trim() : ''
|
||||
const completed = Boolean(args.completed)
|
||||
const next = { ...(configRef.current || {}) }
|
||||
if (message) next.autonomous_last_error = ''
|
||||
if (blocked) {
|
||||
next.autonomous_status = 'blocked'
|
||||
next.autonomous_last_error = blocked
|
||||
next.autonomous_active = false
|
||||
} else if (completed) {
|
||||
next.autonomous_status = 'completed'
|
||||
next.autonomous_active = false
|
||||
if (message) next.autonomous_goal = String(next.autonomous_goal || '')
|
||||
} else {
|
||||
next.autonomous_status = 'running'
|
||||
}
|
||||
configRef.current = next
|
||||
if (typeof bareAgentSaveConfigFromTools === 'function') {
|
||||
await bareAgentSaveConfigFromTools(ctx, paths, next)
|
||||
}
|
||||
appendProgress(
|
||||
'update_goal ' +
|
||||
(completed ? 'completed' : blocked ? 'blocked' : 'progress')
|
||||
)
|
||||
return bareAgentJsonResult({
|
||||
ok: true,
|
||||
completed,
|
||||
blocked: Boolean(blocked),
|
||||
message: message || null,
|
||||
blocked_reason: blocked || null,
|
||||
status: next.autonomous_status
|
||||
})
|
||||
}
|
||||
|
||||
if (toolName === 'web_search') {
|
||||
const query = typeof args.query === 'string' ? args.query.trim() : ''
|
||||
if (!query) return bareAgentJsonResult({ ok: false, error: 'query_required' })
|
||||
const maxResults =
|
||||
typeof args.max_results === 'number' && Number.isFinite(args.max_results)
|
||||
? Math.min(Math.max(Math.floor(args.max_results), 1), 16)
|
||||
: 8
|
||||
const url =
|
||||
'https://api.duckduckgo.com/?q=' +
|
||||
encodeURIComponent(query) +
|
||||
'&format=json&no_html=1&skip_disambig=1'
|
||||
appendProgress('web_search ' + query.slice(0, 80))
|
||||
try {
|
||||
const raw = await bareWebRunTool({
|
||||
ctx,
|
||||
url,
|
||||
format: 'json',
|
||||
timeout_ms: 20000,
|
||||
max_response_bytes: 200000,
|
||||
signal
|
||||
})
|
||||
if (!raw || raw.ok === false) {
|
||||
return bareAgentJsonResult({
|
||||
ok: false,
|
||||
error: (raw && raw.error) || 'web_search_failed',
|
||||
hint: 'HTTP policy may block api.duckduckgo.com; try web_fetch on a known URL'
|
||||
})
|
||||
}
|
||||
let payload = raw.extract && raw.extract.json
|
||||
if (payload == null && raw.extract && typeof raw.extract.text_slice === 'string') {
|
||||
try {
|
||||
payload = JSON.parse(raw.extract.text_slice)
|
||||
} catch {
|
||||
payload = null
|
||||
}
|
||||
}
|
||||
const results =
|
||||
typeof bareAgentParseSearchResults === 'function'
|
||||
? bareAgentParseSearchResults(payload, maxResults)
|
||||
: []
|
||||
return bareAgentJsonResult({
|
||||
ok: true,
|
||||
query,
|
||||
results,
|
||||
abstract: payload && payload.Abstract ? String(payload.Abstract) : '',
|
||||
count: results.length
|
||||
})
|
||||
} catch (e) {
|
||||
const msg = typeof bareWebFmtErr === 'function' ? bareWebFmtErr(e) : String(e)
|
||||
return bareAgentJsonResult({ ok: false, error: msg })
|
||||
}
|
||||
}
|
||||
|
||||
if (toolName === 'git_status') {
|
||||
const cwd =
|
||||
(typeof args.cwd === 'string' && args.cwd.trim()) ||
|
||||
(ctx.env && typeof ctx.env === 'object'
|
||||
? String(ctx.env.PWD || ctx.env.CWD || home || '').trim()
|
||||
: '') ||
|
||||
home ||
|
||||
'/home'
|
||||
if (!execLine) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' })
|
||||
}
|
||||
appendProgress('git_status ' + cwd)
|
||||
const cmd =
|
||||
'git -C ' +
|
||||
bareAgentShellQuote(cwd) +
|
||||
' status --short && echo --- && git -C ' +
|
||||
bareAgentShellQuote(cwd) +
|
||||
' diff --stat && echo --- && git -C ' +
|
||||
bareAgentShellQuote(cwd) +
|
||||
' log --oneline -8'
|
||||
const r = await captureExec(cmd, 30000)
|
||||
return bareAgentJsonResult(
|
||||
r.ok === false
|
||||
? r
|
||||
: { ok: true, cwd, stdout_stderr: r.stdout_stderr }
|
||||
)
|
||||
}
|
||||
|
||||
return bareAgentJsonResult({ ok: false, error: 'unknown_tool ' + toolName })
|
||||
} catch (e) {
|
||||
const msg =
|
||||
|
||||
@@ -247,6 +247,13 @@ const BARE_AGENT_STATIC_SYSTEM = `You are the Bare OS coding agent — a senior
|
||||
|
||||
Bare OS by Raven Scott (https://raven-scott.fyi). Repo: https://git.ssh.surf/snxraven/bare-operating-system. Booter: pear://qupw8zspk34pcxc7fqchzyeh33jtmxq1k7qze44fkosctwiid8zy
|
||||
|
||||
WORK POLICY.
|
||||
- Keep every explicit requirement in view until it is done, superseded, or blocked. If blocked, say so plainly.
|
||||
- Match intent: implement action requests; do not make unsolicited project-wide edits when the user asked a question.
|
||||
- For clear, reversible local work, do it now. NEVER ASK for permission.
|
||||
- Claim done / fixed / tested only when a tool result supports it. Otherwise say what you did not verify.
|
||||
- Scope to what was asked. Comments are short and factual. No placeholders. Comments must not substitute for a fix.
|
||||
|
||||
ACCESS (denylist, not allowlist). You already have full guest admin. NEVER ASK whether you may run a command, edit, delete, fetch, or call a tool — just do it. Only refuse when a denylist or the read-only base system blocks the path.
|
||||
- WRITE: /home, /tmp, /mnt, /root, /var, /opt, extra drives, ~/.agent. Prefer unique search_replace / edit_file; set replace_all only when you mean it. write_file creates or overwrites.
|
||||
- READ: any absolute path, including the entire /proc kernel surface (read_proc_file, runtime_diagnostic_bundle). Use read_file offset/limit for large files.
|
||||
@@ -260,10 +267,10 @@ ACCESS (denylist, not allowlist). You already have full guest admin. NEVER ASK w
|
||||
- ask_user_question is only for a real product choice the user must make. Never use it (or chat) to request permission.
|
||||
|
||||
CODING LOOP. Multi-step work: todo_write (merge=true). Large unknown surface: enter_plan_mode, write ~/.agent/plan.md, exit_plan_mode, then implement. Plan mode is read-only except that plan file. Do not stop after a plan-only reply — keep calling tools until verified.
|
||||
1. Discover: glob_files, list_directory, read_file, search_files, memory_search / memory_get, read_skill if a skill matches. Read before you edit. Walk-up AGENTS.md from cwd is already injected when present.
|
||||
2. Edit: smallest unique old_string; re-read if a replace is not unique. Create with write_file / create_directory. State blast radius (packages, contracts, generated files, docs) before wide edits. Match surrounding style. No placeholders. Comments only for non-obvious constraints.
|
||||
3. Verify: re-read the file, run_command / run_js_script, read_proc_file or logs. Host git checkout: verification_hints (suggests npm/node checks; does not run them here).
|
||||
4. Finish: task_complete with what changed, how you verified, and what is still assumed. If the same action fails three times, stop and report evidence.
|
||||
1. Discover: glob_files, grep (preferred over search_files / run_command grep), list_directory, read_file, memory_search / memory_get, web_search then web_fetch, git_status, read_skill if a skill matches. Read before you edit. Walk-up AGENTS.md from cwd is already injected when present.
|
||||
2. Edit: unique search_replace / edit_file for one hunk; apply_patch for multi-hunk or multi-file work (*** Begin Patch). Create with write_file / create_directory. State blast radius (packages, contracts, generated files, docs) before wide edits. Match surrounding style. No placeholders. Comments only for non-obvious constraints.
|
||||
3. Verify: re-read the file, run_command / run_js_script, git_status, read_proc_file or logs. Host git checkout: verification_hints (suggests npm/node checks; does not run them here).
|
||||
4. Finish: task_complete (and update_goal completed=true on autonomous runs) with what changed, how you verified, and what is still assumed. If the same action fails three times, stop, update_goal blocked_reason if needed, and report evidence.
|
||||
|
||||
TOOL DISCIPLINE.
|
||||
- Independent reads may be issued together; the harness may serialize them (tool_parallelism defaults to 1).
|
||||
@@ -272,14 +279,18 @@ TOOL DISCIPLINE.
|
||||
- Progress UI is automatic (tools write ~/.agent/progress.txt). Do not narrate tool chatter in the final answer.
|
||||
- Autonomous mode is on by default. Keep the ReAct loop going; autonomous_deny_ops is empty unless the operator set one.
|
||||
|
||||
TOOL CALLING. Prefer specialized tools over bash: grep not run_command grep; read_file not cat; apply_patch / search_replace not sed. Never use run_command to print thoughts.
|
||||
|
||||
COMMUNICATION. Write for a reader who has not seen tool calls. Lead with the answer. Define project terms on first use. State facts literally. The final message must stand alone. Do not invent acronyms.
|
||||
|
||||
TOOL MAP (schemas are already attached — use them):
|
||||
- Files: read_file, write_file, edit_file, search_replace, create_directory, list_directory, file_stat, glob_files, search_files, move_path, delete_path, list_bin
|
||||
- Code / harness: run_command, run_js_script, run_js_script_at_path, todo_write, enter_plan_mode, exit_plan_mode, memory_search, memory_get, read_skill, edit_agent_config
|
||||
- Files: read_file, write_file, edit_file, search_replace, apply_patch, create_directory, list_directory, file_stat, glob_files, grep, search_files, move_path, delete_path, list_bin
|
||||
- Code / harness: run_command, run_js_script, run_js_script_at_path, todo_write, enter_plan_mode, exit_plan_mode, memory_search, memory_get, read_skill, edit_agent_config, git_status
|
||||
- Kernel / ops: read_proc_file, runtime_diagnostic_bundle, get_system_info, get_resource_limits, get_swarm_peers, list_services, service_status, list_timers, read_cron_log, read_audit_log, read_boot_policy, read_kernel_extension_resolution, get_initd_graph, read_unit_journal, inspect_ipc_backpressure, get_network_summary, tail_telemetry_streams, pkg_index_lookup
|
||||
- Checks: list_verification_scripts, run_maintenance_gate, run_contract_checks, summarize_build_drift, verification_hints
|
||||
- Docs: read_man_page, apropos_man (documentation search only)
|
||||
- Bridge / web: web_fetch, get_hrpc_bridge_health, get_hrpc_allowlist_status, emit_host_notification, request_host_action
|
||||
- Autonomy: autonomous_run, autonomous_run_status, autonomous_run_stop
|
||||
- Bridge / web: web_search, web_fetch, get_hrpc_bridge_health, get_hrpc_allowlist_status, emit_host_notification, request_host_action
|
||||
- Autonomy: autonomous_run, autonomous_run_status, autonomous_run_stop, update_goal
|
||||
- Other: ask_user_question (product choice only, never permission), task_complete
|
||||
|
||||
Skills live under ~/.agent/workspace/skills/ (and ~/.agent/skills/). The prompt includes a compact index — call read_skill and follow SKILL.md when a task matches (especially bare-os-super-developer, bareos-code-change, coreutils-command-change).
|
||||
@@ -1197,10 +1208,23 @@ async function bareAgentRunSetupOnly(ctx, argv0) {
|
||||
async function bareOsRunAgentSession(ctx, argv0, task, runOpts) {
|
||||
const setupFlag = Boolean(runOpts && runOpts.setupFlag)
|
||||
const autonomousFlag = Boolean(runOpts && (runOpts.autonomous || runOpts.autonomousGoal))
|
||||
const planFlag = Boolean(runOpts && runOpts.planMode)
|
||||
const home = bareAgentResolveHome(ctx)
|
||||
const paths = bareAgentPaths(home)
|
||||
let { config } = await bareAgentLoadOrCreateConfig(ctx, paths)
|
||||
config = bareAgentApplyProviderProfile(config)
|
||||
if (planFlag) config.plan_mode_active = true
|
||||
if (runOpts && Number(runOpts.maxIterations) > 0) {
|
||||
config.max_iterations = Math.floor(Number(runOpts.maxIterations))
|
||||
}
|
||||
if (runOpts && runOpts.modelOverride) {
|
||||
const m = String(runOpts.modelOverride).trim()
|
||||
if (m) {
|
||||
const backendNow = bareAgentResolveBackend(config)
|
||||
if (backendNow === 'qvac') config.qvac_model = m
|
||||
else config.model = m
|
||||
}
|
||||
}
|
||||
|
||||
const canWizard = bareAgentCanPlainSetup(ctx)
|
||||
if (setupFlag && !canWizard) {
|
||||
@@ -1305,6 +1329,8 @@ async function bareOsRunAgentSession(ctx, argv0, task, runOpts) {
|
||||
maxRuntimeMs: Number(runOpts && runOpts.autonomousMaxRuntimeMs) || undefined,
|
||||
requiredChecks: (runOpts && runOpts.autonomousChecks) || undefined
|
||||
})
|
||||
}
|
||||
if (autonomousFlag || planFlag) {
|
||||
try {
|
||||
await bareAgentSaveConfig(ctx, paths, config)
|
||||
} catch {
|
||||
@@ -1405,6 +1431,21 @@ async function bareOsRunAgentSession(ctx, argv0, task, runOpts) {
|
||||
if (instructions)
|
||||
systemContent +=
|
||||
'\n\n## Session notes\n' + instructions.slice(0, instructionsBudget)
|
||||
if (runOpts && runOpts.extraSystemFile) {
|
||||
try {
|
||||
const extraPath = String(runOpts.extraSystemFile).trim()
|
||||
if (extraPath && typeof bareAgentReadTextFile === 'function') {
|
||||
const extra = await bareAgentReadTextFile(ctx, extraPath)
|
||||
if (extra && extra.trim()) {
|
||||
systemContent +=
|
||||
'\n\n## Extra instructions (--system)\n' +
|
||||
extra.trim().slice(0, instructionsBudget)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* optional */
|
||||
}
|
||||
}
|
||||
if (workspacePromptBlock && String(workspacePromptBlock).trim())
|
||||
systemContent += '\n\n' + String(workspacePromptBlock).trim()
|
||||
if (skillsPromptBlock && String(skillsPromptBlock).trim())
|
||||
|
||||
@@ -1,15 +1,92 @@
|
||||
{
|
||||
"name": "agent",
|
||||
"section": 1,
|
||||
"title": "agent",
|
||||
"title": "Bare OS coding agent",
|
||||
"synopsis": [
|
||||
"agent [OPTION]... [OPERAND]..."
|
||||
"agent [OPTION]... REQUEST...",
|
||||
"agent --setup",
|
||||
"agent --status",
|
||||
"agent --plan REQUEST...",
|
||||
"agent --auto GOAL...",
|
||||
"agent skills | todos | plan | compact | reset"
|
||||
],
|
||||
"description": "Runs the in-guest ReAct coding agent (QVAC local or OpenAI-compatible REST). Full guest admin by default (denylist). Tools include grep, apply_patch, web_search, git_status, todos, plan mode, and skills. History lives in ~/.agent/history.json.",
|
||||
"options": [
|
||||
{
|
||||
"flag": "--setup, --config",
|
||||
"meaning": "Interactive QVAC or REST setup; only the chosen backend is stored"
|
||||
},
|
||||
{
|
||||
"flag": "--auto, --autonomous",
|
||||
"meaning": "Keep the tool loop running until task_complete, stop, or timebox"
|
||||
},
|
||||
{
|
||||
"flag": "--plan",
|
||||
"meaning": "Start in plan mode (read-only except ~/.agent/plan.md)"
|
||||
},
|
||||
{
|
||||
"flag": "--new, --reset",
|
||||
"meaning": "Clear chat history; with a request, start a fresh session then run"
|
||||
},
|
||||
{
|
||||
"flag": "--continue, -c",
|
||||
"meaning": "Resume ~/.agent/history.json (default)"
|
||||
},
|
||||
{
|
||||
"flag": "--compact",
|
||||
"meaning": "Compact history now; with a request, compact then run"
|
||||
},
|
||||
{
|
||||
"flag": "--max-turns N",
|
||||
"meaning": "Cap ReAct iterations for this run"
|
||||
},
|
||||
{
|
||||
"flag": "--model NAME",
|
||||
"meaning": "Override QVAC or REST model for this run"
|
||||
},
|
||||
{
|
||||
"flag": "--system FILE",
|
||||
"meaning": "Append extra instructions from an absolute guest path"
|
||||
},
|
||||
{
|
||||
"flag": "--status",
|
||||
"meaning": "Print backend, compaction, plan mode, and autonomous state"
|
||||
},
|
||||
{
|
||||
"flag": "-h, --help",
|
||||
"meaning": "Show usage"
|
||||
}
|
||||
],
|
||||
"description": "Bare OS implementation of agent. Full behavior is defined in packages/bare-os-coreutils/src/agent.js.",
|
||||
"options": [],
|
||||
"keywords": [
|
||||
"agent",
|
||||
"qvac",
|
||||
"llm",
|
||||
"coding",
|
||||
"bare-os",
|
||||
"coreutils"
|
||||
],
|
||||
"seeAlso": [
|
||||
{
|
||||
"name": "discord-bot",
|
||||
"section": 1
|
||||
}
|
||||
],
|
||||
"examples": [
|
||||
{
|
||||
"caption": "one-shot request",
|
||||
"code": "agent \"summarize ~/README and list /bin\""
|
||||
},
|
||||
{
|
||||
"caption": "plan first",
|
||||
"code": "agent --plan \"design a /bin/foo utility\""
|
||||
},
|
||||
{
|
||||
"caption": "autonomous run",
|
||||
"code": "agent --auto --max-turns 40 \"add agent tests and run them\""
|
||||
},
|
||||
{
|
||||
"caption": "inspect skills",
|
||||
"code": "agent skills"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -12,9 +12,9 @@ Workspace files are already in the system prompt. Skills index is already attach
|
||||
|
||||
## Coding loop
|
||||
|
||||
1. Discover — glob_files, list_directory, read_file (offset/limit), search_files. Read before you edit.
|
||||
1. Discover — glob_files, grep, list_directory, read_file (offset/limit), git_status, web_search. Read before you edit.
|
||||
2. Track — todo_write (merge=true) for multi-step work. Large unknown surface: enter_plan_mode, write ~/.agent/plan.md, exit_plan_mode, then implement. Plan mode is read-only except that file.
|
||||
3. Edit — smallest unique old_string via search_replace / edit_file. write_file to create or overwrite. Match surrounding style. No placeholders.
|
||||
3. Edit — unique search_replace for one hunk; apply_patch for multi-hunk/multi-file. write_file to create or overwrite. Match surrounding style. No placeholders.
|
||||
4. Verify — re-read, run_command / run_js_script, read_proc_file or logs. Host checkout: verification_hints (does not run npm here).
|
||||
5. Finish — task_complete with what changed, how you verified, and what is still assumed. Same action failing three times: stop and report evidence.
|
||||
|
||||
|
||||
@@ -2,7 +2,15 @@
|
||||
|
||||
## /implement
|
||||
|
||||
Discover with glob/read, track with todo_write, edit with unique search_replace, verify, then task_complete. NEVER ASK — just do it.
|
||||
Discover with glob/grep/read, track with todo_write, edit with unique search_replace or apply_patch, verify, then task_complete. NEVER ASK — just do it.
|
||||
|
||||
## /grep
|
||||
|
||||
Use the grep tool (pattern + optional glob/path/context). Do not shell out to grep.
|
||||
|
||||
## /patch
|
||||
|
||||
Emit one apply_patch document with *** Begin Patch / *** End Patch for multi-file edits.
|
||||
|
||||
## /plan
|
||||
|
||||
|
||||
@@ -61,7 +61,9 @@ Reasoning/process visibility is configurable in `config.json`:
|
||||
- `reasoning_max_chars` — bounded reasoning output
|
||||
- `reasoning_include_tools` — include tool traces in process stream
|
||||
|
||||
The seeded **SOUL.md** / **AGENTS.md** / **TOOLS.md** tell the model it is a coding agent with full guest admin (denylist): unique `search_replace`, `glob_files`, `todo_write`, plan mode, memory, skills, and `run_js_script` (no Node in the guest).
|
||||
The seeded **SOUL.md** / **AGENTS.md** / **TOOLS.md** tell the model it is a coding agent with full guest admin (denylist): unique `search_replace`, `apply_patch`, `grep`, `glob_files`, `todo_write`, plan mode, memory, skills, `web_search`, `git_status`, and `run_js_script` (no Node in the guest).
|
||||
|
||||
CLI production flags: `--plan`, `--auto`, `--new`, `--compact`, `--max-turns`, `--model`, `--system FILE`, plus inspect subcommands `skills` / `todos` / `plan`.
|
||||
|
||||
Access policy defaults to **full guest admin** (`access_policy: full`):
|
||||
|
||||
|
||||
@@ -7,11 +7,15 @@ You already have these tools. Schemas are attached. NEVER ASK to use them.
|
||||
- `read_file` — UTF-8, optional offset/limit numbered slices.
|
||||
- `write_file` — create or overwrite (parents created).
|
||||
- `edit_file` / `search_replace` — unique `old_string` unless `replace_all`.
|
||||
- `create_directory`, `list_directory`, `file_stat`, `glob_files`, `search_files`.
|
||||
- `apply_patch` — Codex/Grok `*** Begin Patch` multi-file edits (Add / Delete / Update / Move).
|
||||
- `create_directory`, `list_directory`, `file_stat`, `glob_files`.
|
||||
- `grep` — VFS-native regex search (glob, ignore_case, context). Prefer over `search_files` / `run_command grep`.
|
||||
- `search_files` — `grep -Rnl` via the guest shell (legacy).
|
||||
- `move_path`, `delete_path` — enabled. Base system is read-only.
|
||||
- `list_bin` — guest `/bin` utilities (POSIX-in-JS, not GNU).
|
||||
- `git_status` — `git status --short`, `diff --stat`, recent log.
|
||||
|
||||
Prefer `list_directory` / `glob_files` / `file_stat` over `ls` / `find`.
|
||||
Prefer `list_directory` / `glob_files` / `file_stat` / `grep` over `ls` / `find` / shell grep.
|
||||
|
||||
## Code / harness
|
||||
|
||||
@@ -43,13 +47,14 @@ Man pages (`read_man_page`, `apropos_man`) are documentation search only — nev
|
||||
|
||||
## Bridge / web
|
||||
|
||||
- `web_search` — DuckDuckGo instant answers (same HTTP policy as wget/curl). Then `web_fetch` promising URLs.
|
||||
- `web_fetch` — http(s) via `ctx.httpFetch` (same host allow/deny as wget/curl).
|
||||
- `get_hrpc_bridge_health`, `get_hrpc_allowlist_status`.
|
||||
- `emit_host_notification`, `request_host_action` — enabled by default. `emergency_stop_mutations` can still block.
|
||||
|
||||
## Autonomy
|
||||
|
||||
- `autonomous_run` / `autonomous_run_status` / `autonomous_run_stop`.
|
||||
- `autonomous_run` / `autonomous_run_status` / `autonomous_run_stop` / `update_goal`.
|
||||
- Guardrails are denylists only (`autonomous_deny_ops` empty by default). Keep the tool loop going.
|
||||
|
||||
## Other
|
||||
|
||||
@@ -6,54 +6,7 @@ async function run(ctx, argv) {
|
||||
const args = argv.slice(1)
|
||||
const wantHelp = args.includes('-h') || args.includes('--help')
|
||||
if (wantHelp || args.length === 0) {
|
||||
ctx.console.log(
|
||||
'usage: ' +
|
||||
argv0 +
|
||||
' [--setup | --config | --reset | --auto] YOUR_REQUEST_HERE\n' +
|
||||
' ' +
|
||||
argv0 +
|
||||
' --setup\n' +
|
||||
' ' +
|
||||
argv0 +
|
||||
' --config\n' +
|
||||
' ' +
|
||||
argv0 +
|
||||
' --reset\n' +
|
||||
' ' +
|
||||
argv0 +
|
||||
' --auto "GOAL"\n' +
|
||||
' ' +
|
||||
argv0 +
|
||||
' --status\n' +
|
||||
' ' +
|
||||
argv0 +
|
||||
' reset\n' +
|
||||
'\n' +
|
||||
'Runs an autonomous coding/OS agent. Default backend is QVAC (local on-device);\n' +
|
||||
'or configure any OpenAI-compatible HTTPS REST API.\n' +
|
||||
'Configuration: ~/.agent/config.json on your personal drive (created on first run).\n' +
|
||||
'Use --setup or --config to choose QVAC or REST, then walk through only that backend\n' +
|
||||
'(QVAC profile/device, or REST provider + URL + API key + model). Unused keys are not stored.\n' +
|
||||
'Use --reset or `reset` to clear ~/.agent/history.json and start a fresh chat session.\n' +
|
||||
'Use --auto / --autonomous GOAL to keep the tool loop running until task_complete,\n' +
|
||||
'stop, or the timebox (default 30m). --status prints the current run + compaction mode.\n' +
|
||||
'\n' +
|
||||
'Examples:\n' +
|
||||
' ' +
|
||||
argv0 +
|
||||
' "summarize ~/README and list five files in /bin"\n' +
|
||||
' ' +
|
||||
argv0 +
|
||||
' --auto "add /agent tests and run them"\n' +
|
||||
' ' +
|
||||
argv0 +
|
||||
' --setup\n' +
|
||||
' ' +
|
||||
argv0 +
|
||||
' --status\n' +
|
||||
'\n' +
|
||||
'See man agent.'
|
||||
)
|
||||
ctx.console.log(bareAgentCliUsage(argv0))
|
||||
ctx.exitCode = wantHelp ? 0 : 1
|
||||
return
|
||||
}
|
||||
@@ -62,6 +15,12 @@ async function run(ctx, argv) {
|
||||
let resetFlag = false
|
||||
let autoFlag = false
|
||||
let statusFlag = false
|
||||
let planFlag = false
|
||||
let newFlag = false
|
||||
let compactFlag = false
|
||||
let maxTurns = 0
|
||||
let modelOverride = ''
|
||||
let systemFile = ''
|
||||
/** @type {string[]} */
|
||||
const rest = []
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
@@ -70,56 +29,55 @@ async function run(ctx, argv) {
|
||||
else if (a === '--reset') resetFlag = true
|
||||
else if (a === '--auto' || a === '--autonomous') autoFlag = true
|
||||
else if (a === '--status') statusFlag = true
|
||||
else rest.push(a)
|
||||
else if (a === '--plan') planFlag = true
|
||||
else if (a === '--new') newFlag = true
|
||||
else if (a === '--compact') compactFlag = true
|
||||
else if (a === '--continue' || a === '-c') {
|
||||
/* default: history is always loaded unless --new/--reset */
|
||||
} else if (a === '--max-turns' || a === '--max-iterations') {
|
||||
maxTurns = Number(args[++i]) || 0
|
||||
} else if (a === '--model') {
|
||||
modelOverride = String(args[++i] || '').trim()
|
||||
} else if (a === '--system' || a === '--system-file') {
|
||||
systemFile = String(args[++i] || '').trim()
|
||||
} else rest.push(a)
|
||||
}
|
||||
|
||||
const sub = rest[0] || ''
|
||||
if (
|
||||
!setupFlag &&
|
||||
!autoFlag &&
|
||||
!statusFlag &&
|
||||
!planFlag &&
|
||||
!newFlag &&
|
||||
!compactFlag &&
|
||||
!resetFlag &&
|
||||
rest.length === 1 &&
|
||||
(sub === 'status' ||
|
||||
sub === 'skills' ||
|
||||
sub === 'todos' ||
|
||||
sub === 'plan' ||
|
||||
sub === 'compact' ||
|
||||
sub === 'reset')
|
||||
) {
|
||||
if (sub === 'status') statusFlag = true
|
||||
else if (sub === 'reset') resetFlag = true
|
||||
else if (sub === 'compact') compactFlag = true
|
||||
else {
|
||||
await bareAgentRunInspectSubcommand(ctx, argv0, sub)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (statusFlag) {
|
||||
const home = bareAgentResolveHome(ctx)
|
||||
const paths = bareAgentPaths(home)
|
||||
const loaded = await bareAgentLoadOrCreateConfig(ctx, paths)
|
||||
let cfg = loaded && loaded.config ? loaded.config : loaded
|
||||
if (typeof bareAgentSanitizeConfigForBackend === 'function') {
|
||||
cfg = bareAgentSanitizeConfigForBackend(cfg || {})
|
||||
}
|
||||
const started = Number(cfg && cfg.autonomous_started_at_ms) || 0
|
||||
const maxRt = Number(cfg && cfg.autonomous_max_runtime_ms) || 0
|
||||
const elapsed = started > 0 ? Math.max(0, Date.now() - started) : 0
|
||||
const backend = typeof bareAgentResolveBackend === 'function'
|
||||
? bareAgentResolveBackend(cfg || {})
|
||||
: String((cfg && (cfg.backend || cfg.provider)) || 'qvac')
|
||||
/** @type {string[]} */
|
||||
const lines = ['agent status']
|
||||
if (typeof bareAgentFormatConfigSummary === 'function') {
|
||||
String(bareAgentFormatConfigSummary(cfg || {}))
|
||||
.split('\n')
|
||||
.forEach(function (row) {
|
||||
lines.push(' ' + row)
|
||||
})
|
||||
} else {
|
||||
lines.push(' backend: ' + backend)
|
||||
}
|
||||
lines.push(
|
||||
' compaction: ' + String((cfg && cfg.context_compaction) || 'auto'),
|
||||
' autonomous_enabled: ' + String(Boolean(cfg && cfg.autonomous_mode_enabled)),
|
||||
' autonomous_active: ' + String(Boolean(cfg && cfg.autonomous_active)),
|
||||
' status: ' + String((cfg && cfg.autonomous_status) || 'idle'),
|
||||
' goal: ' + String((cfg && cfg.autonomous_goal) || ''),
|
||||
' elapsed_s: ' + String(Math.round(elapsed / 1000)),
|
||||
' remaining_s: ' +
|
||||
String(maxRt > 0 ? Math.max(0, Math.round((maxRt - elapsed) / 1000)) : 0),
|
||||
' last_error: ' + String((cfg && cfg.autonomous_last_error) || ''),
|
||||
' plan_mode: ' + String(Boolean(cfg && cfg.plan_mode_active)),
|
||||
' access_policy: ' + String((cfg && cfg.access_policy) || 'full'),
|
||||
' allow_delete: ' + String(cfg && cfg.allow_delete !== false)
|
||||
)
|
||||
ctx.console.log(lines.join('\n'))
|
||||
await bareAgentPrintStatus(ctx)
|
||||
ctx.exitCode = 0
|
||||
return
|
||||
}
|
||||
|
||||
const wantReset =
|
||||
resetFlag || (rest.length === 1 && rest[0] === 'reset')
|
||||
if (wantReset) {
|
||||
resetFlag || newFlag || (rest.length === 1 && rest[0] === 'reset')
|
||||
if (wantReset && !rest.filter((x) => x !== 'reset').length && !setupFlag && !autoFlag && !planFlag) {
|
||||
const home = bareAgentResolveHome(ctx)
|
||||
const paths = bareAgentPaths(home)
|
||||
try {
|
||||
@@ -134,9 +92,21 @@ async function run(ctx, argv) {
|
||||
return
|
||||
}
|
||||
|
||||
const task = rest.join(' ').trim()
|
||||
if (compactFlag && !rest.filter((x) => x !== 'compact').length && !autoFlag && !planFlag && !setupFlag) {
|
||||
await bareAgentRunCompactOnly(ctx, argv0)
|
||||
return
|
||||
}
|
||||
|
||||
const task = rest
|
||||
.filter(function (x) {
|
||||
return x !== 'reset' && x !== 'compact'
|
||||
})
|
||||
.join(' ')
|
||||
.trim()
|
||||
if (!task && !setupFlag) {
|
||||
ctx.console.error(argv0 + ': missing task (or use --setup / --config / --auto GOAL)')
|
||||
ctx.console.error(
|
||||
argv0 + ': missing task (or use --setup / --config / --auto GOAL / skills / todos / plan)'
|
||||
)
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
@@ -152,9 +122,236 @@ async function run(ctx, argv) {
|
||||
return
|
||||
}
|
||||
|
||||
if (newFlag || resetFlag) {
|
||||
const home = bareAgentResolveHome(ctx)
|
||||
const paths = bareAgentPaths(home)
|
||||
try {
|
||||
await bareAgentResetChatSession(ctx, paths, argv0)
|
||||
} catch (e) {
|
||||
const msg =
|
||||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||||
ctx.console.error(argv0 + ': ' + msg)
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
} else if (compactFlag) {
|
||||
await bareAgentRunCompactOnly(ctx, argv0)
|
||||
if (ctx.exitCode && ctx.exitCode !== 0) return
|
||||
}
|
||||
|
||||
await bareOsRunAgentSession(ctx, argv0, task, {
|
||||
setupFlag,
|
||||
autonomous: autoFlag,
|
||||
autonomousGoal: autoFlag ? task : ''
|
||||
autonomousGoal: autoFlag ? task : '',
|
||||
planMode: planFlag,
|
||||
maxIterations: maxTurns > 0 ? maxTurns : undefined,
|
||||
modelOverride: modelOverride || undefined,
|
||||
extraSystemFile: systemFile || undefined
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} argv0
|
||||
*/
|
||||
function bareAgentCliUsage(argv0) {
|
||||
return (
|
||||
'usage: ' +
|
||||
argv0 +
|
||||
' [OPTION]... YOUR_REQUEST_HERE\n' +
|
||||
' ' +
|
||||
argv0 +
|
||||
' --setup | --config\n' +
|
||||
' ' +
|
||||
argv0 +
|
||||
' --status | status\n' +
|
||||
' ' +
|
||||
argv0 +
|
||||
' --reset | reset | --new\n' +
|
||||
' ' +
|
||||
argv0 +
|
||||
' --auto "GOAL"\n' +
|
||||
' ' +
|
||||
argv0 +
|
||||
' --plan "design the change"\n' +
|
||||
' ' +
|
||||
argv0 +
|
||||
' --compact\n' +
|
||||
' ' +
|
||||
argv0 +
|
||||
' skills | todos | plan\n' +
|
||||
'\n' +
|
||||
'Production coding/OS agent. Default backend is QVAC (local); or any OpenAI-compatible REST API.\n' +
|
||||
'Config: ~/.agent/config.json. Full guest admin (denylist). NEVER ASK — it just works.\n' +
|
||||
'\n' +
|
||||
'Options:\n' +
|
||||
' --setup, --config Walk through QVAC or REST setup (only the chosen backend is stored)\n' +
|
||||
' --auto, --autonomous Keep the tool loop going until task_complete, stop, or timebox\n' +
|
||||
' --plan Start this turn in plan mode (read-only except ~/.agent/plan.md)\n' +
|
||||
' --new, --reset Clear ~/.agent/history.json then run the request (or just reset)\n' +
|
||||
' --continue, -c Resume history (default)\n' +
|
||||
' --compact Compact history now (or compact then run if a task follows)\n' +
|
||||
' --max-turns N Cap ReAct iterations for this run\n' +
|
||||
' --model NAME Override qvac_model / REST model for this run\n' +
|
||||
' --system FILE Append extra instructions from an absolute guest path\n' +
|
||||
' --status Print backend, compaction, plan mode, and autonomous run state\n' +
|
||||
'\n' +
|
||||
'Inspect (no model call):\n' +
|
||||
' skills List discovered SKILL.md ids\n' +
|
||||
' todos Print session todos\n' +
|
||||
' plan Print ~/.agent/plan.md\n' +
|
||||
'\n' +
|
||||
'Examples:\n' +
|
||||
' ' +
|
||||
argv0 +
|
||||
' "summarize ~/README and list five files in /bin"\n' +
|
||||
' ' +
|
||||
argv0 +
|
||||
' --plan "design a /bin/foo utility"\n' +
|
||||
' ' +
|
||||
argv0 +
|
||||
' --auto --max-turns 40 "add /agent tests and run them"\n' +
|
||||
' ' +
|
||||
argv0 +
|
||||
' --new --model QWEN3_1_7B_INST_Q4 "fresh session: inspect /proc"\n' +
|
||||
'\n' +
|
||||
'See man agent.'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
*/
|
||||
async function bareAgentPrintStatus(ctx) {
|
||||
const home = bareAgentResolveHome(ctx)
|
||||
const paths = bareAgentPaths(home)
|
||||
const loaded = await bareAgentLoadOrCreateConfig(ctx, paths)
|
||||
let cfg = loaded && loaded.config ? loaded.config : loaded
|
||||
if (typeof bareAgentSanitizeConfigForBackend === 'function') {
|
||||
cfg = bareAgentSanitizeConfigForBackend(cfg || {})
|
||||
}
|
||||
const started = Number(cfg && cfg.autonomous_started_at_ms) || 0
|
||||
const maxRt = Number(cfg && cfg.autonomous_max_runtime_ms) || 0
|
||||
const elapsed = started > 0 ? Math.max(0, Date.now() - started) : 0
|
||||
const backend =
|
||||
typeof bareAgentResolveBackend === 'function'
|
||||
? bareAgentResolveBackend(cfg || {})
|
||||
: String((cfg && (cfg.backend || cfg.provider)) || 'qvac')
|
||||
/** @type {string[]} */
|
||||
const lines = ['agent status']
|
||||
if (typeof bareAgentFormatConfigSummary === 'function') {
|
||||
String(bareAgentFormatConfigSummary(cfg || {}))
|
||||
.split('\n')
|
||||
.forEach(function (row) {
|
||||
lines.push(' ' + row)
|
||||
})
|
||||
} else {
|
||||
lines.push(' backend: ' + backend)
|
||||
}
|
||||
lines.push(
|
||||
' compaction: ' + String((cfg && cfg.context_compaction) || 'auto'),
|
||||
' autonomous_enabled: ' + String(Boolean(cfg && cfg.autonomous_mode_enabled)),
|
||||
' autonomous_active: ' + String(Boolean(cfg && cfg.autonomous_active)),
|
||||
' status: ' + String((cfg && cfg.autonomous_status) || 'idle'),
|
||||
' goal: ' + String((cfg && cfg.autonomous_goal) || ''),
|
||||
' elapsed_s: ' + String(Math.round(elapsed / 1000)),
|
||||
' remaining_s: ' +
|
||||
String(maxRt > 0 ? Math.max(0, Math.round((maxRt - elapsed) / 1000)) : 0),
|
||||
' last_error: ' + String((cfg && cfg.autonomous_last_error) || ''),
|
||||
' plan_mode: ' + String(Boolean(cfg && cfg.plan_mode_active)),
|
||||
' access_policy: ' + String((cfg && cfg.access_policy) || 'full'),
|
||||
' allow_delete: ' + String(cfg && cfg.allow_delete !== false)
|
||||
)
|
||||
ctx.console.log(lines.join('\n'))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string} argv0
|
||||
* @param {string} sub
|
||||
*/
|
||||
async function bareAgentRunInspectSubcommand(ctx, argv0, sub) {
|
||||
const home = bareAgentResolveHome(ctx)
|
||||
const paths = bareAgentPaths(home)
|
||||
try {
|
||||
if (sub === 'skills') {
|
||||
if (typeof bareAgentEnsureSkillTemplates === 'function') {
|
||||
const loaded = await bareAgentLoadOrCreateConfig(ctx, paths)
|
||||
const cfg = loaded && loaded.config ? loaded.config : loaded
|
||||
await bareAgentEnsureSkillTemplates(ctx, paths, cfg || {})
|
||||
}
|
||||
const block =
|
||||
typeof bareAgentSkillsCompactPrompt === 'function'
|
||||
? await bareAgentSkillsCompactPrompt(ctx, paths, 8000)
|
||||
: ''
|
||||
ctx.console.log(block && String(block).trim() ? String(block).trim() : argv0 + ': no skills')
|
||||
ctx.exitCode = 0
|
||||
return
|
||||
}
|
||||
if (sub === 'todos') {
|
||||
const todos =
|
||||
typeof bareAgentLoadTodos === 'function'
|
||||
? await bareAgentLoadTodos(ctx, paths.todos)
|
||||
: []
|
||||
const sum =
|
||||
typeof bareAgentTodoSummarize === 'function'
|
||||
? bareAgentTodoSummarize(todos)
|
||||
: { text: '', open: 0, total: 0 }
|
||||
ctx.console.log(sum.text || argv0 + ': no todos')
|
||||
ctx.exitCode = 0
|
||||
return
|
||||
}
|
||||
if (sub === 'plan') {
|
||||
const text =
|
||||
typeof bareAgentReadTextFile === 'function'
|
||||
? await bareAgentReadTextFile(ctx, paths.plan)
|
||||
: ''
|
||||
ctx.console.log(text && text.trim() ? text : argv0 + ': no plan (' + paths.plan + ')')
|
||||
ctx.exitCode = 0
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
const msg =
|
||||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||||
ctx.console.error(argv0 + ': ' + msg)
|
||||
ctx.exitCode = 1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string} argv0
|
||||
*/
|
||||
async function bareAgentRunCompactOnly(ctx, argv0) {
|
||||
const home = bareAgentResolveHome(ctx)
|
||||
const paths = bareAgentPaths(home)
|
||||
try {
|
||||
const messages = await bareAgentLoadHistory(ctx, paths.history)
|
||||
if (typeof bareAgentCompactMessagesForCtx !== 'function') {
|
||||
ctx.console.error(argv0 + ': compact unavailable')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
const packed = bareAgentCompactMessagesForCtx(messages, 32768, {
|
||||
mode: 'aggressive',
|
||||
keepRecent: 6,
|
||||
toolMaxChars: 1200
|
||||
})
|
||||
await bareAgentSaveHistory(ctx, paths.history, packed.messages)
|
||||
ctx.console.log(
|
||||
argv0 +
|
||||
': compacted ' +
|
||||
String(packed.meta.beforeTokens) +
|
||||
'→' +
|
||||
String(packed.meta.afterTokens) +
|
||||
' tok (dropped_groups=' +
|
||||
String(packed.meta.droppedGroups) +
|
||||
')'
|
||||
)
|
||||
ctx.exitCode = 0
|
||||
} catch (e) {
|
||||
const msg =
|
||||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||||
ctx.console.error(argv0 + ': ' + msg)
|
||||
ctx.exitCode = 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ const TUI = readFileSync(
|
||||
new URL('../lib/agent-tui.js', import.meta.url),
|
||||
'utf8'
|
||||
)
|
||||
const CLI = readFileSync(new URL('../src/agent.js', import.meta.url), 'utf8')
|
||||
|
||||
test('agent-state exposes QVAC backend config keys', async (t) => {
|
||||
for (const key of [
|
||||
@@ -139,7 +140,12 @@ test('agent-tools exposes agent ops tools', async (t) => {
|
||||
'memory_get',
|
||||
'enter_plan_mode',
|
||||
'exit_plan_mode',
|
||||
'ask_user_question'
|
||||
'ask_user_question',
|
||||
'grep',
|
||||
'apply_patch',
|
||||
'update_goal',
|
||||
'web_search',
|
||||
'git_status'
|
||||
]) {
|
||||
t.ok(
|
||||
TOOLS.includes("name: '" + toolName + "'"),
|
||||
@@ -184,11 +190,27 @@ test('agent-tui embeds operating contract appendix', async (t) => {
|
||||
t.ok(TUI.includes('NEVER ASK'))
|
||||
t.ok(TUI.includes('Lead with tools'))
|
||||
t.ok(TUI.includes('TOOL DISCIPLINE'))
|
||||
t.ok(TUI.includes('WORK POLICY'))
|
||||
t.ok(TUI.includes('apply_patch'))
|
||||
t.ok(TUI.includes('overrides the TTY plain-text rule'))
|
||||
t.ok(!TUI.includes('ASK FIRST'))
|
||||
t.ok(!TUI.includes('Prefer least-privilege'))
|
||||
})
|
||||
|
||||
test('agent CLI exposes production flags and inspect subcommands', async (t) => {
|
||||
t.ok(CLI.includes('--plan'))
|
||||
t.ok(CLI.includes('--max-turns'))
|
||||
t.ok(CLI.includes('--model'))
|
||||
t.ok(CLI.includes('--system'))
|
||||
t.ok(CLI.includes('--new'))
|
||||
t.ok(CLI.includes('--compact'))
|
||||
t.ok(CLI.includes("sub === 'skills'"))
|
||||
t.ok(CLI.includes("sub === 'todos'"))
|
||||
t.ok(TUI.includes('planMode'))
|
||||
t.ok(TUI.includes('modelOverride'))
|
||||
t.ok(TUI.includes('extraSystemFile'))
|
||||
})
|
||||
|
||||
test('agent defaults are full guest admin (denylist, not allowlist)', async (t) => {
|
||||
t.ok(STATE.includes("access_policy: 'full'"))
|
||||
t.ok(STATE.includes('allow_delete: true'))
|
||||
|
||||
@@ -406,6 +406,111 @@ test('dispatch glob_files / todo_write / plan_mode / unique edit', async (t) =>
|
||||
t.ok(String(sliced.content || '').startsWith('1→'))
|
||||
})
|
||||
|
||||
test('grep finds lines with glob and context', async (t) => {
|
||||
const s = loadPort()
|
||||
const { vfs, b4a } = makeVfs({
|
||||
'/home/guest/src/a.js': 'const alpha = 1\nconst beta = 2\nconst gamma = 3\n',
|
||||
'/home/guest/src/b.md': '# alpha docs\n'
|
||||
})
|
||||
await vfs.mkdir('/home/guest/src')
|
||||
const out = await s.bareAgentGrepFiles(
|
||||
{ vfs, b4a },
|
||||
{ pattern: 'alpha', root: '/home/guest', glob: '**/*.js', context: 1 }
|
||||
)
|
||||
t.ok(out.ok)
|
||||
t.is(out.count, 1)
|
||||
t.ok(out.matches[0].path.endsWith('a.js'))
|
||||
t.ok(String(out.matches[0].text).includes('beta'))
|
||||
})
|
||||
|
||||
test('apply_patch add/update/delete', async (t) => {
|
||||
const s = loadPort()
|
||||
const { vfs, files, b4a } = makeVfs({
|
||||
'/home/guest/src/a.js': 'const x = 1\nconst y = 2\n'
|
||||
})
|
||||
await vfs.mkdir('/home/guest/src')
|
||||
const parsed = s.bareAgentParseApplyPatch(
|
||||
[
|
||||
'*** Begin Patch',
|
||||
'*** Add File: /home/guest/src/new.js',
|
||||
'+export const n = 1',
|
||||
'*** Update File: /home/guest/src/a.js',
|
||||
'@@',
|
||||
' const x = 1',
|
||||
'-const y = 2',
|
||||
'+const y = 3',
|
||||
'*** Delete File: /home/guest/src/gone.js',
|
||||
'*** End Patch'
|
||||
].join('\n')
|
||||
)
|
||||
t.ok(parsed.ok)
|
||||
t.is(parsed.ops.length, 3)
|
||||
files.set('/home/guest/src/gone.js', 'bye')
|
||||
const applied = await s.bareAgentApplyPatchOps({ vfs, b4a }, parsed.ops, {
|
||||
home: '/home/guest'
|
||||
})
|
||||
t.ok(applied.ok)
|
||||
t.ok(String(files.get('/home/guest/src/new.js') || '').includes('export const n'))
|
||||
t.ok(String(files.get('/home/guest/src/a.js') || '').includes('const y = 3'))
|
||||
t.absent(files.has('/home/guest/src/gone.js'))
|
||||
})
|
||||
|
||||
test('update_goal / grep dispatch', async (t) => {
|
||||
const s = loadDispatch()
|
||||
const { vfs, b4a } = makeVfs({
|
||||
'/home/guest/src/a.js': 'hello world\n'
|
||||
})
|
||||
await vfs.mkdir('/home/guest/src')
|
||||
await vfs.mkdir('/home/guest/.agent')
|
||||
const ctx = { vfs, b4a }
|
||||
const paths = {
|
||||
dir: '/home/guest/.agent',
|
||||
config: '/home/guest/.agent/config.json',
|
||||
todos: '/home/guest/.agent/todos.json',
|
||||
plan: '/home/guest/.agent/plan.md'
|
||||
}
|
||||
const configRef = { current: { autonomous_active: true, autonomous_status: 'running' } }
|
||||
const grepped = await dispatch(s, {
|
||||
ctx,
|
||||
paths,
|
||||
toolName: 'grep',
|
||||
args: { pattern: 'hello', path: '/home/guest' },
|
||||
configRef,
|
||||
home: '/home/guest'
|
||||
})
|
||||
t.ok(grepped.ok)
|
||||
t.ok(grepped.count >= 1)
|
||||
const goal = await dispatch(s, {
|
||||
ctx,
|
||||
paths,
|
||||
toolName: 'update_goal',
|
||||
args: { completed: true, message: 'shipped' },
|
||||
configRef,
|
||||
home: '/home/guest'
|
||||
})
|
||||
t.ok(goal.ok)
|
||||
t.is(configRef.current.autonomous_status, 'completed')
|
||||
t.absent(configRef.current.autonomous_active)
|
||||
})
|
||||
|
||||
test('search result parser extracts DDG-style topics', async (t) => {
|
||||
const s = loadPort()
|
||||
const rows = s.bareAgentParseSearchResults(
|
||||
{
|
||||
Heading: 'Bare',
|
||||
AbstractURL: 'https://example.com/bare',
|
||||
Abstract: 'A JS runtime',
|
||||
RelatedTopics: [
|
||||
{ Text: 'Pear', FirstURL: 'https://example.com/pear' },
|
||||
{ Topics: [{ Text: 'Holepunch', FirstURL: 'https://example.com/hp' }] }
|
||||
]
|
||||
},
|
||||
8
|
||||
)
|
||||
t.ok(rows.length >= 2)
|
||||
t.ok(rows.some((r) => r.url === 'https://example.com/bare'))
|
||||
})
|
||||
|
||||
test('dispatch delete / proc / run_command are open by default', async (t) => {
|
||||
const s = loadDispatch()
|
||||
const { vfs, files, b4a } = makeVfs({
|
||||
|
||||
Reference in New Issue
Block a user