total-recall-brain 3.43.3 → 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 (252) hide show
  1. package/bin/total-recall.mjs +6 -4
  2. package/docs/ARCHITECTURE.md +10 -7
  3. package/docs/reference/cli-reference.md +16 -9
  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 +3 -3
  9. package/scaffold/.agent/skills/plugin/SKILL.md +7 -2
  10. package/scaffold/.agent/skills/plugin/evals/README.md +5 -0
  11. package/scaffold/.agent/skills/plugin/evals/cases/01-fabricated-seed.mjs +1 -0
  12. package/scaffold/.agent/skills/plugin/evals/cases/02-reassuring-default.mjs +1 -0
  13. package/scaffold/.agent/skills/plugin/evals/cases/03-declared-template.mjs +1 -0
  14. package/scaffold/.agent/skills/plugin/evals/cases/04-card-collects-credential.mjs +1 -0
  15. package/scaffold/.agent/skills/plugin/evals/cases/05-host-leak-in-scaffold.mjs +1 -0
  16. package/scaffold/.agent/skills/plugin/evals/cases/06-correct-plugin.mjs +1 -0
  17. package/scaffold/.agent/skills/plugin/evals/cases/07-missing-reuse.mjs +11 -0
  18. package/scaffold/.agent/skills/plugin/evals/cases/08-unresolved-owner-command.mjs +12 -0
  19. package/scaffold/.agent/skills/plugin/evals/cases/09-missing-brick.mjs +12 -0
  20. package/scaffold/.agent/skills/plugin/prompts/01-function.md +4 -3
  21. package/scaffold/.agent/skills/plugin/prompts/08-verify.md +1 -0
  22. package/scaffold/.agent/skills/plugin/references/surfaces.md +12 -2
  23. package/scaffold/.agent/skills/plugin/scripts/record.mjs +59 -2
  24. package/scaffold/.agent/skills/total-recall/plugins/auth/README.md +8 -0
  25. package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +15 -14
  26. package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +3 -3
  27. package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +1 -1
  28. package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-gh-login.mjs +102 -0
  29. package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +38 -2
  30. package/scaffold/.agent/skills/total-recall/plugins/auth/src/login-projections.mjs +157 -13
  31. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-store.mjs +12 -1
  32. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-sync.mjs +161 -17
  33. package/scaffold/.agent/skills/total-recall/plugins/automations/BRICK.md +7 -0
  34. package/scaffold/.agent/skills/total-recall/plugins/automations/LICENSE +21 -0
  35. package/scaffold/.agent/skills/total-recall/plugins/automations/README.md +66 -0
  36. package/scaffold/.agent/skills/total-recall/plugins/automations/adapters.mjs +231 -0
  37. package/scaffold/.agent/skills/total-recall/plugins/automations/capability/index.mjs +16 -0
  38. package/scaffold/.agent/skills/total-recall/plugins/automations/cli.mjs +121 -0
  39. package/{src/core/plugin-cron.mjs → scaffold/.agent/skills/total-recall/plugins/automations/cron.mjs} +1 -3
  40. package/scaffold/.agent/skills/total-recall/plugins/automations/inventory.mjs +88 -0
  41. package/scaffold/.agent/skills/total-recall/plugins/automations/package-lock.json +7661 -0
  42. package/scaffold/.agent/skills/total-recall/plugins/automations/package.json +35 -0
  43. package/scaffold/.agent/skills/total-recall/plugins/automations/plugin.json +167 -0
  44. package/scaffold/.agent/skills/total-recall/plugins/automations/scheduler.mjs +246 -0
  45. package/scaffold/.agent/skills/total-recall/plugins/automations/ui/DESIGN.md +42 -0
  46. package/scaffold/.agent/skills/total-recall/plugins/automations/ui/automations.js +2 -0
  47. package/scaffold/.agent/skills/total-recall/plugins/automations/ui/card.js +273 -0
  48. package/scaffold/.agent/skills/total-recall/plugins/code-quality/commands/run-core.mjs +1 -1
  49. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/check.mjs +57 -5
  50. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/report-data.mjs +40 -14
  51. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/report.mjs +15 -40
  52. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +15 -14
  53. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +3 -3
  54. package/scaffold/.agent/skills/total-recall/plugins/code-quality/plugin.json +1 -1
  55. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +15 -14
  56. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +3 -3
  57. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +1 -1
  58. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +15 -14
  59. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +3 -3
  60. package/scaffold/.agent/skills/total-recall/plugins/embeddings/plugin.json +1 -1
  61. package/scaffold/.agent/skills/total-recall/plugins/git/README.md +112 -0
  62. package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/cli.mjs +135 -23
  63. package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/package-lock.json +17 -16
  64. package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/package.json +5 -4
  65. package/scaffold/.agent/skills/total-recall/plugins/git/plugin.json +161 -0
  66. package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/context.mjs +94 -0
  67. package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/phase3-spec-support.mjs +50 -0
  68. package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/runtime.mjs +40 -0
  69. package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/spec-support.mjs +51 -0
  70. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/_support.mjs +195 -0
  71. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/auth.mjs +201 -0
  72. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/bisect.mjs +40 -0
  73. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/cli-meta.mjs +159 -0
  74. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/commit-memory.mjs +30 -0
  75. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/commit.mjs +46 -0
  76. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/fleet.mjs +156 -0
  77. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/force-push.mjs +58 -0
  78. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/group.mjs +119 -0
  79. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/handoff.mjs +59 -0
  80. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/message.mjs +22 -0
  81. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/resolve.mjs +60 -0
  82. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/rules.mjs +81 -0
  83. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/secret-firewall.mjs +66 -0
  84. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/undo.mjs +132 -0
  85. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/why.mjs +16 -0
  86. package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/ui/card.js +2 -2
  87. package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +15 -14
  88. package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +3 -3
  89. package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +1 -1
  90. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/README.md +59 -10
  91. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/BRICK.md +1 -1
  92. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/index.mjs +18 -6
  93. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +160 -29
  94. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +15 -14
  95. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +4 -4
  96. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +231 -16
  97. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/skills/meta-harness/SKILL.md +19 -4
  98. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/adoption.mjs +104 -0
  99. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/alerts.mjs +3 -3
  100. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/cascade.mjs +101 -16
  101. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/control.mjs +145 -0
  102. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +54 -21
  103. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch-request.mjs +40 -0
  104. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch.mjs +68 -13
  105. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/documentation.mjs +55 -11
  106. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/executor.mjs +64 -15
  107. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/hermes.mjs +29 -1
  108. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/index.mjs +1 -1
  109. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/included-usage.mjs +249 -0
  110. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/jobs.mjs +84 -0
  111. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/learner.mjs +6 -12
  112. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/maintenance.mjs +17 -0
  113. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/mcp-server.mjs +228 -0
  114. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/pool.mjs +16 -8
  115. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/process-manager.mjs +26 -2
  116. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/reasoning.mjs +6 -1
  117. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/remote-operation.mjs +6 -2
  118. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/review.mjs +34 -3
  119. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/reviews.mjs +25 -0
  120. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +28 -12
  121. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/runtime.mjs +28 -6
  122. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/claude.mjs +319 -0
  123. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/codex.mjs +387 -0
  124. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/result.mjs +67 -0
  125. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/resume.mjs +373 -0
  126. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/run.mjs +223 -0
  127. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session-jobs.mjs +72 -0
  128. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/setup.mjs +6 -3
  129. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/termination.mjs +16 -12
  130. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/threads.mjs +32 -1
  131. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/usage-queue.mjs +110 -0
  132. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/verify-ssss.mjs +3 -3
  133. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +597 -70
  134. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worktree.mjs +76 -9
  135. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/DESIGN.md +5 -5
  136. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +3 -3
  137. package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +47 -18
  138. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +15 -14
  139. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +3 -3
  140. package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +1 -1
  141. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh-diagnostics.mjs +44 -0
  142. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +120 -113
  143. package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +15 -14
  144. package/scaffold/.agent/skills/total-recall/plugins/security/package.json +3 -3
  145. package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +1 -1
  146. package/scaffold/.agent/skills/total-recall/plugins/ssss/assets/scaffold.json +5 -5
  147. package/scaffold/.agent/skills/total-recall/plugins/ssss/package-lock.json +6 -6
  148. package/scaffold/.agent/skills/total-recall/plugins/ssss/package.json +2 -2
  149. package/scaffold/.agent/skills/total-recall/plugins/ssss/plugin.json +1 -1
  150. package/scaffold/.agent/skills/total-recall/references/operations.md +1 -1
  151. package/src/cli/agent.mjs +1 -1
  152. package/src/cli/command.mjs +41 -2
  153. package/src/cli/connect.mjs +2 -29
  154. package/src/cli/context.mjs +1 -0
  155. package/src/cli/dream.mjs +93 -69
  156. package/src/cli/harness.mjs +4 -4
  157. package/src/cli/help.mjs +1 -1
  158. package/src/cli/ingest-okf.mjs +4 -4
  159. package/src/cli/init.mjs +0 -25
  160. package/src/cli/lessons.mjs +51 -0
  161. package/src/cli/memory-plugin.mjs +1 -1
  162. package/src/cli/plugin/copies.mjs +64 -14
  163. package/src/cli/plugin/create.mjs +6 -7
  164. package/src/cli/plugin/index.mjs +11 -7
  165. package/src/cli/plugin/install.mjs +10 -15
  166. package/src/cli/plugin/remove.mjs +1 -1
  167. package/src/cli/plugin/search.mjs +2 -2
  168. package/src/cli/preflight.mjs +19 -6
  169. package/src/cli/proposals.mjs +1 -1
  170. package/src/cli/recall.mjs +66 -23
  171. package/src/cli/release.mjs +4 -1
  172. package/src/cli/remember.mjs +3 -14
  173. package/src/cli/server.mjs +18 -0
  174. package/src/cli/startup.mjs +1 -0
  175. package/src/cli/task.mjs +9 -2
  176. package/src/cli/update.mjs +10 -3
  177. package/src/core/access-ledger.mjs +78 -0
  178. package/src/core/agent-manager.mjs +2 -2
  179. package/src/core/automatic-brain-updates.mjs +11 -6
  180. package/src/core/brain-locate.mjs +15 -0
  181. package/src/core/brain-updates.mjs +42 -12
  182. package/src/core/claude-hooks.mjs +3 -1
  183. package/src/core/cleanup-task.mjs +15 -0
  184. package/src/core/cli-commands.mjs +2 -0
  185. package/src/core/cli-maintenance.mjs +4 -1
  186. package/src/core/cli-semantic-index.mjs +23 -3
  187. package/src/core/context-compactor.mjs +7 -2
  188. package/src/core/context-compiler.mjs +26 -1
  189. package/src/core/context-policy.mjs +7 -3
  190. package/src/core/daemon-control.mjs +22 -0
  191. package/src/core/daemon-loop.mjs +39 -62
  192. package/src/core/default-plugins.mjs +1 -1
  193. package/src/core/dream.mjs +33 -105
  194. package/src/core/ensure-dependencies.mjs +148 -0
  195. package/src/core/fast-recall.mjs +2 -2
  196. package/src/core/field-evaluation.mjs +23 -0
  197. package/src/core/gate-mode.mjs +1 -1
  198. package/src/core/global-brain-updates.mjs +1 -1
  199. package/src/core/harness-hooks.mjs +14 -2
  200. package/src/core/lesson-hooks.mjs +74 -0
  201. package/src/core/lesson-memory.mjs +24 -0
  202. package/src/core/lesson-sessions.mjs +52 -0
  203. package/src/core/lessons.mjs +96 -0
  204. package/src/core/mesh-brain-updates.mjs +2 -1
  205. package/src/core/meta-harness.mjs +2 -2
  206. package/src/core/optimizer.mjs +66 -165
  207. package/src/core/plugin-contracts/card.mjs +3 -0
  208. package/src/core/plugin-loader.mjs +12 -3
  209. package/src/core/plugin-runtime.mjs +40 -1
  210. package/src/core/plugin-store.mjs +35 -23
  211. package/src/core/post-mortem.mjs +52 -36
  212. package/src/core/preflight-gate.mjs +23 -2
  213. package/src/core/proposal-applier.mjs +31 -2
  214. package/src/core/repo-sync.mjs +4 -6
  215. package/src/core/required-plugins.mjs +26 -4
  216. package/src/core/rule-length.mjs +23 -0
  217. package/src/core/runtime.mjs +1 -1
  218. package/src/core/scheduler.mjs +35 -43
  219. package/src/core/schema.mjs +1 -2
  220. package/src/core/search.mjs +4 -3
  221. package/src/core/server-restart.mjs +128 -7
  222. package/src/core/server-service.mjs +148 -0
  223. package/src/core/session-watcher.mjs +19 -1
  224. package/src/core/skill-gap.mjs +16 -0
  225. package/src/core/ssss-kernel-bridge.mjs +33 -6
  226. package/src/core/startup-health.mjs +6 -1
  227. package/src/core/surface.mjs +68 -57
  228. package/src/core/task-context.mjs +9 -8
  229. package/src/core/task-executors.mjs +54 -106
  230. package/src/core/validated-write.mjs +5 -0
  231. package/src/core/vault-files.mjs +8 -2
  232. package/src/core/vault.mjs +1 -1
  233. package/src/core/vector-field.mjs +134 -192
  234. package/src/server/index.mjs +3 -1
  235. package/src/server/invoke-core-child.mjs +24 -0
  236. package/src/server/invoke-core-operations.mjs +102 -0
  237. package/src/server/route-manifest.json +0 -56
  238. package/src/server/routes/brains.mjs +0 -18
  239. package/src/server/routes/context.mjs +0 -41
  240. package/src/server/routes/invoke.mjs +26 -1
  241. package/src/server/routes/ssss.mjs +0 -169
  242. package/src/server/routes/system.mjs +0 -15
  243. package/src/server/routes/tasks.mjs +0 -26
  244. package/src/server/routes/vault.mjs +2 -23
  245. package/frontend/dist/assets/index-DD4qONvH.js +0 -5607
  246. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/README.md +0 -53
  247. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/plugin.json +0 -129
  248. package/src/core/append-log.mjs +0 -131
  249. package/src/core/plugin-tasks.mjs +0 -173
  250. package/templates/onboarding-interview.md +0 -82
  251. /package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/LICENSE +0 -0
  252. /package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/ui/DESIGN.md +0 -0
@@ -9,16 +9,52 @@ import { recordRunReceipt } from './learner.mjs';
9
9
  import { resolvePool, localExecutionNode } from './pool.mjs';
10
10
  import { routeTask } from './router.mjs';
11
11
  import { runQualityGate } from './gate.mjs';
12
- import { isGitRepo, createWorktree, removeWorktree, commitAll, linkDependencies, deleteBranch } from './worktree.mjs';
12
+ import { isGitRepo, isDirty, pushBranch, createWorktree, removeWorktree, commitAll, linkDependencies, deleteBranch, provideDependencies } from './worktree.mjs';
13
13
  import { forwardRemoteOperation } from './remote-operation.mjs';
14
+ import { queueIncludedJob } from './usage-queue.mjs';
15
+ import { configDir } from './paths.mjs';
16
+ import path from 'node:path';
17
+ import { cancellationResult } from './worker.mjs';
18
+ import { executeCodeReview, reviewBlocksPush } from './review.mjs';
19
+ import { sessionOptions } from './session/run.mjs';
20
+ import fs from 'node:fs';
21
+ import { jobPaths } from './control.mjs';
22
+
23
+ // A routed attempt gets time in proportion to the work the route measured (A-101); --timeout-ms overrides.
24
+ export const ROUTED_TIMEOUT_MS = { local: 15 * 60_000, budget: 30 * 60_000, frontier: 90 * 60_000 };
25
+
26
+ /**
27
+ * What Metaharness itself recorded about this job, for the reviewer (A-122): questions the agent
28
+ * asked, the operator's replies, the goal check and the gate. The reviewer sees only the diff
29
+ * otherwise and cannot verify claims about how the job ran.
30
+ */
31
+ export function reviewEvidence(jobId, attempt = {}) {
32
+ const evidence = { goalCheck: attempt.goalCheck ?? null, gate: attempt.gate ? { status: attempt.gate.status, passed: attempt.gate.passed } : null, questions: [], replies: [] };
33
+ if (!jobId) return evidence;
34
+ let paths;
35
+ try { paths = jobPaths(jobId); } catch { return evidence; }
36
+ const lines = file => { try { return fs.readFileSync(file, 'utf8').split('\n').filter(Boolean).map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean); } catch { return []; } };
37
+ for (const e of lines(paths.events)) if (e.kind === 'question') evidence.questions.push({ requestId: e.requestId, questions: e.questions || e.text, source: e.source, t: e.t });
38
+ for (const c of lines(paths.inbox)) if (c.type === 'reply' || c.command === 'reply') evidence.replies.push({ requestId: c.requestId || null, text: c.text, t: c.t });
39
+ return evidence;
40
+ }
14
41
 
15
42
  export async function executeRoutedTask(task, options = {}) {
43
+ if (cancellationResult()) return cancellationResult();
16
44
  const remote = await forwardRemoteOperation('route', task, { ...options, execute: true });
17
45
  if (remote) return remote;
18
46
  if (options.node) localExecutionNode(options);
19
47
  const cwd = options.cwd || process.cwd();
48
+ const controls = sessionOptions({ ...options, cwd });
20
49
  if (!isGitRepo(cwd)) throw new Error('Routed execution writes on its own branch, so it must run inside a git repository');
21
50
  const route = options.route || await routeTask(task, options);
51
+ const replay = { kind: 'route', gate: options.gate === true, gateOptions: options.gateOptions, controls };
52
+ if (!route.selected && route.deferred) {
53
+ const blocked = { ok: false, failureKind: 'quota', error: 'The included usage allowance is exhausted.', resetsAt: route.deferred.resetsAt };
54
+ const job = await queueIncludedJob({ agent: route.deferred.harness, cwd, prompt: task }, blocked,
55
+ { role: 'Routed', replay, launchPrevented: true });
56
+ return { ...blocked, route, attempts: [], queued: true, queueId: job.id };
57
+ }
22
58
  if (!route.selected) return { ok: false, route, attempts: [], error: 'No harness can take this task right now.' };
23
59
  const node = localExecutionNode(options, route.placement);
24
60
  const pool = options.pool || (await resolvePool(options)).ready.map(h => h.id);
@@ -27,32 +63,50 @@ export async function executeRoutedTask(task, options = {}) {
27
63
  const attempts = [];
28
64
  const alerts = [];
29
65
  const tried = new Set();
30
- let harness = route.selected, diagnostic = null, previous = null;
66
+ let harness = route.selected, diagnostic = null, previous = null, carried = null;
67
+ const timeoutMs = options.timeoutMs || ROUTED_TIMEOUT_MS[route.tier] || ROUTED_TIMEOUT_MS.budget;
31
68
 
32
69
  for (let index = 1; index <= maxAttempts && harness; index += 1) {
70
+ if (cancellationResult()) return { route, attempts, alerts, ...cancellationResult() };
33
71
  tried.add(harness);
34
72
  const branch = `meta-harness/${id}-${index}-${harness}`;
35
- const directory = createWorktree(cwd, { branch });
36
- const attempt = { harness, branch: null, commit: null, gate: null, escalatedFrom: previous };
73
+ // An escalation continues from the previous attempt's unfinished work instead of starting over (A-102).
74
+ const directory = createWorktree(cwd, { branch, ...(carried ? { ref: carried } : {}) });
75
+ // A-107: each attempt streams its agent's events here live; the path is announced first.
76
+ const progressFile = options.progressDir === false ? undefined
77
+ : path.join(options.progressDir || path.join(configDir(), 'jobs'), `${id}-${index}-${harness}.events.jsonl`);
78
+ const attempt = { harness, branch: null, commit: null, gate: null, escalatedFrom: previous, ...(progressFile ? { progressFile } : {}) };
37
79
  attempts.push(attempt);
80
+ if (progressFile) console.error(JSON.stringify({ progress: progressFile, attempt: index, harness }));
81
+ // A-109: the agent gets the repository's dependencies in its own checkout, so its tests run.
82
+ let stopped;
38
83
  try {
84
+ attempt.dependencies = (options.provideDependencies || provideDependencies)(cwd, directory);
39
85
  const prompt = `Work only in this checkout. Do not commit; the harness commits your changes.\nTask:\n${task}${diagnostic ? `\n\nA previous attempt by ${previous} failed. Diagnostic:\n${diagnostic}` : ''}`;
40
- const result = await dispatchWithReceipt({ agent: harness, node, cwd: directory, contextCwd: cwd, contextTask: task, prompt, permissions: { read: true, write: true }, allowUnknownAuth: options.allowUnknownAuth === true }, { dispatchFn: options.dispatchFn, role: 'Routed', complexityScore: route.complexity?.score, escalatedFrom: previous });
86
+ const result = await dispatchWithReceipt({ agent: harness, node, branch, cwd: directory, contextCwd: cwd, contextTask: task, prompt, permissions: { read: true, write: true }, timeoutMs,
87
+ ...controls, goal: controls.goal || task,
88
+ ...(progressFile ? { progressFile } : {}), allowUnknownAuth: options.allowUnknownAuth === true }, { dispatchFn: options.dispatchFn, role: 'Routed', complexityScore: route.complexity?.score, escalatedFrom: previous, replay, retryId: options.retryId });
41
89
  attempt.ok = result.ok === true;
90
+ if (result.cancelled) stopped = result;
42
91
  attempt.durationMs = result.durationMs;
43
- attempt.output = result.response || null;
92
+ attempt.timeoutMs = timeoutMs;
93
+ attempt.output = result.response || result.partialOutput || null;
94
+ if (result.sessionId) { attempt.sessionId = result.sessionId; attempt.branch = branch; }
95
+ attempt.goalCheck = result.goalCheck;
96
+ attempt.structured = result.structured;
97
+ attempt.usage = result.usage;
98
+ if (!attempt.ok && result.stderr) attempt.stderr = result.stderr;
44
99
  if (result.ok) {
45
100
  attempt.commit = commitAll(directory, `meta-harness: ${task.slice(0, 60)}\n\nAgent: ${harness}`);
46
101
  if (attempt.commit) attempt.branch = branch;
47
- else {
48
- attempt.ok = false; attempt.error = 'The agent reported success but changed no files.';
49
- await recordRunReceipt({ harness, role: 'Implementer', task, success: false, failureKind: 'no-change', error: attempt.error });
50
- }
102
+ // Keep the session branch even when the goal required no repository edit.
103
+ else if (result.sessionId) attempt.branch = branch;
51
104
  } else {
52
105
  attempt.error = result.error;
53
106
  attempt.failureKind = result.failureKind;
107
+ if (result.queued || result.queueError || (result.failureKind === 'quota' && options.retryId) || ['billing-policy', 'limit', 'interrupted'].includes(result.failureKind)) stopped = result;
54
108
  }
55
- if (attempt.ok && options.gate) {
109
+ if (attempt.ok && options.gate && !cancellationResult()) {
56
110
  linkDependencies(cwd, directory);
57
111
  attempt.gate = await (options.gateFn || runQualityGate)(directory, options.gateOptions);
58
112
  // A gate that could not run says nothing about this agent's work, so it does not escalate.
@@ -64,23 +118,54 @@ export async function executeRoutedTask(task, options = {}) {
64
118
  await recordRunReceipt({ harness, role: 'Gate', task, success: attempt.gate.passed, failureKind: attempt.gate.passed ? null : 'gate', error: attempt.gate.passed ? null : attempt.gate.summary, gate: attempt.gate.status });
65
119
  attempt.ok = attempt.gate.passed;
66
120
  }
121
+ if (attempt.ok && attempt.commit && !cancellationResult()) {
122
+ attempt.review = await (options.reviewFn || executeCodeReview)({ cwd, author: harness, task,
123
+ evidence: reviewEvidence(options.jobId || process.env.TR_METAHARNESS_JOB, attempt),
124
+ target: `HEAD...${attempt.commit}`, dispatchFn: options.dispatchFn, reviewer: options.reviewer,
125
+ allowUnknownAuth: options.allowUnknownAuth === true, availableHarnesses: options.availableHarnesses });
126
+ if (reviewBlocksPush(attempt.review)) {
127
+ attempt.ok = false; attempt.failureKind = 'review';
128
+ attempt.error = attempt.review.error || JSON.stringify(attempt.review.review?.findings || []) || 'Review blocked the push.';
129
+ }
130
+ }
67
131
  } catch (error) {
68
132
  attempt.ok = false;
69
133
  attempt.error = String(error.message);
70
134
  } finally {
71
- removeWorktree(cwd, directory);
72
- // Only the branch this run hands back survives; the attempt record keeps what happened.
73
- if (!attempt.ok) { deleteBranch(cwd, branch); attempt.branch = null; }
135
+ if (cancellationResult()) {
136
+ Object.assign(attempt, cancellationResult());
137
+ stopped = cancellationResult();
138
+ }
139
+ // A failed attempt's edits are kept as an unfinished commit on its branch for review and for the
140
+ // next attempt to continue from; only an attempt that changed nothing loses its branch (A-102).
141
+ if (!attempt.ok && !attempt.commit) {
142
+ let wip = null;
143
+ try { wip = commitAll(directory, `meta-harness: unfinished attempt by ${harness}\n\n${String(attempt.error || '').slice(0, 300)}`); }
144
+ catch (error) { attempt.wipError = String(error.message); }
145
+ if (wip) { attempt.wip = wip; attempt.branch = branch; carried = wip; }
146
+ } else if (!attempt.ok && attempt.commit) carried = attempt.commit;
147
+ // Edits that could not be committed are never deleted (A-114): the checkout and branch stay for recovery.
148
+ if (attempt.wipError && isDirty(directory)) { attempt.keptWorktree = directory; attempt.branch = branch; }
149
+ else {
150
+ removeWorktree(cwd, directory);
151
+ if (!attempt.ok && !attempt.wip && !attempt.commit && !attempt.sessionId) { deleteBranch(cwd, branch); attempt.branch = null; }
152
+ }
74
153
  }
75
154
 
76
- if (attempt.ok) return { ok: true, route, gateApplied: Boolean(options.gate), branch: attempt.branch, commit: attempt.commit, harness, attempts, alerts };
155
+ if (stopped?.cancelled) return { ...stopped, route, attempts, alerts, branch: attempt.branch, commit: attempt.commit, wip: attempt.wip, harness };
156
+ if (attempt.ok) {
157
+ const push = options.push === false ? { pushed: false, reason: 'push disabled for this run' } : pushBranch(cwd, attempt.branch);
158
+ return { ok: true, route, goalCheck: attempt.goalCheck, structured: attempt.structured, review: attempt.review,
159
+ gateApplied: Boolean(options.gate), branch: attempt.branch, commit: attempt.commit, push, harness, attempts, alerts };
160
+ }
161
+ if (stopped) return { ...stopped, route, attempts, alerts };
77
162
 
78
163
  const remaining = pool.filter(item => !tried.has(item));
79
164
  const next = remaining.length ? evaluateGateCascade(harness, { testsPassed: false }, remaining) : null;
80
165
  diagnostic = attempt.gate?.summary || attempt.error || 'The previous attempt failed.';
81
166
  previous = harness;
82
167
  harness = next?.recommendedHarness && !tried.has(next.recommendedHarness) ? next.recommendedHarness : null;
83
- if (harness) alerts.push(await notifyAlert({ title: 'Meta Harness: escalating', message: `${previous} failed on "${task.slice(0, 60)}"; escalating to ${harness}.`, severity: 'warning' }));
168
+ if (harness) alerts.push(await notifyAlert({ title: 'Metaharness: escalating', message: `${previous} failed on "${task.slice(0, 60)}"; escalating to ${harness}.`, severity: 'warning' }));
84
169
  }
85
170
  return { ok: false, route, gateApplied: Boolean(options.gate), attempts, alerts, error: 'No attempt passed.' };
86
171
  }
@@ -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
+ }
@@ -8,6 +8,7 @@ import { recordRunReceipt } from './learner.mjs';
8
8
  import { classifyTaskComplexity, synthesizeOneWayValve } from '../capability/index.mjs';
9
9
  import { notifyAlert } from './alerts.mjs';
10
10
  import { dispatchWithReceipt } from './dispatch.mjs';
11
+ import { queueIncludedJob } from './usage-queue.mjs';
11
12
  import { resolvePool, localExecutionNode } from './pool.mjs';
12
13
  import { createThread, appendThreadTurn, updateThread } from './threads.mjs';
13
14
  import { executeCodeReview } from './review.mjs';
@@ -15,6 +16,8 @@ import { isGitRepo, isDirty, headCommit, createWorktree, removeWorktree, commitA
15
16
  import { lastJsonWith } from './json-reply.mjs';
16
17
  import { runQualityGate } from './gate.mjs';
17
18
  import { forwardRemoteOperation } from './remote-operation.mjs';
19
+ import { cancellationResult } from './worker.mjs';
20
+ import { sessionOptions } from './session/run.mjs';
18
21
 
19
22
  const OPEN_THREADS = new Set();
20
23
  const MAX_BLUEPRINT = 20000;
@@ -100,10 +103,14 @@ async function runPipeline(task, poolInfo, options) {
100
103
  const branch = options.branch || `meta-harness/${thread.id}`;
101
104
  const run = async (stage, harness, prompt, { workdir = cwd, permissions, role = stage.role } = {}) => {
102
105
  stage.status = 'running';
103
- 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 });
104
- stage.status = result.ok ? 'completed' : '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';
105
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];
106
112
  stage.failureKind = result.ok ? undefined : result.failureKind;
113
+ if (result.queueId) stage.queueId = result.queueId;
107
114
  appendThreadTurn(thread.id, { role: role.toLowerCase(), agent: harness.id, content: stage.output || '' });
108
115
  return result;
109
116
  };
@@ -126,17 +133,14 @@ async function runPipeline(task, poolInfo, options) {
126
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 });
127
134
  if (!implementer.ok) { ok = false; return finish(); }
128
135
  commit = commitAll(implementerDir, `meta-harness: ${task.slice(0, 60)}\n\nImplementer: ${roles.implementer.id}`);
129
- if (!commit) {
130
- Object.assign(stage('Implementer'), { status: 'failed', failureKind: 'agent', output: 'The Implementer reported success but changed no files.' });
131
- await recordRunReceipt({ harness: roles.implementer.id, role: 'Implementer', task, success: false, failureKind: 'no-change', error: 'The Implementer reported success but changed no files.' });
132
- ok = false; return finish();
133
- }
136
+ if (!commit) commit = baseCommit;
134
137
  stage('Implementer').commit = commit;
135
138
  updateThread(thread.id, { branch, worktreePath: null });
136
139
 
137
140
  if (options.gate) {
138
141
  linkDependencies(cwd, implementerDir);
139
142
  gate = await (options.gateFn || runQualityGate)(implementerDir, options.gateOptions);
143
+ if (cancellationResult()) { ok = false; return finish(); }
140
144
  if (!gate.passed) { ok = false; stage('Verifier').status = 'skipped'; stage('Verifier').output = `Quality gate did not pass: ${gate.summary}`; return finish(); }
141
145
  }
142
146
 
@@ -163,16 +167,24 @@ async function runPipeline(task, poolInfo, options) {
163
167
  return finish();
164
168
  } finally {
165
169
  removeWorktree(cwd, implementerDir);
166
- if (implementerDir && (!commit || !ok)) deleteBranch(cwd, branch);
170
+ if (implementerDir && (!commit || (!ok && !cancellationResult()))) deleteBranch(cwd, branch);
167
171
  }
168
172
 
169
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
+ }
170
179
  return (async () => {
171
180
  for (const item of stages) if (item.status === 'planned') item.status = 'skipped';
172
181
  OPEN_THREADS.delete(thread.id);
173
- updateThread(thread.id, { status: ok ? 'completed' : 'failed', branch: ok && commit ? branch : null, commit, baseCommit });
174
- 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' });
175
- 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 };
176
188
  })();
177
189
  }
178
190
  }
@@ -185,13 +197,15 @@ async function runCouncil(task, poolInfo, options) {
185
197
  if (!seats.length) throw new Error('No harness can take work right now');
186
198
  const cwd = options.cwd || process.cwd();
187
199
  const votes = await Promise.all(seats.map(async harness => {
188
- 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' });
189
- return { harness, ok: result.ok === true, ...(result.ok ? parseVote(result.response) : { vote: 'ABSTAIN', reason: null, error: result.error }), feedback: result.response || null };
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 }),
202
+ ...(result.queueId ? { queueId: result.queueId } : {}), feedback: result.response || null };
190
203
  }));
191
204
  const count = vote => votes.filter(item => item.vote === vote).length;
192
205
  const majority = votes.length / 2;
193
206
  const consensus = count('APPROVE') > majority ? 'approved' : count('REJECT') > majority ? 'rejected' : 'no-consensus';
194
- 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 };
195
209
  }
196
210
 
197
211
  async function runRace(task, poolInfo, options) {
@@ -209,12 +223,21 @@ async function runRace(task, poolInfo, options) {
209
223
  try {
210
224
  const started = Date.now();
211
225
  directory = createWorktree(cwd, { branch });
212
- 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' });
213
- const commit = result.ok ? commitAll(directory, `meta-harness race: ${task.slice(0, 60)}\n\nAgent: ${harness}`) : null;
214
- if (commit && options.gate) linkDependencies(cwd, directory);
215
- const gate = commit ? await (options.gateFn || runQualityGate)(directory, options.gateOptions) : null;
216
- keep = Boolean(commit) && gate?.passed === true;
217
- 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,
240
+ ...(result.queueId ? { queueId: result.queueId } : {}) };
218
241
  } catch (error) {
219
242
  return { harness, branch: null, commit: null, ok: false, gate: null, durationMs: null, error: String(error.message) };
220
243
  } finally {
@@ -223,10 +246,12 @@ async function runRace(task, poolInfo, options) {
223
246
  }
224
247
  }));
225
248
  const finishers = entries.filter(entry => entry.ok).sort((a, b) => a.completedAt - b.completedAt);
226
- 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 };
227
251
  }
228
252
 
229
253
  export async function coordinateTask(taskDescription, options = {}) {
254
+ if (cancellationResult()) return cancellationResult();
230
255
  const task = String(taskDescription || '').trim();
231
256
  if (!task) throw new Error('Task description is required for coordination');
232
257
  const remote = await forwardRemoteOperation('coordinate', task, options);
@@ -235,6 +260,14 @@ export async function coordinateTask(taskDescription, options = {}) {
235
260
  if (!['pipeline', 'orchestrate', 'council', 'race'].includes(mode)) throw new Error(`Unsupported coordination mode: ${mode}`);
236
261
  localExecutionNode(options);
237
262
  const poolInfo = await resolvePool(options);
263
+ const wall = poolInfo.excluded.find(item => item.failureKind === 'quota');
264
+ const minimum = mode === 'race' || ((mode === 'pipeline' || mode === 'orchestrate') && options.allowSameFamily !== true) ? 2 : 1;
265
+ if (options.execute === true && wall && poolInfo.ready.length < minimum) {
266
+ const blocked = { ok: false, failureKind: 'quota', error: 'Coordination is waiting for included usage.', resetsAt: wall.resetsAt };
267
+ const job = await queueIncludedJob({ agent: wall.harness, cwd: options.cwd || process.cwd(), prompt: task }, blocked,
268
+ { role: 'Coordinate', launchPrevented: true });
269
+ return { ...blocked, mode, executedLive: false, queued: true, queueId: job.id, needsReview: job.needsReview };
270
+ }
238
271
  if (mode === 'council') return runCouncil(task, poolInfo, options);
239
272
  if (mode === 'race') return runRace(task, poolInfo, options);
240
273
  return runPipeline(task, poolInfo, options);
@@ -0,0 +1,40 @@
1
+ import path from 'node:path';
2
+ import { sessionOptions } from './session/run.mjs';
3
+
4
+ const DEFAULT_PERMISSIONS = Object.freeze({ read: true, write: false, send: false, destructive: false });
5
+ export { DEFAULT_PERMISSIONS };
6
+
7
+ export function commandPermissions(value = DEFAULT_PERMISSIONS) {
8
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Command permissions must be boolean values');
9
+ for (const key of Object.keys(value)) {
10
+ if (!Object.hasOwn(DEFAULT_PERMISSIONS, key) || typeof value[key] !== 'boolean') throw new Error(`Invalid permission ${key}`);
11
+ }
12
+ return { ...DEFAULT_PERMISSIONS, ...value };
13
+ }
14
+
15
+ const PUBLIC_FIELDS = [
16
+ 'agent', 'cwd', 'prompt', 'permissions', 'node', 'allowUnknownAuth', 'timeoutMs',
17
+ 'model', 'tools', 'settingSources', 'allowedTools',
18
+ 'goal', 'maxTurns', 'maxTokens', 'maxBudgetUsd', 'resultSchema', 'approvalPolicy', 'questionTimeoutMs', 'effort', 'fallbackModel', 'name',
19
+ ];
20
+
21
+ export function validateDispatchRequest(request) {
22
+ if (!request || typeof request !== 'object' || Array.isArray(request)) throw new Error('Dispatch requires a JSON object');
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');
24
+ if (typeof request.agent !== 'string' || !/^[a-z][a-z0-9-]{0,63}$/.test(request.agent) ||
25
+ typeof request.cwd !== 'string' || !path.isAbsolute(request.cwd) || request.cwd.includes('\0') ||
26
+ typeof request.prompt !== 'string' || !request.prompt.trim() || Buffer.byteLength(request.prompt) > 65536) {
27
+ throw new Error('Dispatch requires an explicit agent, absolute workspace, and bounded nonempty prompt');
28
+ }
29
+ if (request.node !== undefined && (typeof request.node !== 'string' || !request.node.trim() || /[\0\r\n]/.test(request.node))) throw new Error('Invalid dispatch node');
30
+ if (request.allowUnknownAuth !== undefined && typeof request.allowUnknownAuth !== 'boolean') throw new Error('allowUnknownAuth must be a boolean');
31
+ const timeoutMs = request.timeoutMs === undefined ? 300000 : request.timeoutMs;
32
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 540000) throw new Error('Foreground dispatch timeout must be 1..540000 ms');
33
+ return { ...request, ...sessionOptions(request), permissions: commandPermissions(request.permissions), timeoutMs };
34
+ }
35
+
36
+ /** The installed destination starts from its own project context and decides containment itself. */
37
+ export function destinationDispatchRequest(request) {
38
+ const fields = PUBLIC_FIELDS.filter(key => key !== 'node' && request[key] !== undefined);
39
+ return validateDispatchRequest(Object.fromEntries(fields.map(key => [key, request[key]])));
40
+ }