@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.
- package/fixtures/truncation-corpus/_generate-corpus.js +367 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/snapshots/v-midO-1-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/snapshots/v-midO-2-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/snapshots/v-midO-3-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/snapshots/v-midO-4-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/snapshots/v-midO-5-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/snapshots/v-noEv-1-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/snapshots/v-noEv-2-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/snapshots/v-noEv-3-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/snapshots/v-noEv-4-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/snapshots/v-noEv-5-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/snapshots/v-errT-1-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/snapshots/v-errT-3-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/snapshots/v-errT-5-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/target/.ijfw/state/workflow.json +1 -0
- package/package.json +1 -1
- package/src/active-extension-writer.js +144 -64
- package/src/api-client.js +43 -5
- package/src/audit-roster.js +80 -5
- package/src/blackboard.js +298 -6
- package/src/cli-run.js +33 -5
- package/src/codex-agents.js +96 -5
- package/src/cost/aggregator.js +39 -9
- package/src/cost/pricing.js +57 -0
- package/src/cost/readers/gemini.js +1 -1
- package/src/cross-audit-chunker.js +189 -0
- package/src/cross-dispatcher.js +124 -21
- package/src/cross-orchestrator-cli.js +550 -14
- package/src/cross-orchestrator.js +1171 -10
- package/src/cross-project-search.js +195 -9
- package/src/dashboard-client-planning.html +273 -0
- package/src/dashboard-client-waves.html +304 -0
- package/src/dashboard-client.html +17 -2
- package/src/dashboard-server.js +152 -0
- package/src/deploy-alerts.js +150 -0
- package/src/design/iframe-bridge.js +242 -0
- package/src/design-companion.js +144 -0
- package/src/dispatch/checkpoint-cli.js +97 -0
- package/src/dispatch/colon-syntax.js +81 -1
- package/src/dispatch/extension.js +27 -1
- package/src/dispatch/registry-cli.js +4 -1
- package/src/dispatch/wave-cli.js +323 -0
- package/src/dispatch/worktree-cli.js +40 -0
- package/src/dispatch-planner.js +97 -2
- package/src/dream/runner.mjs +47 -11
- package/src/dream/stage-runner.js +40 -0
- package/src/dream/state-file.js +102 -0
- package/src/extension-installer.js +70 -24
- package/src/extension-quota-tracker.js +4 -2
- package/src/extension-registry.js +289 -35
- package/src/feedback-detector.js +26 -0
- package/src/fs-lock.js +259 -7
- package/src/gate-result.js +95 -1
- package/src/hero-line.js +86 -5
- package/src/intent-router.js +35 -0
- package/src/lib/a11y-contract.js +117 -0
- package/src/lib/atomic-io.js +29 -8
- package/src/lib/cache-keepalive.js +150 -0
- package/src/lib/jsonl-rotation.js +104 -0
- package/src/lib/lighthouse-pillar.js +121 -0
- package/src/lib/llm-call.js +121 -0
- package/src/lib/playwright-baseline.js +205 -0
- package/src/lib/rekor-bridge.js +221 -0
- package/src/lib/repo-map.js +392 -0
- package/src/lib/shasum-verify.js +164 -0
- package/src/lib/sketches-gc.js +132 -0
- package/src/lib/tmp-suffix.js +62 -0
- package/src/lib/ui-review-runner.js +554 -0
- package/src/lib/uispec-drift.js +301 -0
- package/src/lib/uispec-intake.js +381 -0
- package/src/lib/worktree-guards.js +118 -0
- package/src/lib/worktree-recovery.js +100 -0
- package/src/memory/auto-linker.js +152 -0
- package/src/memory/benchmark.js +498 -0
- package/src/memory/dedup.js +126 -0
- package/src/memory/embedding-cache.js +136 -0
- package/src/memory/fact-extractor.js +168 -0
- package/src/memory/fts5.js +65 -1
- package/src/memory/migrations/004-bitemporal.js +91 -0
- package/src/memory/migrations/005-vector-cache.js +61 -0
- package/src/memory/migrations/006-obsidian-graph.js +46 -0
- package/src/memory/migrations/007-skill-telemetry.js +24 -0
- package/src/memory/migrations/008-write-provenance.js +41 -0
- package/src/memory/obsidian-parser.js +91 -0
- package/src/memory/query-dataview.js +86 -0
- package/src/memory/search.js +10 -0
- package/src/memory/temporal.js +529 -0
- package/src/memory/tokenize.js +10 -0
- package/src/memory-facts-handler.js +37 -0
- package/src/memory-feedback.js +260 -2
- package/src/model-refresh.js +292 -0
- package/src/observability/cost-anomaly.js +166 -0
- package/src/observability/evaluator-checkpoint-contract.js +117 -0
- package/src/observability/trace-id.js +163 -0
- package/src/orchestrator/agents-md-blackboard.js +152 -0
- package/src/orchestrator/checkpoint-contract.md +140 -0
- package/src/orchestrator/debug-trident.js +570 -0
- package/src/orchestrator/merge-block-aware.js +350 -0
- package/src/orchestrator/plan-checker.js +475 -0
- package/src/orchestrator/post-done-runner.js +249 -0
- package/src/orchestrator/review.js +136 -0
- package/src/orchestrator/runtime-loop.js +430 -0
- package/src/orchestrator/skill-telemetry-sink.js +29 -0
- package/src/orchestrator/skill-telemetry.js +37 -0
- package/src/orchestrator/state-events.js +459 -0
- package/src/orchestrator/state-sdk.js +1764 -0
- package/src/orchestrator/status-protocol.js +235 -0
- package/src/orchestrator/subagent-telemetry.js +452 -0
- package/src/orchestrator/termination.js +160 -0
- package/src/orchestrator/verification-gate.js +281 -0
- package/src/orchestrator/wave-state.js +564 -0
- package/src/orchestrator/worktree-provision.js +77 -0
- package/src/override-use-registry.js +111 -5
- package/src/receipts.js +36 -4
- package/src/recovery/checkpoint.js +56 -3
- package/src/recovery/code-fixer.js +656 -0
- package/src/recovery/truncation.js +317 -0
- package/src/redactor.js +75 -6
- package/src/runtime-mediator.js +15 -0
- package/src/sanitizer.js +10 -0
- package/src/search-hybrid.js +139 -0
- package/src/server.js +603 -59
- package/src/swarm/worktree.js +27 -4
- package/src/swarm-config.js +113 -12
- package/src/team/domain-templates/book.json +51 -0
- package/src/team/domain-templates/business.json +41 -0
- package/src/team/domain-templates/content.json +50 -0
- package/src/team/domain-templates/design.json +44 -0
- package/src/team/domain-templates/research.json +41 -0
- package/src/team/domain-templates/software.json +40 -0
- package/src/team/generator.js +278 -3
- package/src/team/modify.js +203 -0
- package/src/team/schemas.js +48 -0
- 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 };
|
package/src/design-companion.js
CHANGED
|
@@ -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, '&')
|
|
65
|
+
.replace(/</g, '<')
|
|
66
|
+
.replace(/>/g, '>')
|
|
67
|
+
.replace(/"/g, '"')
|
|
68
|
+
.replace(/'/g, ''');
|
|
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
|
-
|
|
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);
|