@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,242 @@
1
+ /**
2
+ * IJFW design iframe bridge -- optional vercel:vercel-sandbox composition.
3
+ *
4
+ * IJFW core has zero runtime deps and ships a static viewer for design mockups.
5
+ * When the peer `vercel:vercel-sandbox` skill is present (or the user has set
6
+ * `IJFW_VERCEL_SANDBOX_URL` to a provisioner endpoint), this bridge upgrades
7
+ * the static viewer to live iframes running each mockup in an isolated
8
+ * Firecracker microVM via the vercel-sandbox skill.
9
+ *
10
+ * **Every entrypoint graceful-fails.** A missing CLI, an unset env var, a
11
+ * malformed response, or a network error all return null/false rather than
12
+ * throwing. The caller MUST fall back to the static-srcdoc viewer in that case.
13
+ *
14
+ * Why composition over a hard dep: IJFW is a meta-tool. Pinning vercel-sandbox
15
+ * would import sandboxing concerns into IJFW's trust model. Peer-skill detection
16
+ * keeps the boundary clean (and keeps the npm install size at zero).
17
+ */
18
+
19
+ import { spawnSync } from 'node:child_process';
20
+ import { mkdirSync, writeFileSync } from 'node:fs';
21
+ import { tmpdir } from 'node:os';
22
+ import { join } from 'node:path';
23
+ import { randomUUID } from 'node:crypto';
24
+ import http from 'node:http';
25
+ import https from 'node:https';
26
+
27
+ const SANDBOX_URL_ENV = 'IJFW_VERCEL_SANDBOX_URL';
28
+ const PROVISION_TIMEOUT_MS = 15_000;
29
+ const DESTROY_TIMEOUT_MS = 5_000;
30
+
31
+ /** In-process registry of sandbox ids → provisioner URL for destroySandbox(). */
32
+ const _sandboxRegistry = new Map();
33
+
34
+ /**
35
+ * Returns true when EITHER the `vercel` CLI is on PATH OR
36
+ * `IJFW_VERCEL_SANDBOX_URL` env var is set.
37
+ *
38
+ * Cheap. Safe to call repeatedly (a few ms `which` shell-out worst case).
39
+ */
40
+ export function hasVercelSandbox() {
41
+ if (process.env[SANDBOX_URL_ENV]) return true;
42
+ try {
43
+ const which = process.platform === 'win32' ? 'where' : 'which';
44
+ const r = spawnSync(which, ['vercel'], { encoding: 'utf8', timeout: 2_000 });
45
+ if (r.status === 0 && r.stdout && r.stdout.trim()) return true;
46
+ } catch {
47
+ // graceful: missing `which`/`where` is the same as no CLI
48
+ }
49
+ return false;
50
+ }
51
+
52
+ /**
53
+ * Provision a sandbox preview for an HTML mockup. Returns
54
+ * { iframeUrl, sandboxId } on success
55
+ * null when bridge unavailable OR any failure
56
+ *
57
+ * The function is never expected to throw. All errors are logged advisory
58
+ * to stderr so the user understands why fallback kicked in, then null is
59
+ * returned and the caller renders the static-srcdoc viewer.
60
+ *
61
+ * @param {{ html: string, name?: string }} args
62
+ * @returns {Promise<{iframeUrl: string, sandboxId: string} | null>}
63
+ */
64
+ export async function createPreviewSandbox({ html, name } = {}) {
65
+ if (typeof html !== 'string' || !html.trim()) {
66
+ _advise('createPreviewSandbox: html missing -- skipping');
67
+ return null;
68
+ }
69
+ if (!hasVercelSandbox()) return null;
70
+
71
+ const safeName = String(name || 'mockup').replace(/[^a-zA-Z0-9_-]/g, '-').slice(0, 64) || 'mockup';
72
+ const sandboxId = `ijfw-${safeName}-${randomUUID().slice(0, 8)}`;
73
+
74
+ // Write the html to a temp file so the provisioner can read it.
75
+ let tmpFile = null;
76
+ try {
77
+ const dir = join(tmpdir(), 'ijfw-design-sandboxes');
78
+ mkdirSync(dir, { recursive: true });
79
+ tmpFile = join(dir, `${sandboxId}.html`);
80
+ writeFileSync(tmpFile, html, 'utf8');
81
+ } catch (err) {
82
+ _advise(`createPreviewSandbox: temp write failed -- ${err.message}`);
83
+ return null;
84
+ }
85
+
86
+ // Prefer the env-configured HTTP provisioner when present (test-friendly,
87
+ // matches how the vercel-sandbox MCP skill exposes its provisioning API).
88
+ const url = process.env[SANDBOX_URL_ENV];
89
+ if (url) {
90
+ const result = await _provisionViaHttp(url, { html, name: safeName, sandboxId });
91
+ if (result) {
92
+ _sandboxRegistry.set(sandboxId, { mode: 'http', url });
93
+ return result;
94
+ }
95
+ return null;
96
+ }
97
+
98
+ // Fall back to shell-out to `vercel sandbox` CLI. Best-effort: the CLI
99
+ // surface for vercel-sandbox is evolving; we accept any JSON line that
100
+ // contains a `url` field.
101
+ try {
102
+ const r = spawnSync('vercel', ['sandbox', 'create', '--file', tmpFile, '--name', sandboxId], {
103
+ encoding: 'utf8',
104
+ timeout: PROVISION_TIMEOUT_MS,
105
+ });
106
+ if (r.status !== 0) {
107
+ _advise(`createPreviewSandbox: vercel CLI exit ${r.status} -- falling back to static`);
108
+ return null;
109
+ }
110
+ const iframeUrl = _extractUrl(r.stdout);
111
+ if (!iframeUrl) {
112
+ _advise('createPreviewSandbox: vercel CLI produced no URL -- falling back to static');
113
+ return null;
114
+ }
115
+ _sandboxRegistry.set(sandboxId, { mode: 'cli' });
116
+ return { iframeUrl, sandboxId };
117
+ } catch (err) {
118
+ _advise(`createPreviewSandbox: CLI invocation failed -- ${err.message}`);
119
+ return null;
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Tear down a sandbox by id. Never throws. Best-effort.
125
+ */
126
+ export async function destroySandbox(sandboxId) {
127
+ if (!sandboxId) return;
128
+ const entry = _sandboxRegistry.get(sandboxId);
129
+ if (!entry) return;
130
+ _sandboxRegistry.delete(sandboxId);
131
+
132
+ try {
133
+ if (entry.mode === 'http') {
134
+ await _httpRequest(
135
+ 'DELETE',
136
+ `${entry.url.replace(/\/$/, '')}/sandboxes/${encodeURIComponent(sandboxId)}`,
137
+ null,
138
+ DESTROY_TIMEOUT_MS,
139
+ );
140
+ return;
141
+ }
142
+ if (entry.mode === 'cli') {
143
+ spawnSync('vercel', ['sandbox', 'delete', sandboxId], { encoding: 'utf8', timeout: DESTROY_TIMEOUT_MS });
144
+ return;
145
+ }
146
+ } catch (err) {
147
+ _advise(`destroySandbox(${sandboxId}): ${err.message}`);
148
+ }
149
+ }
150
+
151
+ // ---------- internals ----------
152
+
153
+ async function _provisionViaHttp(baseUrl, { html, name, sandboxId }) {
154
+ try {
155
+ const payload = JSON.stringify({ html, name, sandboxId });
156
+ const res = await _httpRequest(
157
+ 'POST',
158
+ `${baseUrl.replace(/\/$/, '')}/sandboxes`,
159
+ payload,
160
+ PROVISION_TIMEOUT_MS,
161
+ );
162
+ if (!res || res.status < 200 || res.status >= 300) {
163
+ _advise(`HTTP provisioner returned ${res ? res.status : 'no response'}`);
164
+ return null;
165
+ }
166
+ let body;
167
+ try { body = JSON.parse(res.body); } catch {
168
+ _advise('HTTP provisioner returned non-JSON');
169
+ return null;
170
+ }
171
+ const iframeUrl = body && (body.iframeUrl || body.url);
172
+ if (!iframeUrl) {
173
+ _advise('HTTP provisioner response missing url field');
174
+ return null;
175
+ }
176
+ return { iframeUrl: String(iframeUrl), sandboxId: String(body.sandboxId || sandboxId) };
177
+ } catch (err) {
178
+ _advise(`HTTP provisioner failed -- ${err.message}`);
179
+ return null;
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Minimal http(s) client using node:http / node:https. Avoids `fetch`
185
+ * because we want deterministic timeouts and zero-dep behavior on every
186
+ * supported Node version.
187
+ */
188
+ function _httpRequest(method, url, body, timeoutMs) {
189
+ return new Promise((resolve) => {
190
+ try {
191
+ const parsed = new URL(url);
192
+ const mod = parsed.protocol === 'https:' ? https : http;
193
+ const opts = {
194
+ method,
195
+ protocol: parsed.protocol,
196
+ hostname: parsed.hostname,
197
+ port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
198
+ path: parsed.pathname + parsed.search,
199
+ headers: body
200
+ ? { 'content-type': 'application/json', 'content-length': Buffer.byteLength(body) }
201
+ : {},
202
+ timeout: timeoutMs,
203
+ };
204
+ const req = mod.request(opts, (res) => {
205
+ const chunks = [];
206
+ res.on('data', (c) => chunks.push(c));
207
+ res.on('end', () => resolve({ status: res.statusCode, body: Buffer.concat(chunks).toString('utf8') }));
208
+ res.on('error', () => resolve(null));
209
+ });
210
+ req.on('error', () => resolve(null));
211
+ req.on('timeout', () => {
212
+ try { req.destroy(); } catch {}
213
+ resolve(null);
214
+ });
215
+ if (body) req.write(body);
216
+ req.end();
217
+ } catch {
218
+ resolve(null);
219
+ }
220
+ });
221
+ }
222
+
223
+ function _extractUrl(text) {
224
+ if (!text) return null;
225
+ // JSON-encoded url field
226
+ const jsonMatch = String(text).match(/"(?:iframeUrl|url)"\s*:\s*"(https?:\/\/[^"\s]+)"/);
227
+ if (jsonMatch) return jsonMatch[1];
228
+ // Bare URL printed by the CLI
229
+ const bare = String(text).match(/(https?:\/\/[^\s"]+\.vercel\.app[^\s"]*)/);
230
+ return bare ? bare[1] : null;
231
+ }
232
+
233
+ function _advise(msg) {
234
+ try {
235
+ process.stderr.write(`[ijfw design] ${msg}\n`);
236
+ } catch {
237
+ // never throw from advisory log
238
+ }
239
+ }
240
+
241
+ // Exported for tests
242
+ export const __internals = { _extractUrl, _sandboxRegistry, SANDBOX_URL_ENV };
@@ -6,6 +6,7 @@
6
6
  import { EventEmitter } from 'node:events';
7
7
  import { existsSync, readdirSync, statSync, watch } from 'node:fs';
8
8
  import { join } from 'node:path';
9
+ import { createPreviewSandbox as defaultCreatePreviewSandbox } from './design/iframe-bridge.js';
9
10
 
10
11
  export const PLACEHOLDER_HTML = `<!DOCTYPE html>
11
12
  <html lang="en">
@@ -52,6 +53,149 @@ export function getNewestFile(contentDir) {
52
53
  return newest;
53
54
  }
54
55
 
56
+ /**
57
+ * HTML-escape helper for the viewer codegen. Mirrors the audit-H3.1
58
+ * dashboard `esc()` -- escapes ampersand, angle brackets, double-quote, AND
59
+ * single-quote so output is safe inside single- or double-quoted attributes.
60
+ */
61
+ export function escHtml(s) {
62
+ if (s === null || s === undefined) return '';
63
+ return String(s)
64
+ .replace(/&/g, '&amp;')
65
+ .replace(/</g, '&lt;')
66
+ .replace(/>/g, '&gt;')
67
+ .replace(/"/g, '&quot;')
68
+ .replace(/'/g, '&#39;');
69
+ }
70
+
71
+ /**
72
+ * Build a tabbed viewer for a list of HTML mockups.
73
+ *
74
+ * Each mockup may carry an optional `iframeUrl` (provisioned via the
75
+ * `vercel:vercel-sandbox` peer skill). When the URL is present the viewer
76
+ * renders a live `<iframe src="...">` running in an isolated Firecracker
77
+ * microVM. When absent, the viewer falls back to a static `<iframe srcdoc>`
78
+ * with the html inlined. Either way the iframe carries
79
+ * `sandbox="allow-scripts"` to prevent top-window escape (v1.5.0 Trident r19
80
+ * dropped allow-same-origin; the combination is a documented MDN sandbox
81
+ * escape — JS still runs in the mockup but the embedded document can't reach
82
+ * window.parent to remove its own sandbox attribute).
83
+ *
84
+ * All user-controlled strings (mockup name, iframe url) flow through
85
+ * `escHtml()` -- the same pattern dashboard `esc()` uses post-audit-H3.1.
86
+ *
87
+ * @param {{ mockups: Array<{name: string, html?: string, iframeUrl?: string|null}>, title?: string }} args
88
+ * @returns {string} HTML document for the viewer.
89
+ */
90
+ export function buildMockupViewer({ mockups = [], title = 'IJFW Design Mockups' } = {}) {
91
+ const items = Array.isArray(mockups) ? mockups : [];
92
+ const safeTitle = escHtml(title);
93
+
94
+ const tabs = items
95
+ .map((m, i) => {
96
+ const name = escHtml(m && m.name ? m.name : `mockup-${i + 1}`);
97
+ return `<button class="tab" data-i="${i}" ${i === 0 ? 'aria-selected="true"' : ''}>${name}</button>`;
98
+ })
99
+ .join('');
100
+
101
+ const panes = items
102
+ .map((m, i) => {
103
+ const name = escHtml(m && m.name ? m.name : `mockup-${i + 1}`);
104
+ const isLive = m && typeof m.iframeUrl === 'string' && m.iframeUrl;
105
+ // v1.5.0 Trident r19 fix: drop allow-same-origin. With both allow-scripts
106
+ // AND allow-same-origin set, the embedded document can programmatically
107
+ // remove the sandbox attribute via window.parent.document (MDN sandbox
108
+ // escape). allow-scripts alone keeps the mockup dynamic while preventing
109
+ // any cross-origin reach into the host viewer.
110
+ const inner = isLive
111
+ ? `<iframe class="preview" src="${escHtml(m.iframeUrl)}" title="${name}" sandbox="allow-scripts" loading="lazy"></iframe>`
112
+ : `<iframe class="preview" srcdoc="${escHtml(m && m.html ? m.html : '<!doctype html><meta charset=utf-8><p>(no preview)</p>')}" title="${name}" sandbox="allow-scripts" loading="lazy"></iframe>`;
113
+ const badge = isLive
114
+ ? '<span class="badge live" title="Provisioned via vercel:vercel-sandbox">LIVE</span>'
115
+ : '<span class="badge static" title="Static srcdoc preview -- install vercel CLI or set IJFW_VERCEL_SANDBOX_URL for live sandbox">STATIC</span>';
116
+ return `<section class="pane" data-i="${i}" ${i === 0 ? '' : 'hidden'}>
117
+ <header class="phead">${name} ${badge}</header>
118
+ ${inner}
119
+ </section>`;
120
+ })
121
+ .join('\n');
122
+
123
+ return `<!DOCTYPE html>
124
+ <html lang="en">
125
+ <head>
126
+ <meta charset="UTF-8">
127
+ <meta name="viewport" content="width=device-width,initial-scale=1">
128
+ <title>${safeTitle}</title>
129
+ <style>
130
+ *{box-sizing:border-box;margin:0;padding:0}
131
+ body{background:#0f172a;color:#e2e8f0;font-family:system-ui,-apple-system,sans-serif;min-height:100vh;display:flex;flex-direction:column}
132
+ .tabs{display:flex;flex-wrap:wrap;gap:4px;padding:8px;background:#1e293b;border-bottom:1px solid #334155}
133
+ .tab{background:#334155;color:#e2e8f0;border:1px solid #475569;border-radius:6px;padding:6px 12px;cursor:pointer;font-size:13px}
134
+ .tab[aria-selected="true"]{background:#0369a1;border-color:#0ea5e9}
135
+ .pane{flex:1;display:flex;flex-direction:column;min-height:0}
136
+ .phead{padding:6px 12px;background:#0f172a;border-bottom:1px solid #1e293b;font-size:12px;color:#94a3b8;display:flex;gap:8px;align-items:center}
137
+ .badge{font-size:10px;font-weight:600;padding:2px 6px;border-radius:4px;letter-spacing:.04em}
138
+ .badge.live{background:#15803d;color:#fff}
139
+ .badge.static{background:#475569;color:#cbd5e1}
140
+ .preview{flex:1;width:100%;border:0;background:#fff;min-height:400px}
141
+ </style>
142
+ </head>
143
+ <body>
144
+ <nav class="tabs" role="tablist">${tabs || '<span style="color:#64748b;padding:6px 12px">No mockups yet.</span>'}</nav>
145
+ ${panes}
146
+ <script>
147
+ (function(){
148
+ var tabs = document.querySelectorAll('.tab');
149
+ var panes = document.querySelectorAll('.pane');
150
+ tabs.forEach(function(t){
151
+ t.addEventListener('click', function(){
152
+ var i = t.getAttribute('data-i');
153
+ tabs.forEach(function(x){ x.setAttribute('aria-selected', x === t ? 'true' : 'false'); });
154
+ panes.forEach(function(p){
155
+ if (p.getAttribute('data-i') === i) p.removeAttribute('hidden'); else p.setAttribute('hidden','');
156
+ });
157
+ });
158
+ });
159
+ })();
160
+ </script>
161
+ </body>
162
+ </html>`;
163
+ }
164
+
165
+ /**
166
+ * Provision per-mockup iframes via the vercel-sandbox bridge when available,
167
+ * then render the tabbed viewer. Falls back to static `<iframe srcdoc>` for
168
+ * any mockup whose provisioning failed (or for all of them if the bridge
169
+ * is unavailable).
170
+ *
171
+ * @param {object} args
172
+ * @param {Array<{name: string, html: string}>} args.mockups Mockup inputs.
173
+ * @param {Function} [args.createSandbox] Override for the bridge (test seam).
174
+ * Should match the createPreviewSandbox signature.
175
+ * @param {string} [args.title]
176
+ * @returns {Promise<{html: string, sandboxIds: string[]}>}
177
+ */
178
+ export async function renderMockupViewerWithBridge({ mockups = [], createSandbox, title } = {}) {
179
+ const fn = typeof createSandbox === 'function' ? createSandbox : defaultCreatePreviewSandbox;
180
+ const enriched = [];
181
+ const sandboxIds = [];
182
+ for (const m of mockups) {
183
+ let iframeUrl = null;
184
+ try {
185
+ const r = await fn({ html: m.html, name: m.name });
186
+ if (r && r.iframeUrl) {
187
+ iframeUrl = r.iframeUrl;
188
+ if (r.sandboxId) sandboxIds.push(r.sandboxId);
189
+ }
190
+ } catch {
191
+ // bridge promised never to throw; this catch is defense-in-depth.
192
+ iframeUrl = null;
193
+ }
194
+ enriched.push({ name: m.name, html: m.html, iframeUrl });
195
+ }
196
+ return { html: buildMockupViewer({ mockups: enriched, title }), sandboxIds };
197
+ }
198
+
55
199
  /**
56
200
  * Watches contentDir for new/changed .html files.
57
201
  * Returns an EventEmitter that emits 'new-content' (with the file path) on change.
@@ -0,0 +1,97 @@
1
+ /**
2
+ * dispatch/checkpoint-cli.js — IJFW v1.5.0 / S1 subagent checkpoint CLI.
3
+ *
4
+ * Frozen export contract (v1.4.3 dispatch module convention):
5
+ * export const handlers = { '<subcommand>': async (args, ctx) => ({ ok, output?, error? }) };
6
+ * export const subcommandHelp = { '<subcommand>': 'one-line description' };
7
+ *
8
+ * Subcommands owned by this module:
9
+ * - checkpoint <waveId> <subId> <jsonPayload>
10
+ *
11
+ * Writes via mcp-server/src/orchestrator/subagent-telemetry.js (W11-A0).
12
+ * Used by implementer subagents to persist progress before the Claude Code
13
+ * harness ~20-tool / 60s wall-clock cap fires (v1.5.0 S1 — closes 8/13
14
+ * truncation pattern from v1.4.4 Wave 10 + v1.5.0 research).
15
+ *
16
+ * Wire-up into extension.js is handled by orchestrator post-Wave-11-A.
17
+ */
18
+
19
+ import { recordCheckpoint } from '../orchestrator/subagent-telemetry.js';
20
+
21
+ function tokenize(args) {
22
+ if (Array.isArray(args)) return args.filter((x) => x !== undefined && x !== null);
23
+ if (typeof args !== 'string') return [];
24
+ // Checkpoint args are: <waveId> <subId> <jsonPayload>.
25
+ // The JSON payload may contain spaces — split into at most 3 tokens so the
26
+ // payload survives intact regardless of internal whitespace.
27
+ const trimmed = args.trim();
28
+ if (!trimmed) return [];
29
+ const firstSpace = trimmed.indexOf(' ');
30
+ if (firstSpace === -1) return [trimmed];
31
+ const secondSpace = trimmed.indexOf(' ', firstSpace + 1);
32
+ if (secondSpace === -1) return [trimmed.slice(0, firstSpace), trimmed.slice(firstSpace + 1)];
33
+ return [
34
+ trimmed.slice(0, firstSpace),
35
+ trimmed.slice(firstSpace + 1, secondSpace),
36
+ trimmed.slice(secondSpace + 1),
37
+ ];
38
+ }
39
+
40
+ async function handleCheckpoint(args, ctx) {
41
+ const tokens = tokenize(args);
42
+ const [waveId, subId, payloadJson] = tokens;
43
+ if (!waveId || !subId || !payloadJson) {
44
+ return {
45
+ ok: false,
46
+ error: 'Usage: ijfw checkpoint <waveId> <subId> <jsonPayload>',
47
+ };
48
+ }
49
+
50
+ let payload;
51
+ try {
52
+ payload = JSON.parse(payloadJson);
53
+ } catch (err) {
54
+ return {
55
+ ok: false,
56
+ error: `ijfw checkpoint: invalid JSON payload — ${err.message}`,
57
+ };
58
+ }
59
+ if (payload === null || typeof payload !== 'object' || Array.isArray(payload)) {
60
+ return {
61
+ ok: false,
62
+ error: 'ijfw checkpoint: JSON payload must be a JSON object (not array/null/scalar)',
63
+ };
64
+ }
65
+
66
+ const projectRoot = (ctx && ctx.projectRoot) || process.cwd();
67
+
68
+ // v1.5.0-major S01: log the effective root (parent vs worktree) to stderr for
69
+ // debugging worktree-mode checkpoint visibility issues.
70
+ const effectiveRoot = process.env.IJFW_PARENT_PROJECT_ROOT ?? projectRoot;
71
+ try {
72
+ process.stderr.write(
73
+ `ijfw checkpoint: writing to ${effectiveRoot}/.ijfw/wave-${waveId}/\n`,
74
+ );
75
+ } catch {
76
+ // stderr write failure must never break the checkpoint path
77
+ }
78
+
79
+ try {
80
+ await recordCheckpoint(waveId, subId, payload, projectRoot);
81
+ return { ok: true, output: `ok: wrote checkpoint for ${waveId}/${subId}` };
82
+ } catch (err) {
83
+ return {
84
+ ok: false,
85
+ error: `ijfw checkpoint: ${err && err.message ? err.message : String(err)}`,
86
+ };
87
+ }
88
+ }
89
+
90
+ export const handlers = Object.freeze({
91
+ checkpoint: handleCheckpoint,
92
+ });
93
+
94
+ export const subcommandHelp = Object.freeze({
95
+ checkpoint:
96
+ 'checkpoint <waveId> <subId> <jsonPayload> — record a subagent checkpoint (v1.5.0 S1)',
97
+ });
@@ -47,6 +47,11 @@ import { dirname, join } from 'path';
47
47
  // so the user-facing spelling 'domain-manifest:<op>' (matching the error
48
48
  // message and CLI surface) parses + routes uniformly. The set entry is the
49
49
  // hyphenated form so copy-paste from the error string Just Works.
50
+ // v1.5.0 (T12): added 'state' — the state-SDK CLI face. Routes
51
+ // `state:<verb> <json-payload>` straight into orchestrator/state-sdk.js
52
+ // `query(verb, payload, ctx)`. This is the external-tooling surface for the
53
+ // state-SDK; the JS module is the in-process surface and the MCP tool is the
54
+ // remote surface (contract §0).
50
55
  const RUN_NAMESPACES = new Set([
51
56
  'compute',
52
57
  'index',
@@ -55,6 +60,7 @@ const RUN_NAMESPACES = new Set([
55
60
  'override',
56
61
  'extension',
57
62
  'domain-manifest',
63
+ 'state',
58
64
  ]);
59
65
  const SEARCH_NAMESPACES = new Set(['compute', 'graph']);
60
66
 
@@ -166,13 +172,87 @@ export async function dispatchRun(parsed, ctx = {}) {
166
172
  const m = await import('./domain-manifest.js');
167
173
  return m.domainManifestDispatch({ command: parsed.command, args: parsed.args, projectRoot });
168
174
  }
175
+ if (parsed.namespace === 'state') {
176
+ return dispatchState(parsed, { projectRoot, sessionId });
177
+ }
169
178
 
170
179
  return {
171
180
  ok: false,
172
- error: 'Unknown ijfw_run sub-command. Supported: compute:python, compute:js, index:<source>, detect:project_type, graph:traverse, override:<op>, extension:<op>, domain-manifest:<op>',
181
+ error: 'Unknown ijfw_run sub-command. Supported: compute:python, compute:js, index:<source>, detect:project_type, graph:traverse, override:<op>, extension:<op>, domain-manifest:<op>, state:<verb>',
173
182
  };
174
183
  }
175
184
 
185
+ // --- state:<verb> dispatch -------------------------------------------------
186
+ //
187
+ // T12: external tooling reaches the state-SDK via the CLI colon-namespace.
188
+ // `state:<verb> [json-payload]` parses the payload as JSON, then calls
189
+ // orchestrator/state-sdk.js `query(verb, payload, ctx)`. The verb name is the
190
+ // part after `state:` (e.g. `workflow.get`, `wave.advance`). The payload
191
+ // defaults to `{}` when no args are supplied — read-only verbs like
192
+ // `workflow.get` work with an empty payload.
193
+ //
194
+ // Errors come back as `{ ok: false, error, code }` so callers (cli-run.js,
195
+ // shell hooks) can JSON.parse the stdout uniformly. Unknown verbs throw
196
+ // inside `query()` and the throw is caught + surfaced with `code: 'UNKNOWN_VERB'`.
197
+ async function dispatchState(parsed, { projectRoot, sessionId }) {
198
+ const verb = parsed.command;
199
+ if (!verb) {
200
+ return { ok: false, error: 'state:<verb> requires a verb after the colon.', code: 'NO_VERB' };
201
+ }
202
+
203
+ // Parse the JSON payload. Empty args -> `{}`. The colon-syntax parser
204
+ // already strips a single matching outer quote pair so callers can use
205
+ // either `state:foo '{"k":"v"}'` (shell-style) or `state:foo {"k":"v"}`
206
+ // (already-stripped) and reach the same handler.
207
+ const raw = String(parsed.args || '').trim();
208
+ let payload = {};
209
+ if (raw.length > 0) {
210
+ try {
211
+ payload = JSON.parse(raw);
212
+ } catch (err) {
213
+ return {
214
+ ok: false,
215
+ error: `state:${verb} payload is not valid JSON: ${err && err.message ? err.message : String(err)}`,
216
+ code: 'INVALID_JSON',
217
+ };
218
+ }
219
+ if (payload === null || typeof payload !== 'object' || Array.isArray(payload)) {
220
+ return {
221
+ ok: false,
222
+ error: `state:${verb} payload must be a JSON object, got ${Array.isArray(payload) ? 'array' : typeof payload}.`,
223
+ code: 'INVALID_JSON',
224
+ };
225
+ }
226
+ }
227
+
228
+ // Lazy-import the SDK so this module stays cheap for the non-state
229
+ // dispatch paths (e.g. compute:python) and so a state-sdk init error
230
+ // surfaces as a structured ok:false instead of a top-level module crash.
231
+ let query;
232
+ try {
233
+ ({ query } = await import('../orchestrator/state-sdk.js'));
234
+ } catch (err) {
235
+ return {
236
+ ok: false,
237
+ error: `state:${verb} could not load state-sdk: ${err && err.message ? err.message : String(err)}`,
238
+ code: 'SDK_LOAD',
239
+ };
240
+ }
241
+
242
+ try {
243
+ const result = await query(verb, payload, { projectRoot, sessionId });
244
+ return result;
245
+ } catch (err) {
246
+ const msg = err && err.message ? err.message : String(err);
247
+ const isUnknownVerb = /unknown verb/i.test(msg);
248
+ return {
249
+ ok: false,
250
+ error: `state:${verb} did not complete: ${msg}`,
251
+ code: isUnknownVerb ? 'UNKNOWN_VERB' : (err && err.code) || 'ERROR',
252
+ };
253
+ }
254
+ }
255
+
176
256
  async function dispatchCompute(parsed, { projectRoot, sessionId /*, provenance unused for compute runs */ }) {
177
257
  const cmd = parsed.command;
178
258
  if (cmd !== 'js' && cmd !== 'python') {
@@ -17,6 +17,26 @@
17
17
  * current project's platform dirs. Fired by the
18
18
  * session-start hook so org/user-scoped extensions
19
19
  * become available in every project session.
20
+ *
21
+ * TODO(v1.5.0-major S01 — IJFW_PARENT_PROJECT_ROOT env passthrough):
22
+ * The Agent({ isolation: 'worktree' }) spawn path lives in the Claude Code
23
+ * harness (Task tool / SDK), NOT in this MCP server's dispatch flow. When the
24
+ * harness eventually exposes a hook for env passthrough on worktree dispatch,
25
+ * the dispatcher MUST set:
26
+ *
27
+ * IJFW_PARENT_PROJECT_ROOT=<absolute path to the orchestrator's projectRoot>
28
+ *
29
+ * so that the subagent's `ijfw checkpoint` writes land in the PARENT project's
30
+ * .ijfw/wave-<id>/ instead of the disposable worktree's .ijfw/. Until that
31
+ * harness hook exists, the contract is documented in
32
+ * `mcp-server/src/orchestrator/checkpoint-contract.md` ("Worktree isolation
33
+ * drain protocol") and the orchestrator MUST run `ijfw worktree-drain
34
+ * <waveId> <worktreePath>` BEFORE `git worktree remove` as a belt-and-
35
+ * suspenders fallback (see dispatch/wave-cli.js worktree-drain handler).
36
+ *
37
+ * Subagent-side: any agent template that may run in worktree isolation should
38
+ * read `process.env.IJFW_PARENT_PROJECT_ROOT` (already honored transparently
39
+ * by orchestrator/subagent-telemetry.js record/read/list).
20
40
  */
21
41
 
22
42
  import {
@@ -613,11 +633,14 @@ async function cmdDeactivate() {
613
633
  let _v143Handlers = null;
614
634
  async function loadV143Handlers() {
615
635
  if (_v143Handlers !== null) return _v143Handlers;
616
- const [registry, signer, quota, active] = await Promise.all([
636
+ const [registry, signer, quota, active, wave, checkpoint, worktree] = await Promise.all([
617
637
  import('./registry-cli.js'),
618
638
  import('./signer-cli.js'),
619
639
  import('./quota-cli.js'),
620
640
  import('./active-cli.js'),
641
+ import('./wave-cli.js'), // v1.4.4 N9 — wave-status / wave-list
642
+ import('./checkpoint-cli.js'), // v1.5.0 W11-A1 — ijfw checkpoint (S1)
643
+ import('./worktree-cli.js'), // v1.5.0 W11-A2 — ijfw worktree provision (S2)
621
644
  ]);
622
645
  _v143Handlers = Object.assign(
623
646
  Object.create(null),
@@ -625,6 +648,9 @@ async function loadV143Handlers() {
625
648
  signer.handlers || {},
626
649
  quota.handlers || {},
627
650
  active.handlers || {},
651
+ wave.handlers || {},
652
+ checkpoint.handlers || {},
653
+ worktree.handlers || {},
628
654
  );
629
655
  return _v143Handlers;
630
656
  }
@@ -28,6 +28,8 @@ import { readFile, writeFile, mkdir, stat } from 'node:fs/promises';
28
28
  import { join } from 'node:path';
29
29
  import { homedir as osHomedir } from 'node:os';
30
30
  import { createPublicKey } from 'node:crypto';
31
+ // v1.5.0 audit-LOW-update-#13: shared tmp-suffix helper.
32
+ import { tmpSuffix } from '../lib/tmp-suffix.js';
31
33
 
32
34
  import {
33
35
  loadRegistrySources,
@@ -70,7 +72,8 @@ async function readRegistriesFile(ctx) {
70
72
  async function writeRegistriesFile(ctx, doc) {
71
73
  const path = registriesConfigPath(ctx);
72
74
  await mkdir(join(homedir(ctx), '.ijfw'), { recursive: true });
73
- const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
75
+ // v1.5.0 audit-LOW-update-#13: consolidate tmp-suffix shape via helper.
76
+ const tmp = `${path}.tmp.${tmpSuffix()}`;
74
77
  await writeFile(tmp, JSON.stringify(doc, null, 2) + '\n', 'utf8');
75
78
  const { rename } = await import('node:fs/promises');
76
79
  await rename(tmp, path);