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
@@ -0,0 +1,84 @@
1
+ /** Local job identity and cancellation. A terminal log result is the completion record. */
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { configDir } from './paths.mjs';
6
+ import { createProgress } from './worker.mjs';
7
+
8
+ const jobsDir = () => path.join(configDir(), 'jobs');
9
+ export const newJobId = () => `${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`;
10
+
11
+ export function startJob(operation, { id = newJobId() } = {}) {
12
+ if (!/^[a-z0-9-]+$/.test(id)) throw new Error('Invalid job ID.');
13
+ const directory = jobsDir();
14
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
15
+ const log = path.join(directory, `${id}.log`);
16
+ const pidFile = path.join(directory, `${id}.pid`);
17
+ const record = { job: id, pid: process.pid, operation, startedAt: new Date().toISOString() };
18
+ fs.appendFileSync(log, `${JSON.stringify(record)}\n`, { mode: 0o600 });
19
+ fs.writeFileSync(pidFile, `${process.pid}\n`, { mode: 0o600, flag: 'wx' });
20
+ const progress = createProgress({ file: path.join(directory, `${id}.events.jsonl`), harness: operation });
21
+ return { ...record, log, finish(result) {
22
+ progress.close(result);
23
+ fs.appendFileSync(log, `${JSON.stringify(result)}\n`, { mode: 0o600 });
24
+ fs.rmSync(pidFile, { force: true });
25
+ } };
26
+ }
27
+
28
+ function readJob(log) {
29
+ const lines = fs.readFileSync(log, 'utf8').trim().split('\n');
30
+ let record;
31
+ try { record = JSON.parse(lines[0]); } catch { /* old log without PID */ }
32
+ let result = null;
33
+ for (const line of lines.slice(1).reverse()) {
34
+ try {
35
+ const value = JSON.parse(line);
36
+ if (typeof value.ok === 'boolean') { result = value; break; }
37
+ } catch { /* progress text */ }
38
+ }
39
+ return { record, result };
40
+ }
41
+
42
+ export function resolveJob(target) {
43
+ if (!target) throw new Error('cancel requires a job ID or log path.');
44
+ const directory = jobsDir();
45
+ const prefix = path.basename(target).replace(/\.(?:log|pid|events\.jsonl|status\.json)$/, '');
46
+ const files = fs.existsSync(directory) ? fs.readdirSync(directory) : [];
47
+ let names = files.filter(name => name.endsWith('.log') && name.startsWith(prefix));
48
+ if (!names.length) {
49
+ // An attempt ID from watch resolves to its owning job, never directly to a PID.
50
+ const events = files.filter(name => name.endsWith('.events.jsonl') && name.startsWith(prefix))
51
+ .sort((a, b) => fs.statSync(path.join(directory, b)).mtimeMs - fs.statSync(path.join(directory, a)).mtimeMs);
52
+ let pid;
53
+ try { pid = JSON.parse(fs.readFileSync(path.join(directory, events[0].replace('.events.jsonl', '.status.json')), 'utf8')).pid; } catch { /* no matching status */ }
54
+ if (pid) names = files.filter(name => name.endsWith('.log') && readJob(path.join(directory, name)).record?.pid === pid);
55
+ }
56
+ const exact = names.find(name => name === `${prefix}.log`);
57
+ if (!names.length) throw new Error(`No job log matches ${target}.`);
58
+ if (!exact && names.length > 1) throw new Error(`More than one job matches ${target}. Use a longer job ID.`);
59
+ const log = path.join(directory, exact || names[0]);
60
+ return { id: path.basename(log, '.log'), log, ...readJob(log) };
61
+ }
62
+
63
+ export async function cancelJob(target, { timeoutMs = 60_000, pollMs = 100 } = {}) {
64
+ const job = resolveJob(target);
65
+ if (job.result) throw new Error(`Job ${job.id} has finished${job.result.cancelled ? ' (cancelled)' : ''}.`);
66
+ const pidFile = path.join(jobsDir(), `${job.id}.pid`);
67
+ let pid;
68
+ try { pid = Number(fs.readFileSync(pidFile, 'utf8').trim()); } catch { /* stale or old job */ }
69
+ if (!Number.isSafeInteger(pid) || pid <= 1 || job.record?.pid !== pid || job.record?.job !== job.id) {
70
+ throw new Error(`Job ${job.id} has no active PID record.`);
71
+ }
72
+ try { process.kill(pid, 'SIGTERM'); }
73
+ catch (error) { throw new Error(`Cannot cancel job ${job.id}: ${error.code === 'ESRCH' ? 'the process has stopped' : error.message}.`); }
74
+ const deadline = Date.now() + Math.min(timeoutMs, 60_000);
75
+ do {
76
+ const { result } = readJob(job.log);
77
+ if (result) {
78
+ if (!result.cancelled) throw new Error(`Job ${job.id} finished before it could be cancelled.`);
79
+ return { ...result, job: job.id, log: job.log };
80
+ }
81
+ await new Promise(resolve => setTimeout(resolve, pollMs));
82
+ } while (Date.now() < deadline);
83
+ throw new Error(`Job ${job.id} did not report cancellation within ${Math.min(timeoutMs, 60_000) / 1000} seconds. Log: ${job.log}`);
84
+ }
@@ -2,7 +2,6 @@
2
2
  * Execution receipts and the learning loop.
3
3
  * Receipts are an append-only SSSS ledger. Scorecards, quota walls and proposals fold from it.
4
4
  */
5
- import { spawnSync } from 'node:child_process';
6
5
  import { computeEmpiricalScorecard } from '../capability/index.mjs';
7
6
  import { receiptsFile } from './paths.mjs';
8
7
  import { classifyFailure } from './worker.mjs';
@@ -49,6 +48,9 @@ export async function recordRunReceipt(receipt = {}) {
49
48
  success: receipt.success === true,
50
49
  durationMs: receipt.durationMs || 0,
51
50
  failureKind: receipt.failureKind || null,
51
+ ...(receipt.billingMode ? { billingMode: receipt.billingMode } : {}),
52
+ ...(Array.isArray(receipt.tools) && receipt.tools.length ? { tools: receipt.tools.map(({ tool, status, exitCode }) =>
53
+ ({ tool, status, ...(Number.isInteger(exitCode) ? { exitCode } : {}) })) } : {}),
52
54
  error: receipt.error ? String(receipt.error).slice(0, 500) : null,
53
55
  resetsAt: receipt.resetsAt || null,
54
56
  gate: receipt.gate || null,
@@ -102,18 +104,10 @@ export function activeCredentialBlocks(entries = [], now = Date.now(), holdMs =
102
104
  return blocks;
103
105
  }
104
106
 
105
- /** With `emit`, proposals are written through the memory owner. A failed write is reported, never hidden. */
106
- export async function learnFromHistory({ emit = false, runner = spawnSync } = {}) {
107
+ /** Read the same receipts the router uses. Proposals stay in this report. */
108
+ export async function learnFromHistory() {
107
109
  const entries = await readReceipts();
108
110
  const scorecard = computeEmpiricalScorecard(entries);
109
111
  const proposals = proposalsFrom(scorecard, entries);
110
- const emitted = [];
111
- if (emit) for (const proposal of proposals) {
112
- const result = runner(process.env.TOTAL_RECALL_BIN || 'total-recall', ['remember', 'fact', `${proposal.reason} ${proposal.recommendation}`, '--project', '--slug', proposal.id, '--tags', `meta-harness,${proposal.id}`], { encoding: 'utf8', timeout: 30000 });
113
- const output = `${result.stdout || ''}\n${result.stderr || ''}`;
114
- // The slug is the proposal's identity, so an open proposal is reported once, however often learn runs.
115
- const existing = result.status !== 0 && /already exists/i.test(output);
116
- emitted.push({ id: proposal.id, ok: result.status === 0 || existing, ...(existing ? { existing: true } : {}), error: result.status === 0 || existing ? undefined : String(result.stderr || result.error || 'remember failed').slice(0, 300) });
117
- }
118
- return { ok: emitted.every(row => row.ok), totalHistoryRuns: entries.length, scorecard, walls: activeQuotaWalls(entries), credentialBlocks: activeCredentialBlocks(entries), proposals, ...(emit ? { emitted } : {}) };
112
+ return { ok: true, totalHistoryRuns: entries.length, scorecard, walls: activeQuotaWalls(entries), credentialBlocks: activeCredentialBlocks(entries), proposals };
119
113
  }
@@ -0,0 +1,17 @@
1
+ /** Run each maintenance owner even when another owner fails. The daemon records the result. */
2
+ import { setupIntegrations } from './setup.mjs';
3
+ import { maintainDocumentation } from './documentation.mjs';
4
+
5
+ export async function maintainIntegrations(options = {}, owners = {}) {
6
+ const steps = {
7
+ setup: owners.setup || (() => setupIntegrations()),
8
+ gates: owners.gates || (async () => (await import('./gate-adapters/index.mjs')).installAll()),
9
+ docs: owners.docs || (() => maintainDocumentation(options)),
10
+ };
11
+ const result = {};
12
+ for (const [name, execute] of Object.entries(steps)) {
13
+ try { result[name] = await execute(); }
14
+ catch (error) { result[name] = { ok: false, error: error.message }; }
15
+ }
16
+ return { ...result, ok: Object.values(result).every(step => step?.ok === true) };
17
+ }
@@ -0,0 +1,228 @@
1
+ /**
2
+ * MCP stdio (JSONL), with the initialize lifecycle used by Claude and Codex.
3
+ * Protocol: https://modelcontextprotocol.io/specification/2025-11-25
4
+ * Approval result: https://code.claude.com/docs/en/agent-sdk/user-input
5
+ */
6
+ import { createInterface } from 'node:readline';
7
+ import { execFile } from 'node:child_process';
8
+ import { promisify } from 'node:util';
9
+ import { randomUUID } from 'node:crypto';
10
+ import { fileURLToPath, pathToFileURL } from 'node:url';
11
+ import { brainDir } from './paths.mjs';
12
+ import { appendEvent, jobPaths, tailInbox } from './control.mjs';
13
+
14
+ const execute = promisify(execFile);
15
+ export const QUESTION_TIMEOUT_MS = 10 * 60_000;
16
+ export const NO_ANSWER = 'No answer; proceed on your best judgment';
17
+ const versions = ['2025-11-25', '2025-06-18', '2025-03-26', '2024-11-05'];
18
+ const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
19
+ const textResult = text => ({ content: [{ type: 'text', text }] });
20
+ const fault = (code, message) => Object.assign(new Error(message), { code });
21
+
22
+ export const MCP_TOOLS = [
23
+ { name: 'ask', description: 'Ask the operator questions and wait for a reply.', inputSchema: {
24
+ type: 'object', properties: { questions: { type: 'array', minItems: 1, items: { oneOf: [{ type: 'string', minLength: 1 }, { type: 'object' }] } } }, required: ['questions'], additionalProperties: false,
25
+ } },
26
+ { name: 'approve', description: 'Answer a Claude Code permission prompt under the job approval policy.', inputSchema: {
27
+ type: 'object', properties: { tool_name: { type: 'string', minLength: 1 }, input: { type: 'object' }, tool_use_id: { type: 'string' } }, required: ['tool_name', 'input'],
28
+ } },
29
+ ];
30
+
31
+ /** Return Claude's --mcp-config object and Codex's -c arguments. No shell parses these values. */
32
+ export function mcpConfigFor(job, options = {}) {
33
+ const id = jobPaths(job).id;
34
+ const command = process.execPath;
35
+ const args = [fileURLToPath(import.meta.url), '--approvals', options.approvalPolicy || 'auto',
36
+ '--question-timeout-ms', String(options.questionTimeoutMs ?? options.timeoutMs ?? QUESTION_TIMEOUT_MS)];
37
+ const env = { TR_METAHARNESS_JOB: id, TR_SECRETS_BRAIN: brainDir() };
38
+ if (process.env.TOTAL_RECALL_BIN) env.TOTAL_RECALL_BIN = process.env.TOTAL_RECALL_BIN;
39
+ const claude = { mcpServers: { metaharness: { command, args, env } } };
40
+ const tomlEnv = `{ ${Object.entries(env).map(([key, value]) => `${key} = ${JSON.stringify(value)}`).join(', ')} }`;
41
+ const codex = ['-c', `mcp_servers.metaharness.command=${JSON.stringify(command)}`,
42
+ '-c', `mcp_servers.metaharness.args=${JSON.stringify(args)}`, '-c', `mcp_servers.metaharness.env=${tomlEnv}`,
43
+ '-c', `mcp_servers.metaharness.tool_timeout_sec=${Math.ceil(Number(args.at(-1)) / 1000) + 60}`];
44
+ return { claude, codex, claudeArgs: ['--mcp-config', JSON.stringify(claude), '--permission-prompt-tool', 'mcp__metaharness__approve'], codexArgs: codex };
45
+ }
46
+
47
+ /** The decision owner returns a typed choice. Missing or failed decisions deny the call. */
48
+ async function decideApproval(request, { signal } = {}) {
49
+ const questions = { approval: { type: 'choice', instructions: 'Apply Total Recall rules to this tool request. Allow only if the request is authorized. Deny if authorization is unclear.',
50
+ criteria: { allow: 'Authorized by the operator and the applicable rules.', deny: 'Not authorized or prohibited by a rule.' } } };
51
+ const { stdout } = await execute(process.env.TOTAL_RECALL_BIN || 'total-recall',
52
+ ['decision', 'run', '--state', JSON.stringify({ request }), '--questions', JSON.stringify(questions), '--json'],
53
+ { encoding: 'utf8', timeout: 60_000, maxBuffer: 1024 * 1024, signal });
54
+ const record = JSON.parse(stdout);
55
+ if (record.ok === false) throw new Error(record.error || 'The decision owner failed.');
56
+ const answer = record.answers?.approval ?? record.decision?.answers?.approval ?? record.result?.approval;
57
+ const value = typeof answer === 'string' ? answer : answer?.choice;
58
+ if (!['allow', 'deny'].includes(value)) throw new Error('The decision owner returned no allow or deny answer.');
59
+ return { value, record };
60
+ }
61
+
62
+ /** Resolve all pending calls on close; no poll timer or child command outlives the server. */
63
+ export function createMcpTools({ job = process.env.TR_METAHARNESS_JOB, approvalPolicy = 'auto',
64
+ questionTimeoutMs = QUESTION_TIMEOUT_MS, timeoutMs, decisionFn = decideApproval, alertFn,
65
+ deferQuestions = process.env.TR_METAHARNESS_DEFER_QUESTIONS !== '0' } = {}) {
66
+ jobPaths(job);
67
+ if (!['auto', 'operator', 'rules'].includes(approvalPolicy)) throw new Error('Approval policy must be auto, operator, or rules.');
68
+ questionTimeoutMs = timeoutMs ?? questionTimeoutMs;
69
+ if (!Number.isSafeInteger(questionTimeoutMs) || questionTimeoutMs < 1) throw new Error('Question timeout must be a positive number of milliseconds.');
70
+ const pending = new Map();
71
+ const lifetime = new AbortController();
72
+ const children = new Set();
73
+ const notify = event => {
74
+ const task = alertFn ? Promise.resolve().then(() => alertFn(event)) : execute(process.env.TOTAL_RECALL_BIN || 'total-recall',
75
+ ['alerts', 'send', '--title', `Metaharness: ${event.kind}`, '--message', `Job ${job}: ${event.text}`, '--severity', 'info', '--source', 'meta-harness'],
76
+ { timeout: 15_000, maxBuffer: 65536, signal: lifetime.signal });
77
+ children.add(task);
78
+ task.catch(() => {}).finally(() => children.delete(task));
79
+ };
80
+ const watcher = tailInbox(job, command => {
81
+ const wait = pending.get(command.requestId);
82
+ if (!wait || command.type !== (wait.kind === 'question' ? 'reply' : 'approve')) return false;
83
+ wait.finish({ value: wait.kind === 'question' ? command.text : command.value, commandId: command.id });
84
+ return { via: 'metaharness' };
85
+ }, { recipient: 'mcp', onError: error => {
86
+ for (const wait of pending.values()) wait.fail(error);
87
+ } });
88
+
89
+ const waitFor = (event, signal) => new Promise((resolve, reject) => {
90
+ let timer;
91
+ const cleanup = () => { clearTimeout(timer); pending.delete(event.requestId); signal?.removeEventListener('abort', abort); };
92
+ const finish = value => { cleanup(); resolve(value); };
93
+ const fail = error => { cleanup(); reject(error); };
94
+ const abort = () => fail(new Error('The request was cancelled.'));
95
+ pending.set(event.requestId, { kind: event.kind, finish, fail });
96
+ signal?.addEventListener('abort', abort, { once: true });
97
+ if (signal?.aborted || lifetime.signal.aborted) { abort(); return; }
98
+ timer = setTimeout(() => finish({ value: event.kind === 'question' ? NO_ANSWER : 'deny', timeout: true }), questionTimeoutMs);
99
+ });
100
+
101
+ return {
102
+ async call(name, args, { signal, defer = deferQuestions } = {}) {
103
+ if (lifetime.signal.aborted) throw new Error('The MCP server has closed.');
104
+ if (name === 'ask') {
105
+ if (!object(args) || !Array.isArray(args.questions) || !args.questions.length ||
106
+ args.questions.some(q => typeof q === 'string' ? !q.trim() : !object(q))) throw fault(-32602, 'ask requires a non-empty questions array.');
107
+ } else if (name === 'approve') {
108
+ if (!object(args) || typeof args.tool_name !== 'string' || !args.tool_name.trim() || !object(args.input) ||
109
+ (args.tool_use_id !== undefined && typeof args.tool_use_id !== 'string')) throw fault(-32602, 'approve requires tool_name and input.');
110
+ } else throw fault(-32602, `Unknown tool: ${name}`);
111
+ const requestId = randomUUID();
112
+ if (name === 'ask' && defer) {
113
+ // A question costs nothing while it waits (A-125): record it, tell the agent to end its turn,
114
+ // and let the session runner start the next turn with the operator's answer. Holding the tool
115
+ // call open made Codex call the model every few seconds while it waited.
116
+ const event = { kind: 'question', source: 'deferred', requestId, text: JSON.stringify(args.questions), questions: args.questions };
117
+ appendEvent(job, event);
118
+ notify(event);
119
+ return textResult(`Your question went to the operator (request ${requestId}). End your turn now and do no other work. The operator's answer arrives as the next message in this session.`);
120
+ }
121
+ const event = { kind: name === 'ask' ? 'question' : 'approval', source: 'mcp', requestId,
122
+ text: name === 'ask' ? JSON.stringify(args.questions) : `${args.tool_name}: ${JSON.stringify(args.input)}`,
123
+ ...(name === 'ask' ? { questions: args.questions } : { request: args, policy: approvalPolicy }) };
124
+ appendEvent(job, event);
125
+ let answer;
126
+ try {
127
+ if (name === 'ask' || approvalPolicy === 'operator') {
128
+ // Register the waiter before notifying the operator.
129
+ const waiting = waitFor(event, signal);
130
+ notify(event);
131
+ answer = await waiting;
132
+ } else if (approvalPolicy === 'auto') answer = { value: 'allow', policy: 'auto' };
133
+ else {
134
+ try { answer = { ...await decisionFn(args, { signal: signal ? AbortSignal.any([signal, lifetime.signal]) : lifetime.signal }), policy: 'rules' }; }
135
+ catch (error) { answer = { value: 'deny', policy: 'rules', error: error.message }; }
136
+ if (!['allow', 'deny'].includes(answer.value)) answer = { value: 'deny', policy: 'rules', error: 'Invalid decision answer.' };
137
+ }
138
+ } catch (error) {
139
+ appendEvent(job, { kind: 'ack', command: name, requestId, ok: true, cancelled: true, text: error.message });
140
+ throw error;
141
+ }
142
+ // tailInbox records delivery; this event records the actual answer and policy decision.
143
+ appendEvent(job, { kind: 'ack', command: name, requestId, ok: true, ...answer, text: String(answer.value) });
144
+ if (name === 'ask') return textResult(String(answer.value));
145
+ const result = answer.value === 'allow' ? { behavior: 'allow', updatedInput: args.input }
146
+ : { behavior: 'deny', message: answer.timeout ? 'Approval timed out.' : answer.error || 'The request was denied.' };
147
+ return textResult(JSON.stringify(result));
148
+ },
149
+ async close() {
150
+ lifetime.abort();
151
+ for (const wait of pending.values()) wait.fail(new Error('The MCP server has closed.'));
152
+ await watcher.close();
153
+ await Promise.allSettled(children);
154
+ },
155
+ };
156
+ }
157
+
158
+ /** Never log on stdout here: every line must be a JSON-RPC message. */
159
+ export async function runMcpServer({ input = process.stdin, output = process.stdout, ...options } = {}) {
160
+ const tools = createMcpTools(options);
161
+ const lines = createInterface({ input, crlfDelay: Infinity });
162
+ const calls = new Map();
163
+ let initialized = false, ready = false, closed = false;
164
+ const write = message => { if (!closed) output.write(JSON.stringify(message) + '\n'); };
165
+ const respond = (id, result) => write({ jsonrpc: '2.0', id, result });
166
+ const error = (id, code, message) => write({ jsonrpc: '2.0', id, error: { code, message } });
167
+ const handle = async message => {
168
+ if (!object(message) || message.jsonrpc !== '2.0' || typeof message.method !== 'string' ||
169
+ (Object.hasOwn(message, 'id') && typeof message.id !== 'string' && typeof message.id !== 'number') ||
170
+ (message.params !== undefined && !object(message.params))) {
171
+ error(null, -32600, 'Invalid Request'); return;
172
+ }
173
+ const { id, method, params = {} } = message;
174
+ if (id === undefined) {
175
+ if (method === 'notifications/initialized' && initialized) ready = true;
176
+ if (method === 'notifications/cancelled') calls.get(params.requestId)?.controller.abort();
177
+ return;
178
+ }
179
+ if (calls.has(id)) { error(id, -32600, 'Duplicate request ID'); return; }
180
+ if (method === 'initialize') {
181
+ if (initialized) { error(id, -32600, 'Already initialized'); return; }
182
+ if (typeof params.protocolVersion !== 'string' || !object(params.capabilities) || !object(params.clientInfo)) {
183
+ error(id, -32602, 'initialize requires protocolVersion, capabilities, and clientInfo'); return;
184
+ }
185
+ initialized = true;
186
+ respond(id, { protocolVersion: versions.includes(params.protocolVersion) ? params.protocolVersion : versions[0],
187
+ capabilities: { tools: {} }, serverInfo: { name: 'metaharness', version: '1.0.0' } });
188
+ return;
189
+ }
190
+ if (method === 'ping') { respond(id, {}); return; }
191
+ if (!ready) { error(id, -32002, 'Initialize the MCP session first'); return; }
192
+ if (method === 'tools/list') { respond(id, { tools: MCP_TOOLS }); return; }
193
+ if (method !== 'tools/call') { error(id, -32601, 'Method not found'); return; }
194
+ if (typeof params.name !== 'string') { error(id, -32602, 'tools/call requires a tool name'); return; }
195
+ const controller = new AbortController();
196
+ const call = { controller };
197
+ calls.set(id, call);
198
+ call.done = (async () => {
199
+ try { respond(id, await tools.call(params.name, params.arguments, { signal: controller.signal })); }
200
+ catch (failure) {
201
+ if (typeof failure.code === 'number') error(id, failure.code, failure.message);
202
+ else respond(id, { ...textResult(failure.message), isError: true });
203
+ } finally { calls.delete(id); }
204
+ })();
205
+ };
206
+ lines.on('line', line => {
207
+ let message;
208
+ try { message = JSON.parse(line); } catch { error(null, -32700, 'Parse error'); return; }
209
+ void handle(message).catch(failure => error(message?.id ?? null, -32603, failure.message));
210
+ });
211
+ try { await new Promise((resolve, reject) => { lines.once('close', resolve); input.once('error', reject); output.once('error', reject); }); }
212
+ finally {
213
+ closed = true;
214
+ lines.close();
215
+ for (const call of calls.values()) call.controller.abort();
216
+ await tools.close();
217
+ await Promise.allSettled([...calls.values()].map(call => call.done));
218
+ }
219
+ }
220
+
221
+ export const startMcpServer = runMcpServer;
222
+
223
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
224
+ const value = flag => { const i = process.argv.indexOf(flag); return i < 0 ? undefined : process.argv[i + 1]; };
225
+ await runMcpServer({ approvalPolicy: value('--approvals') || 'auto',
226
+ questionTimeoutMs: value('--question-timeout-ms') === undefined ? QUESTION_TIMEOUT_MS : Number(value('--question-timeout-ms')) })
227
+ .catch(error => { console.error(`Metaharness MCP: ${error.message}`); process.exitCode = 1; });
228
+ }
@@ -3,6 +3,7 @@ import { ADAPTER_CATALOG } from '../capability/index.mjs';
3
3
  import { inspectAgents, getActiveAdapters } from './worker.mjs';
4
4
  import { readReceipts, activeQuotaWalls, activeCredentialBlocks } from './learner.mjs';
5
5
  import { measuredWalls } from './telemetry.mjs';
6
+ import { claudeProofCurrent, INCLUDED_AGENTS, readIncludedPolicy } from './included-usage.mjs';
6
7
  import os from 'node:os';
7
8
 
8
9
  const describe = (id, extra = {}) => {
@@ -23,16 +24,23 @@ export function exclusionReason(agent, wall, credentialBlock, { allowUnknownAuth
23
24
  }
24
25
 
25
26
  /** An explicit list is the caller's decision and is not filtered. */
26
- export async function resolvePool({ availableHarnesses, inspect = inspectAgents, receipts = readReceipts, allowUnknownAuth = false } = {}) {
27
- if (Array.isArray(availableHarnesses) && availableHarnesses.length) return { ready: availableHarnesses.map(id => describe(id)), excluded: [], walls: {} };
28
- const [{ agents = [] }, entries] = await Promise.all([inspect({ env: process.env }), receipts()]);
29
- const walls = { ...activeQuotaWalls(entries), ...measuredWalls() };
30
- const blocks = activeCredentialBlocks(entries);
27
+ export async function resolvePool({ availableHarnesses, inspect = inspectAgents, receipts = readReceipts, allowUnknownAuth = false, policy } = {}) {
28
+ policy ??= await readIncludedPolicy();
29
+ if (!policy.enabled && Array.isArray(availableHarnesses) && availableHarnesses.length) return { ready: availableHarnesses.map(id => describe(id)), excluded: [], walls: {} };
30
+ const claudePlan = policy.enabled ? policy.claudePlan?.organization : undefined;
31
+ const [{ agents = [] }, entries] = await Promise.all([inspect({ env: process.env, claudePlan }), receipts()]);
32
+ const billingEntries = policy.enabled ? entries.filter(run => run.billingMode === 'included') : entries;
33
+ const walls = { ...activeQuotaWalls(billingEntries), ...(!policy.enabled ? measuredWalls() : {}) };
34
+ const blocks = activeCredentialBlocks(billingEntries);
31
35
  const ready = [], excluded = [];
32
- for (const agent of agents) {
33
- const reason = exclusionReason(agent, walls[agent.id], blocks[agent.id], { allowUnknownAuth });
36
+ for (const installed of agents) {
37
+ const agent = policy.enabled && installed.id === 'claude' && !claudePlan ? { ...installed, authenticated: claudeProofCurrent(policy) } : installed;
38
+ const policyBlocked = policy.enabled && (!INCLUDED_AGENTS.includes(agent.id) || (Array.isArray(availableHarnesses) && !availableHarnesses.includes(agent.id)));
39
+ const reason = policyBlocked ? 'outside the included usage route' : exclusionReason(agent, walls[agent.id], blocks[agent.id], { allowUnknownAuth: !policy.enabled && allowUnknownAuth });
34
40
  if (!reason) ready.push(describe(agent.id, { authVerified: agent.authenticated === true }));
35
- else if (agent.installed) excluded.push({ harness: agent.id, reason });
41
+ else if (agent.installed) excluded.push({ harness: agent.id, reason,
42
+ ...(policy.enabled && !policyBlocked && agent.authenticated === true && agent.versionVerified && !agent.blockedBy && walls[agent.id]
43
+ ? { failureKind: 'quota', resetsAt: walls[agent.id].resetReported ? walls[agent.id].until : null } : {}) });
36
44
  }
37
45
  return { ready, excluded, walls };
38
46
  }
@@ -161,7 +161,7 @@ export async function executeProcessTask(job, dependencies = {}) {
161
161
  tools: job.options.tools, settingSources: job.options.settingSources, model: job.options.model,
162
162
  allowUnknownAuth: job.options.allowUnknownAuth === true, timeoutMs: job.options.timeoutMs,
163
163
  }, { role: 'Process', node: 'local' });
164
- if (result.ok && worktree) commit = commitAll(worktree, 'Meta Harness managed agent work');
164
+ if (result.ok && worktree) commit = commitAll(worktree, 'Metaharness managed agent work');
165
165
  } catch (error) {
166
166
  result = { ok: false, error: error.message };
167
167
  }
@@ -169,8 +169,18 @@ export async function executeProcessTask(job, dependencies = {}) {
169
169
  if (worktree) { (dependencies.removeWorktree || removeWorktree)(job.cwd, worktree); worktree = null; }
170
170
  if (branch && !commit) { (dependencies.deleteBranch || deleteBranch)(job.cwd, branch); branch = null; }
171
171
  } catch (error) { result = { ...result, ok: false, error: `${result.error ? result.error + '; ' : ''}Isolation cleanup failed: ${error.message}` }; }
172
- return updateAgentRecord(job.id, { status: result.ok ? 'completed' : 'failed', exitCode: result.exitCode ?? (result.ok ? 0 : 1), error: result.ok ? null : result.error,
172
+ const record = updateAgentRecord(job.id, { status: result.ok ? 'completed' : 'failed', exitCode: result.exitCode ?? (result.ok ? 0 : 1), error: result.ok ? null : result.error,
173
+ ...(result.queueId ? { queueId: result.queueId } : {}),
173
174
  result: result.response || null, branch, commit, worktreePath: worktree, endedAt: new Date().toISOString() });
175
+ return {
176
+ ...record,
177
+ ...(Array.isArray(result.toolResults) ? {
178
+ toolResults: result.toolResults,
179
+ toolExecutionVerified: result.toolExecutionVerified,
180
+ commandExecutionVerified: result.commandExecutionVerified,
181
+ } : {}),
182
+ ...(result.toolEvidenceError ? { toolEvidenceError: result.toolEvidenceError } : {}),
183
+ };
174
184
  }
175
185
 
176
186
  export async function killAgent(idOrPid, dependencies = {}) {
@@ -210,6 +220,20 @@ export async function getAgentLogs(idOrPid, tailLines = 50, dependencies = {}) {
210
220
  if (record.remote) {
211
221
  const result = await remoteCall(record.node, record.cwd, ['logs', record.id, '--tail', String(tailLines)], dependencies);
212
222
  if (typeof result.logs !== 'string') throw new Error('Remote owner returned no logs');
223
+ if (['starting', 'running'].includes(record.status)) {
224
+ for (const line of result.logs.trimEnd().split('\n').reverse()) {
225
+ let terminal;
226
+ try { terminal = JSON.parse(line); } catch { continue; }
227
+ if (terminal?.id !== record.id || !['completed', 'failed', 'killed'].includes(terminal.status) ||
228
+ typeof terminal.endedAt !== 'string' || !Number.isFinite(Date.parse(terminal.endedAt))) continue;
229
+ updateAgentRecord(record.id, {
230
+ status: terminal.status,
231
+ exitCode: Number.isInteger(terminal.exitCode) ? terminal.exitCode : null,
232
+ endedAt: terminal.endedAt,
233
+ });
234
+ break;
235
+ }
236
+ }
213
237
  return result.logs;
214
238
  }
215
239
  if (path.dirname(record.logFile || '') !== logDirectory()) throw new Error('Invalid managed log path');
@@ -1,4 +1,4 @@
1
- /** Headless reasoning belongs to Meta Harness, including config, readiness and receipts. */
1
+ /** Headless reasoning belongs to Metaharness, including config, readiness and receipts. */
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { brainDir } from './paths.mjs';
@@ -81,6 +81,11 @@ export async function callLocalRuntime(prompt, system, config, dependencies = {}
81
81
  model: agent.model || undefined, write: false, role: 'Reasoning', allowUnknownAuth: config.allowUnknownAuth === true,
82
82
  });
83
83
  if (result.ok === true || result.success === true) return result.response || '';
84
+ if (result.queued || result.queueError || result.failureKind === 'billing-policy') {
85
+ const error = new Error(result.queued ? `Included usage exhausted. Queued as ${result.queueId}.` : result.queueError || result.error);
86
+ error.queueId = result.queueId;
87
+ throw error;
88
+ }
84
89
  failures.push(`${agent.name}: ${result.error || 'dispatch failed'}`);
85
90
  }
86
91
  throw new Error(`All CLI agents failed (${failures.join('; ')})`);
@@ -9,6 +9,7 @@ const BOOLEAN_FLAGS = {
9
9
  };
10
10
  const VALUE_FLAGS = {
11
11
  mode: '--mode', orchestrator: '--orchestrator', explorer: '--explorer', architect: '--architect', implementer: '--implementer',
12
+ goal: '--goal', maxTurns: '--max-turns', maxBudgetUsd: '--max-budget-usd', maxTokens: '--max-tokens', approvalPolicy: '--approvals', questionTimeoutMs: '--question-timeout-ms', resultSchema: '--result-schema',
12
13
  verifier: '--verifier', reviewer: '--reviewer', author: '--author', threadId: '--thread', branch: '--branch',
13
14
  };
14
15
  const OPERATION_KEYS = {
@@ -33,9 +34,12 @@ export async function forwardRemoteOperation(operation, task, options = {}) {
33
34
  if (typeof task !== 'string' || !task.trim()) throw new Error('Remote operation requires a task description');
34
35
  argv.push(task);
35
36
  }
36
- for (const key of OPERATION_KEYS[operation]) {
37
+ const controls = ['route', 'coordinate'].includes(operation) ? ['goal', 'maxTurns', 'maxBudgetUsd', 'maxTokens', 'approvalPolicy', 'questionTimeoutMs', 'resultSchema'] : [];
38
+ for (const key of [...OPERATION_KEYS[operation], ...controls]) {
37
39
  if (BOOLEAN_FLAGS[key]) { if (options[key] === true) argv.push(BOOLEAN_FLAGS[key]); }
38
40
  else if (options[key] !== undefined && options[key] !== null) {
41
+ if (['maxTurns', 'maxBudgetUsd', 'maxTokens', 'questionTimeoutMs'].includes(key)) { argv.push(VALUE_FLAGS[key], String(options[key])); continue; }
42
+ if (key === 'resultSchema' && typeof options[key] !== 'string') { argv.push('--result-schema-json', JSON.stringify(options[key])); continue; }
39
43
  if (typeof options[key] !== 'string' || !options[key] || /\0/.test(options[key])) throw new Error(`Invalid remote ${key}`);
40
44
  argv.push(VALUE_FLAGS[key], options[key]);
41
45
  }
@@ -60,7 +64,7 @@ export async function forwardRemoteOperation(operation, task, options = {}) {
60
64
  error: transport.error || 'Remote operation returned no valid structured result' };
61
65
  }
62
66
  const result = { ...payload, ok: transport.ok === true && payload.ok === true, execution, transportExitCode: transport.exitCode ?? null,
63
- ...(!transport.ok ? { failureKind: 'transport' } : {}) };
67
+ ...(!transport.ok && !payload.failureKind ? { failureKind: 'transport' } : {}) };
64
68
  if (!result.ok && !result.error) result.error = transport.error || `Remote ${operation} failed`;
65
69
  if (result.ok && payload.branch) result.deliverable = { node: options.node, workspace: options.cwd, branch: payload.branch, commit: payload.commit ?? null };
66
70
  const threadId = payload.threadId || options.threadId;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Meta Harness Universal Cross-Model Adversarial Code Review Engine
2
+ * Metaharness Universal Cross-Model Adversarial Code Review Engine
3
3
  *
4
4
  * Runs automated code reviews across integrated agent platforms (claude, codex, agy,
5
5
  * grok, dsh, hermes, etc.).
@@ -19,6 +19,7 @@ import { resolvePool, localExecutionNode } from './pool.mjs';
19
19
  import { lastJsonWith } from './json-reply.mjs';
20
20
  import { createWorktree, removeWorktree, commitAll, headCommit } from './worktree.mjs';
21
21
  import { forwardRemoteOperation } from './remote-operation.mjs';
22
+ import { recordReview } from './reviews.mjs';
22
23
 
23
24
  const MAX_DIFF = 30000;
24
25
  // Generated surfaces say nothing a reviewer can act on, and they crowd out the source.
@@ -129,11 +130,13 @@ export function buildReviewPrompt(diff, context = {}) {
129
130
  You are an independent, highly critical senior security and architecture engineer performing an adversarial code review.
130
131
  You did NOT write this code. Do not rubber-stamp. Look for vulnerabilities, silent failures, regressions, anti-patterns, and test coverage gaps.
131
132
 
132
- ${task}${branch}${context.stat ? `Files changed:\n${context.stat}\n\n` : ''}${context.untracked?.length ? `New untracked files, not in the diff; read them too:\n${context.untracked.slice(0, 100).join('\n')}\n\n` : ''}${diff.length > MAX_DIFF ? `This diff is cut at ${MAX_DIFF} of ${diff.length} characters. Read the remainder from the repository and say so if you could not.\n` : ''}GIT DIFF TO REVIEW:
133
+ ${task}${branch}${context.evidence ? `Job evidence (recorded by Metaharness, not by the author):\n${JSON.stringify(context.evidence, null, 2).slice(0, 8000)}\n\n` : ''}${context.stat ? `Files changed:\n${context.stat}\n\n` : ''}${context.untracked?.length ? `New untracked files, not in the diff; read them too:\n${context.untracked.slice(0, 100).join('\n')}\n\n` : ''}${diff.length > MAX_DIFF ? `This diff is cut at ${MAX_DIFF} of ${diff.length} characters. Read the remainder from the repository and say so if you could not.\n` : ''}GIT DIFF TO REVIEW:
133
134
  \`\`\`diff
134
135
  ${diff.slice(0, MAX_DIFF)}
135
136
  \`\`\`
136
137
 
138
+ Judge the diff. Use the job evidence for claims about how the job ran (questions asked, replies, the goal check, the gate). A claim you cannot verify from the diff or the evidence is a "suggestion" marked unverified in its issue text; it is never "critical" or blocking. Mark a finding "critical" only for a defect in the diff itself.
139
+
137
140
  Evaluate the diff against these strict criteria:
138
141
  1. Security & Blast Radius (privilege escalation, token leakage, unhandled exceptions)
139
142
  2. Invariant & Rule Adherence (does it break existing contracts or introduce silent side effects?)
@@ -166,14 +169,39 @@ export function parseReviewVerdict(response) {
166
169
  return { verdict: 'NEEDS_DISCUSSION', parsed: false, riskScore: null, summary: text.slice(0, 300) || 'The reviewer returned nothing readable.', findings: [] };
167
170
  }
168
171
 
172
+ export function reviewBlocksPush(result) {
173
+ if (result?.ok !== true) return true;
174
+ if (result.verdict === 'NO_DIFF') return false;
175
+ const review = result.review;
176
+ // R45.8: only blocking findings stop the push; other findings go to the job result.
177
+ // An unreadable review cannot clear a push, so it blocks.
178
+ return !review || review.parsed === false ||
179
+ review.findings?.some(f => f.blocking === true || ['blocking', 'critical'].includes(f.severity)) === true;
180
+ }
181
+
169
182
  export async function executeCodeReview(options = {}) {
170
183
  const remote = await forwardRemoteOperation('review', null, options);
171
184
  if (remote) return remote;
172
185
  localExecutionNode(options);
186
+ const selection = { workspace: options.cwd || process.cwd(), target: options.target || (options.staged ? '--staged' : 'working tree'), reviewer: options.reviewer || null };
187
+ let result;
188
+ try { result = await executeLocalReview(options, selection); }
189
+ catch (error) {
190
+ // Preserve the existing CLI error contract, but retain failed attempts too.
191
+ try { recordReview({ ok: false, error: error.message }, selection); }
192
+ catch (storeError) { throw new Error(`${error.message}; review store failed: ${storeError.message}`); }
193
+ throw error;
194
+ }
195
+ try { return { ...result, reviewId: recordReview(result, selection).id }; }
196
+ catch (error) { return { ...result, ok: false, reviewStoreError: error.message }; }
197
+ }
198
+
199
+ async function executeLocalReview(options, selection) {
173
200
  const cwd = options.cwd || process.cwd();
174
201
  const author = options.author || null;
175
202
  const diffResult = extractGitDiff({ cwd, target: options.target, staged: options.staged });
176
203
  if (!diffResult.ok) throw new Error(`Failed to read the diff: ${diffResult.error}`);
204
+ Object.assign(selection, { diff: diffResult.diff, untracked: diffResult.untracked, target: diffResult.target, commit: headCommit(cwd) });
177
205
  if (!diffResult.diff && !diffResult.untracked?.length) return { ok: true, verdict: 'NO_DIFF', target: diffResult.target, summary: `Nothing to review for ${diffResult.target}.` };
178
206
 
179
207
  const { ready, excluded } = await resolvePool(options);
@@ -184,13 +212,16 @@ export async function executeCodeReview(options = {}) {
184
212
  const reviewerFamily = ADAPTER_CATALOG[reviewer.id]?.family || reviewer.family || null;
185
213
  if (author && (reviewer.id === author || (authorFamily && authorFamily === reviewerFamily))) throw new Error(`${reviewer.id} shares a model family with the author ${author}, so it cannot review this independently`);
186
214
  const independence = author ? 'verified' : 'unverified: the author was not named';
215
+ selection.reviewer = reviewer.id;
187
216
  const truncated = diffResult.diff.length > MAX_DIFF;
188
217
  const snapshot = reviewSnapshot(cwd, diffResult);
218
+ selection.commit = snapshot.commit;
189
219
  let result;
190
220
  try { result = await dispatchWithReceipt({
191
221
  agent: reviewer.id, node: 'local', cwd: snapshot.directory, contextCwd: cwd, contextTask: options.task || `Review the code changes in ${diffResult.target}`,
192
- prompt: buildReviewPrompt(diffResult.diff, { task: options.task, branch: options.branch || diffResult.target, stat: diffResult.stat, untracked: diffResult.untracked }),
222
+ prompt: buildReviewPrompt(diffResult.diff, { task: options.task, evidence: options.evidence, branch: options.branch || diffResult.target, stat: diffResult.stat, untracked: diffResult.untracked }),
193
223
  permissions: { read: true, write: false }, allowUnknownAuth: options.allowUnknownAuth === true,
224
+ ...(reviewer.id === 'codex' ? { reviewTarget: { type: 'custom', instructions: buildReviewPrompt(diffResult.diff, { task: options.task, evidence: options.evidence, branch: diffResult.target }) } } : {}),
194
225
  }, { dispatchFn: options.dispatchFn, role: 'Reviewer' }); }
195
226
  finally { removeWorktree(cwd, snapshot.directory); }
196
227
  if (!result.ok) return { ok: false, target: diffResult.target, reviewer: reviewer.id, error: result.error, failureKind: result.failureKind, excluded };
@@ -0,0 +1,25 @@
1
+ /** Review results are append-only SSSS records, independent of thread lifetime. */
2
+ import crypto from 'node:crypto';
3
+ import path from 'node:path';
4
+ import { threadsFile } from './paths.mjs';
5
+ import { PluginStateStore } from './ssss-store.mjs';
6
+
7
+ const store = () => new PluginStateStore(path.join(path.dirname(threadsFile()), 'meta-harness-reviews.jsonl'), 'meta-harness');
8
+ const dispositions = { APPROVE: 'approved', REQUEST_CHANGES: 'changes_requested', NEEDS_DISCUSSION: 'needs_discussion', NO_DIFF: 'no_diff', FAILED: 'failed' };
9
+
10
+ export function readReviews() { return store().read([]); }
11
+ export function recordReview(result, selection = {}) {
12
+ const review = result.review;
13
+ const verdict = review?.verdict || result.verdict || 'FAILED';
14
+ if (!dispositions[verdict]) throw new Error('Invalid review verdict');
15
+ const entry = {
16
+ id: `review-${crypto.randomUUID()}`, timestamp: new Date().toISOString(),
17
+ workspace: selection.workspace, target: review?.target || result.target || selection.target || null,
18
+ commit: review?.snapshotCommit || selection.commit || null,
19
+ diff: selection.diff || '', untracked: selection.untracked || [],
20
+ reviewer: review?.reviewer || result.reviewer || selection.reviewer || null,
21
+ verdict, disposition: dispositions[verdict], result,
22
+ };
23
+ store().append(entry);
24
+ return entry;
25
+ }