@ijfw/memory-server 1.4.4 → 1.5.1
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/bin/ijfw-memorize +14 -7
- package/fixtures/team/book.json +6 -6
- package/fixtures/team/business.json +146 -20
- package/fixtures/team/content.json +6 -6
- package/fixtures/team/design.json +148 -20
- package/fixtures/team/mixed.json +206 -27
- package/fixtures/team/research.json +146 -20
- package/fixtures/team/software.json +148 -20
- 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 +6 -3
- 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 +754 -159
- package/src/cross-orchestrator.js +1065 -17
- package/src/cross-project-search.js +195 -9
- package/src/dashboard-client-waves.html +304 -0
- package/src/dashboard-client.html +5 -1
- package/src/dashboard-server.js +73 -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 +26 -2
- package/src/dispatch/registry-cli.js +4 -1
- package/src/dispatch/wave-cli.js +201 -6
- 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/hardware-signer.js +4 -2
- 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 +595 -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 +267 -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/migration-runner.js +6 -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/migrations/009-obsidian-backfill.js +50 -0
- package/src/memory/obsidian-parser.js +152 -0
- package/src/memory/query-dataview.js +86 -0
- package/src/memory/search.js +46 -15
- 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-trigger.js +374 -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 +277 -0
- package/src/orchestrator/review.js +38 -3
- 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 +1932 -0
- package/src/orchestrator/status-protocol.js +84 -17
- package/src/orchestrator/subagent-telemetry.js +471 -0
- package/src/orchestrator/termination.js +160 -0
- package/src/orchestrator/verification-gate.js +200 -16
- package/src/orchestrator/wave-state.js +332 -23
- package/src/orchestrator/worktree-provision.js +77 -0
- package/src/override-resolver.js +5 -3
- 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 +961 -0
- package/src/recovery/truncation.js +317 -0
- package/src/redactor.js +75 -6
- package/src/runtime-mediator.js +15 -1
- package/src/sanitizer.js +10 -0
- package/src/search-hybrid.js +139 -0
- package/src/server.js +795 -112
- package/src/swarm/worktree.js +27 -4
- package/src/swarm-config.js +102 -17
- package/src/team/domain-templates/book.json +51 -0
- package/src/team/domain-templates/business.json +44 -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 +44 -0
- package/src/team/domain-templates/software.json +40 -0
- package/src/team/generator.js +440 -3
- package/src/team/modify.js +203 -0
- package/src/team/schemas.js +48 -0
- package/src/update-apply.js +19 -3
- package/src/dashboard-charts.js +0 -239
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* deploy-alerts.js — v1.5.0 audit-MED-update-M8 (F-REL-2).
|
|
3
|
+
*
|
|
4
|
+
* When `extension-installer.installExtension` exits with `deploy_partial: true`,
|
|
5
|
+
* the failure detail used to be returned in the install reply only — the next
|
|
6
|
+
* prelude had no way to surface "you have a half-deployed extension somewhere".
|
|
7
|
+
*
|
|
8
|
+
* This module persists each partial deploy to a jsonl tail at
|
|
9
|
+
* `~/.ijfw/state/deploy-failures.jsonl` so the memory prelude (handlePrelude in
|
|
10
|
+
* `server.js`) can read the last N entries and emit a "Deploy alerts" line.
|
|
11
|
+
*
|
|
12
|
+
* File contract:
|
|
13
|
+
* - JSONL, one record per line.
|
|
14
|
+
* - Each record:
|
|
15
|
+
* {
|
|
16
|
+
* ts: ISO8601,
|
|
17
|
+
* extension: <manifest.name>,
|
|
18
|
+
* scope: 'project' | 'org' | 'user',
|
|
19
|
+
* failures: Array<{platform, skillName, error}>,
|
|
20
|
+
* }
|
|
21
|
+
* - Soft cap: 200 lines. Older lines drop off via a one-shot trim on write.
|
|
22
|
+
* - Append-only and atomic in the common case (single writeFile call); the
|
|
23
|
+
* trim path rewrites the whole tail under the same atomic shape.
|
|
24
|
+
* - Failure to write is non-fatal — alert path is best-effort observability.
|
|
25
|
+
*
|
|
26
|
+
* The reader is bounded at N=10 by default — short-tail "what's wrong right
|
|
27
|
+
* now" surfacing, not an audit log.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
31
|
+
import { homedir } from 'node:os';
|
|
32
|
+
import { join } from 'node:path';
|
|
33
|
+
|
|
34
|
+
const ALERT_FILE_NAME = 'deploy-failures.jsonl';
|
|
35
|
+
const MAX_LINES_ON_DISK = 200;
|
|
36
|
+
const DEFAULT_READ_TAIL = 10;
|
|
37
|
+
|
|
38
|
+
function statePath() {
|
|
39
|
+
return join(homedir(), '.ijfw', 'state');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function deployFailuresPath() {
|
|
43
|
+
return join(statePath(), ALERT_FILE_NAME);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Record a partial-deploy event.
|
|
48
|
+
*
|
|
49
|
+
* @param {object} record
|
|
50
|
+
* @param {string} record.extension
|
|
51
|
+
* @param {'project'|'org'|'user'} record.scope
|
|
52
|
+
* @param {Array<{platform:string, skillName?:string, error:string}>} record.failures
|
|
53
|
+
* @returns {Promise<{ok:boolean, path?:string, error?:string}>}
|
|
54
|
+
*/
|
|
55
|
+
export async function recordDeployFailure(record) {
|
|
56
|
+
if (!record || typeof record !== 'object') {
|
|
57
|
+
return { ok: false, error: 'record must be an object' };
|
|
58
|
+
}
|
|
59
|
+
if (typeof record.extension !== 'string' || record.extension.length === 0) {
|
|
60
|
+
return { ok: false, error: 'extension is required' };
|
|
61
|
+
}
|
|
62
|
+
if (!Array.isArray(record.failures)) {
|
|
63
|
+
return { ok: false, error: 'failures must be an array' };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const entry = {
|
|
67
|
+
ts: new Date().toISOString(),
|
|
68
|
+
extension: record.extension,
|
|
69
|
+
scope: record.scope || 'project',
|
|
70
|
+
failures: record.failures.map((f) => ({
|
|
71
|
+
// v1.5.0 wire-W2.design-misc — was `typeof f && f.platform`, which is
|
|
72
|
+
// effectively `f.platform` because `typeof f` is always a truthy string
|
|
73
|
+
// (even for null/undefined). That meant a null entry in the failures
|
|
74
|
+
// array threw a TypeError instead of falling back to 'unknown'. The
|
|
75
|
+
// adjacent skillName + error fields already use the correct guard.
|
|
76
|
+
platform: f && f.platform ? String(f.platform) : 'unknown',
|
|
77
|
+
skillName: f && f.skillName ? String(f.skillName) : null,
|
|
78
|
+
error: f && f.error ? String(f.error).slice(0, 500) : 'unknown',
|
|
79
|
+
})),
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const path = deployFailuresPath();
|
|
83
|
+
try {
|
|
84
|
+
await mkdir(statePath(), { recursive: true });
|
|
85
|
+
// Trim-on-overflow: if existing file already > cap, rewrite the tail.
|
|
86
|
+
let existing = '';
|
|
87
|
+
try {
|
|
88
|
+
existing = await readFile(path, 'utf8');
|
|
89
|
+
} catch {
|
|
90
|
+
existing = '';
|
|
91
|
+
}
|
|
92
|
+
const lines = existing ? existing.split('\n').filter((l) => l.trim()) : [];
|
|
93
|
+
lines.push(JSON.stringify(entry));
|
|
94
|
+
const trimmed = lines.slice(-MAX_LINES_ON_DISK);
|
|
95
|
+
await writeFile(path, trimmed.join('\n') + '\n', { encoding: 'utf8', mode: 0o600 });
|
|
96
|
+
return { ok: true, path };
|
|
97
|
+
} catch (err) {
|
|
98
|
+
return { ok: false, error: err && err.message ? err.message : String(err) };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Read the last N deploy-failure records (default 10). Returns oldest-first.
|
|
104
|
+
*
|
|
105
|
+
* @param {{limit?:number}} [opts]
|
|
106
|
+
* @returns {Promise<Array>}
|
|
107
|
+
*/
|
|
108
|
+
export async function readDeployFailures(opts = {}) {
|
|
109
|
+
const limit = Number.isInteger(opts.limit) && opts.limit > 0 ? opts.limit : DEFAULT_READ_TAIL;
|
|
110
|
+
let raw;
|
|
111
|
+
try {
|
|
112
|
+
raw = await readFile(deployFailuresPath(), 'utf8');
|
|
113
|
+
} catch {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
const lines = raw.split('\n').filter((l) => l.trim());
|
|
117
|
+
const tail = lines.slice(-limit);
|
|
118
|
+
const out = [];
|
|
119
|
+
for (const line of tail) {
|
|
120
|
+
try {
|
|
121
|
+
const parsed = JSON.parse(line);
|
|
122
|
+
if (parsed && typeof parsed === 'object') out.push(parsed);
|
|
123
|
+
} catch {
|
|
124
|
+
// skip malformed line — don't fail the read
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Render the last N entries as terse prelude lines. Empty array → empty string.
|
|
132
|
+
*
|
|
133
|
+
* @param {{limit?:number}} [opts]
|
|
134
|
+
* @returns {Promise<string>}
|
|
135
|
+
*/
|
|
136
|
+
export async function renderDeployAlertsForPrelude(opts = {}) {
|
|
137
|
+
const entries = await readDeployFailures(opts);
|
|
138
|
+
if (entries.length === 0) return '';
|
|
139
|
+
const lines = ['## Deploy alerts'];
|
|
140
|
+
for (const e of entries) {
|
|
141
|
+
const fcount = Array.isArray(e.failures) ? e.failures.length : 0;
|
|
142
|
+
const platforms = Array.isArray(e.failures)
|
|
143
|
+
? Array.from(new Set(e.failures.map((f) => f.platform).filter(Boolean))).join(',')
|
|
144
|
+
: '';
|
|
145
|
+
const head = `- ${e.ts} — ${e.extension} (scope=${e.scope || 'project'}): ${fcount} failure${fcount === 1 ? '' : 's'}${platforms ? ` [${platforms}]` : ''}`;
|
|
146
|
+
lines.push(head);
|
|
147
|
+
}
|
|
148
|
+
lines.push('');
|
|
149
|
+
return lines.join('\n');
|
|
150
|
+
}
|
|
@@ -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
|
+
});
|