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.
Files changed (212) hide show
  1. package/bin/total-recall.mjs +6 -4
  2. package/docs/ARCHITECTURE.md +10 -7
  3. package/docs/reference/cli-reference.md +13 -8
  4. package/frontend/dist/assets/index-DACh1dhx.js +5613 -0
  5. package/frontend/dist/index.html +1 -1
  6. package/frontend/src/App.tsx +2 -2
  7. package/frontend/src/pages/PluginsPage.tsx +1 -1
  8. package/package.json +1 -1
  9. package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +7 -7
  10. package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +1 -1
  11. package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +1 -1
  12. package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +20 -1
  13. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-store.mjs +12 -1
  14. package/scaffold/.agent/skills/total-recall/plugins/automations/BRICK.md +7 -0
  15. package/scaffold/.agent/skills/total-recall/plugins/automations/LICENSE +21 -0
  16. package/scaffold/.agent/skills/total-recall/plugins/automations/README.md +66 -0
  17. package/scaffold/.agent/skills/total-recall/plugins/automations/adapters.mjs +231 -0
  18. package/scaffold/.agent/skills/total-recall/plugins/automations/capability/index.mjs +16 -0
  19. package/scaffold/.agent/skills/total-recall/plugins/automations/cli.mjs +121 -0
  20. package/{src/core/plugin-cron.mjs → scaffold/.agent/skills/total-recall/plugins/automations/cron.mjs} +1 -3
  21. package/scaffold/.agent/skills/total-recall/plugins/automations/inventory.mjs +88 -0
  22. package/scaffold/.agent/skills/total-recall/plugins/automations/package-lock.json +7661 -0
  23. package/scaffold/.agent/skills/total-recall/plugins/automations/package.json +35 -0
  24. package/scaffold/.agent/skills/total-recall/plugins/automations/plugin.json +167 -0
  25. package/scaffold/.agent/skills/total-recall/plugins/automations/scheduler.mjs +246 -0
  26. package/scaffold/.agent/skills/total-recall/plugins/automations/ui/DESIGN.md +42 -0
  27. package/scaffold/.agent/skills/total-recall/plugins/automations/ui/automations.js +2 -0
  28. package/scaffold/.agent/skills/total-recall/plugins/automations/ui/card.js +273 -0
  29. package/scaffold/.agent/skills/total-recall/plugins/code-quality/commands/run-core.mjs +1 -1
  30. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/check.mjs +57 -5
  31. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/report-data.mjs +40 -14
  32. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/report.mjs +15 -40
  33. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +7 -7
  34. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +1 -1
  35. package/scaffold/.agent/skills/total-recall/plugins/code-quality/plugin.json +1 -1
  36. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +7 -7
  37. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +1 -1
  38. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +1 -1
  39. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +7 -7
  40. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +1 -1
  41. package/scaffold/.agent/skills/total-recall/plugins/embeddings/plugin.json +1 -1
  42. package/scaffold/.agent/skills/total-recall/plugins/git/README.md +112 -0
  43. package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/cli.mjs +135 -23
  44. package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/package-lock.json +7 -7
  45. package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/package.json +2 -1
  46. package/scaffold/.agent/skills/total-recall/plugins/git/plugin.json +161 -0
  47. package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/context.mjs +94 -0
  48. package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/phase3-spec-support.mjs +50 -0
  49. package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/runtime.mjs +40 -0
  50. package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/spec-support.mjs +51 -0
  51. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/_support.mjs +195 -0
  52. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/auth.mjs +201 -0
  53. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/bisect.mjs +40 -0
  54. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/cli-meta.mjs +159 -0
  55. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/commit-memory.mjs +30 -0
  56. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/commit.mjs +46 -0
  57. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/fleet.mjs +156 -0
  58. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/force-push.mjs +58 -0
  59. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/group.mjs +119 -0
  60. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/handoff.mjs +59 -0
  61. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/message.mjs +22 -0
  62. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/resolve.mjs +60 -0
  63. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/rules.mjs +81 -0
  64. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/secret-firewall.mjs +66 -0
  65. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/undo.mjs +132 -0
  66. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/why.mjs +16 -0
  67. package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/ui/card.js +2 -2
  68. package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +7 -7
  69. package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +1 -1
  70. package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +1 -1
  71. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/README.md +25 -9
  72. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/BRICK.md +1 -1
  73. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/index.mjs +18 -6
  74. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +124 -24
  75. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +7 -7
  76. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +2 -2
  77. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +147 -14
  78. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/skills/meta-harness/SKILL.md +17 -3
  79. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/adoption.mjs +2 -2
  80. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/alerts.mjs +3 -3
  81. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/cascade.mjs +92 -18
  82. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/control.mjs +145 -0
  83. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +42 -21
  84. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch-request.mjs +4 -2
  85. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch.mjs +33 -9
  86. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/documentation.mjs +55 -11
  87. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/executor.mjs +26 -4
  88. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/hermes.mjs +29 -1
  89. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/index.mjs +1 -1
  90. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/included-usage.mjs +1 -1
  91. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/jobs.mjs +84 -0
  92. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/learner.mjs +3 -12
  93. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/maintenance.mjs +17 -0
  94. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/mcp-server.mjs +228 -0
  95. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/process-manager.mjs +25 -2
  96. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/reasoning.mjs +1 -1
  97. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/remote-operation.mjs +5 -1
  98. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/review.mjs +34 -3
  99. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/reviews.mjs +25 -0
  100. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +1 -1
  101. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/claude.mjs +319 -0
  102. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/codex.mjs +387 -0
  103. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/result.mjs +67 -0
  104. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/resume.mjs +373 -0
  105. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/run.mjs +223 -0
  106. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session-jobs.mjs +72 -0
  107. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/setup.mjs +6 -3
  108. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/termination.mjs +16 -12
  109. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/threads.mjs +32 -1
  110. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/usage-queue.mjs +1 -1
  111. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/verify-ssss.mjs +3 -3
  112. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +285 -40
  113. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worktree.mjs +76 -9
  114. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/DESIGN.md +5 -5
  115. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +3 -3
  116. package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +47 -18
  117. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +7 -7
  118. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +1 -1
  119. package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +1 -1
  120. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh-diagnostics.mjs +44 -0
  121. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +120 -113
  122. package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +7 -7
  123. package/scaffold/.agent/skills/total-recall/plugins/security/package.json +1 -1
  124. package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +1 -1
  125. package/scaffold/.agent/skills/total-recall/references/operations.md +1 -1
  126. package/src/cli/agent.mjs +1 -1
  127. package/src/cli/command.mjs +41 -2
  128. package/src/cli/dream.mjs +93 -69
  129. package/src/cli/harness.mjs +4 -4
  130. package/src/cli/help.mjs +1 -1
  131. package/src/cli/init.mjs +0 -25
  132. package/src/cli/lessons.mjs +51 -0
  133. package/src/cli/memory-plugin.mjs +1 -1
  134. package/src/cli/plugin/copies.mjs +64 -14
  135. package/src/cli/plugin/create.mjs +6 -7
  136. package/src/cli/plugin/index.mjs +11 -7
  137. package/src/cli/plugin/install.mjs +10 -15
  138. package/src/cli/plugin/remove.mjs +1 -1
  139. package/src/cli/plugin/search.mjs +2 -2
  140. package/src/cli/preflight.mjs +19 -6
  141. package/src/cli/proposals.mjs +1 -1
  142. package/src/cli/recall.mjs +66 -23
  143. package/src/cli/remember.mjs +3 -14
  144. package/src/cli/server.mjs +18 -0
  145. package/src/cli/startup.mjs +1 -0
  146. package/src/cli/task.mjs +9 -2
  147. package/src/cli/update.mjs +6 -2
  148. package/src/core/access-ledger.mjs +78 -0
  149. package/src/core/agent-manager.mjs +2 -2
  150. package/src/core/brain-locate.mjs +15 -0
  151. package/src/core/brain-updates.mjs +31 -11
  152. package/src/core/claude-hooks.mjs +3 -1
  153. package/src/core/cleanup-task.mjs +15 -0
  154. package/src/core/cli-commands.mjs +2 -0
  155. package/src/core/cli-maintenance.mjs +4 -1
  156. package/src/core/cli-semantic-index.mjs +23 -3
  157. package/src/core/context-compiler.mjs +20 -0
  158. package/src/core/daemon-loop.mjs +39 -62
  159. package/src/core/default-plugins.mjs +1 -1
  160. package/src/core/dream.mjs +33 -105
  161. package/src/core/ensure-dependencies.mjs +148 -0
  162. package/src/core/fast-recall.mjs +2 -2
  163. package/src/core/field-evaluation.mjs +23 -0
  164. package/src/core/gate-mode.mjs +1 -1
  165. package/src/core/harness-hooks.mjs +14 -2
  166. package/src/core/lesson-hooks.mjs +74 -0
  167. package/src/core/lesson-memory.mjs +24 -0
  168. package/src/core/lesson-sessions.mjs +52 -0
  169. package/src/core/lessons.mjs +96 -0
  170. package/src/core/meta-harness.mjs +2 -2
  171. package/src/core/optimizer.mjs +66 -165
  172. package/src/core/plugin-contracts/card.mjs +3 -0
  173. package/src/core/plugin-loader.mjs +12 -3
  174. package/src/core/plugin-runtime.mjs +40 -1
  175. package/src/core/plugin-store.mjs +35 -23
  176. package/src/core/post-mortem.mjs +52 -36
  177. package/src/core/preflight-gate.mjs +23 -2
  178. package/src/core/proposal-applier.mjs +31 -2
  179. package/src/core/repo-sync.mjs +4 -6
  180. package/src/core/required-plugins.mjs +26 -4
  181. package/src/core/rule-length.mjs +23 -0
  182. package/src/core/runtime.mjs +1 -1
  183. package/src/core/scheduler.mjs +35 -43
  184. package/src/core/schema.mjs +1 -2
  185. package/src/core/search.mjs +4 -3
  186. package/src/core/server-service.mjs +148 -0
  187. package/src/core/session-watcher.mjs +19 -1
  188. package/src/core/skill-gap.mjs +16 -0
  189. package/src/core/ssss-kernel-bridge.mjs +32 -4
  190. package/src/core/startup-health.mjs +6 -1
  191. package/src/core/surface.mjs +65 -54
  192. package/src/core/task-executors.mjs +54 -106
  193. package/src/core/validated-write.mjs +1 -0
  194. package/src/core/vector-field.mjs +134 -192
  195. package/src/server/invoke-core-child.mjs +24 -0
  196. package/src/server/invoke-core-operations.mjs +102 -0
  197. package/src/server/route-manifest.json +0 -56
  198. package/src/server/routes/brains.mjs +0 -18
  199. package/src/server/routes/context.mjs +0 -41
  200. package/src/server/routes/invoke.mjs +26 -1
  201. package/src/server/routes/ssss.mjs +0 -169
  202. package/src/server/routes/system.mjs +0 -15
  203. package/src/server/routes/tasks.mjs +0 -26
  204. package/src/server/routes/vault.mjs +2 -23
  205. package/frontend/dist/assets/index-DD4qONvH.js +0 -5607
  206. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/README.md +0 -53
  207. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/plugin.json +0 -129
  208. package/src/core/append-log.mjs +0 -131
  209. package/src/core/plugin-tasks.mjs +0 -173
  210. package/templates/onboarding-interview.md +0 -82
  211. /package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/LICENSE +0 -0
  212. /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, permissions: permissions || stage.permissions, allowUnknownAuth: options.allowUnknownAuth === true }, { dispatchFn: options.dispatchFn, role, complexityScore: complexity.score });
105
- stage.status = result.ok ? 'completed' : result.queued ? 'queued' : 'failed';
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 ? 'Meta Harness: pipeline passed' : 'Meta Harness: pipeline failed', message: `${task.slice(0, 80)}${commit ? ` (branch ${branch})` : ''}`, severity: ok ? 'info' : 'warning' });
177
- return { ...base, ok, threadId: thread.id, branch: ok && commit ? branch : null, commit, baseCommit, gate, warnings, durationMs: Date.now() - started, alert };
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
- return { ok: votes.some(item => item.ok), mode: 'council', task, executedLive: true, consensus, excluded: poolInfo.excluded, votes };
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 (commit && options.gate) linkDependencies(cwd, directory);
218
- const gate = commit ? await (options.gateFn || runQualityGate)(directory, options.gateOptions) : null;
219
- keep = Boolean(commit) && gate?.passed === true;
220
- return { harness, branch: keep ? branch : null, commit: keep ? commit : null, ok: keep, gate, completedAt: Date.now(), durationMs: Date.now() - started, error: result.ok ? (commit ? (keep ? undefined : 'The quality gate failed') : 'No files changed') : result.error,
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
- return { ok: finishers.length > 0, mode: 'race', task, executedLive: true, gateApplied: true, verification: finishers.length ? 'passed' : 'failed', winner: finishers[0]?.harness || null, excluded: poolInfo.excluded, candidates: entries };
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 Meta Harness');
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
- let scratch = null, result, billingMode = null, launchPrevented = false;
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
- scratch = createWorktree(request.cwd, { prefix: 'meta-harness-readonly-' });
30
- effective = { ...effective, cwd: scratch, contextCwd: request.contextCwd || request.cwd, isolated: true };
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) removeWorktree(request.cwd, 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) return { ...result, launchPrevented, durationMs };
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 { loadCustomAdapters } from './worker.mjs';
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-Meta-Harness-Docs','Accept':'*/*'}});
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
- export async function refreshDocumentation({ directory = documentationDirectory(), agent, fetchImpl = fetch, now = () => new Date(), maxPages = 1500 } = {}) {
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
- const tree=JSON.parse(await download(`https://api.github.com/repos/${source.repo}/git/trees/${source.branch}?recursive=1`,fetchImpl));
105
- if(tree.truncated)throw new Error('Official repository documentation tree is truncated');
106
- for(const entry of tree.tree || [])if(entry.type==='blob' && /\.(md|mdx)$/i.test(entry.path) && source.directories.some(dir=>entry.path.startsWith(dir)))enqueue(`https://raw.githubusercontent.com/${source.repo}/${source.branch}/${entry.path}`);
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 next=status.agents.filter(row=>!row.fresh).sort((a,b)=>String(a.lastAttempt || '').localeCompare(String(b.lastAttempt || '')))[0];
160
- if(!next)return {...status,scope:'all-agent-documentation-current'};
161
- const refreshed=await refreshDocumentation({...options,agent:next.id});
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 {...refreshed,scope:'one-stale-agent-refreshed',coverage_complete:current.ok,pending:current.agents.filter(row=>!row.fresh).map(row=>row.id)};
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
- const source = fs.readFileSync(new URL('./worker.mjs', import.meta.url), 'utf8');
38
- return source + '\nrunWorker(' + JSON.stringify(request) + ').then(result=>console.log(JSON.stringify(result))).catch(error=>{console.error(String(error?.message||"Selected agent worker failed"));process.exitCode=1;});\n';
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 Meta Harness on ${node} returned no valid dispatch outcome (exit ${result.exitCode ?? 'unknown'})`);
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 = { operation: 'direct', cwd: request.cwd, argv: ['total-recall', 'meta-harness', 'dispatch', '--stdin', '--json'], input, timeoutMs: 600000 };
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]));
@@ -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
- if (!dryRun) fs.writeFileSync(configPath, text.replace(ENTRY_LINES, ''));
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 };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Gate Adapters registry for Meta Harness (tr-plugin-cli-agents).
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: 'Meta Harness', version: '1' } } });
198
+ send({ id: 1, method: 'initialize', params: { clientInfo: { name: 'meta_harness', title: 'Metaharness', version: '1' } } });
199
199
  });
200
200
  }
201
201