Updates
Release rolling / release (push) Successful in 16m49s

This commit is contained in:
2026-08-18 13:25:51 -04:00
parent e7b00df454
commit 154c13d902
28 changed files with 7052 additions and 708 deletions
+1
View File
@@ -135,6 +135,7 @@ const preamble = {
'agent-sse-parse.js',
'agent-openai.js',
'agent-web-fetch.js',
'agent-grok-port.js',
'agent-tools.js',
'agent-markdown.js',
'agent-think-panel.js',
+330 -100
View File
@@ -171,12 +171,83 @@ function bareAgentIsCompactionMessage(msg) {
const m = /** @type {Record<string, unknown>} */ (msg)
if (m.bare_os_compaction === true) return true
const c = typeof m.content === 'string' ? m.content : ''
const t = c.trim()
return (
String(m.role || '') === 'user' &&
/^\[context compaction\]/i.test(c.trim())
(/^\[context compaction\]/i.test(t) ||
/^This session is being continued from a previous conversation/i.test(t) ||
m.bare_os_reminder === true)
)
}
/** Grok-style continuation carrier minimum (extractive; slightly below LLM 500). */
var BARE_AGENT_MIN_SUMMARY_SEED_CHARS = 160
/**
* Strip drafting tags and neutralize leftover <summary>/<analysis> tokens
* so they cannot prime the next turn (Grok format_compact_summary).
* @param {string} raw
*/
function bareAgentFormatCompactSummary(raw) {
let result = String(raw || '')
result = result.replace(/<analysis>[\s\S]*?<\/analysis>/gi, '')
result = result.replace(/<\/?summary>/gi, '')
result = result
.replace(/<\/summary>/g, '<\u200b/summary>')
.replace(/<summary>/g, '<\u200bsummary>')
.replace(/<\/analysis>/g, '<\u200b/analysis>')
.replace(/<analysis>/g, '<\u200banalysis>')
while (result.indexOf('\n\n\n') >= 0) result = result.replace(/\n\n\n/g, '\n\n')
return result.trim()
}
/**
* @param {string} raw
*/
function bareAgentFormatCompactSummaryContent(raw) {
const cleaned = bareAgentFormatCompactSummary(raw)
return (
'This session is being continued from a previous conversation that ran out of context. ' +
'The summary below covers the earlier portion of the conversation.\n\n' +
cleaned
)
}
/**
* @param {string} text
*/
function bareAgentWrapUserQuery(text) {
const t = String(text || '').trim()
if (!t) return ''
if (/^<user_query>/.test(t)) return t
return '<user_query>\n' + t + '\n</user_query>'
}
/**
* @param {string} raw
*/
function bareAgentIsDegenerateSummary(raw) {
return bareAgentFormatCompactSummary(raw).length < BARE_AGENT_MIN_SUMMARY_SEED_CHARS
}
/**
* Collect unique path-like and command tokens for the artifacts section.
* @param {string} text
* @param {number} max
*/
function bareAgentExtractPathTokens(text, max) {
const s = String(text || '')
const found = []
const re = /(?:~|\/)[A-Za-z0-9._+\-@/]+/g
let m
while ((m = re.exec(s)) && found.length < max) {
const p = m[0]
if (p.length < 3 || found.indexOf(p) >= 0) continue
found.push(p)
}
return found
}
/**
* One-line digest for a message (extractive).
* @param {unknown} msg
@@ -208,45 +279,228 @@ function bareAgentDigestMessage(msg, max) {
return '- ' + role + ' ' + bareAgentClipText(text, max)
}
/**
* Grok-style 7-section extractive summary of older turn groups.
* @param {unknown[][]} groups
* @param {{ maxChars?: number, perMsg?: number }} [opts]
*/
function bareAgentBuildStructuredSummary(groups, opts) {
const maxChars = Math.max(400, Math.floor((opts && opts.maxChars) || 3500))
const perMsg = Math.max(60, Math.floor((opts && opts.perMsg) || 180))
/** @type {string[]} */
const userMsgs = []
/** @type {string[]} */
const tools = []
/** @type {string[]} */
const errors = []
/** @type {string[]} */
const solves = []
/** @type {string[]} */
const concepts = []
/** @type {string[]} */
const files = []
function pushUnique(arr, s, cap) {
const t = String(s || '').trim()
if (!t || arr.indexOf(t) >= 0) return
if (arr.length >= cap) return
arr.push(t)
}
for (const g of groups) {
for (const msg of g) {
if (!msg || typeof msg !== 'object') continue
const m = /** @type {Record<string, unknown>} */ (msg)
const role = String(m.role || '')
const text = bareAgentMessagePlainText(m)
for (const p of bareAgentExtractPathTokens(text, 8)) pushUnique(files, p, 24)
if (role === 'user' && !bareAgentIsCompactionMessage(m)) {
pushUnique(userMsgs, bareAgentClipText(text, perMsg + 80), 16)
} else if (role === 'tool') {
const name = typeof m.name === 'string' ? m.name : 'tool'
pushUnique(tools, name + ': ' + bareAgentClipText(text, perMsg), 20)
if (/error|fail|denied|not found|ENOENT|EXIT:[1-9]/i.test(text)) {
pushUnique(errors, name + ' ' + bareAgentClipText(text, 140), 12)
}
} else if (role === 'assistant') {
if (m.tool_calls && Array.isArray(m.tool_calls)) {
for (const tc of m.tool_calls) {
if (!tc || typeof tc !== 'object') continue
const fn = /** @type {Record<string, unknown>} */ (tc).function
if (fn && typeof fn === 'object') {
const n = /** @type {Record<string, unknown>} */ (fn).name
if (typeof n === 'string' && n) pushUnique(concepts, n, 16)
}
}
}
if (text) pushUnique(solves, bareAgentClipText(text, perMsg), 12)
}
}
}
const intent = userMsgs.length ? userMsgs[0] : '(no explicit user request in compacted prefix)'
/** @type {string[]} */
const lines = [
'1. Primary Request and Intent:',
' ' + intent,
'',
'2. Key Technical Concepts:',
concepts.length ? concepts.map((c) => ' - ' + c).join('\n') : ' - (none extracted)',
'',
'3. Tool Usage & Verification:',
tools.length ? tools.map((c) => ' - ' + c).join('\n') : ' - (none)',
'',
'4. Files & Code Artifacts:',
files.length ? files.map((c) => ' - ' + c).join('\n') : ' - (none)',
'',
'5. Errors and Fixes:',
errors.length ? errors.map((c) => ' - ' + c).join('\n') : ' - (none recorded)',
'',
'6. Problem Solving:',
solves.length ? solves.map((c) => ' - ' + c).join('\n') : ' - (in progress)',
'',
'7. User Messages:',
userMsgs.length ? userMsgs.map((c) => ' - ' + c).join('\n') : ' - (none)'
]
let body = lines.join('\n')
if (body.length > maxChars) body = body.slice(0, maxChars - 20) + '\n… truncated'
return body
}
/**
* Build a rolling summary from older turn groups.
* @param {unknown[][]} groups
* @param {{ maxChars?: number, perMsg?: number }} [opts]
*/
function bareAgentBuildCompactionSummary(groups, opts) {
const maxChars = Math.max(400, Math.floor((opts && opts.maxChars) || 3500))
const perMsg = Math.max(60, Math.floor((opts && opts.perMsg) || 180))
const structured = bareAgentBuildStructuredSummary(groups, opts)
return bareAgentFormatCompactSummaryContent(structured)
}
/**
* Post-compaction <system-reminder> (Grok reminder.rs analogue).
* @param {{
* autonomous?: { active?: boolean, goal?: string, status?: string, remainingMs?: number },
* droppedGroups?: number,
* afterTokens?: number,
* budget?: number
* }} [opts]
*/
function bareAgentBuildSystemReminder(opts) {
const o = opts && typeof opts === 'object' ? opts : {}
/** @type {string[]} */
const lines = [
'[context compaction] Older turns were summarized to fit the model context window. Preserve goals, paths, errors, and decisions below.',
''
]
let used = lines.join('\n').length
let kept = 0
for (const g of groups) {
/** @type {string[]} */
const block = []
for (const msg of g) {
const d = bareAgentDigestMessage(msg, perMsg)
if (d) block.push(d)
}
if (!block.length) continue
const chunk = block.join('\n') + '\n'
if (used + chunk.length > maxChars && kept > 0) {
lines.push('… (' + String(groups.length - kept) + ' older turns omitted)')
break
}
lines.push(chunk.trimEnd())
used += chunk.length
kept++
const parts = []
const auto = o.autonomous
if (auto && auto.active) {
parts.push(
'Autonomous run: ' +
String(auto.goal || '(goal)').slice(0, 240) +
' (status=' +
String(auto.status || 'running') +
(typeof auto.remainingMs === 'number'
? ', remaining=' + String(Math.max(0, Math.round(auto.remainingMs / 1000))) + 's'
: '') +
'). Keep using tools until task_complete.'
)
}
lines.push('')
lines.push(
'Continue from the recent messages after this summary. Prefer tools over guessing.'
)
let body = lines.join('\n')
if (body.length > maxChars) body = body.slice(0, maxChars - 20) + '\n… truncated'
return body
if (typeof o.droppedGroups === 'number' && o.droppedGroups > 0) {
parts.push(
'Context compacted: ' +
String(o.droppedGroups) +
' older turns summarized. Recent messages after the last user query are verbatim.'
)
}
if (typeof o.afterTokens === 'number' && typeof o.budget === 'number' && o.budget > 0) {
parts.push(
'Context usage after compact: ' +
String(o.afterTokens) +
'/' +
String(o.budget) +
' tokens (est.).'
)
}
if (!parts.length) return ''
return '<system-reminder>\n' + parts.join('\n') + '\n</system-reminder>'
}
/**
* Grok assemble: [system, last user query, recent after that turn, summary, reminder].
* @param {{
* system?: unknown | null,
* lastUserQuery?: string,
* recent?: unknown[],
* summaryText?: string,
* reminder?: string
* }} parts
* @returns {unknown[]}
*/
function bareAgentAssembleCompactedHistory(parts) {
const p = parts && typeof parts === 'object' ? parts : {}
/** @type {unknown[]} */
const out = []
if (p.system) out.push(p.system)
const q = String(p.lastUserQuery || '').trim()
if (q) {
out.push({
role: 'user',
content: bareAgentWrapUserQuery(q),
bare_os_user_query: true
})
}
if (Array.isArray(p.recent)) {
for (const m of p.recent) out.push(m)
}
const summary = String(p.summaryText || '').trim()
if (summary) {
out.push({
role: 'user',
content: summary,
bare_os_compaction: true
})
}
const rem = String(p.reminder || '').trim()
if (rem) {
out.push({
role: 'user',
content: rem,
bare_os_compaction: true,
bare_os_reminder: true
})
}
return out
}
/**
* Last non-compaction user text + messages after that turn.
* @param {unknown[][]} groups
*/
function bareAgentSplitLastUserAndRecent(groups) {
let lastUserIdx = -1
let lastUserText = ''
for (let i = groups.length - 1; i >= 0; i--) {
const g = groups[i]
if (!g || !g.length) continue
const first = g[0]
if (!first || typeof first !== 'object') continue
const m = /** @type {Record<string, unknown>} */ (first)
if (String(m.role || '') !== 'user') continue
if (bareAgentIsCompactionMessage(m)) continue
lastUserIdx = i
lastUserText = bareAgentMessagePlainText(m)
break
}
if (lastUserIdx < 0) {
return { lastUserText: '', older: groups, recent: /** @type {unknown[]} */ ([]) }
}
const older = groups.slice(0, lastUserIdx)
/** @type {unknown[]} */
const recent = []
const lastGroup = groups[lastUserIdx] || []
for (let i = 1; i < lastGroup.length; i++) recent.push(lastGroup[i])
for (let i = lastUserIdx + 1; i < groups.length; i++) {
for (const m of groups[i]) recent.push(m)
}
return { lastUserText, older, recent }
}
/**
@@ -259,7 +513,8 @@ function bareAgentBuildCompactionSummary(groups, opts) {
* toolMaxChars?: number,
* summaryMaxChars?: number,
* softRatio?: number,
* mode?: 'auto' | 'off' | 'aggressive'
* mode?: 'auto' | 'off' | 'aggressive',
* autonomous?: { active?: boolean, goal?: string, status?: string, remainingMs?: number }
* }} [opts]
* @returns {{
* messages: unknown[],
@@ -293,7 +548,7 @@ function bareAgentCompactMessagesForCtx(msgs, ctxSize, opts) {
const softRatio =
mode === 'aggressive'
? 0.7
: Math.min(0.95, Math.max(0.55, Number(opts && opts.softRatio) || 0.82))
: Math.min(0.95, Math.max(0.55, Number(opts && opts.softRatio) || 0.85))
const softBudget = Math.floor(budget * softRatio)
const keepRecentDefault = mode === 'aggressive' ? 4 : 8
@@ -358,41 +613,55 @@ function bareAgentCompactMessagesForCtx(msgs, ctxSize, opts) {
}
}
// Merge prior compaction blobs in the middle into the new summary.
const { system, groups } = bareAgentGroupMessageTurns(out)
let droppedGroups = 0
if (groups.length > keepRecent) {
const older = groups.slice(0, Math.max(0, groups.length - keepRecent))
const recent = groups.slice(Math.max(0, groups.length - keepRecent))
// Drop nested prior compaction-only groups from "older" but fold text in.
function applyFullReplace(allGroups, keep) {
const split = bareAgentSplitLastUserAndRecent(allGroups)
/** @type {unknown[][]} */
const olderFlat = []
for (const g of older) {
if (g.length === 1 && bareAgentIsCompactionMessage(g[0])) {
olderFlat.push(g)
} else olderFlat.push(g)
let older = split.older
/** @type {unknown[]} */
let recent = split.recent
// If "recent after last user" is huge, keep only the tail of those messages
// by regrouping them and folding extras into older.
if (keep > 0 && recent.length > keep * 4) {
const tail = recent.slice(-(keep * 3))
const head = recent.slice(0, Math.max(0, recent.length - tail.length))
if (head.length) older = older.concat([head])
recent = tail
}
const summaryText = bareAgentBuildCompactionSummary(olderFlat, {
const summaryText = bareAgentBuildCompactionSummary(older, {
maxChars: summaryMax,
perMsg: mode === 'aggressive' ? 120 : 180
})
const summaryMsg = {
role: 'user',
content: summaryText,
bare_os_compaction: true
if (older.length && bareAgentIsDegenerateSummary(summaryText)) {
tiers.push('degenerate-keep')
return null
}
const reminder = bareAgentBuildSystemReminder({
autonomous: opts && opts.autonomous,
droppedGroups: older.length,
afterTokens: 0,
budget
})
droppedGroups += older.length
return bareAgentAssembleCompactedHistory({
system: system,
lastUserQuery: split.lastUserText,
recent: recent,
summaryText: summaryText,
reminder: reminder
})
}
if (groups.length > keepRecent) {
const next = applyFullReplace(groups, keepRecent)
if (next) {
out = next
tiers.push('full-replace')
}
/** @type {unknown[]} */
const next = []
if (system) next.push(system)
next.push(summaryMsg)
for (const g of recent) for (const m of g) next.push(m)
out = next
droppedGroups = older.length
tiers.push('rolling-summary')
}
// Still over: reduce keepRecent iteratively.
let guard = 0
while (
bareAgentCompactEstimateTokens(out) > budget &&
@@ -402,49 +671,10 @@ function bareAgentCompactMessagesForCtx(msgs, ctxSize, opts) {
keepRecent--
guard++
const again = bareAgentGroupMessageTurns(out)
// Re-split: system + optional summary + recent
/** @type {unknown[][]} */
const gs = again.groups
// Find existing summary at start of groups
let start = 0
if (gs[0] && gs[0].length === 1 && bareAgentIsCompactionMessage(gs[0][0])) {
start = 1
}
const body = gs.slice(start)
if (body.length <= keepRecent) break
const older = body.slice(0, body.length - keepRecent)
const recent = body.slice(body.length - keepRecent)
const priorSummary =
start === 1 && gs[0] && gs[0][0]
? bareAgentMessagePlainText(gs[0][0])
: ''
const folded = priorSummary
? [
[
{
role: 'user',
content: priorSummary,
bare_os_compaction: true
}
],
...older
]
: older
const summaryText = bareAgentBuildCompactionSummary(folded, {
maxChars: summaryMax,
perMsg: 120
})
/** @type {unknown[]} */
const next = []
if (again.system) next.push(again.system)
next.push({
role: 'user',
content: summaryText,
bare_os_compaction: true
})
for (const g of recent) for (const m of g) next.push(m)
if (again.groups.length <= keepRecent) break
const next = applyFullReplace(again.groups, keepRecent)
if (!next) break
out = next
droppedGroups += older.length
tiers.push('tighten-keep=' + String(keepRecent))
}
@@ -0,0 +1,759 @@
/**
* Guest-safe Grok Build harness ports: todos, plan mode, memory, glob,
* AGENTS.md walk-up, JSON hooks, and search_replace uniqueness.
*/
var BARE_AGENT_PLAN_READONLY_TOOLS = Object.freeze({
read_file: 1,
list_directory: 1,
list_dir: 1,
file_stat: 1,
search_files: 1,
glob_files: 1,
glob: 1,
get_system_info: 1,
list_bin: 1,
read_man_page: 1,
apropos_man: 1,
read_proc_file: 1,
get_swarm_peers: 1,
get_resource_limits: 1,
web_fetch: 1,
read_skill: 1,
list_services: 1,
service_status: 1,
list_timers: 1,
read_cron_log: 1,
read_audit_log: 1,
read_boot_policy: 1,
read_kernel_extension_resolution: 1,
get_initd_graph: 1,
read_unit_journal: 1,
inspect_ipc_backpressure: 1,
get_network_summary: 1,
tail_telemetry_streams: 1,
pkg_index_lookup: 1,
list_verification_scripts: 1,
verification_hints: 1,
runtime_diagnostic_bundle: 1,
memory_search: 1,
memory_get: 1,
todo_write: 1,
enter_plan_mode: 1,
exit_plan_mode: 1,
ask_user_question: 1,
update_goal: 1,
autonomous_run_status: 1,
get_hrpc_bridge_health: 1,
get_hrpc_allowlist_status: 1
})
var BARE_AGENT_PLAN_WRITE_TOOLS = Object.freeze({
write_file: 1,
edit_file: 1,
search_replace: 1
})
/**
* @param {string} pattern
* @returns {RegExp | null}
*/
function bareAgentGlobToRegExp(pattern) {
const src = String(pattern || '').trim()
if (!src) return null
let out = '^'
for (let i = 0; i < src.length; i++) {
const c = src.charAt(i)
if (c === '*' && src.charAt(i + 1) === '*') {
const next = src.charAt(i + 2)
if (next === '/' || next === '') {
out += '.*'
i += next === '/' ? 2 : 1
continue
}
}
if (c === '*') {
out += '[^/]*'
continue
}
if (c === '?') {
out += '[^/]'
continue
}
if ('\\^$+()[]{}|.'.indexOf(c) !== -1) out += '\\' + c
else out += c
}
out += '$'
try {
return new RegExp(out)
} catch {
return null
}
}
/**
* @param {string} rel
* @param {string} pattern
*/
function bareAgentGlobMatch(rel, pattern) {
const re = bareAgentGlobToRegExp(pattern)
if (!re) return false
const n = String(rel || '').replace(/^\/+/, '')
if (re.test(n)) return true
const base = n.split('/').pop() || n
return re.test(base)
}
/**
* @param {unknown} updates
* @param {{ merge?: boolean }} [opts]
* @param {{ id: string, content: string, status: string }[]} [prev]
*/
function bareAgentTodoApply(updates, opts, prev) {
const merge = Boolean(opts && opts.merge)
/** @type {{ id: string, content: string, status: string }[]} */
const list = merge && Array.isArray(prev) ? prev.slice() : []
const byId = Object.create(null)
for (let i = 0; i < list.length; i++) byId[list[i].id] = i
const rows = Array.isArray(updates) ? updates : []
const seen = Object.create(null)
for (let i = 0; i < rows.length; i++) {
const row = rows[i] && typeof rows[i] === 'object' ? rows[i] : {}
const id = String(row.id || '').trim()
if (!id) throw new Error('todo_id_required')
if (seen[id]) throw new Error('duplicate_todo_id:' + id)
seen[id] = 1
const statusRaw = String(row.status || 'pending').trim().toLowerCase()
const status =
statusRaw === 'in_progress' || statusRaw === 'completed' || statusRaw === 'cancelled'
? statusRaw
: 'pending'
const content = String(row.content == null ? '' : row.content).trim()
if (byId[id] != null) {
const cur = list[byId[id]]
if (content) cur.content = content
cur.status = status
} else {
list.push({ id, content: content || id, status })
byId[id] = list.length - 1
}
}
return list
}
/**
* @param {{ id: string, content: string, status: string }[]} todos
*/
function bareAgentTodoSummarize(todos) {
const rows = Array.isArray(todos) ? todos : []
let pending = 0
let inProgress = 0
let completed = 0
const lines = []
for (let i = 0; i < rows.length; i++) {
const t = rows[i]
const st = String(t.status || 'pending')
if (st === 'completed' || st === 'cancelled') completed++
else if (st === 'in_progress') inProgress++
else pending++
lines.push('- [' + st + '] ' + t.id + ': ' + String(t.content || '').slice(0, 160))
}
return {
total: rows.length,
pending,
in_progress: inProgress,
completed,
open: pending + inProgress,
text: lines.join('\n')
}
}
/**
* @param {{ open?: number, turnsSinceTodoWrite?: number, nudgeEnabled?: boolean }} opts
*/
function bareAgentTodoNudgeText(opts) {
const o = opts && typeof opts === 'object' ? opts : {}
if (o.nudgeEnabled === false) return ''
const turns = Number(o.turnsSinceTodoWrite) || 0
const open = Number(o.open) || 0
if (open > 0 && turns >= 3) {
return (
'Open todos remain (' +
String(open) +
'). Use todo_write to mark progress or complete items before stopping.'
)
}
if (open === 0 && turns >= 5) {
return 'Multi-step work: call todo_write to track remaining steps (merge=true to update one id).'
}
return ''
}
/**
* @param {string} toolName
* @param {Record<string, unknown>} args
* @param {{ plan?: string }} paths
*/
function bareAgentPlanModeToolAllowed(toolName, args, paths) {
const name = String(toolName || '')
if (BARE_AGENT_PLAN_READONLY_TOOLS[name]) return true
if (!BARE_AGENT_PLAN_WRITE_TOOLS[name]) return false
const plan = String((paths && paths.plan) || '')
const p = String(args.path || args.file_path || '')
return Boolean(plan && p && p === plan)
}
/**
* @param {string} hay
* @param {string} needle
*/
function bareAgentCountOccurrences(hay, needle) {
if (!needle) return 0
let n = 0
let from = 0
while (from <= hay.length) {
const i = hay.indexOf(needle, from)
if (i === -1) break
n++
from = i + Math.max(needle.length, 1)
}
return n
}
/**
* @param {string} text
* @param {string} oldStr
* @param {string} newStr
* @param {boolean} replaceAll
*/
function bareAgentSearchReplaceApply(text, oldStr, newStr, replaceAll) {
const prev = String(text == null ? '' : text)
if (!oldStr) {
if (prev.trim()) {
return { ok: false, error: 'empty_old_string_cannot_overwrite' }
}
return { ok: true, next: String(newStr == null ? '' : newStr), replacements: 1 }
}
const count = bareAgentCountOccurrences(prev, oldStr)
if (count === 0) return { ok: false, error: 'old_string not found', count: 0 }
if (count > 1 && !replaceAll) {
return {
ok: false,
error: 'old_string not unique',
count,
hint: 'add surrounding lines to make the match unique, or set replace_all true'
}
}
const next = replaceAll ? prev.split(oldStr).join(newStr) : prev.replace(oldStr, newStr)
return { ok: true, next, replacements: replaceAll ? count : 1 }
}
/**
* @param {string} text
* @param {{ offset?: number, limit?: number, numbered?: boolean }} [opts]
*/
function bareAgentSliceFileLines(text, opts) {
const o = opts && typeof opts === 'object' ? opts : {}
const raw = String(text == null ? '' : text)
const lines = raw.split('\n')
if (lines.length && lines[lines.length - 1] === '') lines.pop()
const offset =
typeof o.offset === 'number' && o.offset >= 1 ? Math.floor(o.offset) : 1
const limit =
typeof o.limit === 'number' && o.limit >= 1
? Math.floor(o.limit)
: lines.length
const start = Math.min(lines.length, offset - 1)
const end = Math.min(lines.length, start + limit)
const slice = lines.slice(start, end)
const numbered = o.numbered !== false
const body = numbered
? slice
.map(function (line, i) {
return String(start + i + 1) + '→' + line
})
.join('\n')
: slice.join('\n')
return {
content: body,
start_line: start + 1,
end_line: end,
total_lines: lines.length,
truncated: start > 0 || end < lines.length
}
}
/**
* @param {string} query
*/
function bareAgentMemoryTokens(query) {
return String(query || '')
.toLowerCase()
.split(/[^a-z0-9_./-]+/)
.filter(function (t) {
return t.length > 1
})
}
/**
* @param {string} text
* @param {string[]} tokens
*/
function bareAgentMemoryScore(text, tokens) {
const low = String(text || '').toLowerCase()
if (!tokens.length || !low) return 0
let hits = 0
for (let i = 0; i < tokens.length; i++) {
if (low.indexOf(tokens[i]) !== -1) hits++
}
return hits / tokens.length
}
/**
* @param {Record<string, unknown>} ctx
* @param {string} startDir
* @param {number} [maxHops]
* @returns {Promise<string[]>}
*/
async function bareAgentDiscoverAgentsMdPaths(ctx, startDir, maxHops) {
const vfs = ctx && ctx.vfs
if (!vfs || typeof vfs.readFile !== 'function') return []
/** @type {string[]} */
const found = []
const seen = Object.create(null)
let dir = String(startDir || '').replace(/\/+$/, '') || '/'
const hops = Math.min(Math.max(Number(maxHops) || 12, 1), 24)
const names = ['AGENTS.md', 'Claude.md', 'CLAUDE.md']
for (let n = 0; n < hops; n++) {
for (let i = 0; i < names.length; i++) {
const p = (dir === '/' ? '' : dir) + '/' + names[i]
if (seen[p]) continue
seen[p] = 1
try {
const buf = await vfs.readFile(p)
if (buf && buf.length) found.push(p)
} catch {
/* missing */
}
}
const rulesDirs = [dir + '/.grok/rules', dir + '/.agent/rules']
for (let r = 0; r < rulesDirs.length; r++) {
if (typeof vfs.readdir !== 'function') continue
let entries = []
try {
entries = await vfs.readdir(rulesDirs[r])
} catch {
continue
}
if (!Array.isArray(entries)) continue
entries = entries.slice().sort()
for (let e = 0; e < entries.length; e++) {
const name = String(entries[e] || '')
if (!/\.md$/i.test(name)) continue
const p = rulesDirs[r] + '/' + name
if (seen[p]) continue
seen[p] = 1
try {
const buf = await vfs.readFile(p)
if (buf && buf.length) found.push(p)
} catch {
/* skip */
}
}
}
if (dir === '/' || dir === '') break
const slash = dir.lastIndexOf('/')
dir = slash <= 0 ? '/' : dir.slice(0, slash)
}
return found
}
/**
* @param {Record<string, unknown>} ctx
* @param {string[]} files
* @param {number} [maxChars]
*/
async function bareAgentLoadAgentsMdPrompt(ctx, files, maxChars) {
const vfs = ctx && ctx.vfs
if (!vfs || typeof vfs.readFile !== 'function') return ''
const cap = Math.min(Math.max(Number(maxChars) || 6000, 500), 16000)
const parts = []
let used = 0
for (let i = 0; i < files.length; i++) {
if (used >= cap) break
try {
const buf = await vfs.readFile(files[i])
if (!buf || !buf.length) continue
const t =
typeof ctx.b4a !== 'undefined' &&
ctx.b4a &&
typeof ctx.b4a.toString === 'function'
? ctx.b4a.toString(buf)
: String(new TextDecoder().decode(buf))
const body = String(t || '').trim()
if (!body) continue
const block = '### ' + files[i] + '\n' + body
const room = cap - used
parts.push(block.slice(0, room))
used += Math.min(block.length, room)
} catch {
/* skip */
}
}
if (!parts.length) return ''
return (
'## Discovered project agent files (cwd walk-up, Grok-style)\n' +
parts.join('\n\n')
)
}
/**
* @param {Record<string, unknown>} hook
* @param {string} toolName
* @param {Record<string, unknown>} args
*/
function bareAgentHookDenies(hook, toolName, args) {
if (!hook || typeof hook !== 'object') return ''
const event = String(hook.event || hook.type || 'PreToolUse')
if (event !== 'PreToolUse' && event !== 'pre') return ''
const tools = Array.isArray(hook.tools) ? hook.tools.map(String) : []
if (tools.length && tools.indexOf(toolName) === -1) return ''
const reSrc = String(hook.deny_regex || hook.denyRegex || '').trim()
if (!reSrc) return ''
let re
try {
re = new RegExp(reSrc, 'i')
} catch {
return ''
}
const blob =
toolName +
' ' +
String(args.command || args.path || args.file_path || args.pattern || '')
if (!re.test(blob)) return ''
return String(hook.reason || hook.message || 'blocked by ~/.agent/hooks').slice(0, 240)
}
/**
* @param {Record<string, unknown>} ctx
* @param {string} hooksDir
* @param {string} toolName
* @param {Record<string, unknown>} args
*/
async function bareAgentRunPreToolHooks(ctx, hooksDir, toolName, args) {
const vfs = ctx && ctx.vfs
if (!vfs || typeof vfs.readdir !== 'function' || typeof vfs.readFile !== 'function') {
return ''
}
let names = []
try {
names = await vfs.readdir(hooksDir)
} catch {
return ''
}
if (!Array.isArray(names)) return ''
names = names.filter(function (n) {
return /\.json$/i.test(String(n || ''))
})
names.sort()
for (let i = 0; i < names.length; i++) {
try {
const buf = await vfs.readFile(hooksDir + '/' + names[i])
if (!buf || !buf.length) continue
const t =
typeof ctx.b4a !== 'undefined' &&
ctx.b4a &&
typeof ctx.b4a.toString === 'function'
? ctx.b4a.toString(buf)
: String(new TextDecoder().decode(buf))
const hook = JSON.parse(t)
const reason = bareAgentHookDenies(hook, toolName, args)
if (reason) return reason
} catch {
/* ignore bad hook files */
}
}
return ''
}
/**
* @param {Record<string, unknown>} ctx
* @param {unknown} buf
*/
function bareAgentPortDecode(ctx, buf) {
if (!buf || !buf.length) return ''
if (
typeof ctx.b4a !== 'undefined' &&
ctx.b4a &&
typeof ctx.b4a.toString === 'function'
) {
return ctx.b4a.toString(buf)
}
return String(new TextDecoder().decode(buf))
}
/**
* @param {Record<string, unknown>} ctx
* @param {string} text
*/
function bareAgentPortEncode(ctx, text) {
if (
typeof ctx.b4a !== 'undefined' &&
ctx.b4a &&
typeof ctx.b4a.from === 'function'
) {
return ctx.b4a.from(String(text == null ? '' : text))
}
return new TextEncoder().encode(String(text == null ? '' : text))
}
/**
* @param {Record<string, unknown>} ctx
* @param {string} path
*/
async function bareAgentReadTextFile(ctx, path) {
const vfs = ctx && ctx.vfs
if (!vfs || typeof vfs.readFile !== 'function') return ''
try {
const buf = await vfs.readFile(path)
return bareAgentPortDecode(ctx, buf)
} catch {
return ''
}
}
/**
* @param {Record<string, unknown>} ctx
* @param {string} path
* @param {string} text
*/
async function bareAgentWriteTextFile(ctx, path, text) {
const vfs = ctx && ctx.vfs
if (!vfs || typeof vfs.writeFile !== 'function') {
throw new Error('vfs.writeFile unavailable')
}
if (typeof vfs.mkdir === 'function') {
const dir = String(path || '').replace(/\/[^/]+$/, '')
if (dir && dir !== path) {
try {
await vfs.mkdir(dir, { recursive: true })
} catch {
/* parent may already exist */
}
}
}
await vfs.writeFile(path, bareAgentPortEncode(ctx, text))
}
/**
* @param {Record<string, unknown>} ctx
* @param {string} path
* @param {unknown} fallback
*/
async function bareAgentReadJsonFile(ctx, path, fallback) {
const t = await bareAgentReadTextFile(ctx, path)
if (!t.trim()) return fallback
try {
return JSON.parse(t)
} catch {
return fallback
}
}
/**
* @param {Record<string, unknown>} ctx
* @param {string} path
* @param {unknown} value
*/
async function bareAgentWriteJsonFile(ctx, path, value) {
await bareAgentWriteTextFile(ctx, path, JSON.stringify(value, null, 2) + '\n')
}
/**
* @param {Record<string, unknown>} ctx
* @param {string} path
*/
async function bareAgentVfsIsDir(ctx, path) {
const vfs = ctx && ctx.vfs
if (!vfs) return false
if (typeof vfs.lstat === 'function' || typeof vfs.stat === 'function') {
try {
const st =
typeof vfs.lstat === 'function' ? await vfs.lstat(path) : await vfs.stat(path)
if (st && typeof st.isDirectory === 'function') return Boolean(st.isDirectory())
if (st && typeof st === 'object' && 'isDirectory' in st) {
return Boolean(/** @type {{ isDirectory?: unknown }} */ (st).isDirectory)
}
} catch {
return false
}
}
if (typeof vfs.readdir === 'function') {
try {
await vfs.readdir(path)
return true
} catch {
return false
}
}
return false
}
/**
* @param {Record<string, unknown>} ctx
* @param {string} root
* @param {{ maxFiles?: number, maxDepth?: number }} [opts]
* @returns {Promise<string[]>}
*/
async function bareAgentVfsWalkFiles(ctx, root, opts) {
const vfs = ctx && ctx.vfs
if (!vfs || typeof vfs.readdir !== 'function') return []
const maxFiles = Math.min(Math.max(Number(opts && opts.maxFiles) || 400, 1), 4000)
const maxDepth = Math.min(Math.max(Number(opts && opts.maxDepth) || 8, 1), 16)
const skip = Object.create(null)
skip['.git'] = 1
skip['node_modules'] = 1
skip['.bare-os'] = 1
/** @type {string[]} */
const out = []
/** @type {{ dir: string, depth: number }[]} */
const queue = [
{ dir: String(root || '/').replace(/\/+$/, '') || '/', depth: 0 }
]
while (queue.length && out.length < maxFiles) {
const cur = queue.shift()
if (!cur) break
let names
try {
names = await vfs.readdir(cur.dir)
} catch {
continue
}
if (!Array.isArray(names)) continue
for (let i = 0; i < names.length && out.length < maxFiles; i++) {
const name = String(names[i] || '')
if (!name || name === '.' || name === '..' || skip[name]) continue
const full = (cur.dir === '/' ? '' : cur.dir) + '/' + name
const isDir = await bareAgentVfsIsDir(ctx, full)
if (isDir) {
if (cur.depth + 1 < maxDepth) queue.push({ dir: full, depth: cur.depth + 1 })
} else {
out.push(full)
}
}
}
return out
}
/**
* @param {Record<string, unknown>} ctx
* @param {string} root
* @param {string} pattern
* @param {{ maxFiles?: number, maxDepth?: number }} [opts]
*/
async function bareAgentGlobFiles(ctx, root, pattern, opts) {
const files = await bareAgentVfsWalkFiles(ctx, root, opts)
const rootN = String(root || '/').replace(/\/+$/, '') || '/'
/** @type {string[]} */
const hits = []
for (let i = 0; i < files.length; i++) {
const abs = files[i]
let rel = abs
if (rootN !== '/' && abs.indexOf(rootN + '/') === 0) rel = abs.slice(rootN.length + 1)
else if (abs.charAt(0) === '/') rel = abs.slice(1)
if (bareAgentGlobMatch(rel, pattern) || bareAgentGlobMatch(abs, pattern)) {
hits.push(abs)
}
}
return hits
}
/**
* @param {Record<string, unknown>} ctx
* @param {string[]} files
* @param {string} query
* @param {{ maxHits?: number, snippet?: number }} [opts]
*/
async function bareAgentMemorySearchFiles(ctx, files, query, opts) {
const tokens = bareAgentMemoryTokens(query)
const maxHits = Math.min(Math.max(Number(opts && opts.maxHits) || 8, 1), 24)
const snippet = Math.min(Math.max(Number(opts && opts.snippet) || 280, 80), 1200)
/** @type {{ path: string, score: number, snippet: string }[]} */
const scored = []
for (let i = 0; i < files.length; i++) {
const path = files[i]
const text = await bareAgentReadTextFile(ctx, path)
if (!text) continue
const score = tokens.length ? bareAgentMemoryScore(text, tokens) : 0.15
if (score <= 0) continue
let snip = text.trim().replace(/\s+/g, ' ').slice(0, snippet)
if (tokens.length) {
const low = text.toLowerCase()
let at = -1
for (let t = 0; t < tokens.length; t++) {
const j = low.indexOf(tokens[t])
if (j !== -1 && (at === -1 || j < at)) at = j
}
if (at >= 0) {
const start = Math.max(0, at - 40)
snip =
(start > 0 ? '…' : '') +
text.slice(start, start + snippet).replace(/\s+/g, ' ')
}
}
scored.push({ path, score: Math.round(score * 1000) / 1000, snippet: snip })
}
scored.sort(function (a, b) {
return b.score - a.score
})
return scored.slice(0, maxHits)
}
/**
* @param {Record<string, unknown>} ctx
* @param {string} path
*/
async function bareAgentLoadTodos(ctx, path) {
const raw = await bareAgentReadJsonFile(ctx, path, [])
if (!Array.isArray(raw)) return []
/** @type {{ id: string, content: string, status: string }[]} */
const out = []
for (let i = 0; i < raw.length; i++) {
const row = raw[i] && typeof raw[i] === 'object' ? raw[i] : null
if (!row) continue
const id = String(row.id || '').trim()
if (!id) continue
const statusRaw = String(row.status || 'pending').trim().toLowerCase()
const status =
statusRaw === 'in_progress' || statusRaw === 'completed' || statusRaw === 'cancelled'
? statusRaw
: 'pending'
out.push({
id,
content: String(row.content == null ? id : row.content),
status
})
}
return out
}
/**
* @param {Record<string, unknown>} ctx
* @param {string} path
* @param {{ id: string, content: string, status: string }[]} todos
*/
async function bareAgentSaveTodos(ctx, path, todos) {
await bareAgentWriteJsonFile(ctx, path, Array.isArray(todos) ? todos : [])
}
/**
* @param {Record<string, unknown>} args
*/
function bareAgentToolPathArg(args) {
if (!args || typeof args !== 'object') return ''
if (typeof args.path === 'string' && args.path.trim()) return args.path.trim()
if (typeof args.file_path === 'string' && args.file_path.trim()) {
return args.file_path.trim()
}
return ''
}
+92 -14
View File
@@ -38,7 +38,11 @@ function bareAgentPaths(home) {
workspace: base + '/workspace',
workspaceMemory: base + '/workspace/memory',
workspaceSkills: base + '/workspace/skills',
skillsGlobal: base + '/skills'
skillsGlobal: base + '/skills',
todos: base + '/todos.json',
plan: base + '/plan.md',
hooks: base + '/hooks',
ask: base + '/ask.json'
}
}
@@ -81,11 +85,7 @@ function bareAgentDefaultConfig() {
autonomous_deny_ops: [
'delete_path',
'request_host_action',
'emit_host_notification',
'list_verification_scripts',
'run_maintenance_gate',
'run_contract_checks',
'summarize_build_drift'
'emit_host_notification'
],
autonomous_active: false,
autonomous_started_at_ms: 0,
@@ -95,7 +95,9 @@ function bareAgentDefaultConfig() {
autonomous_last_error: '',
context_compaction: 'auto',
compaction_keep_recent: 8,
compaction_tool_chars: 1600
compaction_tool_chars: 1600,
plan_mode_active: false,
todo_nudge_enabled: true
}
}
@@ -158,7 +160,9 @@ function bareAgentMergeConfig(defaults, src) {
'autonomous_last_error',
'context_compaction',
'compaction_keep_recent',
'compaction_tool_chars'
'compaction_tool_chars',
'plan_mode_active',
'todo_nudge_enabled'
])
for (const k of Object.keys(src)) {
if (k.startsWith('x-')) continue
@@ -241,7 +245,9 @@ function bareAgentMergeConfig(defaults, src) {
k === 'emergency_stop_mutations' ||
k === 'autonomous_mode_enabled' ||
k === 'autonomous_active' ||
k === 'autonomous_stop_requested'
k === 'autonomous_stop_requested' ||
k === 'plan_mode_active' ||
k === 'todo_nudge_enabled'
) {
out[k] = Boolean(val)
continue
@@ -305,7 +311,9 @@ function bareAgentValidateConfigShape(raw) {
'autonomous_last_error',
'context_compaction',
'compaction_keep_recent',
'compaction_tool_chars'
'compaction_tool_chars',
'plan_mode_active',
'todo_nudge_enabled'
]
if (!known.includes(k)) {
throw new Error('unknown config key: ' + k)
@@ -602,11 +610,81 @@ async function bareAgentLoadHistory(ctx, path) {
}
/**
* Clear persisted chat session (history + progress log; keeps config and instructions).
* @param {Record<string, unknown>} ctx
* @param {ReturnType<typeof bareAgentPaths>} paths
* @param {string} argv0
* Start an autonomous run on a config object (does not persist).
* @param {Record<string, unknown>} cfg
* @param {{ goal: string, maxRuntimeMs?: number, requiredChecks?: string[], scopePath?: string }} opts
*/
function bareAgentBeginAutonomousRun(cfg, opts) {
const goal = String((opts && opts.goal) || '').trim()
const maxRuntimeMs = Math.min(
Math.max(Math.floor(Number((opts && opts.maxRuntimeMs) || cfg.autonomous_max_runtime_ms) || 0), 60000),
7_200_000
)
const requiredChecks = Array.isArray(opts && opts.requiredChecks)
? opts.requiredChecks.map((x) => String(x || '').trim()).filter(Boolean)
: Array.isArray(cfg.autonomous_completion_required_checks)
? cfg.autonomous_completion_required_checks.map((x) => String(x || '').trim()).filter(Boolean)
: []
return {
...cfg,
autonomous_mode_enabled: true,
autonomous_active: true,
autonomous_stop_requested: false,
autonomous_started_at_ms: Date.now(),
autonomous_goal: goal,
autonomous_status: 'running',
autonomous_last_error: '',
autonomous_max_runtime_ms: maxRuntimeMs,
autonomous_completion_required_checks: requiredChecks
}
}
/**
* @param {Record<string, unknown>} cfg
* @param {string} [reason]
*/
function bareAgentStopAutonomousRun(cfg, reason) {
return {
...cfg,
autonomous_stop_requested: true,
autonomous_status: 'stopping',
autonomous_last_error: reason || String(cfg.autonomous_last_error || '')
}
}
/**
* @param {Record<string, unknown>} cfg
* @param {{ remainingMs?: number, lastError?: string }} [extra]
*/
function bareAgentAutonomousContinuationPrompt(cfg, extra) {
const goal = String(cfg.autonomous_goal || '').trim() || '(goal unset)'
const remain =
extra && typeof extra.remainingMs === 'number'
? Math.max(0, Math.round(extra.remainingMs / 1000))
: 0
return (
'AUTONOMOUS RUN still active. Do not stop with a plan — take the next concrete tool action.\n' +
'Goal: ' +
goal +
'\n' +
(remain > 0 ? 'Time remaining: ' + String(remain) + 's.\n' : '') +
(extra && extra.lastError ? 'Last gate error: ' + extra.lastError + '\n' : '') +
'Call task_complete(summary) only when the goal is actually finished and verified.'
)
}
/**
* @param {Record<string, unknown>} cfg
* @param {{ completed?: boolean, hasTools?: boolean, stopRequested?: boolean }} state
*/
function bareAgentAutonomousShouldContinue(cfg, state) {
if (!cfg || !cfg.autonomous_active) return false
if (state && state.stopRequested) return false
if (state && state.completed) return false
if (state && state.hasTools) return false
return true
}
async function bareAgentResetChatSession(ctx, paths, argv0) {
const vfs = ctx.vfs
if (!vfs || typeof vfs.mkdir !== 'function' || typeof vfs.writeFile !== 'function') {
+584 -36
View File
@@ -69,14 +69,27 @@ function bareAgentToolDefinitions() {
function: {
name: 'read_file',
description:
'Read a UTF-8 text file from the VFS. Path must be absolute (e.g. /home/guest/...).',
'Read a UTF-8 text file from the VFS. Path must be absolute (e.g. /home/guest/...). Optional offset/limit return numbered line slices (Grok-style).',
parameters: {
type: 'object',
properties: {
path: { type: 'string', description: 'Absolute file path' },
file_path: { type: 'string', description: 'Alias for path' },
max_bytes: {
type: 'integer',
description: 'Max bytes to read (default 256000)'
},
offset: {
type: 'integer',
description: '1-based start line for a numbered slice'
},
limit: {
type: 'integer',
description: 'Max lines to return from offset'
},
numbered: {
type: 'boolean',
description: 'Prefix N→ on each line when slicing (default true if offset/limit set)'
}
},
required: ['path']
@@ -104,11 +117,12 @@ function bareAgentToolDefinitions() {
function: {
name: 'edit_file',
description:
'Edit a text file: either replace entire content, or replace first occurrence of old_string with new_string.',
'Edit a text file: full replacement via content, or search/replace. old_string must be unique unless replace_all is true.',
parameters: {
type: 'object',
properties: {
path: { type: 'string' },
file_path: { type: 'string', description: 'Alias for path' },
content: {
type: 'string',
description: 'If set (non-empty), full file replacement'
@@ -117,7 +131,11 @@ function bareAgentToolDefinitions() {
type: 'string',
description: 'Search string (used with new_string)'
},
new_string: { type: 'string', description: 'Replacement text' }
new_string: { type: 'string', description: 'Replacement text' },
replace_all: {
type: 'boolean',
description: 'Replace every occurrence of old_string (default false)'
}
},
required: ['path']
}
@@ -821,7 +839,7 @@ function bareAgentToolDefinitions() {
function: {
name: 'autonomous_run',
description:
'Start an autonomous coding run with goal, optional scope path, and runtime cap. This enables autonomous mode in config.',
'Start an autonomous coding run with a goal. Enables autonomous mode, keeps the ReAct loop going until task_complete, stop, or timebox. Optional scope path, runtime cap, and quality-gate ids.',
parameters: {
type: 'object',
properties: {
@@ -898,6 +916,173 @@ function bareAgentToolDefinitions() {
}
}
},
{
type: 'function',
function: {
name: 'search_replace',
description:
'Replace old_string with new_string in a file. Match must be unique unless replace_all is true. Same uniqueness rules as Grok Build edit_file.',
parameters: {
type: 'object',
properties: {
path: { type: 'string' },
file_path: { type: 'string', description: 'Alias for path' },
old_string: { type: 'string' },
new_string: { type: 'string' },
replace_all: { type: 'boolean' }
},
required: ['old_string', 'new_string']
}
}
},
{
type: 'function',
function: {
name: 'glob_files',
description:
'Walk the VFS from root and return paths matching a glob (supports ** and *). Prefer this over run_command find/ls.',
parameters: {
type: 'object',
properties: {
pattern: {
type: 'string',
description: 'Glob such as **/*.js or src/**/foo.md'
},
root: {
type: 'string',
description: 'Directory to walk (default session home)'
},
max_results: { type: 'integer', description: 'Default 100, cap 400' }
},
required: ['pattern']
}
}
},
{
type: 'function',
function: {
name: 'todo_write',
description:
'Create or update the session todo list (Grok-style). merge=true updates by id; merge=false replaces the list.',
parameters: {
type: 'object',
properties: {
todos: {
type: 'array',
description: 'Items with id, optional content, status pending|in_progress|completed|cancelled',
items: {
type: 'object',
properties: {
id: { type: 'string' },
content: { type: 'string' },
status: { type: 'string' }
},
required: ['id']
}
},
merge: {
type: 'boolean',
description: 'Default true: update matching ids, keep others'
}
},
required: ['todos']
}
}
},
{
type: 'function',
function: {
name: 'memory_search',
description:
'Search ~/.agent/workspace/memory, MEMORY.md, and compact.md by keyword (guest-safe, no embeddings).',
parameters: {
type: 'object',
properties: {
query: { type: 'string' },
max_hits: { type: 'integer', description: 'Default 8' }
},
required: ['query']
}
}
},
{
type: 'function',
function: {
name: 'memory_get',
description:
'Read one memory file. Path must be under ~/.agent/workspace/memory or a named file there.',
parameters: {
type: 'object',
properties: {
path: { type: 'string', description: 'Absolute path or basename under workspace/memory' },
max_bytes: { type: 'integer' }
},
required: ['path']
}
}
},
{
type: 'function',
function: {
name: 'enter_plan_mode',
description:
'Switch to read-only plan mode. Writes are denied except ~/.agent/plan.md until exit_plan_mode.',
parameters: {
type: 'object',
properties: {
note: { type: 'string', description: 'Optional starter text appended to plan.md' }
}
}
}
},
{
type: 'function',
function: {
name: 'exit_plan_mode',
description: 'Leave plan mode and allow mutating tools again.',
parameters: {
type: 'object',
properties: {
summary: { type: 'string' }
}
}
}
},
{
type: 'function',
function: {
name: 'ask_user_question',
description:
'Pose one or more multiple-choice questions to the user (Grok-style). Persists to ~/.agent/ask.json.',
parameters: {
type: 'object',
properties: {
questions: {
type: 'array',
items: {
type: 'object',
properties: {
question: { type: 'string' },
options: {
type: 'array',
items: {
type: 'object',
properties: {
label: { type: 'string' },
description: { type: 'string' }
}
}
},
multi_select: { type: 'boolean' }
},
required: ['question']
}
}
},
required: ['questions']
}
}
},
{
type: 'function',
function: {
@@ -969,6 +1154,7 @@ async function bareAgentDispatchTool(o) {
manCacheRef,
onTaskComplete
} = o
const internalGate = Boolean(o.internalGate)
const manDbCache = manCacheRef || { db: null }
/** @type {Record<string, unknown>} */
let args = {}
@@ -979,12 +1165,43 @@ async function bareAgentDispatchTool(o) {
}
const cfgNow = configRef.current || {}
if (
!internalGate &&
cfgNow.autonomous_active &&
Array.isArray(cfgNow.autonomous_deny_ops) &&
cfgNow.autonomous_deny_ops.map((x) => String(x)).includes(toolName)
) {
return bareAgentJsonResult({ ok: false, error: 'autonomous_op_denied', tool: toolName })
}
if (
!internalGate &&
cfgNow.plan_mode_active &&
typeof bareAgentPlanModeToolAllowed === 'function' &&
!bareAgentPlanModeToolAllowed(toolName, args, paths)
) {
return bareAgentJsonResult({
ok: false,
error: 'plan_mode_readonly',
tool: toolName,
hint: 'exit_plan_mode before mutating, or write only ' + String(paths.plan || '~/.agent/plan.md')
})
}
if (!internalGate && typeof bareAgentRunPreToolHooks === 'function' && paths.hooks) {
const hookReason = await bareAgentRunPreToolHooks(ctx, paths.hooks, toolName, args)
if (hookReason) {
return bareAgentJsonResult({
ok: false,
error: 'hook_denied',
tool: toolName,
reason: hookReason
})
}
}
if (toolName === 'list_dir') {
return bareAgentDispatchTool({ ...o, toolName: 'list_directory' })
}
if (toolName === 'glob') {
return bareAgentDispatchTool({ ...o, toolName: 'glob_files' })
}
const vfs = ctx.vfs
const AUTONOMOUS_DENY_PATH_PREFIXES = [
@@ -1210,6 +1427,262 @@ async function bareAgentDispatchTool(o) {
})
}
if (toolName === 'glob_files') {
const pattern = typeof args.pattern === 'string' ? args.pattern.trim() : ''
if (!pattern) return bareAgentJsonResult({ ok: false, error: 'pattern_required' })
const root =
typeof args.root === 'string' && args.root.trim()
? args.root.trim()
: home || '/home'
if (!bareAgentPathAllowed(root)) {
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
}
const maxResults =
typeof args.max_results === 'number' && Number.isFinite(args.max_results)
? Math.min(Math.max(Math.floor(args.max_results), 1), 400)
: 100
if (typeof bareAgentGlobFiles !== 'function') {
return bareAgentJsonResult({ ok: false, error: 'glob_unavailable' })
}
appendProgress('glob_files ' + pattern + ' @ ' + root)
const files = await bareAgentGlobFiles(ctx, root, pattern, {
maxFiles: Math.max(maxResults * 4, 200),
maxDepth: 10
})
return bareAgentJsonResult({
ok: true,
root,
pattern,
count: Math.min(files.length, maxResults),
truncated: files.length > maxResults,
files: files.slice(0, maxResults)
})
}
if (toolName === 'todo_write') {
const todosPath = paths.todos || paths.dir + '/todos.json'
const merge = args.merge !== false
const prev =
typeof bareAgentLoadTodos === 'function'
? await bareAgentLoadTodos(ctx, todosPath)
: []
let next
try {
next = bareAgentTodoApply(args.todos, { merge }, prev)
} catch (e) {
const msg =
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
return bareAgentJsonResult({ ok: false, error: msg })
}
if (typeof bareAgentSaveTodos === 'function') {
await bareAgentSaveTodos(ctx, todosPath, next)
}
const summary = bareAgentTodoSummarize(next)
appendProgress('todo_write open=' + String(summary.open) + '/' + String(summary.total))
return bareAgentJsonResult({
ok: true,
merge,
todos: next,
summary
})
}
if (toolName === 'memory_search') {
const query = typeof args.query === 'string' ? args.query.trim() : ''
if (!query) return bareAgentJsonResult({ ok: false, error: 'query_required' })
const maxHits =
typeof args.max_hits === 'number' && Number.isFinite(args.max_hits)
? Math.min(Math.max(Math.floor(args.max_hits), 1), 24)
: 8
const memDir =
typeof paths.workspaceMemory === 'string'
? paths.workspaceMemory
: paths.dir + '/workspace/memory'
const workspace =
typeof paths.workspace === 'string' ? paths.workspace : paths.dir + '/workspace'
const seeds = []
if (typeof bareAgentVfsWalkFiles === 'function') {
const walked = await bareAgentVfsWalkFiles(ctx, memDir, {
maxFiles: 80,
maxDepth: 4
})
for (let i = 0; i < walked.length; i++) seeds.push(walked[i])
}
seeds.push(workspace + '/MEMORY.md')
if (paths.compact) seeds.push(paths.compact)
else seeds.push(paths.dir + '/compact.md')
const uniq = []
const seenMem = Object.create(null)
for (let i = 0; i < seeds.length; i++) {
if (seenMem[seeds[i]]) continue
seenMem[seeds[i]] = 1
uniq.push(seeds[i])
}
appendProgress('memory_search ' + query.slice(0, 80))
const hits =
typeof bareAgentMemorySearchFiles === 'function'
? await bareAgentMemorySearchFiles(ctx, uniq, query, { maxHits })
: []
return bareAgentJsonResult({ ok: true, query, hits })
}
if (toolName === 'memory_get') {
const rawPath = typeof args.path === 'string' ? args.path.trim() : ''
if (!rawPath) return bareAgentJsonResult({ ok: false, error: 'path_required' })
const memDir =
typeof paths.workspaceMemory === 'string'
? paths.workspaceMemory
: paths.dir + '/workspace/memory'
const workspace =
typeof paths.workspace === 'string' ? paths.workspace : paths.dir + '/workspace'
let path = rawPath
if (!path.startsWith('/')) path = memDir + '/' + path.replace(/^\/+/, '')
const allowed =
path === workspace + '/MEMORY.md' ||
path === (paths.compact || paths.dir + '/compact.md') ||
path === memDir ||
path.indexOf(memDir + '/') === 0
if (!allowed) {
return bareAgentJsonResult({
ok: false,
error: 'memory_path_denied',
hint: 'path must be under ' + memDir
})
}
const maxB =
typeof args.max_bytes === 'number' && Number.isFinite(args.max_bytes)
? Math.min(Math.floor(args.max_bytes), 256_000)
: 64_000
appendProgress('memory_get ' + path)
let text = await bareAgentReadTextFile(ctx, path)
if (!text) return bareAgentJsonResult({ ok: false, error: 'empty_or_missing', path })
const truncated = text.length > maxB
if (truncated) text = text.slice(0, maxB) + '\n… truncated'
return bareAgentJsonResult({ ok: true, path, content: text, truncated })
}
if (toolName === 'enter_plan_mode') {
const planPath = paths.plan || paths.dir + '/plan.md'
configRef.current = { ...(configRef.current || {}), plan_mode_active: true }
if (typeof bareAgentSaveConfigFromTools === 'function') {
await bareAgentSaveConfigFromTools(ctx, paths, configRef.current)
}
const note = typeof args.note === 'string' ? args.note.trim() : ''
let prev = ''
try {
prev = await bareAgentReadTextFile(ctx, planPath)
} catch {
prev = ''
}
if (!prev.trim()) {
const starter =
'# Plan\n\n' +
(note ? note + '\n' : '- Investigate with read-only tools.\n- Write the plan here.\n- Call exit_plan_mode when ready to implement.\n')
await bareAgentWriteTextFile(ctx, planPath, starter)
} else if (note) {
await bareAgentWriteTextFile(ctx, planPath, prev.replace(/\s*$/, '') + '\n\n' + note + '\n')
}
appendProgress('enter_plan_mode ' + planPath)
return bareAgentJsonResult({
ok: true,
plan_mode_active: true,
plan: planPath
})
}
if (toolName === 'exit_plan_mode') {
const summary = typeof args.summary === 'string' ? args.summary.trim() : ''
configRef.current = { ...(configRef.current || {}), plan_mode_active: false }
if (typeof bareAgentSaveConfigFromTools === 'function') {
await bareAgentSaveConfigFromTools(ctx, paths, configRef.current)
}
if (summary && paths.plan) {
const prev = await bareAgentReadTextFile(ctx, paths.plan)
await bareAgentWriteTextFile(
ctx,
paths.plan,
(prev ? prev.replace(/\s*$/, '') + '\n\n' : '') + '## Exit\n' + summary + '\n'
)
}
appendProgress('exit_plan_mode')
return bareAgentJsonResult({
ok: true,
plan_mode_active: false,
summary: summary || null
})
}
if (toolName === 'ask_user_question') {
const rows = Array.isArray(args.questions) ? args.questions : []
if (!rows.length) {
return bareAgentJsonResult({ ok: false, error: 'questions_required' })
}
/** @type {Record<string, unknown>[]} */
const questions = []
for (let i = 0; i < rows.length; i++) {
const row = rows[i] && typeof rows[i] === 'object' ? rows[i] : {}
const question = String(row.question || '').trim()
if (!question) continue
const opts = Array.isArray(row.options) ? row.options : []
questions.push({
question,
multi_select: Boolean(row.multi_select),
options: opts.map(function (opt) {
if (opt && typeof opt === 'object') {
return {
label: String(opt.label || ''),
description: String(opt.description || '')
}
}
return { label: String(opt || ''), description: '' }
})
})
}
if (!questions.length) {
return bareAgentJsonResult({ ok: false, error: 'questions_required' })
}
const askPath = paths.ask || paths.dir + '/ask.json'
const payload = {
asked_at: new Date().toISOString(),
questions
}
if (typeof bareAgentWriteJsonFile === 'function') {
await bareAgentWriteJsonFile(ctx, askPath, payload)
}
const text = questions
.map(function (q, i) {
const opts = Array.isArray(q.options)
? q.options
.map(function (o, j) {
return (
' ' +
String(j + 1) +
') ' +
String(o.label || '') +
(o.description ? ' — ' + String(o.description) : '')
)
})
.join('\n')
: ''
return (
String(i + 1) +
'. ' +
q.question +
(q.multi_select ? ' (multi-select)' : '') +
(opts ? '\n' + opts : '')
)
})
.join('\n')
appendProgress('ask_user_question n=' + String(questions.length))
return bareAgentJsonResult({
ok: true,
ask: askPath,
questions,
text,
hint: 'Wait for the user to answer these questions on the next turn.'
})
}
if (toolName === 'task_complete') {
const summary = typeof args.summary === 'string' ? args.summary : ''
appendProgress('task_complete: ' + summary.slice(0, 200))
@@ -1225,9 +1698,6 @@ async function bareAgentDispatchTool(o) {
const goal = typeof args.goal === 'string' ? args.goal.trim() : ''
const scopePath = typeof args.scope_path === 'string' ? args.scope_path.trim() : ''
const cfg = configRef.current || {}
if (!cfg.autonomous_mode_enabled) {
return bareAgentJsonResult({ ok: false, error: 'autonomous_mode_disabled' })
}
if (!goal) return bareAgentJsonResult({ ok: false, error: 'goal_required' })
if (scopePath && !autonomousPathAllowed(scopePath)) {
return bareAgentJsonResult({ ok: false, error: 'scope_path_denied' })
@@ -1236,7 +1706,6 @@ async function bareAgentDispatchTool(o) {
typeof args.max_runtime_ms === 'number' && Number.isFinite(args.max_runtime_ms)
? args.max_runtime_ms
: cfg.autonomous_max_runtime_ms
const maxRuntimeMs = Math.min(Math.max(Math.floor(Number(maxRuntimeMsRaw) || 0), 60000), 7_200_000)
const requiredChecks = Array.isArray(args.required_checks)
? args.required_checks.map((x) => String(x || '').trim()).filter(Boolean)
: []
@@ -1244,25 +1713,39 @@ async function bareAgentDispatchTool(o) {
if (unknown.length) {
return bareAgentJsonResult({ ok: false, error: 'unknown_required_checks', unknown, allowlist: Object.keys(AUTONOMOUS_CHECK_ALLOW) })
}
const merged = bareAgentMergeConfigPatch(cfg, {
autonomous_active: true,
autonomous_stop_requested: false,
autonomous_started_at_ms: Date.now(),
autonomous_goal: goal,
autonomous_status: 'running',
autonomous_last_error: '',
autonomous_max_runtime_ms: maxRuntimeMs,
autonomous_completion_required_checks: requiredChecks
})
const started =
typeof bareAgentBeginAutonomousRun === 'function'
? bareAgentBeginAutonomousRun(cfg, {
goal,
maxRuntimeMs: maxRuntimeMsRaw,
requiredChecks
})
: {
...cfg,
autonomous_mode_enabled: true,
autonomous_active: true,
autonomous_stop_requested: false,
autonomous_started_at_ms: Date.now(),
autonomous_goal: goal,
autonomous_status: 'running',
autonomous_last_error: '',
autonomous_max_runtime_ms: Math.min(
Math.max(Math.floor(Number(maxRuntimeMsRaw) || 0), 60000),
7_200_000
),
autonomous_completion_required_checks: requiredChecks
}
const merged = bareAgentMergeConfigPatch(cfg, started)
await bareAgentSaveConfigFromTools(ctx, paths, merged)
configRef.current = merged
appendProgress('autonomous_run start goal=' + goal.slice(0, 160))
return bareAgentJsonResult({
ok: true,
active: true,
enabled: true,
goal,
scope_path: scopePath || null,
max_runtime_ms: maxRuntimeMs,
max_runtime_ms: merged.autonomous_max_runtime_ms,
required_checks: requiredChecks,
status: 'running'
})
@@ -1294,11 +1777,16 @@ async function bareAgentDispatchTool(o) {
if (toolName === 'autonomous_run_stop') {
const cfg = configRef.current || {}
const reason = typeof args.reason === 'string' ? args.reason.trim() : ''
const merged = bareAgentMergeConfigPatch(cfg, {
autonomous_stop_requested: true,
autonomous_status: 'stopped',
autonomous_last_error: reason || String(cfg.autonomous_last_error || '')
})
const stopped =
typeof bareAgentStopAutonomousRun === 'function'
? bareAgentStopAutonomousRun(cfg, reason)
: {
...cfg,
autonomous_stop_requested: true,
autonomous_status: 'stopping',
autonomous_last_error: reason || String(cfg.autonomous_last_error || '')
}
const merged = bareAgentMergeConfigPatch(cfg, stopped)
await bareAgentSaveConfigFromTools(ctx, paths, merged)
configRef.current = merged
appendProgress('autonomous_run_stop ' + (reason || 'requested'))
@@ -1306,7 +1794,12 @@ async function bareAgentDispatchTool(o) {
}
if (toolName === 'read_file') {
const path = typeof args.path === 'string' ? args.path : ''
const path =
typeof bareAgentToolPathArg === 'function'
? bareAgentToolPathArg(args)
: typeof args.path === 'string'
? args.path
: ''
if (!enforceAutonomousPath(path)) {
return bareAgentJsonResult({ ok: false, error: 'autonomous_path_denied' })
}
@@ -1328,6 +1821,23 @@ async function bareAgentDispatchTool(o) {
? ctx.b4a.toString(buf)
: String(new TextDecoder().decode(buf))
if (t.length > maxB) t = t.slice(0, maxB) + '\n… truncated'
const hasSlice =
(typeof args.offset === 'number' && Number.isFinite(args.offset)) ||
(typeof args.limit === 'number' && Number.isFinite(args.limit))
if (hasSlice && typeof bareAgentSliceFileLines === 'function') {
const sliced = bareAgentSliceFileLines(t, {
offset:
typeof args.offset === 'number' && Number.isFinite(args.offset)
? args.offset
: 1,
limit:
typeof args.limit === 'number' && Number.isFinite(args.limit)
? args.limit
: undefined,
numbered: args.numbered !== false
})
return bareAgentJsonResult({ ok: true, path, ...sliced })
}
return bareAgentJsonResult({ ok: true, path, content: t })
}
@@ -1354,15 +1864,20 @@ async function bareAgentDispatchTool(o) {
return bareAgentJsonResult({ ok: true, bytes: body.length })
}
if (toolName === 'edit_file') {
const path = typeof args.path === 'string' ? args.path : ''
if (toolName === 'edit_file' || toolName === 'search_replace') {
const path =
typeof bareAgentToolPathArg === 'function'
? bareAgentToolPathArg(args)
: typeof args.path === 'string'
? args.path
: ''
if (!enforceAutonomousPath(path)) {
return bareAgentJsonResult({ ok: false, error: 'autonomous_path_denied' })
}
if (!bareAgentPathAllowed(path) || !vfs?.readFile || !vfs?.writeFile) {
return bareAgentJsonResult({ ok: false, error: 'path_or_vfs' })
}
appendProgress('edit_file ' + path)
appendProgress(toolName + ' ' + path)
const buf = await vfs.readFile(path)
let prev =
buf && buf.length
@@ -1372,16 +1887,38 @@ async function bareAgentDispatchTool(o) {
? ctx.b4a.toString(buf)
: String(new TextDecoder().decode(buf))
: ''
const full = typeof args.content === 'string' ? args.content : ''
const full =
toolName === 'search_replace'
? ''
: typeof args.content === 'string'
? args.content
: ''
const oldStr = typeof args.old_string === 'string' ? args.old_string : ''
const newStr = typeof args.new_string === 'string' ? args.new_string : ''
const replaceAll = Boolean(args.replace_all)
let next = prev
if (full.length > 0) next = full
else if (oldStr) {
let replacements = 0
if (full.length > 0) {
next = full
replacements = 1
} else if (typeof bareAgentSearchReplaceApply === 'function') {
const applied = bareAgentSearchReplaceApply(prev, oldStr, newStr, replaceAll)
if (!applied.ok) {
return bareAgentJsonResult({
ok: false,
error: applied.error,
count: applied.count,
hint: applied.hint
})
}
next = applied.next
replacements = applied.replacements || 0
} else if (oldStr) {
if (!prev.includes(oldStr)) {
return bareAgentJsonResult({ ok: false, error: 'old_string not found' })
}
next = prev.replace(oldStr, newStr)
next = replaceAll ? prev.split(oldStr).join(newStr) : prev.replace(oldStr, newStr)
replacements = 1
} else {
return bareAgentJsonResult({
ok: false,
@@ -1393,9 +1930,16 @@ async function bareAgentDispatchTool(o) {
? ctx.b4a.from(next)
: new TextEncoder().encode(next)
let dir = path.replace(/\/[^/]+$/, '')
if (dir && dir !== path) await vfs.mkdir(dir, { recursive: true })
if (dir && dir !== path && typeof vfs.mkdir === 'function') {
await vfs.mkdir(dir, { recursive: true })
}
await vfs.writeFile(path, body)
return bareAgentJsonResult({ ok: true, bytes: body.length })
return bareAgentJsonResult({
ok: true,
bytes: body.length,
replacements,
replace_all: replaceAll
})
}
if (toolName === 'create_directory') {
@@ -2641,7 +3185,9 @@ function bareAgentMergeConfigPatch(base, patch) {
'autonomous_last_error',
'context_compaction',
'compaction_keep_recent',
'compaction_tool_chars'
'compaction_tool_chars',
'plan_mode_active',
'todo_nudge_enabled'
]
const numKeys = new Set([
'max_tokens',
@@ -2681,7 +3227,9 @@ function bareAgentMergeConfigPatch(base, patch) {
k === 'emergency_stop_mutations' ||
k === 'autonomous_mode_enabled' ||
k === 'autonomous_active' ||
k === 'autonomous_stop_requested'
k === 'autonomous_stop_requested' ||
k === 'plan_mode_active' ||
k === 'todo_nudge_enabled'
) {
out[k] = Boolean(v)
} else if (k === 'reasoning_mode') {
+256 -57
View File
@@ -257,7 +257,7 @@ Capabilities: ctx.execLine for shell lines; ctx.vfs readFile/writeFile/mkdir/rea
Safety: never exfiltrate ~/.agent/config.json or API keys. Prefer least-privilege commands. Call task_complete(summary) when fully done.
Discovery: man <topic>, /share/man/man.json; Tier-1 utilities in /bin. Tools: list_directory, file_stat, read_man_page, apropos_man, read_proc_file, get_swarm_peers, get_resource_limits, web_fetch (live http(s) pages and APIs; same host allowlist as wget); use list_directory instead of \`ls\` in run_command when only listing.
Discovery: man <topic>, /share/man/man.json; Tier-1 utilities in /bin. Tools: list_directory, file_stat, glob_files, read_man_page, apropos_man, read_proc_file, get_swarm_peers, get_resource_limits, web_fetch (live http(s) pages and APIs; same host allowlist as wget); use list_directory or glob_files instead of \`ls\` in run_command when only listing. Use todo_write for multi-step work, memory_search for prior notes, enter_plan_mode before large edits.
Skills: modular instructions live under ~/.agent/workspace/skills/ (and optionally ~/.agent/skills/). The system message includes a compact skill index; use the read_skill tool to load full SKILL.md when a task matches a listed skill.
@@ -287,7 +287,7 @@ ASK FIRST: Destructive deletes, bridge mutations (emit_host_notification, reques
P2P / TEARDOWN: Hyperswarm peer wait vs offline LKG boot are different — see environment appendix. When diagnosing replication, prefer runtime_diagnostic_bundle and read_proc allowlisted swarm files; closing order is swarm before drives when changing booter lifecycle code.
WORKFLOW: Plan briefly, execute tools, verify with read_proc or logs, then task_complete. Before large edits state blast radius (packages, contracts, docs). Before task_complete, self-review: docs updated? generated regen? wrong runtime assumption?
WORKFLOW: Plan briefly (enter_plan_mode + ~/.agent/plan.md for large changes), execute tools, verify with read_proc or logs, then task_complete. Track steps with todo_write. Before large edits state blast radius (packages, contracts, docs). Before task_complete, self-review: docs updated? generated regen? wrong runtime assumption?
STOP CONDITIONS: If the same failing action repeats three times, stop and summarize evidence; do not loop blindly.
@@ -508,6 +508,42 @@ function bareAgentAutonomousSettings(cfg) {
}
}
/**
* Run configured autonomous completion gates. Internal dispatch skips deny_ops.
* @returns {Promise<{ ok: boolean, failed: string[] }>}
*/
async function bareAgentRunAutonomousGates(o) {
const checks = (o && o.requiredChecks) || []
if (!checks.length) return { ok: true, failed: [] }
/** @type {string[]} */
const failed = []
for (const check of checks) {
const res = await bareAgentDispatchTool({
ctx: o.ctx,
toolName: 'run_maintenance_gate',
argsJson: JSON.stringify({ command: check, timeout_ms: 300000 }),
paths: o.paths,
signal: o.signal,
appendProgress: o.appendProgress,
home: o.home,
configRef: o.configRef,
manCacheRef: o.manCacheRef,
onTaskComplete: o.onTaskComplete,
internalGate: true
})
let ok = false
try {
const j = JSON.parse(res)
const body = typeof j.stdout_stderr === 'string' ? j.stdout_stderr : ''
ok = Boolean(j.ok) && !/EXIT:[1-9]/.test(body)
} catch {
ok = false
}
if (!ok) failed.push(check)
}
return { ok: failed.length === 0, failed }
}
/**
* @param {Record<string, unknown>} ctx
* @param {string} argv0
@@ -850,6 +886,7 @@ 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 home = bareAgentResolveHome(ctx)
const paths = bareAgentPaths(home)
let { config } = await bareAgentLoadOrCreateConfig(ctx, paths)
@@ -952,6 +989,19 @@ async function bareOsRunAgentSession(ctx, argv0, task, runOpts) {
return
}
if (autonomousFlag) {
config = bareAgentBeginAutonomousRun(config, {
goal: String((runOpts && runOpts.autonomousGoal) || task || '').trim(),
maxRuntimeMs: Number(runOpts && runOpts.autonomousMaxRuntimeMs) || undefined,
requiredChecks: (runOpts && runOpts.autonomousChecks) || undefined
})
try {
await bareAgentSaveConfig(ctx, paths, config)
} catch {
/* persist best-effort */
}
}
try {
if (ctx.vfs?.chmod) await ctx.vfs.chmod(paths.config, 0o600)
} catch {
@@ -1042,10 +1092,70 @@ async function bareOsRunAgentSession(ctx, argv0, task, runOpts) {
if (skillsPromptBlock && String(skillsPromptBlock).trim())
systemContent += '\n\n' + String(skillsPromptBlock).trim()
if (typeof bareAgentDiscoverAgentsMdPaths === 'function') {
const envNow =
ctx.env && typeof ctx.env === 'object'
? /** @type {Record<string, string>} */ (ctx.env)
: {}
const cwd = String(envNow.PWD || envNow.CWD || home || '').trim() || home
try {
const extraFiles = await bareAgentDiscoverAgentsMdPaths(ctx, cwd, 12)
const workspaceAgents =
(typeof paths.workspace === 'string' ? paths.workspace : paths.dir + '/workspace') +
'/AGENTS.md'
const filtered = extraFiles.filter(function (p) {
return p !== workspaceAgents
})
if (filtered.length && typeof bareAgentLoadAgentsMdPrompt === 'function') {
const extraBlock = await bareAgentLoadAgentsMdPrompt(ctx, filtered, 4000)
if (extraBlock && extraBlock.trim()) systemContent += '\n\n' + extraBlock.trim()
}
} catch {
/* optional */
}
}
if (config.plan_mode_active) {
systemContent +=
'\n\n## PLAN MODE is ON\n' +
'Read-only tools only. The only allowed write is ' +
String(paths.plan || home + '/.agent/plan.md') +
'. Draft the plan there, then call exit_plan_mode before implementing.\n'
}
if (typeof bareAgentLoadTodos === 'function' && paths.todos) {
try {
const todos = await bareAgentLoadTodos(ctx, paths.todos)
const sum = bareAgentTodoSummarize(todos)
if (sum.total) {
systemContent +=
'\n\n## Session todos\n' +
sum.text +
'\n(open=' +
String(sum.open) +
' completed=' +
String(sum.completed) +
')\n'
}
} catch {
/* optional */
}
}
let userTask = String(task || '')
if (config.autonomous_active) {
userTask =
'AUTONOMOUS RUN. Execute until the goal is done. Do not stop after a plan — use tools, then call task_complete.\n' +
'Goal: ' +
String(config.autonomous_goal || task || '') +
'\n\n' +
userTask
}
if (!messages.length) {
messages = [
{ role: 'system', content: systemContent },
{ role: 'user', content: task }
{ role: 'user', content: userTask }
]
} else {
const hasSystem =
@@ -1057,7 +1167,7 @@ async function bareOsRunAgentSession(ctx, argv0, task, runOpts) {
} else {
messages[0] = { role: 'system', content: systemContent }
}
messages.push({ role: 'user', content: task })
messages.push({ role: 'user', content: userTask })
}
const stdout = /** @type {import('stream').Writable | undefined} */ (
@@ -1114,6 +1224,7 @@ async function bareOsRunAgentSession(ctx, argv0, task, runOpts) {
let reasoningSettings = bareAgentReasoningSettings(configRef.current)
let autonomousSettings = bareAgentAutonomousSettings(configRef.current)
let reasoningCharCount = 0
let turnsSinceTodoWrite = 0
let suspended = replSuspendedForSetup
/** @type {(() => void) | null} */
@@ -1178,7 +1289,7 @@ async function bareOsRunAgentSession(ctx, argv0, task, runOpts) {
}
}
const maxIter = Number(configRef.current.max_iterations) || 64
const maxIterBase = Number(configRef.current.max_iterations) || 64
let iter = 0
for (;;) {
@@ -1188,7 +1299,11 @@ async function bareOsRunAgentSession(ctx, argv0, task, runOpts) {
}
reasoningSettings = bareAgentReasoningSettings(configRef.current)
autonomousSettings = bareAgentAutonomousSettings(configRef.current)
if (completed) break
const autoLive =
Boolean(configRef.current.autonomous_active) &&
!autonomousSettings.stopRequested
const maxIter = autoLive ? Math.max(maxIterBase, 96) : maxIterBase
if (completed && !autoLive) break
if (autonomousSettings.enabled && autonomousSettings.active) {
const now = Date.now()
if (autonomousSettings.stopRequested) {
@@ -1262,7 +1377,19 @@ async function bareOsRunAgentSession(ctx, argv0, task, runOpts) {
),
mode: compactCfg.mode,
keepRecent: compactCfg.keepRecent,
toolMaxChars: compactCfg.toolMaxChars
toolMaxChars: compactCfg.toolMaxChars,
autonomous: autoLive
? {
active: true,
goal: String(configRef.current.autonomous_goal || ''),
status: String(configRef.current.autonomous_status || 'running'),
remainingMs: Math.max(
0,
autonomousSettings.maxRuntimeMs -
(Date.now() - (autonomousSettings.startedAtMs || Date.now()))
)
}
: undefined
})
messages = packed.messages
if (packed.meta.compacted) {
@@ -1349,7 +1476,20 @@ async function bareOsRunAgentSession(ctx, argv0, task, runOpts) {
),
mode: compactCfg.mode,
keepRecent: compactCfg.keepRecent || 12,
toolMaxChars: compactCfg.toolMaxChars
toolMaxChars: compactCfg.toolMaxChars,
autonomous: autoLive
? {
active: true,
goal: String(configRef.current.autonomous_goal || ''),
status: String(configRef.current.autonomous_status || 'running'),
remainingMs: Math.max(
0,
autonomousSettings.maxRuntimeMs -
(Date.now() -
(autonomousSettings.startedAtMs || Date.now()))
)
}
: undefined
})
messages = packed.messages
if (packed.meta.compacted && packed.meta.droppedGroups > 0) {
@@ -1839,56 +1979,27 @@ async function bareOsRunAgentSession(ctx, argv0, task, runOpts) {
if (!hasTools) {
if (
autonomousSettings.enabled &&
autonomousSettings.active &&
autonomousSettings.requiredChecks.length
bareAgentAutonomousShouldContinue(configRef.current, {
completed: completed,
hasTools: false,
stopRequested: autonomousSettings.stopRequested
})
) {
/** @type {string[]} */
const failedChecks = []
for (const check of autonomousSettings.requiredChecks) {
const res = await bareAgentDispatchTool({
ctx,
toolName: 'run_maintenance_gate',
argsJson: JSON.stringify({ command: check, timeout_ms: 300000 }),
paths,
signal: masterAbort.signal,
appendProgress,
home,
configRef,
manCacheRef,
onTaskComplete
const remain = Math.max(
0,
autonomousSettings.maxRuntimeMs -
(Date.now() - (autonomousSettings.startedAtMs || Date.now()))
)
messages.push({
role: 'user',
content: bareAgentAutonomousContinuationPrompt(configRef.current, {
remainingMs: remain,
lastError: String(configRef.current.autonomous_last_error || '')
})
let ok = false
try {
const j = JSON.parse(res)
const body =
typeof j.stdout_stderr === 'string' ? j.stdout_stderr : ''
ok = Boolean(j.ok) && !/EXIT:[1-9]/.test(body)
} catch {
ok = false
}
if (!ok) failedChecks.push(check)
}
if (failedChecks.length) {
configRef.current.autonomous_status = 'needs_fixups'
configRef.current.autonomous_last_error =
'failed_checks:' + failedChecks.join(',')
await bareAgentSaveConfig(ctx, paths, configRef.current)
appendProgress('autonomous checks failed ' + failedChecks.join(','))
messages.push({
role: 'user',
content:
'Autonomous completion gates failed for checks: ' +
failedChecks.join(', ') +
'. Fix the issues, rerun required checks, and only call task_complete when all pass.'
})
continue
}
configRef.current.autonomous_active = false
configRef.current.autonomous_status = 'completed'
configRef.current.autonomous_last_error = ''
await bareAgentSaveConfig(ctx, paths, configRef.current)
appendProgress('autonomous completion gates passed')
})
appendProgress('autonomous continue (no tools this turn)')
await bareAgentSaveHistory(ctx, paths.history, messages)
continue
}
await bareAgentSaveHistory(ctx, paths.history, messages)
bareAgentWriteOut(ctx, stdout, '\n')
@@ -1921,6 +2032,7 @@ async function bareOsRunAgentSession(ctx, argv0, task, runOpts) {
)
}
let sawTodoWrite = false
for (const tc of toolCallsArr) {
const fn =
/** @type {{ id?: string, function?: { name?: string, arguments?: string } }} */ (
@@ -1929,6 +2041,7 @@ async function bareOsRunAgentSession(ctx, argv0, task, runOpts) {
const id = /** @type {{ id?: string }} */ (tc).id || ''
const name = fn?.name || ''
const argsStr = fn?.arguments || '{}'
if (name === 'todo_write') sawTodoWrite = true
if (
reasoningSettings.enabled &&
reasoningSettings.mode === 'trace' &&
@@ -2003,11 +2116,97 @@ async function bareOsRunAgentSession(ctx, argv0, task, runOpts) {
)
}
if (completed) break
if (name === 'enter_plan_mode' || name === 'exit_plan_mode') {
messages.push({
role: 'user',
content:
name === 'enter_plan_mode'
? '[harness] PLAN MODE is now ON. Only write ~/.agent/plan.md until exit_plan_mode.'
: '[harness] PLAN MODE is now OFF. Mutating tools are allowed again.'
})
}
if (name === 'ask_user_question') {
try {
const parsed = JSON.parse(resultStr)
if (parsed && parsed.text) {
bareAgentWriteOut(
ctx,
stdout,
'\n' +
bareEditSgr('keyword', useColor) +
'Questions for you:\n' +
EDIT_ANSI_RESET +
String(parsed.text) +
'\n'
)
}
} catch {
/* ignore */
}
}
if (completed && !configRef.current.autonomous_active) break
}
if (sawTodoWrite) turnsSinceTodoWrite = 0
else turnsSinceTodoWrite++
if (typeof bareAgentTodoNudgeText === 'function' && typeof bareAgentLoadTodos === 'function') {
try {
const todosNow = paths.todos
? await bareAgentLoadTodos(ctx, paths.todos)
: []
const sumNow = bareAgentTodoSummarize(todosNow)
const nudge = bareAgentTodoNudgeText({
open: sumNow.open,
turnsSinceTodoWrite,
nudgeEnabled: configRef.current.todo_nudge_enabled !== false
})
if (nudge) {
messages.push({
role: 'user',
content: '[harness reminder] ' + nudge
})
}
} catch {
/* optional */
}
}
await bareAgentSaveHistory(ctx, paths.history, messages)
if (completed) {
if (configRef.current.autonomous_active) {
const gate = await bareAgentRunAutonomousGates({
ctx,
paths,
signal: masterAbort.signal,
appendProgress,
home,
configRef,
manCacheRef,
onTaskComplete,
requiredChecks: autonomousSettings.requiredChecks
})
if (!gate.ok) {
completed = false
configRef.current.autonomous_status = 'needs_fixups'
configRef.current.autonomous_last_error =
'failed_checks:' + gate.failed.join(',')
await bareAgentSaveConfig(ctx, paths, configRef.current)
appendProgress('autonomous checks failed ' + gate.failed.join(','))
messages.push({
role: 'user',
content:
'Autonomous completion gates failed for checks: ' +
gate.failed.join(', ') +
'. Fix the issues, rerun required checks, and only call task_complete when all pass.'
})
continue
}
configRef.current.autonomous_active = false
configRef.current.autonomous_status = 'completed'
configRef.current.autonomous_last_error = ''
await bareAgentSaveConfig(ctx, paths, configRef.current)
appendProgress('autonomous completion gates passed')
}
bareAgentWriteOut(
ctx,
stdout,
@@ -46,7 +46,7 @@
"DISCORD_ENV_FILE / BARE_OS_DISCORD_ENV_FILE — path to a .env file. On the host this may be a host filesystem path (booter reads it). In the guest it is a VFS path. Preferred guest path: ~/.discord/.env.",
"BARE_OS_DISCORD_INITD — set 0 / false to never register the bare-os-discord systemctl unit, even when ~/.discord/.env exists.",
"DISCORD_GUILD_ID — optional guild for slash-command registration.",
"DISCORD_ID_WHITELIST — comma-separated Discord user ids allowed to use the bot (slash, autocomplete, buttons, selects, modals, and channel ping). Unset or empty still allows guild-installed commands in a server, but user-install / DM / private-channel use is always denied. A user not on a non-empty list is denied (ephemeral reply) in every install context.",
"DISCORD_ID_WHITELIST — comma-separated Discord user ids allowed to use the bot (slash, autocomplete, buttons, selects, modals, and channel ping). Read from the session, ~/.discord/.env, ~/.discord.env, ~/discord.env, ~/.env, and ./.env. Unset or empty still allows guild-installed commands in a server, but user-install / DM / private-channel use is always denied. A user not on a non-empty list is denied (ephemeral reply) in every install context.",
"DISCORD_USER_INSTALL / BARE_OS_DISCORD_USER_INSTALL — user-installable profile app (default on). Set 0 / false / off for a guild-only bot.",
"DISCORD_MESSAGE_CONTENT / BARE_OS_DISCORD_MESSAGE_CONTENT — set 1 to request Message Content Intent (same as --message-content).",
"DISCORD_DEBUG / BARE_OS_DISCORD_DEBUG — set 1 to print discord.js debug lines.",
+1 -1
View File
@@ -7,7 +7,7 @@
"description": "Build JS /bin utilities for bare-operating-system (concat + stage to kernel/)",
"scripts": {
"build": "node ./scripts/ensure-man-pages.mjs && node ./build.mjs",
"test": "node ./test/clear-sequence.test.mjs && node ./test/init-discord.test.mjs && node ./test/help-bin-list.test.mjs && node ./test/whois-rdap.test.mjs && node ./test/edit-key-parse.test.mjs && node ./test/edit-teardown.test.mjs && node ./test/edit-tui-sdk.test.mjs && node ./test/baresay-say-bundle.test.mjs && node ./test/baretop-bundle.test.mjs && node ./test/baretop-tui-sdk.test.mjs && node ./test/baretop-fixture.test.mjs && node ./test/baretop-compose.test.mjs && node ./test/baretop-incremental.test.mjs && node ./test/baretop-interaction-latency.test.mjs && node ./test/baretop-missing-signals.test.mjs && node ./test/baretop-stress-snapshot.test.mjs && node ./test/baretop-ui-helpers.test.mjs && node ./test/baretop-perf-baseline.test.mjs && node ./test/irc-parse.test.mjs && node ./test/irc-client.test.mjs && node ./test/irc-dial.test.mjs && node ./test/irc-commands.test.mjs && node ./test/irc-tui-sdk.test.mjs && node ./test/summon-engine.test.mjs && node ./test/summon-tui-sdk.test.mjs && node ./test/posix-test-int-compare.test.mjs && node ./test/posix-test-bracket-argv.test.mjs && node ./test/posix-utils-edge.test.mjs && node ./test/posix-golden-issue7.test.mjs && node ./test/getconf-pathconf-union-mirror.test.mjs && node ./test/getconf-posix-shm.test.mjs && node ./test/awk-sed-posix-smoke.test.mjs && node ./test/xattr-acl-utils.test.mjs && node ./test/xcu-issue7-sweep.test.mjs && node ./test/expand-tab-stops.test.mjs && node ./test/pkg-swarm-index-pathcap.test.mjs && node ./test/sha224-sha384-sum.test.mjs && node ./test/hardening.test.mjs && node ./test/agent-sse-parse.test.mjs && node ./test/agent-web-fetch.test.mjs && node ./test/agent-helpers.test.mjs && node ./test/agent-tools-run-command.test.mjs && node ./test/agent-workspace.test.mjs && node ./test/agent-skills.test.mjs && node ./test/agent-config-surface.test.mjs && node ./test/agent-trim-ctx.test.mjs && node ./test/agent-qvac.test.mjs && node ./test/telnet-protocol.test.mjs && node ./test/telnet-cli.test.mjs && node ./test/login.test.mjs && node ./test/p2p-suite-bundles.test.mjs && node ./test/p2p-suite-tui-sdk.test.mjs && node ./test/chat-tui-sdk.test.mjs && node ./test/swarmtop-peer-visibility.test.mjs",
"test": "node ./test/clear-sequence.test.mjs && node ./test/init-discord.test.mjs && node ./test/help-bin-list.test.mjs && node ./test/whois-rdap.test.mjs && node ./test/edit-key-parse.test.mjs && node ./test/edit-teardown.test.mjs && node ./test/edit-tui-sdk.test.mjs && node ./test/baresay-say-bundle.test.mjs && node ./test/baretop-bundle.test.mjs && node ./test/baretop-tui-sdk.test.mjs && node ./test/baretop-fixture.test.mjs && node ./test/baretop-compose.test.mjs && node ./test/baretop-incremental.test.mjs && node ./test/baretop-interaction-latency.test.mjs && node ./test/baretop-missing-signals.test.mjs && node ./test/baretop-stress-snapshot.test.mjs && node ./test/baretop-ui-helpers.test.mjs && node ./test/baretop-perf-baseline.test.mjs && node ./test/irc-parse.test.mjs && node ./test/irc-client.test.mjs && node ./test/irc-dial.test.mjs && node ./test/irc-commands.test.mjs && node ./test/irc-tui-sdk.test.mjs && node ./test/summon-engine.test.mjs && node ./test/summon-tui-sdk.test.mjs && node ./test/posix-test-int-compare.test.mjs && node ./test/posix-test-bracket-argv.test.mjs && node ./test/posix-utils-edge.test.mjs && node ./test/posix-golden-issue7.test.mjs && node ./test/getconf-pathconf-union-mirror.test.mjs && node ./test/getconf-posix-shm.test.mjs && node ./test/awk-sed-posix-smoke.test.mjs && node ./test/xattr-acl-utils.test.mjs && node ./test/xcu-issue7-sweep.test.mjs && node ./test/expand-tab-stops.test.mjs && node ./test/pkg-swarm-index-pathcap.test.mjs && node ./test/sha224-sha384-sum.test.mjs && node ./test/hardening.test.mjs && node ./test/agent-sse-parse.test.mjs && node ./test/agent-web-fetch.test.mjs && node ./test/agent-helpers.test.mjs && node ./test/agent-tools-run-command.test.mjs && node ./test/agent-workspace.test.mjs && node ./test/agent-skills.test.mjs && node ./test/agent-config-surface.test.mjs && node ./test/agent-grok-port.test.mjs && node ./test/agent-trim-ctx.test.mjs && node ./test/agent-qvac.test.mjs && node ./test/telnet-protocol.test.mjs && node ./test/telnet-cli.test.mjs && node ./test/login.test.mjs && node ./test/p2p-suite-bundles.test.mjs && node ./test/p2p-suite-tui-sdk.test.mjs && node ./test/chat-tui-sdk.test.mjs && node ./test/swarmtop-peer-visibility.test.mjs",
"perf:baretop": "node ./test/baretop-perf-baseline.test.mjs"
}
}
+55 -7
View File
@@ -9,7 +9,7 @@ async function run(ctx, argv) {
ctx.console.log(
'usage: ' +
argv0 +
' [--setup | --config | --reset] YOUR_REQUEST_HERE\n' +
' [--setup | --config | --reset | --auto] YOUR_REQUEST_HERE\n' +
' ' +
argv0 +
' --setup\n' +
@@ -21,6 +21,12 @@ async function run(ctx, argv) {
' --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' +
@@ -28,6 +34,8 @@ async function run(ctx, argv) {
'Configuration: ~/.agent/config.json on your personal drive (created on first run).\n' +
'Use --setup or --config to choose QVAC vs REST, model profile / API URL+key (plain TTY prompts).\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' +
' ' +
@@ -35,13 +43,13 @@ async function run(ctx, argv) {
' "summarize ~/README and list five files in /bin"\n' +
' ' +
argv0 +
' --auto "add /agent tests and run them"\n' +
' ' +
argv0 +
' --setup\n' +
' ' +
argv0 +
' --config\n' +
' ' +
argv0 +
' --reset\n' +
' --status\n' +
'\n' +
'See man agent.'
)
@@ -51,15 +59,47 @@ async function run(ctx, argv) {
let setupFlag = false
let resetFlag = false
let autoFlag = false
let statusFlag = false
/** @type {string[]} */
const rest = []
for (let i = 0; i < args.length; i++) {
const a = args[i]
if (a === '--setup' || a === '--config') setupFlag = true
else if (a === '--reset') resetFlag = true
else if (a === '--auto' || a === '--autonomous') autoFlag = true
else if (a === '--status') statusFlag = true
else rest.push(a)
}
if (statusFlag) {
const home = bareAgentResolveHome(ctx)
const paths = bareAgentPaths(home)
const loaded = await bareAgentLoadOrCreateConfig(ctx, paths)
const cfg = loaded && loaded.config ? loaded.config : loaded
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
ctx.console.log(
[
'agent status',
' backend: ' + String((cfg && (cfg.backend || cfg.provider)) || 'qvac'),
' 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))
].join('\n')
)
ctx.exitCode = 0
return
}
const wantReset =
resetFlag || (rest.length === 1 && rest[0] === 'reset')
if (wantReset) {
@@ -79,7 +119,7 @@ async function run(ctx, argv) {
const task = rest.join(' ').trim()
if (!task && !setupFlag) {
ctx.console.error(argv0 + ': missing task (or use --setup / --config)')
ctx.console.error(argv0 + ': missing task (or use --setup / --config / --auto GOAL)')
ctx.exitCode = 1
return
}
@@ -89,7 +129,15 @@ async function run(ctx, argv) {
return
}
if (autoFlag && !task) {
ctx.console.error(argv0 + ': --auto requires a goal string')
ctx.exitCode = 1
return
}
await bareOsRunAgentSession(ctx, argv0, task, {
setupFlag
setupFlag,
autonomous: autoFlag,
autonomousGoal: autoFlag ? task : ''
})
}
+17 -3
View File
@@ -236,12 +236,26 @@ function discordApplyDotEnvExtras(ctx, parsed) {
'DISCORD_ID_WHITELIST',
'DISCORD_USER_INSTALL'
]
const maps = [ctx.env]
if (
ctx.vfs &&
ctx.vfs.env &&
typeof ctx.vfs.env === 'object' &&
ctx.vfs.env !== ctx.env
) {
maps.push(ctx.vfs.env)
}
const alias = {
DISCORD_ID_WHITELIST:
parsed.DISCORD_ID_WHITELIST || parsed.BARE_OS_DISCORD_ID_WHITELIST
}
for (let i = 0; i < extras.length; i++) {
const k = extras[i]
const v = parsed[k]
const v = k === 'DISCORD_ID_WHITELIST' ? alias.DISCORD_ID_WHITELIST : parsed[k]
if (v == null || String(v).trim() === '') continue
if (!ctx.env[k] || String(ctx.env[k]).trim() === '') {
ctx.env[k] = String(v).trim()
const next = String(v).trim()
for (let j = 0; j < maps.length; j++) {
if (!maps[j][k] || String(maps[j][k]).trim() === '') maps[j][k] = next
}
}
}
@@ -84,7 +84,9 @@ test('agent-state exposes autonomous mode config keys', async (t) => {
'autonomous_stop_requested',
'autonomous_goal',
'autonomous_status',
'autonomous_last_error'
'autonomous_last_error',
'plan_mode_active',
'todo_nudge_enabled'
]) {
t.ok(
STATE.includes(key),
@@ -120,7 +122,15 @@ test('agent-tools exposes agent ops tools', async (t) => {
'autonomous_run_status',
'autonomous_run_stop',
'verification_hints',
'runtime_diagnostic_bundle'
'runtime_diagnostic_bundle',
'search_replace',
'glob_files',
'todo_write',
'memory_search',
'memory_get',
'enter_plan_mode',
'exit_plan_mode',
'ask_user_question'
]) {
t.ok(
TOOLS.includes("name: '" + toolName + "'"),
@@ -149,6 +159,12 @@ test('agent-tui contains autonomous loop controls and completion gates', async (
t.ok(TUI.includes('autonomous run stopped (timebox expired)'))
t.ok(TUI.includes('Autonomous completion gates failed for checks'))
t.ok(TUI.includes('autonomous completion gates passed'))
t.ok(TUI.includes('bareAgentAutonomousShouldContinue'))
t.ok(TUI.includes('bareAgentAutonomousContinuationPrompt'))
t.ok(TUI.includes('bareAgentBeginAutonomousRun'))
t.ok(TUI.includes('internalGate: true'))
t.ok(TUI.includes('enter_plan_mode'))
t.ok(TUI.includes('todo_write'))
})
test('agent-tui embeds operating contract appendix', async (t) => {
@@ -0,0 +1,388 @@
/**
* Guest-safe Grok Build ports: glob, todos, plan mode, memory, hooks, edits.
*/
import test from 'brittle'
import { readFileSync } from 'node:fs'
import vm from 'node:vm'
const STATE_SRC = readFileSync(new URL('../lib/agent-state.js', import.meta.url), 'utf8')
const PORT_SRC = readFileSync(
new URL('../lib/agent-grok-port.js', import.meta.url),
'utf8'
)
const TOOLS_SRC = readFileSync(new URL('../lib/agent-tools.js', import.meta.url), 'utf8')
function loadPort() {
const sandbox = { TextDecoder, TextEncoder, Uint8Array, console }
vm.createContext(sandbox)
vm.runInContext(PORT_SRC, sandbox, { filename: 'agent-grok-port.js' })
return sandbox
}
function loadDispatch() {
const sandbox = { TextDecoder, TextEncoder, Uint8Array, console }
vm.createContext(sandbox)
vm.runInContext(STATE_SRC, sandbox, { filename: 'agent-state.js' })
vm.runInContext(PORT_SRC, sandbox, { filename: 'agent-grok-port.js' })
vm.runInContext(TOOLS_SRC, sandbox, { filename: 'agent-tools.js' })
return sandbox
}
function makeVfs(initial) {
/** @type {Map<string, string>} */
const files = new Map(Object.entries(initial || {}))
/** @type {Set<string>} */
const dirs = new Set(['/'])
for (const p of files.keys()) {
let dir = String(p).replace(/\/[^/]+$/, '')
while (dir && dir !== '/') {
dirs.add(dir)
dir = dir.replace(/\/[^/]+$/, '')
}
}
const b4a = {
from(v) {
return new TextEncoder().encode(typeof v === 'string' ? v : String(v))
},
toString(v) {
return new TextDecoder().decode(v)
}
}
const vfs = {
async mkdir(p) {
dirs.add(String(p).replace(/\/+$/, '') || '/')
},
async readFile(p) {
if (!files.has(p)) throw new Error('enoent:' + p)
return b4a.from(files.get(p) || '')
},
async writeFile(p, buf) {
const txt = buf instanceof Uint8Array ? new TextDecoder().decode(buf) : String(buf)
files.set(p, txt)
},
async readdir(p) {
const root = String(p).replace(/\/+$/, '') || '/'
if (!dirs.has(root) && ![...files.keys()].some((k) => k.startsWith(root + '/'))) {
throw new Error('enoent:' + p)
}
const prefix = root === '/' ? '/' : root + '/'
const names = new Set()
for (const key of [...files.keys(), ...dirs]) {
if (key === root) continue
if (!String(key).startsWith(prefix)) continue
const rest = String(key).slice(prefix.length)
const name = rest.split('/')[0]
if (name) names.add(name)
}
return [...names]
},
async lstat(p) {
const n = String(p).replace(/\/+$/, '') || '/'
if (dirs.has(n)) return { isDirectory: () => true }
if (files.has(n)) return { isDirectory: () => false }
throw new Error('enoent:' + p)
}
}
return { vfs, files, dirs, b4a }
}
async function dispatch(s, o) {
const fn = /** @type {(arg: object) => Promise<string>} */ (s.bareAgentDispatchTool)
const raw = await fn({
ctx: o.ctx,
paths: o.paths,
toolName: o.toolName,
argsJson: JSON.stringify(o.args || {}),
configRef: o.configRef || { current: {} },
appendProgress: o.appendProgress || (() => {}),
home: o.home || '/home/guest',
signal: null,
onTaskComplete: o.onTaskComplete || (() => {})
})
return JSON.parse(raw)
}
test('glob ** and * match relative and basename paths', async (t) => {
const s = loadPort()
t.ok(s.bareAgentGlobMatch('src/foo.js', '**/*.js'))
t.ok(s.bareAgentGlobMatch('foo.js', '*.js'))
t.ok(s.bareAgentGlobMatch('src/foo.js', '*.js'))
t.absent(s.bareAgentGlobMatch('src/foo.md', '**/*.js'))
})
test('todo merge updates by id and summarize counts open items', async (t) => {
const s = loadPort()
const first = s.bareAgentTodoApply(
[
{ id: 'a', content: 'one', status: 'pending' },
{ id: 'b', content: 'two', status: 'in_progress' }
],
{ merge: false },
[]
)
const next = s.bareAgentTodoApply(
[{ id: 'a', status: 'completed' }],
{ merge: true },
first
)
t.is(next.length, 2)
t.is(next[0].status, 'completed')
t.is(next[0].content, 'one')
const sum = s.bareAgentTodoSummarize(next)
t.is(sum.open, 1)
t.is(sum.completed, 1)
})
test('todo_write rejects empty and duplicate ids', async (t) => {
const s = loadPort()
t.exception(() => s.bareAgentTodoApply([{ content: 'x' }], { merge: false }, []))
t.exception(() =>
s.bareAgentTodoApply(
[
{ id: 'a', content: 'one' },
{ id: 'a', content: 'two' }
],
{ merge: false },
[]
)
)
})
test('search_replace requires unique old_string unless replace_all', async (t) => {
const s = loadPort()
const text = 'aaa\nbbb\naaa\n'
const miss = s.bareAgentSearchReplaceApply(text, 'zzz', 'q', false)
t.absent(miss.ok)
t.is(miss.error, 'old_string not found')
const many = s.bareAgentSearchReplaceApply(text, 'aaa', 'ccc', false)
t.absent(many.ok)
t.is(many.error, 'old_string not unique')
t.is(many.count, 2)
const all = s.bareAgentSearchReplaceApply(text, 'aaa', 'ccc', true)
t.ok(all.ok)
t.is(all.replacements, 2)
t.ok(String(all.next).includes('ccc\nbbb\nccc'))
})
test('slice file lines is 1-based and numbered', async (t) => {
const s = loadPort()
const sliced = s.bareAgentSliceFileLines('a\nb\nc\nd\n', { offset: 2, limit: 2 })
t.is(sliced.start_line, 2)
t.is(sliced.end_line, 3)
t.is(sliced.total_lines, 4)
t.ok(sliced.truncated)
t.is(sliced.content, '2→b\n3→c')
})
test('plan mode allows reads and plan.md writes only', async (t) => {
const s = loadPort()
const plan = '/home/guest/.agent/plan.md'
t.ok(s.bareAgentPlanModeToolAllowed('read_file', { path: '/etc/hosts' }, { plan }))
t.ok(s.bareAgentPlanModeToolAllowed('write_file', { path: plan }, { plan }))
t.absent(
s.bareAgentPlanModeToolAllowed('write_file', { path: '/home/guest/x.js' }, { plan })
)
t.ok(s.bareAgentPlanModeToolAllowed('todo_write', {}, { plan }))
})
test('todo nudge appears after idle turns with open items', async (t) => {
const s = loadPort()
t.is(s.bareAgentTodoNudgeText({ open: 2, turnsSinceTodoWrite: 2 }), '')
t.ok(s.bareAgentTodoNudgeText({ open: 2, turnsSinceTodoWrite: 3 }).includes('Open todos'))
t.ok(
s.bareAgentTodoNudgeText({ open: 0, turnsSinceTodoWrite: 5 }).includes('todo_write')
)
t.is(s.bareAgentTodoNudgeText({ open: 2, turnsSinceTodoWrite: 9, nudgeEnabled: false }), '')
})
test('pre-tool hook denies matching tool + regex', async (t) => {
const s = loadPort()
const reason = s.bareAgentHookDenies(
{
event: 'PreToolUse',
tools: ['run_command'],
deny_regex: 'rm\\s+-rf',
reason: 'no recursive delete'
},
'run_command',
{ command: 'rm -rf /tmp/x' }
)
t.is(reason, 'no recursive delete')
t.is(
s.bareAgentHookDenies(
{ event: 'PreToolUse', tools: ['run_command'], deny_regex: 'rm\\s+-rf' },
'read_file',
{ path: '/tmp/x' }
),
''
)
})
test('memory score ranks files that contain more query tokens', async (t) => {
const s = loadPort()
const tokens = s.bareAgentMemoryTokens('holesail discord whitelist')
t.ok(tokens.includes('holesail'))
t.ok(
s.bareAgentMemoryScore('holesail discord whitelist notes', tokens) >
s.bareAgentMemoryScore('unrelated pear notes', tokens)
)
})
test('walk-up discovers AGENTS.md and .grok/rules', async (t) => {
const s = loadPort()
const { vfs, b4a } = makeVfs({
'/home/guest/proj/src/AGENTS.md': 'leaf',
'/home/guest/proj/AGENTS.md': 'mid',
'/home/guest/proj/.grok/rules/style.md': 'rule'
})
await vfs.mkdir('/home/guest/proj/src')
await vfs.mkdir('/home/guest/proj/.grok/rules')
const found = await s.bareAgentDiscoverAgentsMdPaths(
{ vfs, b4a },
'/home/guest/proj/src',
8
)
t.ok(found.includes('/home/guest/proj/src/AGENTS.md'))
t.ok(found.includes('/home/guest/proj/AGENTS.md'))
t.ok(found.includes('/home/guest/proj/.grok/rules/style.md'))
})
test('dispatch glob_files / todo_write / plan_mode / unique edit', async (t) => {
const s = loadDispatch()
const { vfs, files, b4a } = makeVfs({
'/home/guest/src/a.js': 'const x = 1\nconst y = 1\n',
'/home/guest/src/b.md': '# hi\n',
'/home/guest/.agent/workspace/memory/notes.md': 'remember holesail keys',
'/home/guest/.agent/workspace/MEMORY.md': 'top memory holesail'
})
await vfs.mkdir('/home/guest/src')
await vfs.mkdir('/home/guest/.agent')
await vfs.mkdir('/home/guest/.agent/workspace')
await vfs.mkdir('/home/guest/.agent/workspace/memory')
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',
hooks: '/home/guest/.agent/hooks',
ask: '/home/guest/.agent/ask.json',
workspace: '/home/guest/.agent/workspace',
workspaceMemory: '/home/guest/.agent/workspace/memory',
compact: '/home/guest/.agent/compact.md'
}
const configRef = { current: { plan_mode_active: false } }
const globbed = await dispatch(s, {
ctx,
paths,
toolName: 'glob_files',
args: { pattern: '**/*.js', root: '/home/guest' },
configRef,
home: '/home/guest'
})
t.ok(globbed.ok)
t.ok(globbed.files.includes('/home/guest/src/a.js'))
const todos = await dispatch(s, {
ctx,
paths,
toolName: 'todo_write',
args: {
merge: false,
todos: [{ id: 'g1', content: 'port glob', status: 'in_progress' }]
},
configRef,
home: '/home/guest'
})
t.ok(todos.ok)
t.is(todos.summary.open, 1)
t.ok(String(files.get(paths.todos) || '').includes('port glob'))
const mem = await dispatch(s, {
ctx,
paths,
toolName: 'memory_search',
args: { query: 'holesail' },
configRef,
home: '/home/guest'
})
t.ok(mem.ok)
t.ok(Array.isArray(mem.hits) && mem.hits.length >= 1)
configRef.current.plan_mode_active = true
const denied = await dispatch(s, {
ctx,
paths,
toolName: 'write_file',
args: { path: '/home/guest/src/a.js', content: 'nope' },
configRef,
home: '/home/guest'
})
t.absent(denied.ok)
t.is(denied.error, 'plan_mode_readonly')
const planned = await dispatch(s, {
ctx,
paths,
toolName: 'enter_plan_mode',
args: { note: 'draft first' },
configRef,
home: '/home/guest'
})
t.ok(planned.ok)
t.ok(String(files.get(paths.plan) || '').includes('draft first'))
const exited = await dispatch(s, {
ctx,
paths,
toolName: 'exit_plan_mode',
args: { summary: 'ready' },
configRef,
home: '/home/guest'
})
t.ok(exited.ok)
t.absent(configRef.current.plan_mode_active)
const uniqueFail = await dispatch(s, {
ctx,
paths,
toolName: 'search_replace',
args: {
path: '/home/guest/src/a.js',
old_string: 'const',
new_string: 'let'
},
configRef,
home: '/home/guest'
})
t.absent(uniqueFail.ok)
t.is(uniqueFail.error, 'old_string not unique')
const replaced = await dispatch(s, {
ctx,
paths,
toolName: 'search_replace',
args: {
path: '/home/guest/src/a.js',
old_string: 'const x = 1',
new_string: 'const x = 2'
},
configRef,
home: '/home/guest'
})
t.ok(replaced.ok)
t.is(replaced.replacements, 1)
t.ok(String(files.get('/home/guest/src/a.js') || '').includes('const x = 2'))
const sliced = await dispatch(s, {
ctx,
paths,
toolName: 'read_file',
args: { path: '/home/guest/src/a.js', offset: 1, limit: 1 },
configRef,
home: '/home/guest'
})
t.ok(sliced.ok)
t.ok(String(sliced.content || '').startsWith('1→'))
})
@@ -82,20 +82,26 @@ test('advanced compaction summarizes older turns instead of dropping blindly', a
t.ok(packed.meta.compacted)
t.ok(
packed.meta.droppedGroups > 0 ||
packed.meta.tiers.includes('full-replace') ||
packed.meta.tiers.includes('rolling-summary') ||
packed.meta.tiers.some((x) => String(x).startsWith('tighten-keep'))
)
t.ok(packed.messages.some((m) => bareAgentIsCompactionMessage(m)))
// Recent user question survives.
const last = packed.messages[packed.messages.length - 1]
t.ok(
String(/** @type {any} */ (last).content || '').includes('last files')
)
// Summary mentions tools or paths.
// Last user question survives (Grok assemble keeps it as a wrapped query).
const blob = packed.messages
.map((m) => String(/** @type {any} */ (m).content || ''))
.join('\n')
t.ok(/last files/.test(blob))
t.ok(/<user_query>/.test(blob))
const summary = packed.messages.find((m) => bareAgentIsCompactionMessage(m))
t.ok(summary)
t.ok(
/run_command|\/tmp\/file|assistant|tool:/i.test(
/Primary Request|run_command|\/tmp\/file|Tool Usage/i.test(
String(/** @type {any} */ (summary).content || '')
)
)
t.ok(
/This session is being continued from a previous conversation/.test(
String(/** @type {any} */ (summary).content || '')
)
)
@@ -144,3 +150,81 @@ test('fat tool payloads shrink even under soft budget', async (t) => {
t.ok(packed.meta.afterTokens < before)
t.ok(packed.meta.tiers.includes('shrink-tools') || packed.meta.compacted)
})
test('full-replace assemble keeps last user query and structured summary', async (t) => {
const {
bareAgentAssembleCompactedHistory,
bareAgentBuildStructuredSummary,
bareAgentIsDegenerateSummary,
bareAgentWrapUserQuery,
bareAgentFormatCompactSummaryContent,
bareAgentBeginAutonomousRun,
bareAgentAutonomousShouldContinue,
bareAgentAutonomousContinuationPrompt
} = load()
const assembled = bareAgentAssembleCompactedHistory({
system: { role: 'system', content: 'sys' },
lastUserQuery: 'fix the agent runner',
recent: [{ role: 'assistant', content: 'looking' }],
summaryText: bareAgentFormatCompactSummaryContent(
'1. Primary Request and Intent:\n fix the agent runner\n'
),
reminder: '<system-reminder>\nAutonomous run: fix the agent runner\n</system-reminder>'
})
t.is(/** @type {any} */ (assembled[0]).role, 'system')
t.ok(
String(/** @type {any} */ (assembled[1]).content).includes('<user_query>')
)
t.ok(
String(/** @type {any} */ (assembled[1]).content).includes('fix the agent runner')
)
t.is(/** @type {any} */ (assembled[2]).role, 'assistant')
t.ok(String(/** @type {any} */ (assembled[3]).content).includes('continued from a previous'))
t.ok(String(/** @type {any} */ (assembled[4]).content).includes('<system-reminder>'))
const structured = bareAgentBuildStructuredSummary(
[
[
{ role: 'user', content: 'inspect ~/notes.txt' },
{
role: 'tool',
name: 'read_file',
content: 'ENOENT missing file'
}
]
],
{ maxChars: 2000, perMsg: 80 }
)
t.ok(/1\. Primary Request/.test(structured))
t.ok(/3\. Tool Usage/.test(structured))
t.ok(/notes\.txt|read_file|ENOENT/.test(structured))
t.absent(bareAgentIsDegenerateSummary(structured + '\n'.repeat(5) + 'x'.repeat(80)))
t.ok(bareAgentIsDegenerateSummary('too short'))
t.ok(bareAgentWrapUserQuery('hi').indexOf('<user_query>') === 0)
const started = bareAgentBeginAutonomousRun(
{ autonomous_mode_enabled: false, autonomous_active: false },
{ goal: 'ship compaction', maxRuntimeMs: 120000 }
)
t.ok(started.autonomous_mode_enabled)
t.ok(started.autonomous_active)
t.is(started.autonomous_goal, 'ship compaction')
t.ok(
bareAgentAutonomousShouldContinue(started, {
completed: false,
hasTools: false,
stopRequested: false
})
)
t.absent(
bareAgentAutonomousShouldContinue(started, {
completed: true,
hasTools: false
})
)
t.ok(
/AUTONOMOUS RUN still active/.test(
bareAgentAutonomousContinuationPrompt(started, { remainingMs: 90000 })
)
)
})