@ijfw/memory-server 1.4.3 → 1.5.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 (233) hide show
  1. package/fixtures/truncation-corpus/_generate-corpus.js +367 -0
  2. package/fixtures/truncation-corpus/fx-01-clean-exit-01/events.jsonl +2 -0
  3. package/fixtures/truncation-corpus/fx-01-clean-exit-01/intent-journal.jsonl +2 -0
  4. package/fixtures/truncation-corpus/fx-01-clean-exit-01/meta.json +18 -0
  5. package/fixtures/truncation-corpus/fx-01-clean-exit-01/target/.ijfw/state/workflow.json +1 -0
  6. package/fixtures/truncation-corpus/fx-01-clean-exit-02/events.jsonl +2 -0
  7. package/fixtures/truncation-corpus/fx-01-clean-exit-02/intent-journal.jsonl +2 -0
  8. package/fixtures/truncation-corpus/fx-01-clean-exit-02/meta.json +18 -0
  9. package/fixtures/truncation-corpus/fx-01-clean-exit-02/target/.ijfw/state/workflow.json +1 -0
  10. package/fixtures/truncation-corpus/fx-01-clean-exit-03/events.jsonl +2 -0
  11. package/fixtures/truncation-corpus/fx-01-clean-exit-03/intent-journal.jsonl +2 -0
  12. package/fixtures/truncation-corpus/fx-01-clean-exit-03/meta.json +18 -0
  13. package/fixtures/truncation-corpus/fx-01-clean-exit-03/target/.ijfw/state/workflow.json +1 -0
  14. package/fixtures/truncation-corpus/fx-01-clean-exit-04/events.jsonl +2 -0
  15. package/fixtures/truncation-corpus/fx-01-clean-exit-04/intent-journal.jsonl +2 -0
  16. package/fixtures/truncation-corpus/fx-01-clean-exit-04/meta.json +18 -0
  17. package/fixtures/truncation-corpus/fx-01-clean-exit-04/target/.ijfw/state/workflow.json +1 -0
  18. package/fixtures/truncation-corpus/fx-01-clean-exit-05/events.jsonl +2 -0
  19. package/fixtures/truncation-corpus/fx-01-clean-exit-05/intent-journal.jsonl +2 -0
  20. package/fixtures/truncation-corpus/fx-01-clean-exit-05/meta.json +18 -0
  21. package/fixtures/truncation-corpus/fx-01-clean-exit-05/target/.ijfw/state/workflow.json +1 -0
  22. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/events.jsonl +1 -0
  23. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/intent-journal.jsonl +3 -0
  24. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/meta.json +18 -0
  25. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/snapshots/v-midO-1-advance.json +11 -0
  26. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/target/.ijfw/state/workflow.json +1 -0
  27. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/events.jsonl +1 -0
  28. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/intent-journal.jsonl +3 -0
  29. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/meta.json +18 -0
  30. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/snapshots/v-midO-2-advance.json +11 -0
  31. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/target/.ijfw/state/workflow.json +1 -0
  32. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/events.jsonl +1 -0
  33. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/intent-journal.jsonl +3 -0
  34. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/meta.json +18 -0
  35. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/snapshots/v-midO-3-advance.json +11 -0
  36. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/target/.ijfw/state/workflow.json +1 -0
  37. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/events.jsonl +1 -0
  38. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/intent-journal.jsonl +3 -0
  39. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/meta.json +18 -0
  40. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/snapshots/v-midO-4-advance.json +11 -0
  41. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/target/.ijfw/state/workflow.json +1 -0
  42. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/events.jsonl +1 -0
  43. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/intent-journal.jsonl +3 -0
  44. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/meta.json +18 -0
  45. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/snapshots/v-midO-5-advance.json +11 -0
  46. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/target/.ijfw/state/workflow.json +1 -0
  47. package/fixtures/truncation-corpus/fx-03-mid-append-01/events.jsonl +1 -0
  48. package/fixtures/truncation-corpus/fx-03-mid-append-01/intent-journal.jsonl +3 -0
  49. package/fixtures/truncation-corpus/fx-03-mid-append-01/meta.json +18 -0
  50. package/fixtures/truncation-corpus/fx-03-mid-append-01/target/.ijfw/blackboard/decisions.jsonl +1 -0
  51. package/fixtures/truncation-corpus/fx-03-mid-append-02/events.jsonl +1 -0
  52. package/fixtures/truncation-corpus/fx-03-mid-append-02/intent-journal.jsonl +3 -0
  53. package/fixtures/truncation-corpus/fx-03-mid-append-02/meta.json +18 -0
  54. package/fixtures/truncation-corpus/fx-03-mid-append-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
  55. package/fixtures/truncation-corpus/fx-03-mid-append-03/events.jsonl +1 -0
  56. package/fixtures/truncation-corpus/fx-03-mid-append-03/intent-journal.jsonl +3 -0
  57. package/fixtures/truncation-corpus/fx-03-mid-append-03/meta.json +18 -0
  58. package/fixtures/truncation-corpus/fx-03-mid-append-03/target/.ijfw/blackboard/decisions.jsonl +1 -0
  59. package/fixtures/truncation-corpus/fx-03-mid-append-04/events.jsonl +1 -0
  60. package/fixtures/truncation-corpus/fx-03-mid-append-04/intent-journal.jsonl +3 -0
  61. package/fixtures/truncation-corpus/fx-03-mid-append-04/meta.json +18 -0
  62. package/fixtures/truncation-corpus/fx-03-mid-append-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
  63. package/fixtures/truncation-corpus/fx-03-mid-append-05/events.jsonl +1 -0
  64. package/fixtures/truncation-corpus/fx-03-mid-append-05/intent-journal.jsonl +3 -0
  65. package/fixtures/truncation-corpus/fx-03-mid-append-05/meta.json +18 -0
  66. package/fixtures/truncation-corpus/fx-03-mid-append-05/target/.ijfw/blackboard/decisions.jsonl +1 -0
  67. package/fixtures/truncation-corpus/fx-04-no-events-01/events.jsonl +0 -0
  68. package/fixtures/truncation-corpus/fx-04-no-events-01/intent-journal.jsonl +1 -0
  69. package/fixtures/truncation-corpus/fx-04-no-events-01/meta.json +18 -0
  70. package/fixtures/truncation-corpus/fx-04-no-events-01/snapshots/v-noEv-1-set-phase.json +11 -0
  71. package/fixtures/truncation-corpus/fx-04-no-events-01/target/.ijfw/state/workflow.json +1 -0
  72. package/fixtures/truncation-corpus/fx-04-no-events-02/events.jsonl +0 -0
  73. package/fixtures/truncation-corpus/fx-04-no-events-02/intent-journal.jsonl +1 -0
  74. package/fixtures/truncation-corpus/fx-04-no-events-02/meta.json +18 -0
  75. package/fixtures/truncation-corpus/fx-04-no-events-02/snapshots/v-noEv-2-set-phase.json +11 -0
  76. package/fixtures/truncation-corpus/fx-04-no-events-02/target/.ijfw/state/workflow.json +1 -0
  77. package/fixtures/truncation-corpus/fx-04-no-events-03/events.jsonl +0 -0
  78. package/fixtures/truncation-corpus/fx-04-no-events-03/intent-journal.jsonl +1 -0
  79. package/fixtures/truncation-corpus/fx-04-no-events-03/meta.json +18 -0
  80. package/fixtures/truncation-corpus/fx-04-no-events-03/snapshots/v-noEv-3-set-phase.json +11 -0
  81. package/fixtures/truncation-corpus/fx-04-no-events-03/target/.ijfw/state/workflow.json +1 -0
  82. package/fixtures/truncation-corpus/fx-04-no-events-04/events.jsonl +0 -0
  83. package/fixtures/truncation-corpus/fx-04-no-events-04/intent-journal.jsonl +1 -0
  84. package/fixtures/truncation-corpus/fx-04-no-events-04/meta.json +18 -0
  85. package/fixtures/truncation-corpus/fx-04-no-events-04/snapshots/v-noEv-4-set-phase.json +11 -0
  86. package/fixtures/truncation-corpus/fx-04-no-events-04/target/.ijfw/state/workflow.json +1 -0
  87. package/fixtures/truncation-corpus/fx-04-no-events-05/events.jsonl +0 -0
  88. package/fixtures/truncation-corpus/fx-04-no-events-05/intent-journal.jsonl +1 -0
  89. package/fixtures/truncation-corpus/fx-04-no-events-05/meta.json +18 -0
  90. package/fixtures/truncation-corpus/fx-04-no-events-05/snapshots/v-noEv-5-set-phase.json +11 -0
  91. package/fixtures/truncation-corpus/fx-04-no-events-05/target/.ijfw/state/workflow.json +1 -0
  92. package/fixtures/truncation-corpus/fx-05-error-terminated-01/events.jsonl +2 -0
  93. package/fixtures/truncation-corpus/fx-05-error-terminated-01/intent-journal.jsonl +3 -0
  94. package/fixtures/truncation-corpus/fx-05-error-terminated-01/meta.json +18 -0
  95. package/fixtures/truncation-corpus/fx-05-error-terminated-01/snapshots/v-errT-1-partial.json +11 -0
  96. package/fixtures/truncation-corpus/fx-05-error-terminated-01/target/.ijfw/state/workflow.json +1 -0
  97. package/fixtures/truncation-corpus/fx-05-error-terminated-02/events.jsonl +2 -0
  98. package/fixtures/truncation-corpus/fx-05-error-terminated-02/intent-journal.jsonl +3 -0
  99. package/fixtures/truncation-corpus/fx-05-error-terminated-02/meta.json +18 -0
  100. package/fixtures/truncation-corpus/fx-05-error-terminated-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
  101. package/fixtures/truncation-corpus/fx-05-error-terminated-03/events.jsonl +2 -0
  102. package/fixtures/truncation-corpus/fx-05-error-terminated-03/intent-journal.jsonl +3 -0
  103. package/fixtures/truncation-corpus/fx-05-error-terminated-03/meta.json +18 -0
  104. package/fixtures/truncation-corpus/fx-05-error-terminated-03/snapshots/v-errT-3-partial.json +11 -0
  105. package/fixtures/truncation-corpus/fx-05-error-terminated-03/target/.ijfw/state/workflow.json +1 -0
  106. package/fixtures/truncation-corpus/fx-05-error-terminated-04/events.jsonl +2 -0
  107. package/fixtures/truncation-corpus/fx-05-error-terminated-04/intent-journal.jsonl +3 -0
  108. package/fixtures/truncation-corpus/fx-05-error-terminated-04/meta.json +18 -0
  109. package/fixtures/truncation-corpus/fx-05-error-terminated-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
  110. package/fixtures/truncation-corpus/fx-05-error-terminated-05/events.jsonl +2 -0
  111. package/fixtures/truncation-corpus/fx-05-error-terminated-05/intent-journal.jsonl +3 -0
  112. package/fixtures/truncation-corpus/fx-05-error-terminated-05/meta.json +18 -0
  113. package/fixtures/truncation-corpus/fx-05-error-terminated-05/snapshots/v-errT-5-partial.json +11 -0
  114. package/fixtures/truncation-corpus/fx-05-error-terminated-05/target/.ijfw/state/workflow.json +1 -0
  115. package/package.json +1 -1
  116. package/src/active-extension-writer.js +144 -64
  117. package/src/api-client.js +43 -5
  118. package/src/audit-roster.js +80 -5
  119. package/src/blackboard.js +298 -6
  120. package/src/cli-run.js +33 -5
  121. package/src/codex-agents.js +96 -5
  122. package/src/cost/aggregator.js +39 -9
  123. package/src/cost/pricing.js +57 -0
  124. package/src/cost/readers/gemini.js +1 -1
  125. package/src/cross-audit-chunker.js +189 -0
  126. package/src/cross-dispatcher.js +124 -21
  127. package/src/cross-orchestrator-cli.js +550 -14
  128. package/src/cross-orchestrator.js +1171 -10
  129. package/src/cross-project-search.js +195 -9
  130. package/src/dashboard-client-planning.html +273 -0
  131. package/src/dashboard-client-waves.html +304 -0
  132. package/src/dashboard-client.html +17 -2
  133. package/src/dashboard-server.js +152 -0
  134. package/src/deploy-alerts.js +150 -0
  135. package/src/design/iframe-bridge.js +242 -0
  136. package/src/design-companion.js +144 -0
  137. package/src/dispatch/checkpoint-cli.js +97 -0
  138. package/src/dispatch/colon-syntax.js +81 -1
  139. package/src/dispatch/extension.js +27 -1
  140. package/src/dispatch/registry-cli.js +4 -1
  141. package/src/dispatch/wave-cli.js +323 -0
  142. package/src/dispatch/worktree-cli.js +40 -0
  143. package/src/dispatch-planner.js +97 -2
  144. package/src/dream/runner.mjs +47 -11
  145. package/src/dream/stage-runner.js +40 -0
  146. package/src/dream/state-file.js +102 -0
  147. package/src/extension-installer.js +70 -24
  148. package/src/extension-quota-tracker.js +4 -2
  149. package/src/extension-registry.js +289 -35
  150. package/src/feedback-detector.js +26 -0
  151. package/src/fs-lock.js +259 -7
  152. package/src/gate-result.js +95 -1
  153. package/src/hero-line.js +86 -5
  154. package/src/intent-router.js +35 -0
  155. package/src/lib/a11y-contract.js +117 -0
  156. package/src/lib/atomic-io.js +29 -8
  157. package/src/lib/cache-keepalive.js +150 -0
  158. package/src/lib/jsonl-rotation.js +104 -0
  159. package/src/lib/lighthouse-pillar.js +121 -0
  160. package/src/lib/llm-call.js +121 -0
  161. package/src/lib/playwright-baseline.js +205 -0
  162. package/src/lib/rekor-bridge.js +221 -0
  163. package/src/lib/repo-map.js +392 -0
  164. package/src/lib/shasum-verify.js +164 -0
  165. package/src/lib/sketches-gc.js +132 -0
  166. package/src/lib/tmp-suffix.js +62 -0
  167. package/src/lib/ui-review-runner.js +554 -0
  168. package/src/lib/uispec-drift.js +301 -0
  169. package/src/lib/uispec-intake.js +381 -0
  170. package/src/lib/worktree-guards.js +118 -0
  171. package/src/lib/worktree-recovery.js +100 -0
  172. package/src/memory/auto-linker.js +152 -0
  173. package/src/memory/benchmark.js +498 -0
  174. package/src/memory/dedup.js +126 -0
  175. package/src/memory/embedding-cache.js +136 -0
  176. package/src/memory/fact-extractor.js +168 -0
  177. package/src/memory/fts5.js +65 -1
  178. package/src/memory/migrations/004-bitemporal.js +91 -0
  179. package/src/memory/migrations/005-vector-cache.js +61 -0
  180. package/src/memory/migrations/006-obsidian-graph.js +46 -0
  181. package/src/memory/migrations/007-skill-telemetry.js +24 -0
  182. package/src/memory/migrations/008-write-provenance.js +41 -0
  183. package/src/memory/obsidian-parser.js +91 -0
  184. package/src/memory/query-dataview.js +86 -0
  185. package/src/memory/search.js +10 -0
  186. package/src/memory/temporal.js +529 -0
  187. package/src/memory/tokenize.js +10 -0
  188. package/src/memory-facts-handler.js +37 -0
  189. package/src/memory-feedback.js +260 -2
  190. package/src/model-refresh.js +292 -0
  191. package/src/observability/cost-anomaly.js +166 -0
  192. package/src/observability/evaluator-checkpoint-contract.js +117 -0
  193. package/src/observability/trace-id.js +163 -0
  194. package/src/orchestrator/agents-md-blackboard.js +152 -0
  195. package/src/orchestrator/checkpoint-contract.md +140 -0
  196. package/src/orchestrator/debug-trident.js +570 -0
  197. package/src/orchestrator/merge-block-aware.js +350 -0
  198. package/src/orchestrator/plan-checker.js +475 -0
  199. package/src/orchestrator/post-done-runner.js +249 -0
  200. package/src/orchestrator/review.js +136 -0
  201. package/src/orchestrator/runtime-loop.js +430 -0
  202. package/src/orchestrator/skill-telemetry-sink.js +29 -0
  203. package/src/orchestrator/skill-telemetry.js +37 -0
  204. package/src/orchestrator/state-events.js +459 -0
  205. package/src/orchestrator/state-sdk.js +1764 -0
  206. package/src/orchestrator/status-protocol.js +235 -0
  207. package/src/orchestrator/subagent-telemetry.js +452 -0
  208. package/src/orchestrator/termination.js +160 -0
  209. package/src/orchestrator/verification-gate.js +281 -0
  210. package/src/orchestrator/wave-state.js +564 -0
  211. package/src/orchestrator/worktree-provision.js +77 -0
  212. package/src/override-use-registry.js +111 -5
  213. package/src/receipts.js +36 -4
  214. package/src/recovery/checkpoint.js +56 -3
  215. package/src/recovery/code-fixer.js +656 -0
  216. package/src/recovery/truncation.js +317 -0
  217. package/src/redactor.js +75 -6
  218. package/src/runtime-mediator.js +15 -0
  219. package/src/sanitizer.js +10 -0
  220. package/src/search-hybrid.js +139 -0
  221. package/src/server.js +603 -59
  222. package/src/swarm/worktree.js +27 -4
  223. package/src/swarm-config.js +113 -12
  224. package/src/team/domain-templates/book.json +51 -0
  225. package/src/team/domain-templates/business.json +41 -0
  226. package/src/team/domain-templates/content.json +50 -0
  227. package/src/team/domain-templates/design.json +44 -0
  228. package/src/team/domain-templates/research.json +41 -0
  229. package/src/team/domain-templates/software.json +40 -0
  230. package/src/team/generator.js +278 -3
  231. package/src/team/modify.js +203 -0
  232. package/src/team/schemas.js +48 -0
  233. package/src/update-apply.js +19 -3
@@ -0,0 +1,235 @@
1
+ /**
2
+ * status-protocol.js — 4-value agent status protocol + commit-before-report verification.
3
+ *
4
+ * Every implementer agent must end its report with:
5
+ * Status: <VALUE>
6
+ * Branch: <branch>
7
+ * Commit: <sha>
8
+ * Tests: <summary>
9
+ *
10
+ * Landed in W10-A1 (v1.4.4 N2).
11
+ */
12
+
13
+ import { execFileSync } from 'node:child_process';
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Constants
17
+ // ---------------------------------------------------------------------------
18
+
19
+ export const STATUS_VALUES = Object.freeze([
20
+ 'DONE',
21
+ 'DONE_WITH_CONCERNS',
22
+ 'NEEDS_CONTEXT',
23
+ 'BLOCKED',
24
+ ]);
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // ProtocolViolation
28
+ // ---------------------------------------------------------------------------
29
+
30
+ export class ProtocolViolation extends Error {
31
+ /**
32
+ * @param {string} reason Human-readable explanation
33
+ * @param {string} raw The original report text
34
+ */
35
+ constructor(reason, raw) {
36
+ super(reason);
37
+ this.name = 'ProtocolViolation';
38
+ this.reason = reason;
39
+ this.raw = raw;
40
+ }
41
+ }
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // parseAgentReport
45
+ // ---------------------------------------------------------------------------
46
+
47
+ /**
48
+ * Parse a structured agent report into its constituent fields.
49
+ *
50
+ * Required field: `Status: <VALUE>` — throws ProtocolViolation if missing or invalid.
51
+ * All other fields are extracted best-effort (undefined if absent).
52
+ *
53
+ * @param {string} reportText
54
+ * @returns {{ status: string, commit_sha?: string, branch?: string, tests?: string,
55
+ * concerns?: string, reason?: string, missing?: string, tried?: string,
56
+ * attempts: number, raw: string }}
57
+ * @throws {ProtocolViolation}
58
+ */
59
+ export function parseAgentReport(reportText) {
60
+ const raw = reportText;
61
+
62
+ // v1.5.1 H1.2 (audit workflow.md HIGH-F1): take the LAST `^Status:` match,
63
+ // not the first. The protocol places `Status:` at the end of the report, so
64
+ // when an agent quotes a prior wave's `Status: BLOCKED` mid-body, the FIRST
65
+ // match was hijacking the agent's own status at the end. Same fix applied
66
+ // to `extract()` below for Attempts / Branch / Commit / Tests / etc.
67
+ const statusMatches = [...raw.matchAll(/^Status:\s*(\S+)\s*$/gm)];
68
+ const statusMatch = statusMatches[statusMatches.length - 1];
69
+ if (!statusMatch) {
70
+ throw new ProtocolViolation('missing Status: line in agent report', raw);
71
+ }
72
+ const status = statusMatch[1];
73
+ if (!STATUS_VALUES.includes(status)) {
74
+ throw new ProtocolViolation(
75
+ `invalid status "${status}"; expected one of ${STATUS_VALUES.join(', ')}`,
76
+ raw,
77
+ );
78
+ }
79
+
80
+ return {
81
+ status,
82
+ commit_sha: extract(raw, 'Commit'),
83
+ branch: extract(raw, 'Branch'),
84
+ tests: extract(raw, 'Tests'),
85
+ concerns: extract(raw, 'Concerns'),
86
+ reason: extract(raw, 'Reason'),
87
+ missing: extract(raw, 'Missing'),
88
+ tried: extract(raw, 'Tried'),
89
+ // v1.5.0-major S07 (W12-A): 3-attempt cap signal. Opt-in; defaults to 0
90
+ // when an implementer omits the line — preserving prior behavior for
91
+ // every existing report. Implementers using ijfw-executor.md set this to
92
+ // the max auto-fix attempts on any single issue (Rules 1-3).
93
+ attempts: parseInt(extract(raw, 'Attempts') || '0', 10),
94
+ raw,
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Extract the LAST single-line field value, or undefined if absent.
100
+ *
101
+ * v1.5.1 H1.2 (audit workflow.md HIGH-F1 / MED-C2): take the last match, not
102
+ * the first, so a quoted prior `Attempts: 5` (or any other field) does not
103
+ * override the agent's own value at the end of the report.
104
+ *
105
+ * Callers pass static field names (Commit, Branch, Tests, Attempts, etc.), so
106
+ * regex injection through `field` is not a concern — but we keep this comment
107
+ * as a tripwire: if you ever pass a user-controlled string here, escape it.
108
+ */
109
+ // v1.5.0 audit-LOW-work-L1: defensive escapeRegExp around the field name.
110
+ // Today callers only pass static field names (Commit, Branch, Tests, etc.) so
111
+ // regex injection is impossible -- but the previous comment was a tripwire,
112
+ // not a guard. This makes the function safe even if a future caller forgets.
113
+ function escapeRegExp(s) {
114
+ return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
115
+ }
116
+
117
+ function extract(text, field) {
118
+ const matches = [...text.matchAll(new RegExp(`^${escapeRegExp(field)}:\\s*(.+?)\\s*$`, 'gm'))];
119
+ const last = matches[matches.length - 1];
120
+ return last ? last[1] : undefined;
121
+ }
122
+
123
+ // ---------------------------------------------------------------------------
124
+ // verifyFreshCommit (internal)
125
+ // ---------------------------------------------------------------------------
126
+
127
+ /**
128
+ * Returns true if the commit at `sha` was authored at or after
129
+ * (dispatchTimestamp - 1s tolerance) AND is reachable from the dispatched branch.
130
+ *
131
+ * v1.5.0 S3 (W11-A3): branch-tuple check closes the r13-M-N2 bypass where a
132
+ * stale commit on main could pass as "fresh" because the time window happened
133
+ * to match. Empty/undefined branch falls back to time-only (detached HEAD or
134
+ * implicit-main case — orchestrator's choice whether to enforce).
135
+ *
136
+ * @param {string|undefined} sha
137
+ * @param {string|undefined} branch Dispatched branch name (empty = skip membership check)
138
+ * @param {number} dispatchTimestamp Unix seconds
139
+ * @param {{ projectRoot: string }} ctx
140
+ * @returns {boolean}
141
+ */
142
+ function verifyFreshCommit(sha, branch, dispatchTimestamp, ctx) {
143
+ if (!sha) return false;
144
+ try {
145
+ // 1. Freshness check.
146
+ // r13-M-02: 1s tolerance for clock skew.
147
+ // v1.5.0 audit-LOW-work-L4: bumped to 2s to close the Windows-share
148
+ // mtime-granularity edge case (FAT/SMB filesystems report 2s
149
+ // resolution; a 1s window can false-reject a genuinely-fresh commit
150
+ // when the orchestrator clock and the worktree filesystem disagree
151
+ // by ~1s). 2s is still tight enough that the original "stale commit
152
+ // that happens to match the time window" attack stays out of reach.
153
+ const tsOut = execFileSync(
154
+ 'git',
155
+ ['log', '-1', '--format=%ct', sha],
156
+ { cwd: ctx.projectRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] },
157
+ ).trim();
158
+ const commitTs = parseInt(tsOut, 10);
159
+ if (!Number.isFinite(commitTs) || commitTs < dispatchTimestamp - 2) return false;
160
+
161
+ // 2. v1.5.0 S3: branch-tuple check. Closes the "stale commit from main passes
162
+ // as fresh because the time window happens to match" bypass that r13-M-N2
163
+ // deferred to the structural fix.
164
+ // Empty branch = detached HEAD or implicit-main — skip membership check
165
+ // (orchestrator's choice whether to enforce).
166
+ if (branch && branch.length > 0) {
167
+ const branchOut = execFileSync(
168
+ 'git',
169
+ ['branch', '--contains', sha, '--list', branch],
170
+ { cwd: ctx.projectRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] },
171
+ );
172
+ if (branchOut.trim().length === 0) return false;
173
+ }
174
+ return true;
175
+ } catch {
176
+ return false;
177
+ }
178
+ }
179
+
180
+ // ---------------------------------------------------------------------------
181
+ // handleStatus
182
+ // ---------------------------------------------------------------------------
183
+
184
+ /**
185
+ * Decide the orchestrator action based on a parsed agent report.
186
+ *
187
+ * @param {{ status: string, commit_sha?: string, branch?: string, concerns?: string,
188
+ * missing?: string, reason?: string, tried?: string, attempts?: number }} parsed
189
+ * @param {number} dispatchTimestamp Unix seconds (Date.now()/1000 at dispatch)
190
+ * @param {{ projectRoot: string }} ctx
191
+ * @returns {{ action: string, [key: string]: unknown }}
192
+ */
193
+ export function handleStatus(parsed, dispatchTimestamp, ctx) {
194
+ // v1.5.0-major S07 (W12-A): 3-attempt cap is a hard escalation signal
195
+ // regardless of reported status. If an implementer (ijfw-executor.md) ran
196
+ // out the per-issue auto-fix budget, the orchestrator MUST surface to the
197
+ // user — even if the agent claims DONE — because by definition the
198
+ // remaining issue is documented but unfixed. R2's #1 pattern: convert
199
+ // truncation from a behavior problem to a budget problem.
200
+ if (typeof parsed.attempts === 'number' && parsed.attempts >= 3) {
201
+ return {
202
+ action: 'escalate_to_user',
203
+ reason: '3-attempt-cap-hit',
204
+ original_status: parsed.status,
205
+ original_action: undefined,
206
+ };
207
+ }
208
+ switch (parsed.status) {
209
+ case 'DONE': {
210
+ const fresh = verifyFreshCommit(
211
+ parsed.commit_sha,
212
+ parsed.branch,
213
+ dispatchTimestamp,
214
+ ctx,
215
+ );
216
+ if (!fresh) {
217
+ return { action: 'redispatch_needs_context', missing: 'commit-before-report' };
218
+ }
219
+ return { action: 'proceed_to_review', commit_sha: parsed.commit_sha };
220
+ }
221
+
222
+ case 'DONE_WITH_CONCERNS':
223
+ return { action: 'proceed_with_flag', concerns: parsed.concerns };
224
+
225
+ case 'NEEDS_CONTEXT':
226
+ return { action: 'redispatch_with_context', missing: parsed.missing };
227
+
228
+ case 'BLOCKED':
229
+ return { action: 'escalate_to_user', reason: parsed.reason, tried: parsed.tried };
230
+
231
+ default:
232
+ // STATUS_VALUES is exhaustive; parseAgentReport guards this.
233
+ throw new ProtocolViolation(`unhandled status "${parsed.status}"`, parsed.raw ?? '');
234
+ }
235
+ }
@@ -0,0 +1,452 @@
1
+ /**
2
+ * subagent-telemetry.js — v1.5.0 S1: subagent checkpoint/resume telemetry.
3
+ *
4
+ * Closes 8/13 truncation pattern observed across v1.4.4 Wave 10 + v1.5.0
5
+ * research dispatch. Implementer agents call `recordCheckpoint` via the
6
+ * (forthcoming) `ijfw checkpoint` CLI to persist progress before the
7
+ * Claude Code subagent harness's ~20-tool / 60s wall-clock cap fires.
8
+ * The orchestrator calls `listOrphanedSubagents` post-wave to detect
9
+ * truncations, and `readLastCheckpoint` to resume mid-execution.
10
+ *
11
+ * Storage layout:
12
+ * <projectRoot>/.ijfw/wave-<waveId>/subagent-<subId>.checkpoint.json
13
+ * Lock:
14
+ * <projectRoot>/.ijfw/wave-<waveId>/.subagent-<subId>.lock
15
+ *
16
+ * v1.5.0 T9: checkpoint/summary/violation writes route through the state-SDK
17
+ * verbs (`subagent.checkpoint`, `event.emit`) instead of raw fs writes.
18
+ * Append ops carry stable dedupKeys derived from their inputs.
19
+ *
20
+ * Frozen surface for Wave 11-A (S1 rest, S2, S3) — do not change signatures.
21
+ */
22
+
23
+ import { mkdir, readFile, readdir, copyFile, stat } from 'node:fs/promises';
24
+ import { join } from 'node:path';
25
+ import { createHash } from 'node:crypto';
26
+ import { query } from './state-sdk.js';
27
+ import { pollEvents } from './state-events.js';
28
+ // v1.5.0 N4.obs M1+M2: trace-id + hierarchical observation path.
29
+ import { getTraceId, composePath } from '../observability/trace-id.js';
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Frozen constants — Wave 11-A imports these directly
33
+ // ---------------------------------------------------------------------------
34
+
35
+ export const MAX_CHECKPOINT_SIZE = 4 * 1024;
36
+ export const SUB_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
37
+ export const WAVE_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Internal helpers
41
+ // ---------------------------------------------------------------------------
42
+
43
+ function validateWaveId(waveId) {
44
+ if (typeof waveId !== 'string' || !WAVE_ID_PATTERN.test(waveId)) {
45
+ throw new Error(
46
+ `subagent-telemetry: invalid waveId "${waveId}" — must match ${WAVE_ID_PATTERN}`,
47
+ );
48
+ }
49
+ }
50
+
51
+ function validateSubId(subId) {
52
+ if (typeof subId !== 'string' || !SUB_ID_PATTERN.test(subId)) {
53
+ throw new Error(
54
+ `subagent-telemetry: invalid subId "${subId}" — must match ${SUB_ID_PATTERN}`,
55
+ );
56
+ }
57
+ }
58
+
59
+ function checkpointPaths(waveId, subId, projectRoot) {
60
+ const dir = join(projectRoot, '.ijfw', `wave-${waveId}`);
61
+ return {
62
+ dir,
63
+ file: join(dir, `subagent-${subId}.checkpoint.json`),
64
+ lock: join(dir, `.subagent-${subId}.lock`),
65
+ };
66
+ }
67
+
68
+ /**
69
+ * Derive a short hash string for use in dedupKeys. Stable across calls with
70
+ * the same input — sha256 of JSON-serialised payload, first 12 hex chars.
71
+ */
72
+ function shortHash(obj) {
73
+ return createHash('sha256').update(JSON.stringify(obj)).digest('hex').slice(0, 12);
74
+ }
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // Public API — FROZEN for Wave 11-A
78
+ // ---------------------------------------------------------------------------
79
+
80
+ /**
81
+ * Atomically record a subagent checkpoint. Caller-supplied `checkpoint` is
82
+ * merged into a payload envelope with schema_version/wave_id/sub_id/ts.
83
+ *
84
+ * @param {string} waveId e.g. "W11-A0"
85
+ * @param {string} subId e.g. "W11-A1"
86
+ * @param {object} checkpoint arbitrary JSON (e.g. tool_use_count, last_action)
87
+ * @param {string} projectRoot absolute path to project root
88
+ * @returns {Promise<void>}
89
+ */
90
+ export async function recordCheckpoint(waveId, subId, checkpoint, projectRoot) {
91
+ validateWaveId(waveId);
92
+ validateSubId(subId);
93
+
94
+ // v1.5.0-major S01: when running in a worktree subagent, the parent orchestrator
95
+ // passes IJFW_PARENT_PROJECT_ROOT in env so checkpoints land in the parent's
96
+ // .ijfw/ directory (visible after worktree cleanup). Backward-compatible:
97
+ // missing env var → use projectRoot as before.
98
+ const effectiveRoot = process.env.IJFW_PARENT_PROJECT_ROOT ?? projectRoot;
99
+
100
+ // v1.5.0 N4.obs M1: tag every checkpoint with the orchestrator's trace_id when
101
+ // one is available (env-var inheritance or already-cached). Never fabricate
102
+ // here -- if the env isn't set + the orchestrator never called ensureTraceId,
103
+ // we skip the field so old consumers don't see a half-populated id.
104
+ const traceId = getTraceId();
105
+ // v1.5.0 N4.obs M2: hierarchical observation path. The orchestrator can pass
106
+ // a richer path via `checkpoint.path`; otherwise we synthesise a default that
107
+ // a UI tree-view can group on: /wave-<waveId>/sub-<subId>.
108
+ const observPath = (typeof checkpoint.path === 'string' && checkpoint.path.length > 0)
109
+ ? checkpoint.path
110
+ : composePath({ waveId, subId });
111
+
112
+ // Build the checkpoint envelope (preserving existing field contract for
113
+ // readLastCheckpoint consumers). The envelope is passed as the `checkpoint`
114
+ // object to the SDK verb and stored nested under that key.
115
+ const ts = new Date().toISOString();
116
+ const envelope = {
117
+ schema_version: 1,
118
+ wave_id: waveId,
119
+ sub_id: subId,
120
+ ts,
121
+ ...(traceId ? { trace_id: traceId } : {}),
122
+ path: observPath,
123
+ ...checkpoint,
124
+ };
125
+
126
+ const serialised = JSON.stringify(envelope);
127
+ if (serialised.length > MAX_CHECKPOINT_SIZE) {
128
+ throw new Error(
129
+ `subagent-telemetry: checkpoint size ${serialised.length} exceeds MAX_CHECKPOINT_SIZE ${MAX_CHECKPOINT_SIZE}`,
130
+ );
131
+ }
132
+
133
+ // v1.5.0 T9: route through state-SDK verb — no raw fs write.
134
+ // dedupKey is stable for this (subId, ts) pair: callers that retry within
135
+ // the same millisecond get a no-op rather than a double-write.
136
+ const dedupKey = `checkpoint:${subId}:${ts}`;
137
+ await query(
138
+ 'subagent.checkpoint',
139
+ { waveId, subagentId: subId, checkpoint: envelope, dedupKey },
140
+ { projectRoot: effectiveRoot },
141
+ );
142
+ }
143
+
144
+ /**
145
+ * Read the most recent checkpoint for a (waveId, subId) pair.
146
+ * Returns parsed JSON object, or `null` if the file doesn't exist yet.
147
+ *
148
+ * @param {string} waveId
149
+ * @param {string} subId
150
+ * @param {string} projectRoot
151
+ * @returns {Promise<object|null>}
152
+ */
153
+ export async function readLastCheckpoint(waveId, subId, projectRoot) {
154
+ validateWaveId(waveId);
155
+ validateSubId(subId);
156
+
157
+ // v1.5.0-major S01: honor IJFW_PARENT_PROJECT_ROOT for worktree subagents.
158
+ const effectiveRoot = process.env.IJFW_PARENT_PROJECT_ROOT ?? projectRoot;
159
+
160
+ const { file } = checkpointPaths(waveId, subId, effectiveRoot);
161
+ let raw;
162
+ try {
163
+ raw = await readFile(file, 'utf8');
164
+ } catch (err) {
165
+ if (err && err.code === 'ENOENT') return null;
166
+ throw err;
167
+ }
168
+ const parsed = JSON.parse(raw);
169
+ // v1.5.0 T9: the SDK verb writes `{ waveId, subagentId, dedupKey, checkpoint,
170
+ // updated_at }` — the caller-supplied envelope lives nested under `checkpoint`.
171
+ // Unwrap to restore the flat shape that readLastCheckpoint consumers expect.
172
+ // Old-format files (no `checkpoint` key, have `schema_version`) are returned
173
+ // as-is for backward compatibility during migration.
174
+ if (parsed && typeof parsed.checkpoint === 'object' && parsed.checkpoint !== null
175
+ && typeof parsed.waveId === 'string') {
176
+ return parsed.checkpoint;
177
+ }
178
+ return parsed;
179
+ }
180
+
181
+ /**
182
+ * v1.5.0 T9: Append a summary event for a subagent via the `event.emit` verb.
183
+ * Routes through the state-SDK — no raw fs write.
184
+ *
185
+ * @param {string} waveId
186
+ * @param {string} subId
187
+ * @param {object} data arbitrary JSON (summary body ≤ 4 KiB)
188
+ * @param {string} projectRoot
189
+ * @returns {Promise<{ok: boolean, seq?: number, deduped?: boolean}>}
190
+ */
191
+ export async function appendSummary(waveId, subId, data, projectRoot) {
192
+ validateWaveId(waveId);
193
+ validateSubId(subId);
194
+ if (!data || typeof data !== 'object') {
195
+ throw new Error('subagent-telemetry: appendSummary requires a data object');
196
+ }
197
+ // v1.5.0-major S01: honor IJFW_PARENT_PROJECT_ROOT for worktree subagents.
198
+ const effectiveRoot = process.env.IJFW_PARENT_PROJECT_ROOT ?? projectRoot;
199
+ // dedupKey: stable hash of (waveId, subId, data) — identical retries are no-ops.
200
+ const dedupKey = `summary:${subId}:${shortHash({ waveId, subId, data })}`;
201
+ return query(
202
+ 'event.emit',
203
+ { subagentId: subId, waveId, eventType: 'summary', data, dedupKey },
204
+ { projectRoot: effectiveRoot },
205
+ );
206
+ }
207
+
208
+ /**
209
+ * v1.5.0 T9: Record a violation event for a subagent via the `event.emit` verb.
210
+ * Routes through the state-SDK — no raw fs write.
211
+ *
212
+ * @param {string} waveId
213
+ * @param {string} subId
214
+ * @param {object} data violation payload (type, message, etc.) ≤ 4 KiB
215
+ * @param {string} projectRoot
216
+ * @returns {Promise<{ok: boolean, seq?: number, deduped?: boolean}>}
217
+ */
218
+ export async function recordViolation(waveId, subId, data, projectRoot) {
219
+ validateWaveId(waveId);
220
+ validateSubId(subId);
221
+ if (!data || typeof data !== 'object') {
222
+ throw new Error('subagent-telemetry: recordViolation requires a data object');
223
+ }
224
+ // v1.5.0-major S01: honor IJFW_PARENT_PROJECT_ROOT for worktree subagents.
225
+ const effectiveRoot = process.env.IJFW_PARENT_PROJECT_ROOT ?? projectRoot;
226
+ // dedupKey: stable hash of (waveId, subId, data) — identical retries are no-ops.
227
+ const dedupKey = `violation:${subId}:${shortHash({ waveId, subId, data })}`;
228
+ return query(
229
+ 'event.emit',
230
+ { subagentId: subId, waveId, eventType: 'violation', data, dedupKey },
231
+ { projectRoot: effectiveRoot },
232
+ );
233
+ }
234
+
235
+ /**
236
+ * List subagent IDs that have a checkpoint file but are NOT in the wave's
237
+ * completed-subs set. Used by the orchestrator post-wave to detect truncated
238
+ * subagents.
239
+ *
240
+ * v1 (W11-A0): the completed-subs set is empty (we don't yet consume
241
+ * STATE.md frontmatter.completed_subs). This returns ALL subIds with a
242
+ * checkpoint file. W11-A1 will refine by reading the wave STATE.md.
243
+ *
244
+ * v1.5.0-major S01: accepts optional `additionalRoots: string[]` — additional
245
+ * project roots to scan (e.g. active git worktree paths). This lets the
246
+ * orchestrator see checkpoints written by worktree subagents BEFORE drain runs,
247
+ * by inspecting both the parent root and each worktree root. Honors
248
+ * IJFW_PARENT_PROJECT_ROOT for the primary `projectRoot` argument like
249
+ * record/read do (worktree subagents calling this still see the parent dir).
250
+ *
251
+ * Returns [] if no directory exists. Returned IDs are deduplicated.
252
+ *
253
+ * @param {string} waveId
254
+ * @param {string} projectRoot
255
+ * @param {string[]} [additionalRoots] optional extra roots to scan
256
+ * @returns {Promise<string[]>}
257
+ */
258
+ export async function listOrphanedSubagents(waveId, projectRoot, additionalRoots = []) {
259
+ validateWaveId(waveId);
260
+
261
+ // v1.5.0-major S01: honor IJFW_PARENT_PROJECT_ROOT for the primary root.
262
+ const effectiveRoot = process.env.IJFW_PARENT_PROJECT_ROOT ?? projectRoot;
263
+
264
+ const rootsToScan = [effectiveRoot, ...(Array.isArray(additionalRoots) ? additionalRoots : [])];
265
+
266
+ const seen = new Set();
267
+ for (const root of rootsToScan) {
268
+ if (typeof root !== 'string' || root.length === 0) continue;
269
+ const dir = join(root, '.ijfw', `wave-${waveId}`);
270
+ let entries;
271
+ try {
272
+ entries = await readdir(dir);
273
+ } catch (err) {
274
+ if (err && err.code === 'ENOENT') continue;
275
+ throw err;
276
+ }
277
+
278
+ for (const name of entries) {
279
+ // Match subagent-<subId>.checkpoint.json
280
+ const m = name.match(/^subagent-(.+)\.checkpoint\.json$/);
281
+ if (!m) continue;
282
+ const subId = m[1];
283
+ // Defence in depth: skip entries that wouldn't pass the pattern (e.g.
284
+ // an attacker-crafted filename someone dropped in by hand).
285
+ if (!SUB_ID_PATTERN.test(subId)) continue;
286
+ seen.add(subId);
287
+ }
288
+ }
289
+
290
+ // Completed-subs set is empty in v1 — every subId with a checkpoint is
291
+ // "orphaned" from the orchestrator's POV until W11-A1 wires in STATE.md.
292
+ return [...seen];
293
+ }
294
+
295
+ /**
296
+ * v1.5.0-major S01: drain checkpoint files from a worktree's .ijfw/wave-<id>/
297
+ * directory into the parent project's .ijfw/wave-<id>/ directory.
298
+ *
299
+ * Belt-and-suspenders for the IJFW_PARENT_PROJECT_ROOT env-var passthrough:
300
+ * if a subagent runs without the env var set (older callers, manual `claude`
301
+ * invocation in a worktree, etc.) the checkpoint will land in the worktree's
302
+ * own .ijfw/ — this drain copies them into the parent before
303
+ * `git worktree remove` deletes them forever.
304
+ *
305
+ * Idempotent: re-running with the same source overwrites destination (same
306
+ * checkpoint content). Filenames are preserved.
307
+ *
308
+ * @param {string} waveId
309
+ * @param {string} worktreePath absolute path to the worktree root
310
+ * @param {string} projectRoot absolute path to the parent project root
311
+ * @returns {Promise<{ok: true, drained: number} | {ok: false, reason: string}>}
312
+ */
313
+ export async function drainCheckpoints(waveId, worktreePath, projectRoot) {
314
+ validateWaveId(waveId);
315
+ if (typeof worktreePath !== 'string' || worktreePath.length === 0) {
316
+ return { ok: false, reason: 'invalid worktreePath' };
317
+ }
318
+ if (typeof projectRoot !== 'string' || projectRoot.length === 0) {
319
+ return { ok: false, reason: 'invalid projectRoot' };
320
+ }
321
+
322
+ const sourceDir = join(worktreePath, '.ijfw', `wave-${waveId}`);
323
+ const destDir = join(projectRoot, '.ijfw', `wave-${waveId}`);
324
+
325
+ let entries;
326
+ try {
327
+ entries = await readdir(sourceDir);
328
+ } catch (err) {
329
+ if (err && err.code === 'ENOENT') return { ok: true, drained: 0 };
330
+ return { ok: false, reason: `readdir ${sourceDir}: ${err.message}` };
331
+ }
332
+
333
+ await mkdir(destDir, { recursive: true });
334
+
335
+ // v1.5.0 audit-LOW-work-L3: parallelise the per-file copy. The previous
336
+ // sequential await pinned us to O(N * disk-latency); on a wave with 20+
337
+ // subagents this is the difference between ~1s and "feels instant".
338
+ // Each entry has independent src/dst paths so there's no ordering
339
+ // requirement and no shared state to coordinate.
340
+ const tasks = entries.map(async (name) => {
341
+ const m = name.match(/^subagent-(.+)\.checkpoint\.json$/);
342
+ if (!m) return 0;
343
+ const subId = m[1];
344
+ if (!SUB_ID_PATTERN.test(subId)) return 0;
345
+
346
+ const src = join(sourceDir, name);
347
+ const dst = join(destDir, name);
348
+ try {
349
+ const srcStat = await stat(src);
350
+ if (!srcStat.isFile()) return 0;
351
+ } catch {
352
+ return 0;
353
+ }
354
+ try {
355
+ await copyFile(src, dst);
356
+ return 1;
357
+ } catch {
358
+ return 0;
359
+ }
360
+ });
361
+ const counts = await Promise.all(tasks);
362
+ const drained = counts.reduce((a, b) => a + b, 0);
363
+
364
+ return { ok: true, drained };
365
+ }
366
+
367
+ // ---------------------------------------------------------------------------
368
+ // v1.5.0 T19 (G1) — subagent dispatch + event-stream reader
369
+ //
370
+ // Thin SDK-facing helpers the orchestrator uses to (1) dispatch a subagent
371
+ // via the deterministic `subagent.dispatch` verb and (2) poll its per-
372
+ // subagent event log to see verbs arrive live (every verb the subagent
373
+ // dispatches fires `_emitEvent` per T5 / state-events.js).
374
+ // ---------------------------------------------------------------------------
375
+
376
+ /**
377
+ * Dispatch a subagent via the `subagent.dispatch` state-SDK verb. Wraps the
378
+ * raw `query()` call with the documented orchestrator-side signature
379
+ * (waveId, subId, role, brief, projectRoot, opts) and returns the verb's
380
+ * `{ ok, dispatchBrief, subagentId, waveId, mode, isolation, inheritedEnv,
381
+ * eventLogPath }` shape verbatim.
382
+ *
383
+ * The dispatch brief is DETERMINISTIC on Claude (handed to the native
384
+ * subagent primitive) and a best-effort PROMPT TEMPLATE elsewhere — the
385
+ * `mode` field discriminates. The SDK env-var contract is baked into the
386
+ * brief and returned via `inheritedEnv` for the caller to seed the
387
+ * subagent process when the platform supports env passthrough.
388
+ *
389
+ * @param {string} waveId wave id (matches /^[A-Za-z0-9_-]{1,64}$/)
390
+ * @param {string} subId subagent id (matches /^[A-Za-z0-9_-]{1,64}$/)
391
+ * @param {string} role role label (e.g. 'implementer', 'reviewer')
392
+ * @param {string} brief the task brief markdown
393
+ * @param {string} projectRoot absolute path to the project root
394
+ * @param {{isolation?: 'shared'|'worktree', env?: object, platform?: string,
395
+ * subagentId?: string}} [opts]
396
+ * @returns {Promise<object>} the verb result (see contract §7
397
+ * `subagent.dispatch`).
398
+ */
399
+ export async function dispatchSubagent(waveId, subId, role, brief, projectRoot, opts = {}) {
400
+ validateWaveId(waveId);
401
+ validateSubId(subId);
402
+ if (typeof brief !== 'string' || brief.length === 0) {
403
+ throw new Error('subagent-telemetry: dispatchSubagent requires a non-empty brief');
404
+ }
405
+ if (typeof projectRoot !== 'string' || projectRoot.length === 0) {
406
+ throw new Error('subagent-telemetry: dispatchSubagent requires projectRoot');
407
+ }
408
+ const payload = {
409
+ subagentId: subId,
410
+ waveId,
411
+ brief,
412
+ isolation: opts.isolation === 'shared' ? 'shared' : 'worktree',
413
+ };
414
+ if (typeof role === 'string' && role.length > 0) payload.role = role;
415
+ if (opts.env && typeof opts.env === 'object' && !Array.isArray(opts.env)) {
416
+ payload.env = opts.env;
417
+ }
418
+ const ctx = { projectRoot };
419
+ if (typeof opts.platform === 'string') ctx.platform = opts.platform;
420
+ if (typeof opts.subagentId === 'string') ctx.subagentId = opts.subagentId;
421
+ return query('subagent.dispatch', payload, ctx);
422
+ }
423
+
424
+ /**
425
+ * Stream the per-subagent event log. Returns the events with `seq > since`
426
+ * (the cursor), plus the highest `seq` seen so the caller can feed it back
427
+ * on the next poll. NEVER uses `fs.watch` — explicit-interval polling per
428
+ * contract §5 (works across all 13 platforms).
429
+ *
430
+ * Every verb the subagent calls fires `_emitEvent` after lock release; the
431
+ * tap envelope carries `{ seq, verb, subagentId, ts, verbId, outcome,
432
+ * payloadDigest }` — exactly the rows this poller surfaces.
433
+ *
434
+ * @param {string} waveId
435
+ * @param {string} subId
436
+ * @param {string} projectRoot
437
+ * @param {number} [since] highest seq already processed (default 0)
438
+ * @returns {{events: object[], cursor: number}}
439
+ */
440
+ export function streamSubagentEvents(waveId, subId, projectRoot, since = 0) {
441
+ validateWaveId(waveId);
442
+ validateSubId(subId);
443
+ if (typeof projectRoot !== 'string' || projectRoot.length === 0) {
444
+ throw new Error('subagent-telemetry: streamSubagentEvents requires projectRoot');
445
+ }
446
+ return pollEvents({
447
+ projectRoot,
448
+ waveId,
449
+ subagentId: subId,
450
+ since: Number.isFinite(since) ? since : 0,
451
+ });
452
+ }