total-recall-brain 3.44.0 → 3.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/total-recall.mjs +6 -4
- package/docs/ARCHITECTURE.md +10 -7
- package/docs/reference/cli-reference.md +13 -8
- package/frontend/dist/assets/index-DACh1dhx.js +5613 -0
- package/frontend/dist/index.html +1 -1
- package/frontend/src/App.tsx +2 -2
- package/frontend/src/pages/PluginsPage.tsx +1 -1
- package/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +7 -7
- package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +20 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-store.mjs +12 -1
- package/scaffold/.agent/skills/total-recall/plugins/automations/BRICK.md +7 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/LICENSE +21 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/README.md +66 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/adapters.mjs +231 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/capability/index.mjs +16 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/cli.mjs +121 -0
- package/{src/core/plugin-cron.mjs → scaffold/.agent/skills/total-recall/plugins/automations/cron.mjs} +1 -3
- package/scaffold/.agent/skills/total-recall/plugins/automations/inventory.mjs +88 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/package-lock.json +7661 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/package.json +35 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/plugin.json +167 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/scheduler.mjs +246 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/ui/DESIGN.md +42 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/ui/automations.js +2 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/ui/card.js +273 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/commands/run-core.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/check.mjs +57 -5
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/report-data.mjs +40 -14
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/report.mjs +15 -40
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +7 -7
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +7 -7
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +7 -7
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/git/README.md +112 -0
- package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/cli.mjs +135 -23
- package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/package-lock.json +7 -7
- package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/package.json +2 -1
- package/scaffold/.agent/skills/total-recall/plugins/git/plugin.json +161 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/context.mjs +94 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/phase3-spec-support.mjs +50 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/runtime.mjs +40 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/spec-support.mjs +51 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/_support.mjs +195 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/auth.mjs +201 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/bisect.mjs +40 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/cli-meta.mjs +159 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/commit-memory.mjs +30 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/commit.mjs +46 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/fleet.mjs +156 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/force-push.mjs +58 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/group.mjs +119 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/handoff.mjs +59 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/message.mjs +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/resolve.mjs +60 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/rules.mjs +81 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/secret-firewall.mjs +66 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/undo.mjs +132 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/why.mjs +16 -0
- package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/ui/card.js +2 -2
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +7 -7
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/README.md +25 -9
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/BRICK.md +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/index.mjs +18 -6
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +124 -24
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +7 -7
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +2 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +147 -14
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/skills/meta-harness/SKILL.md +17 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/adoption.mjs +2 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/alerts.mjs +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/cascade.mjs +92 -18
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/control.mjs +145 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +42 -21
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch-request.mjs +4 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch.mjs +33 -9
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/documentation.mjs +55 -11
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/executor.mjs +26 -4
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/hermes.mjs +29 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/index.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/included-usage.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/jobs.mjs +84 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/learner.mjs +3 -12
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/maintenance.mjs +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/mcp-server.mjs +228 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/process-manager.mjs +25 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/reasoning.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/remote-operation.mjs +5 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/review.mjs +34 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/reviews.mjs +25 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/claude.mjs +319 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/codex.mjs +387 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/result.mjs +67 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/resume.mjs +373 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/run.mjs +223 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session-jobs.mjs +72 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/setup.mjs +6 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/termination.mjs +16 -12
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/threads.mjs +32 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/usage-queue.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/verify-ssss.mjs +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +285 -40
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worktree.mjs +76 -9
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/DESIGN.md +5 -5
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +47 -18
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +7 -7
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh-diagnostics.mjs +44 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +120 -113
- package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +7 -7
- package/scaffold/.agent/skills/total-recall/plugins/security/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/references/operations.md +1 -1
- package/src/cli/agent.mjs +1 -1
- package/src/cli/command.mjs +41 -2
- package/src/cli/dream.mjs +93 -69
- package/src/cli/harness.mjs +4 -4
- package/src/cli/help.mjs +1 -1
- package/src/cli/init.mjs +0 -25
- package/src/cli/lessons.mjs +51 -0
- package/src/cli/memory-plugin.mjs +1 -1
- package/src/cli/plugin/copies.mjs +64 -14
- package/src/cli/plugin/create.mjs +6 -7
- package/src/cli/plugin/index.mjs +11 -7
- package/src/cli/plugin/install.mjs +10 -15
- package/src/cli/plugin/remove.mjs +1 -1
- package/src/cli/plugin/search.mjs +2 -2
- package/src/cli/preflight.mjs +19 -6
- package/src/cli/proposals.mjs +1 -1
- package/src/cli/recall.mjs +66 -23
- package/src/cli/remember.mjs +3 -14
- package/src/cli/server.mjs +18 -0
- package/src/cli/startup.mjs +1 -0
- package/src/cli/task.mjs +9 -2
- package/src/cli/update.mjs +6 -2
- package/src/core/access-ledger.mjs +78 -0
- package/src/core/agent-manager.mjs +2 -2
- package/src/core/brain-locate.mjs +15 -0
- package/src/core/brain-updates.mjs +31 -11
- package/src/core/claude-hooks.mjs +3 -1
- package/src/core/cleanup-task.mjs +15 -0
- package/src/core/cli-commands.mjs +2 -0
- package/src/core/cli-maintenance.mjs +4 -1
- package/src/core/cli-semantic-index.mjs +23 -3
- package/src/core/context-compiler.mjs +20 -0
- package/src/core/daemon-loop.mjs +39 -62
- package/src/core/default-plugins.mjs +1 -1
- package/src/core/dream.mjs +33 -105
- package/src/core/ensure-dependencies.mjs +148 -0
- package/src/core/fast-recall.mjs +2 -2
- package/src/core/field-evaluation.mjs +23 -0
- package/src/core/gate-mode.mjs +1 -1
- package/src/core/harness-hooks.mjs +14 -2
- package/src/core/lesson-hooks.mjs +74 -0
- package/src/core/lesson-memory.mjs +24 -0
- package/src/core/lesson-sessions.mjs +52 -0
- package/src/core/lessons.mjs +96 -0
- package/src/core/meta-harness.mjs +2 -2
- package/src/core/optimizer.mjs +66 -165
- package/src/core/plugin-contracts/card.mjs +3 -0
- package/src/core/plugin-loader.mjs +12 -3
- package/src/core/plugin-runtime.mjs +40 -1
- package/src/core/plugin-store.mjs +35 -23
- package/src/core/post-mortem.mjs +52 -36
- package/src/core/preflight-gate.mjs +23 -2
- package/src/core/proposal-applier.mjs +31 -2
- package/src/core/repo-sync.mjs +4 -6
- package/src/core/required-plugins.mjs +26 -4
- package/src/core/rule-length.mjs +23 -0
- package/src/core/runtime.mjs +1 -1
- package/src/core/scheduler.mjs +35 -43
- package/src/core/schema.mjs +1 -2
- package/src/core/search.mjs +4 -3
- package/src/core/server-service.mjs +148 -0
- package/src/core/session-watcher.mjs +19 -1
- package/src/core/skill-gap.mjs +16 -0
- package/src/core/ssss-kernel-bridge.mjs +32 -4
- package/src/core/startup-health.mjs +6 -1
- package/src/core/surface.mjs +65 -54
- package/src/core/task-executors.mjs +54 -106
- package/src/core/validated-write.mjs +1 -0
- package/src/core/vector-field.mjs +134 -192
- package/src/server/invoke-core-child.mjs +24 -0
- package/src/server/invoke-core-operations.mjs +102 -0
- package/src/server/route-manifest.json +0 -56
- package/src/server/routes/brains.mjs +0 -18
- package/src/server/routes/context.mjs +0 -41
- package/src/server/routes/invoke.mjs +26 -1
- package/src/server/routes/ssss.mjs +0 -169
- package/src/server/routes/system.mjs +0 -15
- package/src/server/routes/tasks.mjs +0 -26
- package/src/server/routes/vault.mjs +2 -23
- package/frontend/dist/assets/index-DD4qONvH.js +0 -5607
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/README.md +0 -53
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/plugin.json +0 -129
- package/src/core/append-log.mjs +0 -131
- package/src/core/plugin-tasks.mjs +0 -173
- package/templates/onboarding-interview.md +0 -82
- /package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/LICENSE +0 -0
- /package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/ui/DESIGN.md +0 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/** Job commands and events. The inbox is append-only; ack events are its cursor. */
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { execFile } from 'node:child_process';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
|
+
import { configDir } from './paths.mjs';
|
|
8
|
+
import { resolveJob } from './jobs.mjs';
|
|
9
|
+
|
|
10
|
+
const execute = promisify(execFile);
|
|
11
|
+
export function jobPaths(job) {
|
|
12
|
+
const id = typeof job === 'string' ? job : job?.id || job?.job;
|
|
13
|
+
if (typeof id !== 'string' || !/^[a-z0-9-]+$/.test(id)) throw new Error('Invalid job ID.');
|
|
14
|
+
const base = path.join(configDir(), 'jobs', id);
|
|
15
|
+
return { id, inbox: `${base}.inbox.jsonl`, events: `${base}.events.jsonl` };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function readLines(file) {
|
|
19
|
+
let data;
|
|
20
|
+
try { data = fs.readFileSync(file, 'utf8'); }
|
|
21
|
+
catch (error) { if (error.code === 'ENOENT') return []; throw error; }
|
|
22
|
+
// An append from another process can still be incomplete.
|
|
23
|
+
return data.split('\n').slice(0, -1).filter(Boolean).map(line => JSON.parse(line));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function appendEvent(job, event) {
|
|
27
|
+
const { events } = jobPaths(job);
|
|
28
|
+
fs.mkdirSync(path.dirname(events), { recursive: true, mode: 0o700 });
|
|
29
|
+
const entry = { ...event, t: new Date().toISOString() };
|
|
30
|
+
fs.appendFileSync(events, JSON.stringify(entry) + '\n', { mode: 0o600 });
|
|
31
|
+
return entry;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Read all requests, including requests older than watch's --tail. */
|
|
35
|
+
export function waitingRequests(job) {
|
|
36
|
+
const file = typeof job === 'string' && job.endsWith('.events.jsonl') ? job : jobPaths(job).events;
|
|
37
|
+
const waiting = new Map();
|
|
38
|
+
for (const event of readLines(file)) {
|
|
39
|
+
if (['question', 'approval'].includes(event.kind) && event.requestId) waiting.set(event.requestId, event);
|
|
40
|
+
if (event.kind === 'ack' && event.requestId && event.ok !== false) waiting.delete(event.requestId);
|
|
41
|
+
}
|
|
42
|
+
return [...waiting.values()];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalizeCommand(command) {
|
|
46
|
+
if (!command || typeof command !== 'object' || Array.isArray(command)) throw new Error('A job command is required.');
|
|
47
|
+
const type = command.type || command.command || command.kind;
|
|
48
|
+
if (!['reply', 'approve', 'interrupt', 'goal', 'fork'].includes(type)) throw new Error('Unsupported job command.');
|
|
49
|
+
if (['reply', 'goal'].includes(type) && (typeof command.text !== 'string' || !command.text.trim())) throw new Error(`${type} requires text.`);
|
|
50
|
+
if (type === 'approve' && (typeof command.requestId !== 'string' || !command.requestId || !['allow', 'deny'].includes(command.value))) {
|
|
51
|
+
throw new Error('approve requires a request ID and allow or deny.');
|
|
52
|
+
}
|
|
53
|
+
if (command.requestId !== undefined && (typeof command.requestId !== 'string' || !command.requestId)) throw new Error('Invalid request ID.');
|
|
54
|
+
return { ...command, type, command: type, id: command.id || randomUUID(), t: new Date().toISOString() };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const quote = value => `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
58
|
+
|
|
59
|
+
/** Resolve prefixes through the same owner as cancel. Remote delivery never writes a local inbox. */
|
|
60
|
+
export async function appendCommand(target, command, { local = false, execFileFn = execute } = {}) {
|
|
61
|
+
const job = resolveJob(typeof target === 'string' ? target : target?.id || target?.job);
|
|
62
|
+
if (job.result) throw new Error(`Job ${job.id} has finished.`);
|
|
63
|
+
const entry = normalizeCommand(command);
|
|
64
|
+
const node = job.record?.node;
|
|
65
|
+
if (!local && node && node !== 'local') {
|
|
66
|
+
if (typeof node !== 'string' || node.startsWith('-') || /[\0\r\n]/.test(node)) throw new Error('Invalid job node.');
|
|
67
|
+
const args = [entry.type, job.record.remoteJob || job.id];
|
|
68
|
+
if (entry.type === 'approve') args.push(entry.requestId, entry.value);
|
|
69
|
+
else if (!['interrupt', 'fork'].includes(entry.type)) args.push(entry.text);
|
|
70
|
+
if (entry.type === 'fork' && entry.atTurn) args.push('--at', entry.atTurn);
|
|
71
|
+
args.push('--local-delivery', '--json');
|
|
72
|
+
const remote = `total-recall meta-harness ${args.map(quote).join(' ')}`;
|
|
73
|
+
const result = await execFileFn(process.env.TOTAL_RECALL_BIN || 'total-recall', ['mesh', 'exec', node, remote],
|
|
74
|
+
{ encoding: 'utf8', timeout: 60_000, maxBuffer: 1024 * 1024 });
|
|
75
|
+
if (result.exitCode && result.exitCode !== 0) throw new Error(result.stderr || 'Remote command failed.');
|
|
76
|
+
// mesh exec may wrap the remote result. Transport failures must not look like delivery.
|
|
77
|
+
let report;
|
|
78
|
+
try { report = JSON.parse(result.stdout); } catch { /* mesh can return plain command output */ }
|
|
79
|
+
if (report?.ok === false) throw new Error(report.error || 'Remote command failed.');
|
|
80
|
+
return { ok: true, job: job.id, node, remote: true, command: entry, ...(report ? { delivery: report } : {}) };
|
|
81
|
+
}
|
|
82
|
+
const waiting = waitingRequests(job.id);
|
|
83
|
+
const reserved = new Set(readLines(jobPaths(job.id).inbox).map(row => row.requestId).filter(Boolean));
|
|
84
|
+
const request = entry.requestId ? waiting.find(row => row.requestId === entry.requestId)
|
|
85
|
+
: entry.type === 'reply' ? waiting.find(row => row.kind === 'question' && !reserved.has(row.requestId)) : null;
|
|
86
|
+
if (entry.requestId && !request) throw new Error(`No waiting request matches ${entry.requestId}.`);
|
|
87
|
+
if (request && entry.type === 'approve' && request.kind !== 'approval') throw new Error('The request is not an approval.');
|
|
88
|
+
if (request && entry.type === 'reply' && request.kind !== 'question') throw new Error('The request is not a question.');
|
|
89
|
+
if (request) {
|
|
90
|
+
if (reserved.has(request.requestId)) throw new Error('The request already has an answer in the inbox.');
|
|
91
|
+
entry.requestId = request.requestId;
|
|
92
|
+
if (request.source === 'mcp') entry.recipient = 'mcp';
|
|
93
|
+
// A deferred question is answered by the session runner, which starts the next turn with it.
|
|
94
|
+
else if (request.source === 'deferred') entry.recipient = 'deferred';
|
|
95
|
+
}
|
|
96
|
+
const { inbox } = jobPaths(job.id);
|
|
97
|
+
fs.mkdirSync(path.dirname(inbox), { recursive: true, mode: 0o700 });
|
|
98
|
+
fs.appendFileSync(inbox, JSON.stringify(entry) + '\n', { mode: 0o600 });
|
|
99
|
+
return { ok: true, job: job.id, command: entry };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* One consumer handles commands in file order. A false return leaves a command for
|
|
104
|
+
* another consumer. MCP answers have their own recipient so a worker cannot also
|
|
105
|
+
* send them to the driver. Await close() before releasing the driver.
|
|
106
|
+
*/
|
|
107
|
+
export function tailInbox(job, onCommand, { pollMs = 50, recipient = 'driver', onError = () => {} } = {}) {
|
|
108
|
+
const { inbox, events } = jobPaths(job);
|
|
109
|
+
fs.mkdirSync(path.dirname(inbox), { recursive: true, mode: 0o700 });
|
|
110
|
+
fs.closeSync(fs.openSync(inbox, 'a', 0o600));
|
|
111
|
+
let closed = false, running = null;
|
|
112
|
+
const seen = new Set();
|
|
113
|
+
const poll = () => {
|
|
114
|
+
if (closed || running) return running;
|
|
115
|
+
running = (async () => {
|
|
116
|
+
const acknowledged = new Set(readLines(events).filter(row => row.kind === 'ack' && row.commandId).map(row => row.commandId));
|
|
117
|
+
for (const command of readLines(inbox)) {
|
|
118
|
+
if (closed) break;
|
|
119
|
+
if (!command.id || seen.has(command.id) || acknowledged.has(command.id) || (command.recipient || 'driver') !== recipient) continue;
|
|
120
|
+
let result;
|
|
121
|
+
try { result = await onCommand(command); }
|
|
122
|
+
catch (error) {
|
|
123
|
+
// A failed command is reported once; do not repeat an agent side effect.
|
|
124
|
+
seen.add(command.id);
|
|
125
|
+
appendEvent(job, { kind: 'ack', command: command.type || command.command, commandId: command.id,
|
|
126
|
+
requestId: command.requestId, ok: false, text: error.message });
|
|
127
|
+
onError(error);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (result === false || result?.handled === false) continue;
|
|
131
|
+
seen.add(command.id);
|
|
132
|
+
appendEvent(job, { kind: 'ack', command: command.type || command.command, commandId: command.id,
|
|
133
|
+
requestId: command.requestId, ok: true, text: `Handled ${command.type || command.command}`, ...(result?.via ? { via: result.via } : {}) });
|
|
134
|
+
}
|
|
135
|
+
})().catch(onError).finally(() => { running = null; });
|
|
136
|
+
return running;
|
|
137
|
+
};
|
|
138
|
+
const timer = setInterval(poll, pollMs);
|
|
139
|
+
poll();
|
|
140
|
+
const close = async () => { closed = true; clearInterval(timer); await running; };
|
|
141
|
+
// A function is convenient for existing watcher callers; properties support explicit lifecycle use.
|
|
142
|
+
close.close = close;
|
|
143
|
+
close.poll = poll;
|
|
144
|
+
return close;
|
|
145
|
+
}
|
|
@@ -16,6 +16,8 @@ import { isGitRepo, isDirty, headCommit, createWorktree, removeWorktree, commitA
|
|
|
16
16
|
import { lastJsonWith } from './json-reply.mjs';
|
|
17
17
|
import { runQualityGate } from './gate.mjs';
|
|
18
18
|
import { forwardRemoteOperation } from './remote-operation.mjs';
|
|
19
|
+
import { cancellationResult } from './worker.mjs';
|
|
20
|
+
import { sessionOptions } from './session/run.mjs';
|
|
19
21
|
|
|
20
22
|
const OPEN_THREADS = new Set();
|
|
21
23
|
const MAX_BLUEPRINT = 20000;
|
|
@@ -101,9 +103,12 @@ async function runPipeline(task, poolInfo, options) {
|
|
|
101
103
|
const branch = options.branch || `meta-harness/${thread.id}`;
|
|
102
104
|
const run = async (stage, harness, prompt, { workdir = cwd, permissions, role = stage.role } = {}) => {
|
|
103
105
|
stage.status = 'running';
|
|
104
|
-
const result = await dispatchWithReceipt({ agent: harness.id, node: 'local', cwd: workdir, contextCwd: cwd, contextTask: task, prompt,
|
|
105
|
-
|
|
106
|
+
const result = await dispatchWithReceipt({ agent: harness.id, node: 'local', cwd: workdir, contextCwd: cwd, contextTask: task, prompt,
|
|
107
|
+
...sessionOptions({ ...options, cwd }), goal: stage.role === 'Implementer' ? options.goal || task : prompt.slice(0, 4000),
|
|
108
|
+
permissions: permissions || stage.permissions, allowUnknownAuth: options.allowUnknownAuth === true }, { dispatchFn: options.dispatchFn, role, complexityScore: complexity.score });
|
|
109
|
+
stage.status = result.cancelled ? 'cancelled' : result.ok ? 'completed' : result.queued ? 'queued' : 'failed';
|
|
106
110
|
stage.output = result.response || result.error || null;
|
|
111
|
+
for (const key of ['goalCheck', 'structured', 'sessionId', 'progressFile', 'usage']) if (result[key] !== undefined) stage[key] = result[key];
|
|
107
112
|
stage.failureKind = result.ok ? undefined : result.failureKind;
|
|
108
113
|
if (result.queueId) stage.queueId = result.queueId;
|
|
109
114
|
appendThreadTurn(thread.id, { role: role.toLowerCase(), agent: harness.id, content: stage.output || '' });
|
|
@@ -128,17 +133,14 @@ async function runPipeline(task, poolInfo, options) {
|
|
|
128
133
|
const implementer = await run(stage('Implementer'), roles.implementer, `[ROLE: IMPLEMENTER]\nWork only in this checkout. Implement the task and its tests. Do not commit; the harness commits your changes.\nTask:\n${task}\n\nBlueprint:\n${blueprint}`, { workdir: implementerDir });
|
|
129
134
|
if (!implementer.ok) { ok = false; return finish(); }
|
|
130
135
|
commit = commitAll(implementerDir, `meta-harness: ${task.slice(0, 60)}\n\nImplementer: ${roles.implementer.id}`);
|
|
131
|
-
if (!commit)
|
|
132
|
-
Object.assign(stage('Implementer'), { status: 'failed', failureKind: 'agent', output: 'The Implementer reported success but changed no files.' });
|
|
133
|
-
await recordRunReceipt({ harness: roles.implementer.id, role: 'Implementer', task, success: false, failureKind: 'no-change', error: 'The Implementer reported success but changed no files.' });
|
|
134
|
-
ok = false; return finish();
|
|
135
|
-
}
|
|
136
|
+
if (!commit) commit = baseCommit;
|
|
136
137
|
stage('Implementer').commit = commit;
|
|
137
138
|
updateThread(thread.id, { branch, worktreePath: null });
|
|
138
139
|
|
|
139
140
|
if (options.gate) {
|
|
140
141
|
linkDependencies(cwd, implementerDir);
|
|
141
142
|
gate = await (options.gateFn || runQualityGate)(implementerDir, options.gateOptions);
|
|
143
|
+
if (cancellationResult()) { ok = false; return finish(); }
|
|
142
144
|
if (!gate.passed) { ok = false; stage('Verifier').status = 'skipped'; stage('Verifier').output = `Quality gate did not pass: ${gate.summary}`; return finish(); }
|
|
143
145
|
}
|
|
144
146
|
|
|
@@ -165,16 +167,24 @@ async function runPipeline(task, poolInfo, options) {
|
|
|
165
167
|
return finish();
|
|
166
168
|
} finally {
|
|
167
169
|
removeWorktree(cwd, implementerDir);
|
|
168
|
-
if (implementerDir && (!commit || !ok)) deleteBranch(cwd, branch);
|
|
170
|
+
if (implementerDir && (!commit || (!ok && !cancellationResult()))) deleteBranch(cwd, branch);
|
|
169
171
|
}
|
|
170
172
|
|
|
171
173
|
function finish() {
|
|
174
|
+
const cancelled = cancellationResult();
|
|
175
|
+
if (cancelled) {
|
|
176
|
+
ok = false;
|
|
177
|
+
if (implementerDir) commit = commitAll(implementerDir, 'meta-harness: unfinished pipeline from a cancelled job') || commit;
|
|
178
|
+
}
|
|
172
179
|
return (async () => {
|
|
173
180
|
for (const item of stages) if (item.status === 'planned') item.status = 'skipped';
|
|
174
181
|
OPEN_THREADS.delete(thread.id);
|
|
175
|
-
updateThread(thread.id, { status: ok ? 'completed' : 'failed', branch: ok && commit ? branch : null, commit, baseCommit });
|
|
176
|
-
const alert = await notifyAlert({ title: ok ? '
|
|
177
|
-
|
|
182
|
+
updateThread(thread.id, { status: ok ? 'completed' : 'failed', branch: (ok || cancelled) && commit ? branch : null, commit, baseCommit });
|
|
183
|
+
const alert = cancelled ? null : await notifyAlert({ title: ok ? 'Metaharness: pipeline passed' : 'Metaharness: pipeline failed', message: `${task.slice(0, 80)}${commit ? ` (branch ${branch})` : ''}`, severity: ok ? 'info' : 'warning' });
|
|
184
|
+
const limited = stages.find(item => item.failureKind === 'limit');
|
|
185
|
+
return { ...base, ok, ...cancelled, ...(limited ? { failureKind: 'limit', error: limited.output } : {}), goalCheck: stage('Implementer')?.goalCheck, structured: stage('Implementer')?.structured,
|
|
186
|
+
sessionId: stage('Implementer')?.sessionId, progressFile: stage('Implementer')?.progressFile,
|
|
187
|
+
threadId: thread.id, branch: (ok || cancelled) && commit ? branch : null, commit, baseCommit, gate, warnings, durationMs: Date.now() - started, alert };
|
|
178
188
|
})();
|
|
179
189
|
}
|
|
180
190
|
}
|
|
@@ -187,14 +197,15 @@ async function runCouncil(task, poolInfo, options) {
|
|
|
187
197
|
if (!seats.length) throw new Error('No harness can take work right now');
|
|
188
198
|
const cwd = options.cwd || process.cwd();
|
|
189
199
|
const votes = await Promise.all(seats.map(async harness => {
|
|
190
|
-
const result = await dispatchWithReceipt({ agent: harness, node: 'local', cwd, contextTask: task, prompt: `[ROLE: COUNCIL MEMBER]\nDo not change any file. Assess this proposal, with its trade-offs and risks:\n${task}\n\nEnd your reply with exactly one JSON object: {"vote":"APPROVE"|"REJECT","reason":"..."}`, permissions: READ, allowUnknownAuth: options.allowUnknownAuth === true }, { dispatchFn: options.dispatchFn, role: 'Council' });
|
|
191
|
-
return { harness, ok: result.ok === true, ...(result.ok ? parseVote(result.response) : { vote: 'ABSTAIN', reason: null, error: result.error }),
|
|
200
|
+
const result = await dispatchWithReceipt({ agent: harness, node: 'local', cwd, contextTask: task, ...sessionOptions({ ...options, cwd }), prompt: `[ROLE: COUNCIL MEMBER]\nDo not change any file. Assess this proposal, with its trade-offs and risks:\n${task}\n\nEnd your reply with exactly one JSON object: {"vote":"APPROVE"|"REJECT","reason":"..."}`, permissions: READ, allowUnknownAuth: options.allowUnknownAuth === true }, { dispatchFn: options.dispatchFn, role: 'Council' });
|
|
201
|
+
return { harness, ok: result.ok === true, failureKind: result.failureKind, ...(result.ok ? parseVote(result.response) : { vote: 'ABSTAIN', reason: null, error: result.error }),
|
|
192
202
|
...(result.queueId ? { queueId: result.queueId } : {}), feedback: result.response || null };
|
|
193
203
|
}));
|
|
194
204
|
const count = vote => votes.filter(item => item.vote === vote).length;
|
|
195
205
|
const majority = votes.length / 2;
|
|
196
206
|
const consensus = count('APPROVE') > majority ? 'approved' : count('REJECT') > majority ? 'rejected' : 'no-consensus';
|
|
197
|
-
|
|
207
|
+
const limited = votes.some(item => item.failureKind === 'limit');
|
|
208
|
+
return { ok: !limited && votes.some(item => item.ok), ...(limited ? { failureKind: 'limit' } : {}), ...cancellationResult(), mode: 'council', task, executedLive: true, consensus, excluded: poolInfo.excluded, votes };
|
|
198
209
|
}
|
|
199
210
|
|
|
200
211
|
async function runRace(task, poolInfo, options) {
|
|
@@ -212,12 +223,20 @@ async function runRace(task, poolInfo, options) {
|
|
|
212
223
|
try {
|
|
213
224
|
const started = Date.now();
|
|
214
225
|
directory = createWorktree(cwd, { branch });
|
|
215
|
-
const result = await dispatchWithReceipt({ agent: harness, node: 'local', cwd: directory, contextCwd: cwd, contextTask: task, prompt: `[ROLE: IMPLEMENTER]\nWork only in this checkout. Implement the task and its tests. Do not commit.\nTask:\n${task}`, permissions: WRITE, allowUnknownAuth: options.allowUnknownAuth === true }, { dispatchFn: options.dispatchFn, role: 'Race' });
|
|
216
|
-
const commit = result.ok ? commitAll(directory, `meta-harness race: ${task.slice(0, 60)}\n\nAgent: ${harness}`) : null;
|
|
217
|
-
if (
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
226
|
+
const result = await dispatchWithReceipt({ agent: harness, node: 'local', cwd: directory, contextCwd: cwd, contextTask: task, ...sessionOptions({ ...options, cwd }), goal: options.goal || task, prompt: `[ROLE: IMPLEMENTER]\nWork only in this checkout. Implement the task and its tests. Do not commit.\nTask:\n${task}`, permissions: WRITE, allowUnknownAuth: options.allowUnknownAuth === true }, { dispatchFn: options.dispatchFn, role: 'Race' });
|
|
227
|
+
const commit = result.ok || result.cancelled ? commitAll(directory, `meta-harness race: ${task.slice(0, 60)}\n\nAgent: ${harness}`) : null;
|
|
228
|
+
if (result.cancelled) {
|
|
229
|
+
keep = Boolean(commit);
|
|
230
|
+
return { ...result, harness, branch: keep ? branch : null, wip: commit };
|
|
231
|
+
}
|
|
232
|
+
if (result.ok && options.gate) linkDependencies(cwd, directory);
|
|
233
|
+
const gate = result.ok ? await (options.gateFn || runQualityGate)(directory, options.gateOptions) : null;
|
|
234
|
+
if (cancellationResult()) {
|
|
235
|
+
keep = Boolean(commit);
|
|
236
|
+
return { ...cancellationResult(), harness, branch: keep ? branch : null, commit };
|
|
237
|
+
}
|
|
238
|
+
keep = result.ok === true && gate?.passed === true;
|
|
239
|
+
return { harness, branch: keep ? branch : null, commit: keep ? commit : null, ok: keep, gate, goalCheck: result.goalCheck, structured: result.structured, failureKind: result.failureKind, completedAt: Date.now(), durationMs: Date.now() - started, error: result.ok ? keep ? undefined : 'The quality gate failed' : result.error,
|
|
221
240
|
...(result.queueId ? { queueId: result.queueId } : {}) };
|
|
222
241
|
} catch (error) {
|
|
223
242
|
return { harness, branch: null, commit: null, ok: false, gate: null, durationMs: null, error: String(error.message) };
|
|
@@ -227,10 +246,12 @@ async function runRace(task, poolInfo, options) {
|
|
|
227
246
|
}
|
|
228
247
|
}));
|
|
229
248
|
const finishers = entries.filter(entry => entry.ok).sort((a, b) => a.completedAt - b.completedAt);
|
|
230
|
-
|
|
249
|
+
const limited = entries.some(item => item.failureKind === 'limit');
|
|
250
|
+
return { ok: !limited && finishers.length > 0, ...(limited ? { failureKind: 'limit' } : {}), ...cancellationResult(), mode: 'race', task, executedLive: true, gateApplied: true, verification: !limited && finishers.length ? 'passed' : 'failed', winner: limited ? null : finishers[0]?.harness || null, excluded: poolInfo.excluded, candidates: entries };
|
|
231
251
|
}
|
|
232
252
|
|
|
233
253
|
export async function coordinateTask(taskDescription, options = {}) {
|
|
254
|
+
if (cancellationResult()) return cancellationResult();
|
|
234
255
|
const task = String(taskDescription || '').trim();
|
|
235
256
|
if (!task) throw new Error('Task description is required for coordination');
|
|
236
257
|
const remote = await forwardRemoteOperation('coordinate', task, options);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { sessionOptions } from './session/run.mjs';
|
|
2
3
|
|
|
3
4
|
const DEFAULT_PERMISSIONS = Object.freeze({ read: true, write: false, send: false, destructive: false });
|
|
4
5
|
export { DEFAULT_PERMISSIONS };
|
|
@@ -14,11 +15,12 @@ export function commandPermissions(value = DEFAULT_PERMISSIONS) {
|
|
|
14
15
|
const PUBLIC_FIELDS = [
|
|
15
16
|
'agent', 'cwd', 'prompt', 'permissions', 'node', 'allowUnknownAuth', 'timeoutMs',
|
|
16
17
|
'model', 'tools', 'settingSources', 'allowedTools',
|
|
18
|
+
'goal', 'maxTurns', 'maxTokens', 'maxBudgetUsd', 'resultSchema', 'approvalPolicy', 'questionTimeoutMs', 'effort', 'fallbackModel', 'name',
|
|
17
19
|
];
|
|
18
20
|
|
|
19
21
|
export function validateDispatchRequest(request) {
|
|
20
22
|
if (!request || typeof request !== 'object' || Array.isArray(request)) throw new Error('Dispatch requires a JSON object');
|
|
21
|
-
if (Object.keys(request).some(key => !PUBLIC_FIELDS.includes(key))) throw new Error('Unsupported dispatch field; workspace context and containment are managed by
|
|
23
|
+
if (Object.keys(request).some(key => !PUBLIC_FIELDS.includes(key))) throw new Error('Unsupported dispatch field; workspace context and containment are managed by Metaharness');
|
|
22
24
|
if (typeof request.agent !== 'string' || !/^[a-z][a-z0-9-]{0,63}$/.test(request.agent) ||
|
|
23
25
|
typeof request.cwd !== 'string' || !path.isAbsolute(request.cwd) || request.cwd.includes('\0') ||
|
|
24
26
|
typeof request.prompt !== 'string' || !request.prompt.trim() || Buffer.byteLength(request.prompt) > 65536) {
|
|
@@ -28,7 +30,7 @@ export function validateDispatchRequest(request) {
|
|
|
28
30
|
if (request.allowUnknownAuth !== undefined && typeof request.allowUnknownAuth !== 'boolean') throw new Error('allowUnknownAuth must be a boolean');
|
|
29
31
|
const timeoutMs = request.timeoutMs === undefined ? 300000 : request.timeoutMs;
|
|
30
32
|
if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 540000) throw new Error('Foreground dispatch timeout must be 1..540000 ms');
|
|
31
|
-
return { ...request, permissions: commandPermissions(request.permissions), timeoutMs };
|
|
33
|
+
return { ...request, ...sessionOptions(request), permissions: commandPermissions(request.permissions), timeoutMs };
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
/** The installed destination starts from its own project context and decides containment itself. */
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
/** One path for every agent run: contain it if needed, dispatch it, classify the outcome, write its receipt. */
|
|
2
2
|
import { executeOnNode } from './executor.mjs';
|
|
3
|
-
import { classifyFailure, INCLUDED_BILLING, READ_ONLY_ADAPTERS } from './worker.mjs';
|
|
3
|
+
import { classifyFailure, INCLUDED_BILLING, READ_ONLY_ADAPTERS, cancellationResult } from './worker.mjs';
|
|
4
4
|
import { recordRunReceipt } from './learner.mjs';
|
|
5
|
-
import { isGitRepo, createWorktree, removeWorktree } from './worktree.mjs';
|
|
5
|
+
import { isGitRepo, createWorktree, removeWorktree, commitAll, deleteBranch } from './worktree.mjs';
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
6
7
|
import { includedAdmission, openRouterAdmission, readIncludedPolicy } from './included-usage.mjs';
|
|
7
8
|
import { queueIncludedJob } from './usage-queue.mjs';
|
|
8
9
|
|
|
9
10
|
export async function dispatchWithReceipt(request, { dispatchFn, node = request.node || 'local', role, complexityScore, escalatedFrom,
|
|
10
|
-
record = recordRunReceipt, replay, retryId, usagePolicy = readIncludedPolicy, admit = includedAdmission, admitOpenRouter = openRouterAdmission } = {}) {
|
|
11
|
+
record = recordRunReceipt, replay, retryId, manageWrites = false, usagePolicy = readIncludedPolicy, admit = includedAdmission, admitOpenRouter = openRouterAdmission } = {}) {
|
|
11
12
|
const started = Date.now();
|
|
13
|
+
if (cancellationResult()) return cancellationResult();
|
|
12
14
|
// A harness that cannot be told not to write does read-only work in a checkout that is thrown away.
|
|
13
15
|
const contain = node === 'local' && request.permissions?.write !== true && !READ_ONLY_ADAPTERS.includes(request.agent) && request.isolated !== true && isGitRepo(request.cwd);
|
|
14
|
-
|
|
16
|
+
const ownWrites = manageWrites && node === 'local' && request.permissions?.write === true && isGitRepo(request.cwd);
|
|
17
|
+
let scratch = null, branch = null, result, billingMode = null, launchPrevented = false;
|
|
15
18
|
try {
|
|
16
19
|
let effective = request;
|
|
17
20
|
const policy = await usagePolicy();
|
|
@@ -24,19 +27,32 @@ export async function dispatchWithReceipt(request, { dispatchFn, node = request.
|
|
|
24
27
|
launchPrevented = true;
|
|
25
28
|
} else effective = { ...request, [INCLUDED_BILLING]: { key: admission.key, claudePlan: admission.claudePlan, openRouter: admission.openRouter } };
|
|
26
29
|
}
|
|
30
|
+
result = cancellationResult() || result;
|
|
27
31
|
if (!result) {
|
|
28
|
-
if (contain) {
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
if (contain || ownWrites) {
|
|
33
|
+
branch = ownWrites ? `meta-harness/dispatch-${randomUUID()}` : null;
|
|
34
|
+
scratch = createWorktree(request.cwd, { ...(branch ? { branch } : {}), prefix: ownWrites ? 'meta-harness-dispatch-' : 'meta-harness-readonly-' });
|
|
35
|
+
effective = { ...effective, ...(branch ? { branch } : {}), cwd: scratch, contextCwd: request.contextCwd || request.cwd, isolated: true };
|
|
31
36
|
}
|
|
32
37
|
result = dispatchFn ? await dispatchFn(effective) : await executeOnNode(node, { ...effective, operation: 'dispatch' });
|
|
38
|
+
if (cancellationResult()) result = { ...result, ...cancellationResult() };
|
|
33
39
|
if (contain) result = { ...result, readOnly: 'disposable-checkout' };
|
|
34
40
|
}
|
|
35
41
|
} catch (error) {
|
|
36
42
|
const message = String(error?.message || error);
|
|
37
43
|
result = { ok: false, error: message, agent: request.agent, ...(error?.code === 'NOT_READY' ? { failureKind: 'not-ready' } : classifyFailure(message)) };
|
|
38
44
|
} finally {
|
|
39
|
-
if (scratch)
|
|
45
|
+
if (scratch) {
|
|
46
|
+
try {
|
|
47
|
+
if (branch) {
|
|
48
|
+
const commit = commitAll(scratch, result?.cancelled ? 'meta-harness: unfinished cancelled dispatch' : 'meta-harness: dispatch changes');
|
|
49
|
+
if (commit) result = { ...result, branch, ...(result?.ok ? { commit } : { wip: commit }) };
|
|
50
|
+
else if (result?.sessionId) result = { ...result, branch };
|
|
51
|
+
else deleteBranch(request.cwd, branch);
|
|
52
|
+
}
|
|
53
|
+
} finally { removeWorktree(request.cwd, scratch); }
|
|
54
|
+
if (branch && !result?.branch) deleteBranch(request.cwd, branch);
|
|
55
|
+
}
|
|
40
56
|
}
|
|
41
57
|
const durationMs = Date.now() - started;
|
|
42
58
|
if (result.execution?.receiptOwner === 'destination' && result.receiptId) return { ...result, durationMs };
|
|
@@ -52,7 +68,15 @@ export async function dispatchWithReceipt(request, { dispatchFn, node = request.
|
|
|
52
68
|
}
|
|
53
69
|
}
|
|
54
70
|
}
|
|
55
|
-
if (launchPrevented)
|
|
71
|
+
if (launchPrevented) {
|
|
72
|
+
// A launch refused for quota is a quota wall too (A-124): record it, so routing sees the harness as
|
|
73
|
+
// exhausted until its reset and picks another included harness instead of choosing it again.
|
|
74
|
+
if (result.failureKind === 'quota') {
|
|
75
|
+
try { await record({ harness: request.agent, role, task: String(request.contextTask || request.prompt || '').slice(0, 200), success: false,
|
|
76
|
+
failureKind: 'quota', error: result.error, resetsAt: result.resetsAt || null }); } catch { /* bookkeeping must not hide the refusal */ }
|
|
77
|
+
}
|
|
78
|
+
return { ...result, launchPrevented, durationMs };
|
|
79
|
+
}
|
|
56
80
|
// A finished run keeps its result: the worktree and any cost are real even when bookkeeping fails.
|
|
57
81
|
let receipt, receiptError;
|
|
58
82
|
try {
|
|
@@ -3,7 +3,11 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import crypto from 'node:crypto';
|
|
6
|
-
import {
|
|
6
|
+
import { execFile } from 'node:child_process';
|
|
7
|
+
import { promisify } from 'node:util';
|
|
8
|
+
import { agentEnvironment, loadCustomAdapters } from './worker.mjs';
|
|
9
|
+
|
|
10
|
+
const exec = promisify(execFile);
|
|
7
11
|
|
|
8
12
|
export const DOC_SOURCES = [
|
|
9
13
|
{ id: 'claude', index: 'https://code.claude.com/docs/llms.txt', prefix: 'https://code.claude.com/docs/', catalog:true },
|
|
@@ -69,7 +73,7 @@ async function download(url, fetchImpl, declaredOrigin) {
|
|
|
69
73
|
if (declaredOrigin) allowed.add(declaredOrigin);
|
|
70
74
|
for(let redirects=0;redirects<5;redirects++) {
|
|
71
75
|
if(target.protocol!=='https:' || target.username || target.password || !allowed.has(target.hostname)) throw new Error(`Unapproved official documentation origin: ${target.hostname}`);
|
|
72
|
-
const response=await fetchImpl(target,{redirect:'manual',signal:AbortSignal.timeout(20000),headers:{'User-Agent':'Total-Recall-
|
|
76
|
+
const response=await fetchImpl(target,{redirect:'manual',signal:AbortSignal.timeout(20000),headers:{'User-Agent':'Total-Recall-Metaharness-Docs','Accept':'*/*'}});
|
|
73
77
|
if(response.status>=300 && response.status<400){target=new URL(response.headers.get('location'),target);continue;}
|
|
74
78
|
// Some entries advertise Markdown exports which no longer exist while the
|
|
75
79
|
// same official HTML document remains available. Fetch that representation.
|
|
@@ -91,7 +95,27 @@ function links(text,base,prefix) {
|
|
|
91
95
|
return [...new Set(values.map(value=>{try{const url=new URL(value);url.hash='';url.search='';url.pathname=url.pathname.replace(/\/{2,}/g,'/').replace(/\/$/,'') || '/';return url.href;}catch{return '';}}))]
|
|
92
96
|
.filter(url=>url.startsWith(prefix) && !/%3[CE]|[<>"'`\s]/i.test(url) && !/\.(png|jpg|jpeg|svg|gif|css|js|woff2?|ico|pdf|json|ya?ml|xml|zip|gz|tgz|lock)$/i.test(url));
|
|
93
97
|
}
|
|
94
|
-
|
|
98
|
+
async function gitDocumentationPaths(source) {
|
|
99
|
+
const checkout = fs.mkdtempSync(path.join(os.tmpdir(), 'meta-harness-doc-tree-'));
|
|
100
|
+
const options = {
|
|
101
|
+
cwd: checkout, timeout: 30000, maxBuffer: 4 * 1024 * 1024,
|
|
102
|
+
env: { ...agentEnvironment(), GIT_TERMINAL_PROMPT: '0', GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: os.devNull },
|
|
103
|
+
};
|
|
104
|
+
try {
|
|
105
|
+
await exec('git', ['-c', 'http.followRedirects=false', 'clone', '--quiet', '--filter=blob:none',
|
|
106
|
+
'--depth=1', '--single-branch', '--branch', source.branch, '--no-checkout',
|
|
107
|
+
'--', `https://github.com/${source.repo}.git`, checkout], options);
|
|
108
|
+
const { stdout } = await exec('git', ['ls-tree', '-r', '-z', 'HEAD', '--', ...source.directories], options);
|
|
109
|
+
return stdout.split('\0').flatMap(entry => {
|
|
110
|
+
const match = /^100(?:644|755) blob [a-f0-9]+\t([\s\S]+)$/.exec(entry);
|
|
111
|
+
return match ? [match[1]] : [];
|
|
112
|
+
});
|
|
113
|
+
} catch {
|
|
114
|
+
throw new Error(`Could not read the official documentation tree through Git for ${source.repo}`);
|
|
115
|
+
} finally { fs.rmSync(checkout, { recursive: true, force: true }); }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function refreshDocumentation({ directory = documentationDirectory(), agent, fetchImpl = fetch, now = () => new Date(), maxPages = 1500, readGitPaths = gitDocumentationPaths } = {}) {
|
|
95
119
|
const sources=sourcesFor(agent);
|
|
96
120
|
if(!sources.length)throw new Error('Unknown documentation adapter');
|
|
97
121
|
const reports=[];
|
|
@@ -99,11 +123,27 @@ export async function refreshDocumentation({ directory = documentationDirectory(
|
|
|
99
123
|
const previous=metadata(directory,source.id), pages=[], queue=[source.index], seen=new Set(), queued=new Set(queue), errors=[], discovered=new Set(), skipped=[];
|
|
100
124
|
// `found` marks a link taken from HTML navigation. The vendor's catalog, the index and the repository tree are authoritative; a link found by scraping is not.
|
|
101
125
|
const enqueue=(url,found=false)=>{if(!queued.has(url)){queued.add(url);queue.push(url);if(found)discovered.add(url);}};
|
|
126
|
+
let treeSource;
|
|
102
127
|
try {
|
|
103
128
|
if(source.repo) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
129
|
+
let paths;
|
|
130
|
+
try {
|
|
131
|
+
const tree = JSON.parse(await download(`https://api.github.com/repos/${source.repo}/git/trees/${source.branch}?recursive=1`, fetchImpl));
|
|
132
|
+
if (tree.truncated) throw new Error('Official repository documentation tree is truncated');
|
|
133
|
+
if (!Array.isArray(tree.tree)) throw new Error('Invalid official repository documentation tree');
|
|
134
|
+
paths = tree.tree.filter(entry => entry.type === 'blob').map(entry => entry.path);
|
|
135
|
+
treeSource = 'github-rest';
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (!/^Documentation HTTP (403|429):|^Official repository documentation tree is truncated$/.test(error.message)) throw error;
|
|
138
|
+
paths = await readGitPaths(source);
|
|
139
|
+
treeSource = 'github-git';
|
|
140
|
+
}
|
|
141
|
+
for (const file of paths) {
|
|
142
|
+
if (!/\.(md|mdx)$/i.test(file) || !source.directories.some(dir => file.startsWith(dir))) continue;
|
|
143
|
+
const encoded = file.split('/').map(encodeURIComponent).join('/');
|
|
144
|
+
enqueue(`https://raw.githubusercontent.com/${source.repo}/${source.branch}/${encoded}`);
|
|
145
|
+
}
|
|
146
|
+
if (queue.length === 1) throw new Error('Official repository documentation tree contains no additional pages');
|
|
107
147
|
}
|
|
108
148
|
// Bounded batches; follow the official index/navigation to cover agent features.
|
|
109
149
|
while(queue.length) {
|
|
@@ -138,7 +178,7 @@ export async function refreshDocumentation({ directory = documentationDirectory(
|
|
|
138
178
|
reports.push({id:source.id,ok:false,error,pages:preserved.pages?.length || 0,downloadedPages:pages.length,failedCount:errors.length,failures:errors,...(skipped.length?{skippedCount:skipped.length,skipped}:{})});continue;
|
|
139
179
|
}
|
|
140
180
|
atomic(path.join(directory,source.id,'index.json'),JSON.stringify({id:source.id,complete:true,checkedAt,lastAttempt:checkedAt,pages,...(skipped.length?{skipped}:{})},null,2));
|
|
141
|
-
reports.push({id:source.id,ok:true,pages:pages.length,...(skipped.length?{skippedCount:skipped.length,skipped}:{})});
|
|
181
|
+
reports.push({id:source.id,ok:true,pages:pages.length,...(treeSource?{treeSource}:{}),...(skipped.length?{skippedCount:skipped.length,skipped}:{})});
|
|
142
182
|
} catch(error) {
|
|
143
183
|
atomic(path.join(directory,source.id,'index.json'),JSON.stringify({...previous,id:source.id,lastAttempt:now().toISOString(),error:error.message},null,2));
|
|
144
184
|
reports.push({id:source.id,ok:false,error:error.message,pages:previous?.pages?.length || 0});
|
|
@@ -156,11 +196,15 @@ export async function ensureDocumentation(options={}) {
|
|
|
156
196
|
|
|
157
197
|
export async function maintainDocumentation(options={}) {
|
|
158
198
|
const status=documentationStatus(options);
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
199
|
+
const pending=status.agents.filter(row=>!row.fresh).sort((a,b)=>String(a.lastAttempt || '').localeCompare(String(b.lastAttempt || '')));
|
|
200
|
+
const reports=[];
|
|
201
|
+
// Missing documentation for one harness must not prevent attempts for the others.
|
|
202
|
+
for(const row of pending) {
|
|
203
|
+
try { reports.push(...(await refreshDocumentation({...options,agent:row.id})).agents); }
|
|
204
|
+
catch(error) { reports.push({id:row.id,ok:false,error:error.message}); }
|
|
205
|
+
}
|
|
162
206
|
const current=documentationStatus(options);
|
|
163
|
-
return {...
|
|
207
|
+
return {...current,ok:current.ok && reports.every(row=>row.ok),scope:'all-agent-documentation',refresh:reports,coverage_complete:current.ok,pending:current.agents.filter(row=>!row.fresh).map(row=>row.id)};
|
|
164
208
|
}
|
|
165
209
|
|
|
166
210
|
export function readDocumentation({agent,url,directory=documentationDirectory()}) {
|
|
@@ -34,15 +34,35 @@ export async function listTargets() {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export function workerProgram(request) {
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
// Materialize a portable module graph. The stdin launcher imports only Node
|
|
38
|
+
// built-ins. Relative imports resolve inside this owned bundle on the node.
|
|
39
|
+
const names = ['worker.mjs', 'control.mjs', 'jobs.mjs', 'paths.mjs', 'mcp-server.mjs',
|
|
40
|
+
...fs.readdirSync(new URL('./session/', import.meta.url)).filter(name => name.endsWith('.mjs')).map(name => `session/${name}`)];
|
|
41
|
+
const files = Object.fromEntries(names.map(name => [name, fs.readFileSync(new URL(name, import.meta.url), 'utf8')]));
|
|
42
|
+
return `import fs from 'node:fs';
|
|
43
|
+
import os from 'node:os';
|
|
44
|
+
import path from 'node:path';
|
|
45
|
+
import { pathToFileURL } from 'node:url';
|
|
46
|
+
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'metaharness-worker-'));
|
|
47
|
+
try {
|
|
48
|
+
for (const [name, source] of Object.entries(${JSON.stringify(files)})) {
|
|
49
|
+
const file = path.join(directory, name);
|
|
50
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
51
|
+
fs.writeFileSync(file, source, { mode: 0o600 });
|
|
52
|
+
}
|
|
53
|
+
const { runWorker } = await import(pathToFileURL(path.join(directory, 'worker.mjs')).href);
|
|
54
|
+
console.log(JSON.stringify(await runWorker(${JSON.stringify(request)})));
|
|
55
|
+
} catch (error) { console.error(String(error?.message || 'Selected agent worker failed')); process.exitCode = 1; }
|
|
56
|
+
finally { fs.rmSync(directory, { recursive: true, force: true }); }
|
|
57
|
+
`;
|
|
58
|
+
|
|
39
59
|
}
|
|
40
60
|
|
|
41
61
|
function destinationDispatchResult(result, node, request) {
|
|
42
62
|
let payload;
|
|
43
63
|
try { payload = JSON.parse(result.output); } catch { /* No structured result from the installed owner. */ }
|
|
44
64
|
if (!payload || typeof payload.ok !== 'boolean' || (payload.ok && !result.ok)) {
|
|
45
|
-
throw notReady(`Installed
|
|
65
|
+
throw notReady(`Installed Metaharness on ${node} returned no valid dispatch outcome (exit ${result.exitCode ?? 'unknown'})`);
|
|
46
66
|
}
|
|
47
67
|
return { ...payload, execution: { node, workspace: request.cwd, remote: true,
|
|
48
68
|
...(typeof payload.receiptId === 'string' ? { receiptOwner: 'destination' } : {}) } };
|
|
@@ -60,7 +80,9 @@ export async function executeOnNode(node, request, { worker = runWorker, process
|
|
|
60
80
|
if (dispatch) {
|
|
61
81
|
const input = JSON.stringify(destinationDispatchRequest(request));
|
|
62
82
|
if (Buffer.byteLength(input) > 65536) throw new Error('Remote dispatch request is too large');
|
|
63
|
-
transport
|
|
83
|
+
// The transport outlives the attempt it carries by a minute, so the destination reports its own timeout (A-101).
|
|
84
|
+
transport = { operation: 'direct', cwd: request.cwd, argv: ['total-recall', 'meta-harness', 'dispatch', '--stdin', '--json'], input,
|
|
85
|
+
timeoutMs: Number.isInteger(request.timeoutMs) && request.timeoutMs > 0 ? request.timeoutMs + 60_000 : 600000 };
|
|
64
86
|
}
|
|
65
87
|
const managed = dispatch || (transport.argv?.[1] === 'meta-harness' &&
|
|
66
88
|
['route', 'coordinate', 'review', 'process', 'run-command', 'usage-retry'].includes(transport.argv[2]));
|
package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/hermes.mjs
CHANGED
|
@@ -27,6 +27,32 @@ function withGate(text) {
|
|
|
27
27
|
return `${text.replace(/\n*$/, text ? '\n' : '')}\nhooks:\n pre_tool_call:\n${entry}`;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Remove every hook list item whose command runs the gate. Other tools rewrite config.yaml (the
|
|
32
|
+
* lesson hooks re-serialize it), so the item is found by indentation, not by its exact text: the
|
|
33
|
+
* item runs from its `- ` line to the next line indented no deeper than that dash.
|
|
34
|
+
*/
|
|
35
|
+
export function withoutGate(text) {
|
|
36
|
+
if (!text.includes(GATE)) return text;
|
|
37
|
+
const exact = text.replace(ENTRY_LINES, '');
|
|
38
|
+
if (!exact.includes(GATE)) return exact;
|
|
39
|
+
let lines = text.split('\n');
|
|
40
|
+
const indent = (line) => line.match(/^ */)[0].length;
|
|
41
|
+
for (let guard = 0; guard < 50; guard++) {
|
|
42
|
+
const hit = lines.findIndex((line) => line.includes(GATE));
|
|
43
|
+
if (hit < 0) break;
|
|
44
|
+
let start = hit;
|
|
45
|
+
while (start >= 0 && !/^ *- /.test(lines[start])) start--;
|
|
46
|
+
if (start < 0 || indent(lines[start]) > indent(lines[hit])) break;
|
|
47
|
+
const dash = indent(lines[start]);
|
|
48
|
+
let end = start + 1;
|
|
49
|
+
while (end < lines.length && (lines[end].trim() === '' || indent(lines[end]) > dash)) end++;
|
|
50
|
+
while (end > start + 1 && lines[end - 1].trim() === '') end--;
|
|
51
|
+
lines = [...lines.slice(0, start), ...lines.slice(end)];
|
|
52
|
+
}
|
|
53
|
+
return lines.join('\n');
|
|
54
|
+
}
|
|
55
|
+
|
|
30
56
|
export const hermesAdapter = {
|
|
31
57
|
id: 'hermes',
|
|
32
58
|
level: 'enforced',
|
|
@@ -118,7 +144,9 @@ export const hermesAdapter = {
|
|
|
118
144
|
const present = text.includes(GATE);
|
|
119
145
|
if (remove || off) {
|
|
120
146
|
if (present) {
|
|
121
|
-
|
|
147
|
+
const next = withoutGate(text);
|
|
148
|
+
if (next.includes(GATE)) throw new Error(`could not remove the gate from ${configPath}; remove the pre_tool_call entry that runs "${GATE}" by hand`);
|
|
149
|
+
if (!dryRun) fs.writeFileSync(configPath, next);
|
|
122
150
|
removed.push('managed pre_tool_call gate');
|
|
123
151
|
}
|
|
124
152
|
return { settings: configPath, added, removed };
|
package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Gate Adapters registry for
|
|
2
|
+
* Gate Adapters registry for Metaharness (tr-plugin-cli-agents).
|
|
3
3
|
* Provides harness-specific parsing, emission, installation and verification.
|
|
4
4
|
*/
|
|
5
5
|
import { claudeAdapter } from './claude.mjs';
|
|
@@ -195,7 +195,7 @@ export function queryCodexAccount(cwd, { env = process.env, timeoutMs = 10000 }
|
|
|
195
195
|
child.kill('SIGTERM');
|
|
196
196
|
}
|
|
197
197
|
});
|
|
198
|
-
send({ id: 1, method: 'initialize', params: { clientInfo: { name: 'meta_harness', title: '
|
|
198
|
+
send({ id: 1, method: 'initialize', params: { clientInfo: { name: 'meta_harness', title: 'Metaharness', version: '1' } } });
|
|
199
199
|
});
|
|
200
200
|
}
|
|
201
201
|
|