@@ -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',
|
||||
|
||||
@@ -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 ''
|
||||
}
|
||||
@@ -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') {
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 : ''
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user