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
@@ -2,7 +2,7 @@
2
2
  import fs from 'node:fs';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
- import { spawn } from 'node:child_process';
5
+ import { spawn, execFileSync } from 'node:child_process';
6
6
  import { createInterface } from 'node:readline';
7
7
  import { randomUUID } from 'node:crypto';
8
8
 
@@ -172,9 +172,18 @@ export function findBinary(name, env = process.env) {
172
172
  }
173
173
 
174
174
  /** Process groups this process started and has not finished, so termination can stop them. */
175
- const ACTIVE_GROUPS = new Set();
175
+ const ACTIVE_GROUPS = new Map();
176
+ let cancellation = null;
177
+ export const cancellationResult = () => cancellation && { ...cancellation };
178
+ /** Set the stop state before signalling a child, so its exit cannot start another attempt. */
179
+ export function cancelExecution(signal = 'SIGTERM', graceMs = 500) {
180
+ cancellation ||= { ok: false, cancelled: true, failureKind: 'cancelled', signal,
181
+ exitCode: signal === 'SIGINT' ? 130 : signal === 'SIGHUP' ? 129 : 143, error: `Cancelled by ${signal}.` };
182
+ for (const stop of ACTIVE_GROUPS.values()) stop('cancelled', graceMs);
183
+ return cancellationResult();
184
+ }
176
185
  export function stopActiveProcesses(signal = 'SIGTERM') {
177
- for (const pid of ACTIVE_GROUPS) { try { process.kill(process.platform === 'win32' ? pid : -pid, signal); } catch { /* already gone */ } }
186
+ for (const pid of ACTIVE_GROUPS.keys()) { if (typeof pid !== 'number') continue; try { process.kill(process.platform === 'win32' ? pid : -pid, signal); } catch { /* already gone */ } }
178
187
  return ACTIVE_GROUPS.size;
179
188
  }
180
189
 
@@ -263,25 +272,29 @@ function toolEvidence(toolResults) {
263
272
  };
264
273
  }
265
274
 
266
- export function runProcess(binary, args, { cwd, input = '', timeoutMs = 300000, env = agentEnvironment(), maxBytes = 2 * 1024 * 1024, codexJsonl = false, claudeJsonl = false } = {}) {
275
+ export function runProcess(binary, args, { cwd, input = '', timeoutMs = 300000, env = agentEnvironment(), maxBytes = 2 * 1024 * 1024, codexJsonl = false, claudeJsonl = false, piJsonl = false, onFrame } = {}) {
276
+ if (cancellation) return Promise.resolve({ ...cancellationResult(), stdout: '', stderr: '', reason: 'cancelled' });
267
277
  return new Promise(resolve => {
268
278
  let stdout = '', stderr = '', bytes = 0, reason = null, child, timer, escalation, settled = false;
269
279
  const kill = signal => { try { if (process.platform !== 'win32') process.kill(-child.pid, signal); else child.kill(signal); } catch {} };
270
280
  const finish = (exitCode, signal) => {
271
281
  if (settled) return;
272
282
  settled = true; clearTimeout(timer);
273
- if (child?.pid) ACTIVE_GROUPS.delete(child.pid);
283
+ const complete = () => {
284
+ if (child?.pid) ACTIVE_GROUPS.delete(child.pid);
285
+ resolve({ exitCode, signal, stdout, stderr, reason, ...(reason === 'cancelled' ? cancellationResult() : {}) });
286
+ };
274
287
  // A parent can exit on TERM while a descendant still holds the process group.
275
288
  // Finish the owned group cleanup before releasing an aborted operation.
276
289
  if (reason && escalation) {
277
290
  clearTimeout(escalation);
278
- escalation = setTimeout(() => { kill('SIGKILL'); resolve({ exitCode, signal, stdout, stderr, reason }); }, 500);
279
- } else resolve({ exitCode, signal, stdout, stderr, reason });
291
+ escalation = setTimeout(() => { kill('SIGKILL'); complete(); }, 500);
292
+ } else complete();
280
293
  };
281
- const stop = cause => { if (reason) return; reason = cause; kill('SIGTERM'); escalation = setTimeout(() => kill('SIGKILL'), 500); };
294
+ const stop = (cause, graceMs = 500) => { if (reason) return; reason = cause; kill('SIGTERM'); escalation = setTimeout(() => kill('SIGKILL'), graceMs); };
282
295
  try { child = spawn(binary, args, { cwd, env, shell: false, detached: process.platform !== 'win32', stdio: ['pipe', 'pipe', 'pipe'] }); }
283
296
  catch { finish(null); return; }
284
- if (child.pid) ACTIVE_GROUPS.add(child.pid);
297
+ if (child.pid) ACTIVE_GROUPS.set(child.pid, stop);
285
298
  timer = setTimeout(() => stop('timeout'), timeoutMs);
286
299
  child.on('error', () => { reason = 'spawn-error'; finish(null); });
287
300
  child.stdin.on('error', () => {});
@@ -290,7 +303,7 @@ export function runProcess(binary, args, { cwd, input = '', timeoutMs = 300000,
290
303
  if (output) stdout += part; else stderr += part;
291
304
  if (bytes > maxBytes) stop('output-limit');
292
305
  };
293
- if (codexJsonl || claudeJsonl) {
306
+ if (codexJsonl || claudeJsonl || piJsonl) {
294
307
  let transportBytes = 0, toolFrames = 0;
295
308
  const lines = createInterface({ input: child.stdout, crlfDelay: Infinity });
296
309
  child.stdout.prependListener('data', chunk => {
@@ -304,6 +317,8 @@ export function runProcess(binary, args, { cwd, input = '', timeoutMs = 300000,
304
317
  if (reason || !line.trim()) return;
305
318
  let frame;
306
319
  try { frame = JSON.parse(line); } catch { /* Keep malformed data so validation fails. */ }
320
+ // A-107: every parsed frame reaches the live progress log as it arrives.
321
+ if (frame && onFrame) { try { onFrame(frame); } catch { /* progress never stops the run */ } }
307
322
  if (codexJsonl && typeof frame?.type === 'string' && frame.type.startsWith('item.') &&
308
323
  frame.item?.type !== 'agent_message' && !frameError(frame)) {
309
324
  const toolFrame = completedCodexTool(frame);
@@ -315,6 +330,25 @@ export function runProcess(binary, args, { cwd, input = '', timeoutMs = 300000,
315
330
  if (content.length) collect(Buffer.from(JSON.stringify({ type: frame.type, message: { content } }) + '\n'), true);
316
331
  return;
317
332
  }
333
+ if (piJsonl && frame?.type === 'tool_execution_end') {
334
+ if (typeof frame.toolName === 'string' && typeof frame.isError === 'boolean' && toolFrames++ < TOOL_RESULT_LIMIT) {
335
+ const result = { content: toolOutput(frame.result?.content) };
336
+ collect(Buffer.from(JSON.stringify({ type: frame.type, toolName: frame.toolName, isError: frame.isError, result }) + '\n'), true);
337
+ }
338
+ return;
339
+ }
340
+ if (piJsonl && frame?.type === 'agent_end' && Array.isArray(frame.messages)) {
341
+ const messages = frame.messages.filter(message => message?.role === 'assistant').map(message => ({
342
+ role: message.role,
343
+ stopReason: message.stopReason,
344
+ content: Array.isArray(message.content) ? message.content.filter(part => part?.type === 'text') : [],
345
+ }));
346
+ const model = frame.messages.findLast(message => message?.role === 'assistant' && typeof message.model === 'string')?.model;
347
+ collect(Buffer.from(JSON.stringify({ type: frame.type, model, messages }) + '\n'), true);
348
+ return;
349
+ }
350
+ // Pi's progress events can repeat the full partial assistant message on each delta.
351
+ if (piJsonl && frame && frame.type !== 'agent_end' && frame.type !== 'error' && !frameError(frame)) return;
318
352
  collect(Buffer.from(line + '\n'), true);
319
353
  });
320
354
  } else child.stdout.on('data', chunk => collect(chunk, true));
@@ -358,8 +392,11 @@ function agentFailure(message, stderr = '') {
358
392
  }
359
393
  const PLAIN_TEXT_ADAPTERS = ['ollama', 'openclaw', 'aider', 'goose', 'openhands', 'cline'];
360
394
  export function parseAgentResult(id, result) {
395
+ if (result.cancelled || result.reason === 'cancelled') return { ...result, ok: false, cancelled: true, failureKind: 'cancelled', error: result.error || 'Cancelled.' };
361
396
  const stderr = String(result.stderr || '').trim().slice(-2000);
362
- const processFailure = result.reason ? { ok: false, error: result.reason, failureKind: 'process' } : null;
397
+ // A killed run (timeout, output limit) keeps the tail of what the agent printed, so the failure can be diagnosed (A-103).
398
+ const processFailure = result.reason ? { ok: false, error: result.reason, failureKind: 'process',
399
+ partialOutput: String(result.stdout || '').slice(-8000) || null, stderr: stderr || null } : null;
363
400
  if (PLAIN_TEXT_ADAPTERS.includes(id) || loadCustomAdapters().some(c => c.id === id)) {
364
401
  if (processFailure) return processFailure;
365
402
  if (result.exitCode !== 0) return agentFailure(null, stderr || `Agent process exited ${result.exitCode}`);
@@ -413,7 +450,7 @@ export function parseAgentResult(id, result) {
413
450
  return typeof text === 'string' && text.trim() ? { ok: true, response: text, ...metadata } : { ok: false, error: 'Agent did not produce a recognized completed result', failureKind: 'agent', ...metadata };
414
451
  }
415
452
 
416
- async function readOpenClawResult(execution, { binary, cwd, env, probe, sessionId }) {
453
+ export async function readOpenClawResult(execution, { binary, cwd, env, probe, sessionId }) {
417
454
  const fallback = parseAgentResult('openclaw', execution);
418
455
  let run;
419
456
  try { run = JSON.parse(execution.stdout); } catch { /* No structured run to attribute. */ }
@@ -492,19 +529,18 @@ export async function totalRecallBootstrap(request, { probe = runProcess, env =
492
529
  await acknowledge(cwd);
493
530
  // A receipt is per checkout, and the agent's own gate reads the one where it works.
494
531
  if (request.cwd !== cwd) await acknowledge(request.cwd);
495
- const help = await invoke(['--help']);
496
- const plugins = JSON.parse(await invoke(['plugin', 'library', '--json']));
497
- const commands = JSON.parse(await invoke(['composable-cli', 'library', '--json']));
532
+ // A-121: give the agent the commands that match its task from the semantic CLI index, not whole
533
+ // inventories (the plugin and composed libraries were about 550 KB together). It looks up anything else.
498
534
  const docs = JSON.parse(await invoke(['meta-harness', 'docs-ensure', '--agent', request.agent, '--json']));
499
535
  if (docs.ok !== true) throw notReady('Official agent documentation is unavailable or stale');
500
- let references;
501
- if (request.bootstrapDirectory) {
502
- const file = path.join(request.bootstrapDirectory, 'cli-reference.json');
503
- fs.writeFileSync(file, JSON.stringify({ help, plugins, commands }), { mode: 0o600 });
504
- references = `Complete CLI, installed-plugin and composed-command inventories: ${file}. Retrieve only the applicable operations; use total-recall <namespace> --help for current arguments.`;
505
- } else references = `Complete CLI help:\n${help}\nInstalled plugin command library:\n${JSON.stringify(plugins)}\nComposed command library:\n${JSON.stringify(commands)}`;
536
+ // Node built-ins only here (this file ships to remote nodes): the result is the pretty-printed JSON after any log lines.
537
+ let found = null;
538
+ try { const out = await invoke(['help', task, '--json']); const at = out.indexOf('{\n'); found = JSON.parse(at >= 0 ? out.slice(at) : out); }
539
+ catch (error) { if (error.code === 'NOT_READY') throw error; found = null; }
540
+ const matches = Array.isArray(found?.matches) ? found.matches.slice(0, 12) : [];
541
+ const references = `${matches.length ? `Commands that match this task (Total Recall semantic command search):\n${matches.map(m => `- ${m.verb} — ${m.description}`).join('\n')}\n` : ''}For anything else, run total-recall help "<what you need>" to find the command, then total-recall <namespace> --help for its arguments. Do not load whole command inventories.`;
506
542
  const operatingContext = hasEvidence ? capsule.context.slice(0, evidenceStart).trimEnd() : capsule.context;
507
- const prompt = `Requester: the operator of this Total Recall workspace.\nThe Total Recall preflight is already acknowledged for this checkout, so do not run it again; a sandboxed session cannot write its receipt.\nUse Total Recall and its owning plugin CLI for this task. Context was assembled automatically for this task on this node. Official documentation is cached at ${docs.directory}; read applicable references before changing an integration.\n\nTask context:\n${operatingContext}\n\nCLI reference:\n${references}`;
543
+ const prompt = `Requester: the operator of this Total Recall workspace.\nThe Total Recall preflight is already acknowledged for this checkout, so do not run it again; a sandboxed session cannot write its receipt.\nUse Total Recall and its owning plugin CLI for this task. Context was assembled automatically for this task on this node. Official documentation is cached at ${docs.directory}; read applicable references before changing an integration.\n\nTask context:\n${operatingContext}\n\nCommands:\n${references}`;
508
544
  const requestedTask = `\n\nRequested task:\n${request.prompt}`;
509
545
  if (!hasEvidence) return prompt + requestedTask;
510
546
 
@@ -555,7 +591,7 @@ export async function inspectAgents({ probe = runProcess, env = process.env, ids
555
591
  return { agents, evidence: 'Version and login probes only; actual execution requires a completed run' };
556
592
  }
557
593
 
558
- export async function dispatchAgent(request, { probe = runProcess, env = process.env, bootstrap = totalRecallBootstrap } = {}) {
594
+ export async function dispatchAgent(request, { probe = runProcess, env = process.env, bootstrap = totalRecallBootstrap, sessionRunner } = {}) {
559
595
  const adapter = getActiveAdapters().find(item => item.id === request.agent);
560
596
  if (!adapter || !path.isAbsolute(request.cwd || '') || typeof request.prompt !== 'string' || !request.prompt.trim()) throw new Error('Explicit supported agent, absolute workspace and prompt required');
561
597
  if (Buffer.byteLength(request.prompt) > 65536) throw notReady(`The prompt is ${Buffer.byteLength(request.prompt)} bytes and the limit is 65536, so it was not sent`);
@@ -655,29 +691,67 @@ export async function dispatchAgent(request, { probe = runProcess, env = process
655
691
  }
656
692
  }
657
693
  const allowWrites = request.permissions?.write === true;
658
- if (adapter.id === 'codex') args[args.indexOf('--sandbox') + 1] = allowWrites ? 'workspace-write' : 'read-only';
694
+ // A writing agent gets full access (Greg, 2026-11-09): workspace-write blocked the vault lock,
695
+ // the auth store and the mesh, so routed operational work could not run (A-106).
696
+ // Every launch gets full access (Greg, 2026-11-09). Codex's read-only sandbox and Claude's plan mode
697
+ // block the Keychain, the network and every command, so a read-only job could not unlock the auth
698
+ // store or run the Total Recall CLI (A-110). Read-only work is contained by its disposable checkout
699
+ // (dispatch.mjs), not by the harness sandbox.
700
+ if (adapter.id === 'codex') args[args.indexOf('--sandbox') + 1] = 'danger-full-access';
659
701
  if (billing && adapter.id === 'codex') args.unshift(...CODEX_PLAN_ARGS);
660
702
  if (claudePlan) args.unshift(...CLAUDE_PLAN_ARGS);
661
- if (adapter.id === 'claude') args[args.indexOf('--permission-mode') + 1] = allowWrites ? 'acceptEdits' : 'plan';
703
+ // acceptEdits denied every command a headless run cannot approve, including the preflight and tests (A-104).
704
+ if (adapter.id === 'claude') args[args.indexOf('--permission-mode') + 1] = 'bypassPermissions';
662
705
  if (adapter.id === 'agy') args[args.indexOf('--mode') + 1] = allowWrites ? 'accept-edits' : 'plan';
663
706
  if (adapter.id === 'muse' && !allowWrites) args.push('--disable-write');
664
707
  if (adapter.id === 'pi') args[args.indexOf('--tools') + 1] = allowWrites ? 'read,bash,edit,write' : 'read';
665
708
  if (adapter.id === 'ollama') args.push(env.TR_OLLAMA_MODEL || state.chatModels[0]);
666
- const input = adapter.input === 'stdin' ? prompt : '';
667
- if (adapter.input === 'argument' || adapter.input === 'file') {
668
- const file = path.join(temporary, 'prompt.txt'); fs.writeFileSync(file, prompt, { mode: 0o600 });
669
- if (adapter.input === 'file') args.push('--prompt-file', file);
670
- else args.push(`Read ${file} completely before taking action. It contains the required Total Recall task context, CLI reference location, official documentation location and the requested task. Carry out that task using the supplied context.`);
671
- }
672
- const execution = await probe(binary, args, { cwd: request.cwd, input, timeoutMs: request.timeoutMs || 900000,
673
- codexJsonl: adapter.id === 'codex', claudeJsonl: adapter.id === 'claude', env: { ...agentEnvironment(env), ...credentials, TR_META_HARNESS_RUN: '1' } });
674
- const parsed = openClawSession ? await readOpenClawResult(execution, {
675
- binary, cwd: request.cwd, env: agentEnvironment(env), probe, sessionId: openClawSession,
676
- }) : parseAgentResult(adapter.id, execution);
677
- const frames = parsed.events || [];
678
- return { agent: adapter.id, ...parsed, exitCode: execution.exitCode ?? null,
679
- model: parsed.model ?? frames.find(frame => frame.model)?.model ?? null,
680
- finishedReason: parsed.finishedReason ?? frames.findLast(frame => frame.finished_reason || frame.stopReason)?.finished_reason ?? execution.reason ?? null };
709
+ const progressFile = request.progressFile || path.join(
710
+ env.TR_SECRETS_BRAIN || path.join(env.AGENT_DIR || path.join(env.HOME || os.homedir(), '.agent'), 'skills/total-recall'),
711
+ 'config/meta-harness/jobs', `${request.jobId || process.env.TR_METAHARNESS_JOB || `session-${randomUUID()}`}.events.jsonl`);
712
+ const progress = createProgress({ file: progressFile, harness: adapter.id,
713
+ onStall: reason => sendStallAlert(`Metaharness: ${adapter.id} stalled`, `${reason}. Live log: ${progressFile}`) });
714
+ const ownerId = request.jobId || process.env.TR_METAHARNESS_JOB;
715
+ const ownerFile = ownerId ? path.join(path.dirname(progressFile), `${ownerId}.events.jsonl`) : progressFile;
716
+ const ownerProgress = ownerFile !== progressFile ? createProgress({ file: ownerFile, harness: adapter.id }) : null;
717
+ const liveProgress = ownerProgress ? { event: frame => { progress.event(frame); ownerProgress.event(frame); } } : progress;
718
+ const { runSession, sessionOptions } = await import('./session/run.mjs');
719
+ const savedRequest = Object.fromEntries(['agent', 'model', 'tools', 'settingSources', 'allowedTools', 'permissions', 'allowUnknownAuth',
720
+ 'goal', 'maxTurns', 'maxTokens', 'maxBudgetUsd', 'resultSchema', 'approvalPolicy', 'questionTimeoutMs', 'effort', 'fallbackModel'].filter(key => request[key] !== undefined).map(key => [key, request[key]]));
721
+ if (savedRequest.resultSchema !== undefined) savedRequest.resultSchema = sessionOptions(request).resultSchema;
722
+ const snapshot = JSON.stringify({ workspace: request.contextCwd || request.cwd, branch: request.branch, request: savedRequest });
723
+ fs.writeFileSync(progressFile.replace('.events.jsonl', '.session.json'), snapshot, { mode: 0o600 });
724
+ if (ownerProgress) fs.writeFileSync(ownerFile.replace('.events.jsonl', '.session.json'), snapshot, { mode: 0o600 });
725
+ let parsed;
726
+ const frames = [];
727
+ const captureFrame = frame => {
728
+ if (frames.length >= 80) return;
729
+ if (adapter.id === 'claude') {
730
+ const content = claudeToolBlocks(frame);
731
+ if (content.length) frames.push({ type: frame.type, message: { content } });
732
+ } else if (adapter.id === 'codex') {
733
+ const item = frame.method === 'item/completed' ? frame.params?.item : null;
734
+ if (item?.type === 'commandExecution' && Number.isInteger(item.exitCode)) frames.push({ type: 'item.completed',
735
+ item: { type: 'command_execution', status: item.status, exit_code: item.exitCode, aggregated_output: toolOutput(item.aggregatedOutput) } });
736
+ } else if (['tool_call', 'tool_result', 'tool_execution_end'].includes(frame.type)) frames.push(frame);
737
+ };
738
+ try {
739
+ parsed = await (sessionRunner || runSession)({ request, adapter, binary, args,
740
+ env: { ...agentEnvironment(env), ...credentials, TR_META_HARNESS_RUN: '1' }, prompt, progress: liveProgress, onFrame: captureFrame,
741
+ register: stop => { const id = Symbol('session'); ACTIVE_GROUPS.set(id, stop); return () => ACTIVE_GROUPS.delete(id); } });
742
+ if (cancellationResult()) parsed = { ...parsed, ...cancellationResult() };
743
+ if (frames.length) Object.assign(parsed, toolEvidence(nativeToolResults(adapter.id, frames)));
744
+ if (openClawSession && parsed.execution) {
745
+ const evidence = await readOpenClawResult(parsed.execution, { binary, cwd: request.cwd, env: agentEnvironment(env), probe, sessionId: openClawSession });
746
+ Object.assign(parsed, toolEvidence(evidence.toolResults || []), { provider: evidence.provider, toolEvidenceError: evidence.toolEvidenceError });
747
+ if (!evidence.ok) Object.assign(parsed, { ok: false, error: evidence.error, failureKind: evidence.failureKind });
748
+ delete parsed.execution;
749
+ }
750
+ } catch (error) { parsed = { ok: false, error: error.message, failureKind: error.failureKind || 'agent' }; }
751
+ finally { progress.close(parsed); ownerProgress?.close(parsed); }
752
+ return { agent: adapter.id, ...parsed, progressFile, statusFile: progress.statusFile,
753
+ sessionId: parsed.sessionId || progress.status().session_id,
754
+ exitCode: parsed.ok ? 0 : parsed.exitCode ?? null };
681
755
  } finally { fs.rmSync(temporary, { recursive: true, force: true }); }
682
756
  }
683
757
 
@@ -709,3 +783,174 @@ export async function runWorker(request) {
709
783
  }
710
784
  throw new Error('Unknown meta-harness worker operation');
711
785
  }
786
+
787
+ // ─── Live progress (A-107) ─────────────────────────────────────────────────────
788
+ // Kept inside this file: executor.mjs ships worker.mjs whole to remote nodes, so it may
789
+ // import only Node built-ins.
790
+ /**
791
+ * Live visibility into a dispatched agent (A-107).
792
+ *
793
+ * Claude Code (`--output-format stream-json --verbose`) and Codex (`exec --json`) stream every
794
+ * event while they run. Each event, normalized to one line, goes to the job's event log as it
795
+ * arrives; a status record sits beside it; a watchdog flags a stalled run.
796
+ * <job>.events.jsonl one JSON line per event: { t, kind, text }
797
+ * <job>.status.json { state, harness, session_id, started_at, last_event_at, events, current, in_progress, result }
798
+ */
799
+ export const STALL_MS = 5 * 60_000;
800
+ export const LONG_STEP_MS = 30 * 60_000;
801
+ const CHECK_MS = 30_000;
802
+ const TEXT_MAX = 300;
803
+
804
+ const clip = value => {
805
+ const text = String(value ?? '').replace(/\s+/g, ' ').trim();
806
+ return text.length > TEXT_MAX ? `${text.slice(0, TEXT_MAX - 1)}…` : text;
807
+ };
808
+
809
+ /** One readable event per agent frame, or null for frames that carry nothing a person needs. */
810
+ export function normalizeFrame(harness, frame) {
811
+ if (!frame || typeof frame !== 'object') return null;
812
+ if (typeof frame.kind === 'string') {
813
+ const entry = { ...frame, text: clip(toolOutput(frame.text)) };
814
+ // Native tool frames can be megabytes long and contain command output.
815
+ // The worker keeps bounded tool evidence separately from the live view.
816
+ delete entry.item;
817
+ delete entry.input;
818
+ delete entry.response;
819
+ delete entry.structured;
820
+ delete entry.server;
821
+ if (entry.request) entry.request = toolOutput(JSON.stringify(entry.request));
822
+ return entry;
823
+ }
824
+ if (harness === 'codex') {
825
+ const item = frame.item || {};
826
+ switch (frame.type) {
827
+ case 'thread.started': return { kind: 'session', text: `Codex session ${frame.thread_id || ''}`.trim(), session: frame.thread_id || null };
828
+ case 'turn.started': return { kind: 'turn', text: 'Turn started' };
829
+ case 'turn.completed': return { kind: 'usage', text: `Turn completed (${frame.usage?.input_tokens ?? '?'} in, ${frame.usage?.output_tokens ?? '?'} out tokens)` };
830
+ case 'turn.failed': return { kind: 'error', text: `Turn failed: ${clip(frame.error?.message || frame.error)}` };
831
+ case 'error': return { kind: 'error', text: `Error: ${clip(frame.message || frame.error)}` };
832
+ case 'item.started':
833
+ if (item.type === 'command_execution') return { kind: 'step-start', text: `Running: ${clip(item.command)}` };
834
+ if (item.type === 'mcp_tool_call') return { kind: 'step-start', text: `Calling ${item.server || ''}.${item.tool || ''}` };
835
+ return null;
836
+ case 'item.completed':
837
+ if (item.type === 'command_execution') return { kind: 'step-end', text: `Ran: ${clip(item.command)} (exit ${item.exit_code ?? '?'})` };
838
+ if (item.type === 'mcp_tool_call') return { kind: 'step-end', text: `Called ${item.server || ''}.${item.tool || ''}` };
839
+ if (item.type === 'file_change') return { kind: 'edit', text: `Changed ${clip((item.changes || []).map(change => change.path).join(', '))}` };
840
+ if (item.type === 'agent_message') return { kind: 'message', text: clip(item.text) };
841
+ if (item.type === 'web_search') return { kind: 'search', text: `Searched: ${clip(item.query)}` };
842
+ if (item.type === 'todo_list') return { kind: 'plan', text: `Plan: ${clip((item.items || []).map(entry => `${entry.completed ? '✓' : '·'} ${entry.text}`).join('; '))}` };
843
+ return null;
844
+ default: return null;
845
+ }
846
+ }
847
+ if (harness === 'claude') {
848
+ if (frame.type === 'system' && frame.subtype === 'init') return { kind: 'session', text: `Claude session ${frame.session_id || ''} (${frame.model || 'model unknown'})`.trim(), session: frame.session_id || null };
849
+ if (frame.type === 'system' && frame.subtype === 'api_retry') return { kind: 'retry', text: `API retry ${frame.attempt}/${frame.max_retries}: ${frame.error || 'unknown'}` };
850
+ if (frame.type === 'system' && frame.subtype === 'permission_denied') return { kind: 'error', text: `Permission denied: ${clip(frame.tool_name || frame.message)}` };
851
+ if (frame.type === 'result') return { kind: frame.is_error ? 'error' : 'usage', text: `Result: ${frame.subtype || ''}${frame.total_cost_usd !== undefined ? `, cost $${frame.total_cost_usd}` : ''}`, session: frame.session_id || null };
852
+ const blocks = Array.isArray(frame.message?.content) ? frame.message.content : [];
853
+ if (frame.type === 'assistant') {
854
+ const tool = blocks.find(block => block.type === 'tool_use');
855
+ if (tool) return { kind: 'step-start', text: `${tool.name}: ${clip(tool.input?.command || tool.input?.file_path || tool.input?.pattern || tool.input?.description || JSON.stringify(tool.input || {}))}` };
856
+ const text = blocks.filter(block => block.type === 'text').map(block => block.text).join(' ');
857
+ return text.trim() ? { kind: 'message', text: clip(text) } : null;
858
+ }
859
+ if (frame.type === 'user' && blocks.some(block => block.type === 'tool_result')) {
860
+ const failed = blocks.some(block => block.type === 'tool_result' && block.is_error);
861
+ return { kind: 'step-end', text: failed ? 'Tool returned an error' : 'Tool finished' };
862
+ }
863
+ return null;
864
+ }
865
+ return null;
866
+ }
867
+
868
+ /**
869
+ * Track one dispatched run. `event(frame)` records an agent frame; `close(result)` records the
870
+ * outcome. A watchdog marks the run stalled once, and calls `onStall`, when no event arrives for
871
+ * `stallMs` while no step is running, or one step runs past `longStepMs`.
872
+ */
873
+ export function createProgress({ file, harness, stallMs = STALL_MS, longStepMs = LONG_STEP_MS, onStall, now = () => Date.now(), checkMs = CHECK_MS } = {}) {
874
+ fs.mkdirSync(path.dirname(file), { recursive: true });
875
+ const statusFile = file.replace(/\.events\.jsonl$/, '') + '.status.json';
876
+ const status = { state: 'running', harness, pid: process.pid, session_id: null, started_at: new Date(now()).toISOString(), last_event_at: null,
877
+ events: 0, current: null, in_progress: 0, step_started_at: null, events_file: file, result: null };
878
+ const saveStatus = () => {
879
+ const temp = `${statusFile}.${process.pid}.tmp`;
880
+ try { fs.writeFileSync(temp, JSON.stringify(status, null, 2), { mode: 0o600 }); fs.renameSync(temp, statusFile); } catch { /* status is best effort; events stay authoritative */ }
881
+ };
882
+ const write = (kind, text, fields = {}) => {
883
+ const at = new Date(now()).toISOString();
884
+ try { fs.appendFileSync(file, JSON.stringify({ ...fields, t: at, kind, text }) + '\n', { mode: 0o600 }); } catch { /* keep the run going */ }
885
+ status.events += 1; status.last_event_at = at;
886
+ };
887
+ write('start', `${harness} started`);
888
+ saveStatus();
889
+ let stalled = false;
890
+ const check = () => {
891
+ if (status.state !== 'running') return;
892
+ const idle = now() - new Date(status.last_event_at).getTime();
893
+ const stepAge = status.step_started_at ? now() - new Date(status.step_started_at).getTime() : 0;
894
+ const reason = status.in_progress > 0
895
+ ? (stepAge > longStepMs ? `One step has run ${Math.round(stepAge / 60000)} min: ${status.current}` : null)
896
+ : (idle > stallMs ? `No agent event for ${Math.round(idle / 60000)} min` : null);
897
+ if (reason && !stalled) {
898
+ stalled = true; status.state = 'stalled'; write('stall', reason); saveStatus();
899
+ try { onStall?.(reason, { ...status }); } catch { /* alerting never stops the run */ }
900
+ }
901
+ };
902
+ const timer = setInterval(check, checkMs); timer.unref?.();
903
+ return {
904
+ statusFile,
905
+ status: () => ({ ...status }),
906
+ check,
907
+ event(frame) {
908
+ const entry = normalizeFrame(harness, frame);
909
+ if (!entry) return;
910
+ if (entry.session || entry.sessionId) status.session_id = entry.session || entry.sessionId;
911
+ if (entry.capabilities) status.capabilities = entry.capabilities;
912
+ if (entry.kind === 'usage') status.usage = { ...entry };
913
+ if (entry.kind === 'session') status.cwd = entry.cwd || status.cwd;
914
+ if (entry.kind === 'step-start') { status.in_progress += 1; status.step_started_at = new Date(now()).toISOString(); }
915
+ if (entry.kind === 'step-end') { status.in_progress = Math.max(0, status.in_progress - 1); if (!status.in_progress) status.step_started_at = null; }
916
+ if (['step-start', 'message', 'edit', 'plan', 'search'].includes(entry.kind)) status.current = entry.text;
917
+ // A new event after a stall means the agent is working again.
918
+ if (stalled && status.state === 'stalled') { stalled = false; status.state = 'running'; }
919
+ write(entry.kind, entry.text, entry);
920
+ saveStatus();
921
+ },
922
+ close(result = {}) {
923
+ clearInterval(timer);
924
+ // The CLI and its active attempt can share the job status. Keep the last
925
+ // session snapshot when the CLI writes the terminal bookkeeping record.
926
+ try {
927
+ const latest = JSON.parse(fs.readFileSync(statusFile, 'utf8'));
928
+ for (const key of ['session_id', 'capabilities', 'usage']) if (latest[key] != null) status[key] = latest[key];
929
+ } catch { /* no prior status */ }
930
+ status.state = result.cancelled ? 'cancelled' : result.ok ? 'finished' : 'failed';
931
+ status.in_progress = 0;
932
+ status.step_started_at = null;
933
+ status.result = { ok: result.ok === true, ...(result.cancelled ? { cancelled: true } : {}), error: result.error || null, failureKind: result.failureKind || null };
934
+ write(result.cancelled ? 'cancel' : result.ok ? 'finish' : 'fail', result.cancelled ? 'Cancelled' : result.ok ? 'Finished' : `Failed: ${clip(result.error || 'unknown')}`);
935
+ saveStatus();
936
+ },
937
+ };
938
+ }
939
+
940
+ /** Status and the last `tail` events of a job, for `meta-harness watch`. */
941
+ export function readProgress(file, tail = 20) {
942
+ const statusFile = file.replace(/\.events\.jsonl$/, '') + '.status.json';
943
+ let status = null;
944
+ try { status = JSON.parse(fs.readFileSync(statusFile, 'utf8')); } catch { /* not started or not a job */ }
945
+ let events = [];
946
+ try { events = fs.readFileSync(file, 'utf8').trim().split('\n').filter(Boolean).slice(-tail).map(line => JSON.parse(line)); } catch { /* none yet */ }
947
+ return { status, events };
948
+ }
949
+
950
+
951
+ /** Raise an operator alert without importing alerts.mjs, so remote workers can send it too. */
952
+ function sendStallAlert(title, message) {
953
+ try {
954
+ execFileSync(process.env.TOTAL_RECALL_BIN || 'total-recall', ['alerts', 'send', '--title', title, '--message', message, '--severity', 'warning', '--source', 'meta-harness'], { stdio: 'pipe', timeout: 15000 });
955
+ } catch { /* an unavailable alert channel never stops the run */ }
956
+ }
@@ -20,18 +20,32 @@ export const diffBetween = (cwd, base, head) => must(cwd, ['diff', `${base}...${
20
20
  const ACTIVE_WORKTREES = new Map();
21
21
 
22
22
  /** `branch` creates a new branch from `ref`. Without it, the checkout is detached at `ref` (read-only use). */
23
- export function createWorktree(cwd, { branch, ref = 'HEAD', prefix = 'meta-harness-' } = {}) {
23
+ export function createWorktree(cwd, { branch, ref = 'HEAD', prefix = 'meta-harness-', existingBranch = false } = {}) {
24
24
  const directory = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
25
- const args = branch ? ['worktree', 'add', '-b', branch, directory, ref] : ['worktree', 'add', '--detach', directory, ref];
25
+ const args = branch ? existingBranch ? ['worktree', 'add', directory, branch] : ['worktree', 'add', '-b', branch, directory, ref] : ['worktree', 'add', '--detach', directory, ref];
26
26
  const result = git(cwd, args, 120000);
27
27
  if (result.status !== 0) {
28
28
  fs.rmSync(directory, { recursive: true, force: true });
29
29
  throw new Error(`Creating a worktree failed: ${String(result.stderr).trim().slice(0, 300)}`);
30
30
  }
31
- ACTIVE_WORKTREES.set(directory, { cwd, branch: branch || null });
31
+ ACTIVE_WORKTREES.set(directory, { cwd, branch: branch || null, base: headCommit(directory) });
32
32
  seedMandatorySkills(cwd, directory);
33
+ linkPrivatePlans(cwd, directory);
33
34
  return directory;
34
35
  }
36
+ /**
37
+ * Plans in docs/projects are private and untracked in public repositories, so a checkout lacks them
38
+ * and an agent would start a duplicate plan (A-116). Link the one real plan folder in; never commit it.
39
+ */
40
+ const PLANS = 'docs/projects';
41
+ function linkPrivatePlans(cwd, directory) {
42
+ const root = git(cwd, ['rev-parse', '--show-toplevel']).stdout.trim();
43
+ const source = path.join(root, PLANS), target = path.join(directory, PLANS);
44
+ if (!root || !fs.existsSync(source) || fs.existsSync(target)) return;
45
+ if (git(cwd, ['ls-files', '--', PLANS]).stdout.trim()) return;
46
+ fs.mkdirSync(path.dirname(target), { recursive: true });
47
+ fs.symlinkSync(source, target);
48
+ }
35
49
  /**
36
50
  * An agent's own Total Recall preflight needs these two files at the repository root, and they are
37
51
  * usually gitignored, so a fresh checkout lacks them and every agent inside it would report itself blocked.
@@ -57,17 +71,58 @@ export function linkDependencies(cwd, directory) {
57
71
  const target = path.join(directory, '.agent/skills/code-quality');
58
72
  if (fs.existsSync(quality) && !fs.existsSync(target)) fs.cpSync(quality, target, { recursive: true, filter: source => !source.includes(`${path.sep}reports`) });
59
73
  }
74
+ /**
75
+ * Give an agent's checkout its own dependencies before it starts (A-109). A link would let an
76
+ * agent's `npm install` write into the operator's tree, so the checkout gets a copy-on-write
77
+ * clone of `node_modules` (macOS clonefile, Linux reflink: no extra disk), or, when the operator
78
+ * has none, a frozen install from the lockfile. Returns what it did, for the attempt record.
79
+ */
80
+ export function provideDependencies(cwd, directory, { run = spawnSync } = {}) {
81
+ if (!fs.existsSync(path.join(directory, 'package.json'))) return { action: 'none', reason: 'no package.json' };
82
+ const target = path.join(directory, 'node_modules');
83
+ if (fs.existsSync(target)) return { action: 'present' };
84
+ const modules = path.join(cwd, 'node_modules');
85
+ if (fs.existsSync(modules)) {
86
+ const args = process.platform === 'darwin' ? ['-c', '-R', modules, target] : ['-a', '--reflink=auto', modules, target];
87
+ const copied = run('cp', args, { stdio: 'ignore' });
88
+ if (copied.status === 0) return { action: 'cloned' };
89
+ }
90
+ const lock = [['package-lock.json', 'npm', ['ci', '--no-audit', '--no-fund']], ['pnpm-lock.yaml', 'pnpm', ['install', '--frozen-lockfile']], ['yarn.lock', 'yarn', ['install', '--frozen-lockfile']]]
91
+ .find(([file]) => fs.existsSync(path.join(directory, file)));
92
+ if (!lock) return { action: 'none', reason: 'no lockfile' };
93
+ const installed = run(lock[1], lock[2], { cwd: directory, stdio: 'ignore', timeout: 600000 });
94
+ return installed.status === 0 ? { action: 'installed', manager: lock[1] } : { action: 'failed', manager: lock[1] };
95
+ }
96
+
97
+ /**
98
+ * Push a finished job's branch (A-113, Greg 2026-11-09: agents push finished work themselves).
99
+ * Only `meta-harness/...` branches are pushed, so a job can never push a deploy branch.
100
+ * No `origin` remote means nothing to push. Returns what happened, for the job record.
101
+ */
102
+ export function pushBranch(cwd, branch) {
103
+ if (!branch || !branch.startsWith('meta-harness/')) return { pushed: false, reason: 'not a job branch' };
104
+ if (git(cwd, ['remote', 'get-url', 'origin']).status !== 0) return { pushed: false, reason: 'no origin remote' };
105
+ const result = git(cwd, ['push', '-q', '-u', 'origin', `${branch}:${branch}`], 120000);
106
+ return result.status === 0 ? { pushed: true, remote: 'origin', branch } : { pushed: false, reason: String(result.stderr || result.error || '').trim().slice(0, 300) };
107
+ }
60
108
  export const deleteBranch = (cwd, branch) => git(cwd, ['branch', '-D', branch]).status === 0;
61
109
  export function removeWorktree(cwd, directory) {
62
110
  if (!directory) return;
63
111
  ACTIVE_WORKTREES.delete(directory);
64
- try { if (fs.lstatSync(path.join(directory, 'node_modules')).isSymbolicLink()) fs.rmSync(path.join(directory, 'node_modules')); } catch { /* no link */ }
112
+ for (const link of ['node_modules', PLANS]) {
113
+ try { if (fs.lstatSync(path.join(directory, link)).isSymbolicLink()) fs.rmSync(path.join(directory, link)); } catch { /* no link */ }
114
+ }
65
115
  git(cwd, ['worktree', 'remove', '--force', directory], 60000);
66
116
  fs.rmSync(directory, { recursive: true, force: true });
67
117
  }
68
118
  /** Commit whatever the agent left. Returns the commit, or null when it changed nothing. */
69
119
  export function commitAll(directory, message) {
70
- must(directory, ['add', '-A', '--', '.', ':!node_modules'], 'Staging agent changes');
120
+ // Stage everything git does not ignore. Naming node_modules in the pathspec fails when it is
121
+ // ignored (A-114), so it is unstaged afterwards instead, for repositories that do not ignore it.
122
+ must(directory, ['add', '-A'], 'Staging agent changes');
123
+ git(directory, ['rm', '-r', '--cached', '-q', '--ignore-unmatch', '--', 'node_modules']);
124
+ // The linked private plan folder (A-116) is the operator's, never part of the agent's commit.
125
+ try { if (fs.lstatSync(path.join(directory, PLANS)).isSymbolicLink()) git(directory, ['rm', '-r', '--cached', '-q', '--ignore-unmatch', '--', PLANS]); } catch { /* no link */ }
71
126
  // Seeded skills are scaffolding, not the agent's work. A repository that tracks them keeps normal history.
72
127
  for (const file of MANDATORY_SKILLS) if (git(directory, ['cat-file', '-e', `HEAD:${file}`]).status !== 0) git(directory, ['rm', '--cached', '-q', '--ignore-unmatch', '--', file]);
73
128
  if (git(directory, ['diff', '--cached', '--quiet']).status === 0) return null;
@@ -77,10 +132,22 @@ export function commitAll(directory, message) {
77
132
  must(directory, ['commit', '-m', message], 'Committing agent changes');
78
133
  return headCommit(directory);
79
134
  }
80
- /** A stopped run's checkouts and unfinished branches go with it. A branch already handed back has no active checkout. */
81
- export function removeActiveWorktrees() {
82
- for (const [directory, { cwd, branch }] of [...ACTIVE_WORKTREES]) {
135
+ /** Remove owned checkouts. Cancellation keeps branches that contain work. */
136
+ export function removeActiveWorktrees({ preserveChanges = false } = {}) {
137
+ const saved = [];
138
+ for (const [directory, { cwd, branch, base }] of [...ACTIVE_WORKTREES]) {
139
+ let wip = null;
140
+ if (preserveChanges && branch) {
141
+ try { wip = commitAll(directory, 'meta-harness: unfinished work from a cancelled job'); }
142
+ catch (error) {
143
+ // Do not destroy edits if the WIP commit fails (for example, a credential file).
144
+ saved.push({ branch, directory, error: error.message });
145
+ continue;
146
+ }
147
+ if (wip || headCommit(directory) !== base) saved.push({ branch, wip, commit: headCommit(directory) });
148
+ }
83
149
  removeWorktree(cwd, directory);
84
- if (branch) deleteBranch(cwd, branch);
150
+ if (branch && (!preserveChanges || !saved.some(item => item.branch === branch))) deleteBranch(cwd, branch);
85
151
  }
152
+ return saved;
86
153
  }
@@ -1,9 +1,9 @@
1
- # DESIGN.md — Meta Harness
1
+ # DESIGN.md — Metaharness
2
2
 
3
3
  ```yaml
4
4
  tokens:
5
- brand-name: "Meta Harness"
6
- wordmark: "META HARNESS"
5
+ brand-name: "Metaharness"
6
+ wordmark: "METAHARNESS"
7
7
  color-primary: "#3b82f6"
8
8
  color-background: "#0c1427"
9
9
  color-surface: "#1e293b"
@@ -20,8 +20,8 @@ tokens:
20
20
  ```
21
21
 
22
22
  ## Brand & Typographic Wordmark
23
- - **Name**: Meta Harness
24
- - **Wordmark Logo**: `META HARNESS` (clean typographic badge, no invented cyber delimiters)
23
+ - **Name**: Metaharness
24
+ - **Wordmark Logo**: `METAHARNESS` (clean typographic badge, no invented cyber delimiters)
25
25
  - **Palette**: Primary `#3b82f6`, Surface `#0c1427`, Secondary Surface `#1e293b`, Accent `#38bdf8`, Line `#1e293b`, Text `#f1f5f9`.
26
26
  - **States**: Verified Pass `#10b981`, Warning `#f59e0b`, Danger `#ef4444`.
27
27
 
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Meta Harness — Multi-Agent Coordination & Inspection Console
2
+ * Metaharness — Multi-Agent Coordination & Inspection Console
3
3
  *
4
4
  * Bespoke operator interface for terminal coding agent fleet management.
5
5
  * Displays real-time agent discovery matrix, mesh node targets, authentication
@@ -855,9 +855,9 @@ export class CommandFleetCard extends HTMLElement {
855
855
 
856
856
  <div class="header">
857
857
  <div class="brand-group">
858
- <div class="wordmark-badge">META HARNESS</div>
858
+ <div class="wordmark-badge">METAHARNESS</div>
859
859
  <div class="title-meta">
860
- <h3 class="plugin-title">Meta Harness Fleet</h3>
860
+ <h3 class="plugin-title">Metaharness Fleet</h3>
861
861
  <p class="plugin-subtitle">Multi-agent runtime coordination, quotas & benchmarks</p>
862
862
  </div>
863
863
  </div>