@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
|
@@ -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
|
+
* DEFERRED (harness-dependency — 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,12 +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, wave] = 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'),
|
|
621
|
-
import('./wave-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)
|
|
622
644
|
]);
|
|
623
645
|
_v143Handlers = Object.assign(
|
|
624
646
|
Object.create(null),
|
|
@@ -627,6 +649,8 @@ async function loadV143Handlers() {
|
|
|
627
649
|
quota.handlers || {},
|
|
628
650
|
active.handlers || {},
|
|
629
651
|
wave.handlers || {},
|
|
652
|
+
checkpoint.handlers || {},
|
|
653
|
+
worktree.handlers || {},
|
|
630
654
|
);
|
|
631
655
|
return _v143Handlers;
|
|
632
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);
|
package/src/dispatch/wave-cli.js
CHANGED
|
@@ -13,10 +13,11 @@
|
|
|
13
13
|
* snapshot-based per lock-in #31 — no daemon, no subscriptions.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { readdir, stat } from 'node:fs/promises';
|
|
16
|
+
import { readdir, stat, readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
17
17
|
import { join } from 'node:path';
|
|
18
18
|
|
|
19
19
|
import { readWaveState } from '../orchestrator/wave-state.js';
|
|
20
|
+
import { drainCheckpoints } from '../orchestrator/subagent-telemetry.js';
|
|
20
21
|
|
|
21
22
|
const WAVE_DIR_PREFIX = 'wave-';
|
|
22
23
|
|
|
@@ -111,18 +112,212 @@ export const handlers = {
|
|
|
111
112
|
return { ok: true, output: '(no waves)' };
|
|
112
113
|
}
|
|
113
114
|
waves.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
// v1.5.0 F3 (R3 fold-in): parallel reads via Promise.all.
|
|
116
|
+
// Sequential await over N waves was O(N*disk-latency); parallelised on N>3.
|
|
117
|
+
const states = await Promise.all(waves.map(({ id }) => readWaveState(id, projectRoot)));
|
|
118
|
+
const rows = waves.map(({ id }, i) => {
|
|
119
|
+
const state = states[i];
|
|
117
120
|
const status = state?.frontmatter?.status ?? '?';
|
|
118
121
|
const createdAt = state?.frontmatter?.created_at ?? '';
|
|
119
|
-
|
|
120
|
-
}
|
|
122
|
+
return `${id}\t${status}\t${createdAt}`;
|
|
123
|
+
});
|
|
121
124
|
return { ok: true, output: rows.join('\n') };
|
|
122
125
|
},
|
|
126
|
+
|
|
127
|
+
// r17.1 (item 6): subagent "did anyone go silent?" check. Takes a wave id
|
|
128
|
+
// plus the list of subagent ids you EXPECTED to complete and reports which
|
|
129
|
+
// ones have a checkpoint receipt vs which are MIA. Closes the wayland-
|
|
130
|
+
// session pattern where 5 of 8 dispatched subagents went silent and the
|
|
131
|
+
// orchestrator (Claude) had no way to know without manually scanning.
|
|
132
|
+
//
|
|
133
|
+
// Usage:
|
|
134
|
+
// ijfw wave-missing <waveId> <expectedId1> [<expectedId2> ...]
|
|
135
|
+
// Exits non-zero (via ctx-aware caller) when any expected id is missing.
|
|
136
|
+
//
|
|
137
|
+
// Why this is in IJFW vs a SKILL: the orchestrator harness doesn't notify
|
|
138
|
+
// Claude when an Agent subagent silently fails. IJFW can't fix that — only
|
|
139
|
+
// the harness can. What IJFW CAN do: give the orchestrator a deterministic,
|
|
140
|
+
// mechanical way to ask "are the receipts here?" without scraping the
|
|
141
|
+
// worktree filesystem by hand.
|
|
142
|
+
'wave-missing': async (args, ctx) => {
|
|
143
|
+
const tokens = tokenize(args);
|
|
144
|
+
const waveId = tokens[0];
|
|
145
|
+
let expected = tokens.slice(1);
|
|
146
|
+
|
|
147
|
+
// v1.5.0 audit-MED-work-M4: when the operator omits the expected list,
|
|
148
|
+
// try to read `.ijfw/wave-<id>/expected.json` (written at fan-out by the
|
|
149
|
+
// orchestrator). Closes the "orchestrator forgot to call wave-missing
|
|
150
|
+
// with the right argv" failure mode. argv list still wins when present.
|
|
151
|
+
if (waveId && expected.length === 0) {
|
|
152
|
+
const projectRoot = (ctx && ctx.projectRoot) || process.cwd();
|
|
153
|
+
try {
|
|
154
|
+
const f = join(projectRoot, '.ijfw', `${WAVE_DIR_PREFIX}${waveId}`, 'expected.json');
|
|
155
|
+
const raw = await readFile(f, 'utf8');
|
|
156
|
+
const parsed = JSON.parse(raw);
|
|
157
|
+
if (parsed && Array.isArray(parsed.expected)) {
|
|
158
|
+
expected = parsed.expected.filter((x) => typeof x === 'string' && x.length > 0);
|
|
159
|
+
}
|
|
160
|
+
} catch { /* no expected.json — fall through to usage error below */ }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!waveId || expected.length === 0) {
|
|
164
|
+
return {
|
|
165
|
+
ok: false,
|
|
166
|
+
error: 'Usage: ijfw wave-missing <waveId> <expectedSubId1> [<expectedSubId2> ...]',
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
// r17-M2: validate waveId so it can't traverse out of .ijfw/. The wave
|
|
170
|
+
// directory format is `wave-<id>` and ids are expected to be a small set
|
|
171
|
+
// of safe chars (alnum, dash, dot for versions, underscore).
|
|
172
|
+
if (!/^[A-Za-z0-9._-]+$/.test(waveId) || waveId.includes('..')) {
|
|
173
|
+
return { ok: false, error: `wave-missing: invalid waveId "${waveId}" (must match [A-Za-z0-9._-] and not contain "..")` };
|
|
174
|
+
}
|
|
175
|
+
// Also validate expected ids — they're echoed back to the user but
|
|
176
|
+
// also used in regex construction inside the present-set match. Safe-char
|
|
177
|
+
// gate prevents both reflection of garbage and any future use as paths.
|
|
178
|
+
for (const id of expected) {
|
|
179
|
+
if (!/^[A-Za-z0-9._-]+$/.test(id) || id.includes('..')) {
|
|
180
|
+
return { ok: false, error: `wave-missing: invalid expected id "${id}" (must match [A-Za-z0-9._-] and not contain "..")` };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const projectRoot = (ctx && ctx.projectRoot) || process.cwd();
|
|
184
|
+
const waveDir = join(projectRoot, '.ijfw', `${WAVE_DIR_PREFIX}${waveId}`);
|
|
185
|
+
let dirents = [];
|
|
186
|
+
try {
|
|
187
|
+
dirents = await readdir(waveDir, { withFileTypes: true });
|
|
188
|
+
} catch (err) {
|
|
189
|
+
if (err.code === 'ENOENT') {
|
|
190
|
+
return {
|
|
191
|
+
ok: false,
|
|
192
|
+
output: `Wave ${waveId} has no .ijfw/wave-${waveId}/ directory. All ${expected.length} expected subagent(s) are MISSING.\nMissing: ${expected.join(', ')}`,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
return { ok: false, error: `wave-missing: ${err.message}` };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Two canonical receipt filename forms:
|
|
199
|
+
// 1. Wave-id-prefixed (Phase D synthesized + S01 CLI default):
|
|
200
|
+
// subagent-<waveId>-<subId>.checkpoint.json
|
|
201
|
+
// 2. Bare runtime form (when ijfw checkpoint <waveId> <subId> ... runs
|
|
202
|
+
// and the subId itself has no embedded dashes that collide):
|
|
203
|
+
// subagent-<subId>.checkpoint.json
|
|
204
|
+
// We extract subId by stripping a leading "<waveId>-" if present, then
|
|
205
|
+
// taking everything before ".checkpoint.json". This avoids the regex-
|
|
206
|
+
// overlap bug where greedy captures double-counted a subId that happens
|
|
207
|
+
// to start with a word that resembles another subId.
|
|
208
|
+
const present = new Set();
|
|
209
|
+
const wavePrefix = `subagent-${waveId}-`;
|
|
210
|
+
const barePrefix = 'subagent-';
|
|
211
|
+
const suffix = '.checkpoint.json';
|
|
212
|
+
for (const ent of dirents) {
|
|
213
|
+
if (!ent.isFile()) continue;
|
|
214
|
+
const name = ent.name;
|
|
215
|
+
if (!name.startsWith(barePrefix) || !name.endsWith(suffix)) continue;
|
|
216
|
+
let subId;
|
|
217
|
+
if (name.startsWith(wavePrefix)) {
|
|
218
|
+
subId = name.slice(wavePrefix.length, name.length - suffix.length);
|
|
219
|
+
} else {
|
|
220
|
+
subId = name.slice(barePrefix.length, name.length - suffix.length);
|
|
221
|
+
}
|
|
222
|
+
if (subId.length > 0) present.add(subId);
|
|
223
|
+
}
|
|
224
|
+
const found = expected.filter(id => present.has(id));
|
|
225
|
+
const missing = expected.filter(id => !present.has(id));
|
|
226
|
+
const stray = [...present].filter(id => !expected.includes(id));
|
|
227
|
+
|
|
228
|
+
const lines = [
|
|
229
|
+
`Wave: ${waveId}`,
|
|
230
|
+
`Expected: ${expected.length} subagent(s)`,
|
|
231
|
+
`Present: ${found.length} (${found.join(', ') || 'none'})`,
|
|
232
|
+
`Missing: ${missing.length} (${missing.join(', ') || 'none'})`,
|
|
233
|
+
];
|
|
234
|
+
if (stray.length > 0) {
|
|
235
|
+
lines.push(`Stray: ${stray.length} (${stray.join(', ')}) — present but not expected`);
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
ok: missing.length === 0,
|
|
239
|
+
output: lines.join('\n'),
|
|
240
|
+
};
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
// v1.5.0 audit-MED-work-M4: write `.ijfw/wave-<id>/expected.json` so a
|
|
244
|
+
// later `ijfw wave-missing <id>` call can self-config from disk and the
|
|
245
|
+
// orchestrator-LLM doesn't have to remember the expected subagent list at
|
|
246
|
+
// fan-in time. Usage:
|
|
247
|
+
// ijfw wave-expected <waveId> <expectedSubId1> [<expectedSubId2> ...]
|
|
248
|
+
'wave-expected': async (args, ctx) => {
|
|
249
|
+
const tokens = tokenize(args);
|
|
250
|
+
const waveId = tokens[0];
|
|
251
|
+
const expected = tokens.slice(1);
|
|
252
|
+
if (!waveId || expected.length === 0) {
|
|
253
|
+
return {
|
|
254
|
+
ok: false,
|
|
255
|
+
error: 'Usage: ijfw wave-expected <waveId> <expectedSubId1> [<expectedSubId2> ...]',
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
if (!/^[A-Za-z0-9._-]+$/.test(waveId) || waveId.includes('..')) {
|
|
259
|
+
return { ok: false, error: `wave-expected: invalid waveId "${waveId}" (must match [A-Za-z0-9._-] and not contain "..")` };
|
|
260
|
+
}
|
|
261
|
+
for (const id of expected) {
|
|
262
|
+
if (!/^[A-Za-z0-9._-]+$/.test(id) || id.includes('..')) {
|
|
263
|
+
return { ok: false, error: `wave-expected: invalid expected id "${id}" (must match [A-Za-z0-9._-] and not contain "..")` };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const projectRoot = (ctx && ctx.projectRoot) || process.cwd();
|
|
267
|
+
const waveDir = join(projectRoot, '.ijfw', `${WAVE_DIR_PREFIX}${waveId}`);
|
|
268
|
+
try {
|
|
269
|
+
await mkdir(waveDir, { recursive: true });
|
|
270
|
+
const payload = JSON.stringify(
|
|
271
|
+
{ wave_id: waveId, expected, recorded_at: new Date().toISOString() },
|
|
272
|
+
null, 2,
|
|
273
|
+
) + '\n';
|
|
274
|
+
await writeFile(join(waveDir, 'expected.json'), payload, 'utf8');
|
|
275
|
+
return { ok: true, output: `ok: recorded ${expected.length} expected subagent(s) for ${waveId}` };
|
|
276
|
+
} catch (err) {
|
|
277
|
+
return { ok: false, error: `wave-expected: ${err && err.message ? err.message : String(err)}` };
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
// v1.5.0-major S01: belt-and-suspenders drain of subagent checkpoints from
|
|
282
|
+
// a worktree's .ijfw/wave-<id>/ into the parent project's .ijfw/wave-<id>/.
|
|
283
|
+
// Run BEFORE `git worktree remove` so checkpoints survive cleanup even if
|
|
284
|
+
// the subagent didn't honor IJFW_PARENT_PROJECT_ROOT (older callers, manual
|
|
285
|
+
// claude invocation in a worktree, etc.).
|
|
286
|
+
'worktree-drain': async (args, ctx) => {
|
|
287
|
+
const tokens = tokenize(args);
|
|
288
|
+
const [waveId, worktreePath] = tokens;
|
|
289
|
+
if (!waveId || !worktreePath) {
|
|
290
|
+
return {
|
|
291
|
+
ok: false,
|
|
292
|
+
error: 'Usage: ijfw worktree-drain <waveId> <worktreePath>',
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
const projectRoot = (ctx && ctx.projectRoot) || process.cwd();
|
|
296
|
+
try {
|
|
297
|
+
const result = await drainCheckpoints(waveId, worktreePath, projectRoot);
|
|
298
|
+
if (!result.ok) {
|
|
299
|
+
return { ok: false, error: `ijfw worktree-drain: ${result.reason}` };
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
ok: true,
|
|
303
|
+
output: `ok: drained ${result.drained} checkpoint(s) from ${worktreePath}`,
|
|
304
|
+
};
|
|
305
|
+
} catch (err) {
|
|
306
|
+
return {
|
|
307
|
+
ok: false,
|
|
308
|
+
error: `ijfw worktree-drain: ${err && err.message ? err.message : String(err)}`,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
},
|
|
123
312
|
};
|
|
124
313
|
|
|
125
314
|
export const subcommandHelp = {
|
|
126
315
|
'wave-status': 'wave-status [<id>|latest] — print live state of a wave',
|
|
127
316
|
'wave-list': 'wave-list — list all known waves (newest first)',
|
|
317
|
+
'wave-missing':
|
|
318
|
+
'wave-missing <waveId> [<expectedId1> ...] — list any dispatched subagents that have no checkpoint receipt (catches silent-failure dispatches). When expected ids are omitted, reads from .ijfw/wave-<id>/expected.json (written by `ijfw wave-expected`).',
|
|
319
|
+
'wave-expected':
|
|
320
|
+
'wave-expected <waveId> <expectedId1> [<expectedId2> ...] — record the expected subagent ids for a wave so `ijfw wave-missing <id>` can self-config (v1.5.0 audit-MED-work-M4)',
|
|
321
|
+
'worktree-drain':
|
|
322
|
+
'worktree-drain <waveId> <worktreePath> — copy subagent checkpoints from a worktree to the parent before `git worktree remove` (v1.5.0 S01)',
|
|
128
323
|
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dispatch/worktree-cli.js — v1.5.0 S2 worktree provisioning CLI.
|
|
3
|
+
*
|
|
4
|
+
* Frozen export contract (briefing-locked):
|
|
5
|
+
* export const worktreeHandlers = Object.freeze({ worktree: handler });
|
|
6
|
+
* export const worktreeSubcommandHelp = Object.freeze({ '<subcommand>': '<help>' });
|
|
7
|
+
*
|
|
8
|
+
* Handler returns a numeric exit code (0=ok, 1=failure, 2=usage), distinct
|
|
9
|
+
* from wave-cli's {ok,output} shape because this command writes JSON to stdout
|
|
10
|
+
* directly for downstream orchestrator consumption.
|
|
11
|
+
*
|
|
12
|
+
* Subcommand owned by this module:
|
|
13
|
+
* - worktree provision <path>
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { provisionWorktree } from '../orchestrator/worktree-provision.js';
|
|
17
|
+
import { resolve } from 'node:path';
|
|
18
|
+
|
|
19
|
+
const SUBCOMMAND_HELP = {
|
|
20
|
+
'worktree provision': 'ijfw worktree provision <path> — detect+install deps in a worktree (v1.5.0 S2)',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
async function handleWorktree(args, _ctx) {
|
|
24
|
+
if (args[0] !== 'provision' || !args[1]) {
|
|
25
|
+
process.stderr.write('Usage: ijfw worktree provision <path>\n');
|
|
26
|
+
return 2;
|
|
27
|
+
}
|
|
28
|
+
const path = resolve(args[1]);
|
|
29
|
+
try {
|
|
30
|
+
const result = await provisionWorktree(path);
|
|
31
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
32
|
+
return result.failed.length > 0 ? 1 : 0;
|
|
33
|
+
} catch (err) {
|
|
34
|
+
process.stderr.write(`ijfw worktree: ${err.message}\n`);
|
|
35
|
+
return 1;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const worktreeHandlers = Object.freeze({ worktree: handleWorktree });
|
|
40
|
+
export const worktreeSubcommandHelp = Object.freeze(SUBCOMMAND_HELP);
|
package/src/dispatch-planner.js
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
// computes a dispatch manifest: each sub-wave is either SHARED (no file overlap
|
|
5
5
|
// with peers in the same wave) or WORKTREE (overlaps -> needs isolation).
|
|
6
6
|
//
|
|
7
|
-
// Pure + synchronous. ESM. Zero deps.
|
|
7
|
+
// Pure + synchronous. ESM. Zero deps. No filesystem writes — all state
|
|
8
|
+
// persistence is the caller's responsibility via state-SDK query() (T6).
|
|
9
|
+
// A spy regression test in test-dispatch-planner.js enforces this permanently.
|
|
8
10
|
|
|
9
11
|
// eslint-disable-next-line security/detect-unsafe-regex -- plan markdown is bounded human-authored text; pattern is line-anchored and token-sized.
|
|
10
12
|
const WAVE_HEADER = /^###\s+Wave\s+([0-9]+[A-Z])(?:-([A-Za-z0-9_+]+))?\b/;
|
|
@@ -54,7 +56,25 @@ export function parsePlan(markdown) {
|
|
|
54
56
|
.split(/[,\s]+/)
|
|
55
57
|
.map((s) => s.replace(/^`|`$/g, '').trim())
|
|
56
58
|
.filter(Boolean);
|
|
57
|
-
for (const a of add)
|
|
59
|
+
for (const a of add) {
|
|
60
|
+
// v1.5.0 audit-LOW-work-L7: refuse the universal glob `**` (and its
|
|
61
|
+
// common forms `**/*`, `./**`). It declares "every file under the
|
|
62
|
+
// repo," which guarantees overlap with every peer sub-wave and forces
|
|
63
|
+
// them all into worktree isolation — which is almost always a
|
|
64
|
+
// planner mistake, not a real declaration. Catch it at parse time
|
|
65
|
+
// with a clear message instead of letting it silently neutralise
|
|
66
|
+
// the wave-routing algorithm.
|
|
67
|
+
const norm = a.replace(/^\.\//, '');
|
|
68
|
+
if (norm === '**' || norm === '**/*' || norm === '*' || norm === '*/**') {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`dispatch-planner: refusing universal glob "${a}" in Files: line ` +
|
|
71
|
+
`(declares every file in repo, forces every peer sub-wave into ` +
|
|
72
|
+
`worktree isolation — almost always a planner mistake). ` +
|
|
73
|
+
`Use specific path prefixes instead.`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
if (!target.files.includes(a)) target.files.push(a);
|
|
77
|
+
}
|
|
58
78
|
}
|
|
59
79
|
}
|
|
60
80
|
push(currentSub);
|
|
@@ -182,6 +202,81 @@ export function mergeOrder(manifest) {
|
|
|
182
202
|
|
|
183
203
|
function idOf(sw) { return sw.sub || sw.wave; }
|
|
184
204
|
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
// v1.5.0 T19 (G1) — env-var passthrough composer (pure, no fs writes).
|
|
207
|
+
//
|
|
208
|
+
// Computes the SDK-contract env map a dispatched subagent inherits, given
|
|
209
|
+
// the parent's process env and the per-subagent (waveId, subId, isolation,
|
|
210
|
+
// projectRoot) context. The actual dispatch is performed by the
|
|
211
|
+
// `subagent.dispatch` SDK verb — this is the planner-side pure helper used
|
|
212
|
+
// to construct the env passthrough payload BEFORE the verb call.
|
|
213
|
+
//
|
|
214
|
+
// Stays in dispatch-planner.js because:
|
|
215
|
+
// * Caller intent: the planner is the orchestrator's pure compute layer.
|
|
216
|
+
// * Test invariant: the dispatch-planner spy gate forbids ANY fs write —
|
|
217
|
+
// this helper performs zero fs I/O (read or write).
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Compose the deterministic env-var passthrough for a dispatched subagent.
|
|
222
|
+
*
|
|
223
|
+
* The SDK contract specifies these well-known names (verb contract §7
|
|
224
|
+
* `subagent.dispatch`):
|
|
225
|
+
* * `IJFW_PROJECT_DIR` — absolute project root
|
|
226
|
+
* * `IJFW_PARENT_PROJECT_ROOT` — parent's project root (worktree subagents)
|
|
227
|
+
* * `IJFW_WAVE_ID` — wave id
|
|
228
|
+
* * `IJFW_SUBAGENT_ID` — this subagent's id
|
|
229
|
+
* * `IJFW_ISOLATION` — 'shared' | 'worktree'
|
|
230
|
+
* * `IJFW_SESSION_ID` — orchestrator session id (when set)
|
|
231
|
+
*
|
|
232
|
+
* Caller-supplied `extraEnv` keys override the SDK contract on collision —
|
|
233
|
+
* by design (caller's intent wins). Values are coerced to strings; null /
|
|
234
|
+
* undefined entries are dropped.
|
|
235
|
+
*
|
|
236
|
+
* @param {{projectRoot:string, waveId:string, subagentId:string,
|
|
237
|
+
* isolation?:'shared'|'worktree', parentEnv?:object, extraEnv?:object}} input
|
|
238
|
+
* @returns {Record<string,string>} the composed env map (sorted-stable).
|
|
239
|
+
*/
|
|
240
|
+
export function composeDispatchEnv(input) {
|
|
241
|
+
if (!input || typeof input !== 'object') {
|
|
242
|
+
throw new Error('dispatch-planner.composeDispatchEnv: input object required');
|
|
243
|
+
}
|
|
244
|
+
const { projectRoot, waveId, subagentId } = input;
|
|
245
|
+
if (typeof projectRoot !== 'string' || projectRoot.length === 0) {
|
|
246
|
+
throw new Error('dispatch-planner.composeDispatchEnv: projectRoot required');
|
|
247
|
+
}
|
|
248
|
+
if (typeof waveId !== 'string' || waveId.length === 0) {
|
|
249
|
+
throw new Error('dispatch-planner.composeDispatchEnv: waveId required');
|
|
250
|
+
}
|
|
251
|
+
if (typeof subagentId !== 'string' || subagentId.length === 0) {
|
|
252
|
+
throw new Error('dispatch-planner.composeDispatchEnv: subagentId required');
|
|
253
|
+
}
|
|
254
|
+
const isolation = input.isolation === 'shared' ? 'shared' : 'worktree';
|
|
255
|
+
const parentEnv = (input.parentEnv && typeof input.parentEnv === 'object')
|
|
256
|
+
? input.parentEnv : {};
|
|
257
|
+
const extraEnv = (input.extraEnv && typeof input.extraEnv === 'object'
|
|
258
|
+
&& !Array.isArray(input.extraEnv)) ? input.extraEnv : {};
|
|
259
|
+
|
|
260
|
+
const composed = {
|
|
261
|
+
IJFW_PROJECT_DIR: projectRoot,
|
|
262
|
+
IJFW_PARENT_PROJECT_ROOT: typeof parentEnv.IJFW_PARENT_PROJECT_ROOT === 'string'
|
|
263
|
+
&& parentEnv.IJFW_PARENT_PROJECT_ROOT.length > 0
|
|
264
|
+
? parentEnv.IJFW_PARENT_PROJECT_ROOT : projectRoot,
|
|
265
|
+
IJFW_WAVE_ID: waveId,
|
|
266
|
+
IJFW_SUBAGENT_ID: subagentId,
|
|
267
|
+
IJFW_ISOLATION: isolation,
|
|
268
|
+
};
|
|
269
|
+
if (typeof parentEnv.IJFW_SESSION_ID === 'string' && parentEnv.IJFW_SESSION_ID.length > 0) {
|
|
270
|
+
composed.IJFW_SESSION_ID = parentEnv.IJFW_SESSION_ID;
|
|
271
|
+
}
|
|
272
|
+
for (const [k, v] of Object.entries(extraEnv)) {
|
|
273
|
+
if (v === null || v === undefined) continue;
|
|
274
|
+
composed[k] = String(v);
|
|
275
|
+
}
|
|
276
|
+
// Stable sort for deterministic output ordering.
|
|
277
|
+
return Object.fromEntries(Object.keys(composed).sort().map((k) => [k, composed[k]]));
|
|
278
|
+
}
|
|
279
|
+
|
|
185
280
|
// Glob-aware intersection. Treats `*`/`**` as wildcards so a declaration
|
|
186
281
|
// like `claude/commands/*.md` conflicts with `claude/commands/status.md`.
|
|
187
282
|
// Returns true on any exact match OR glob-vs-literal match.
|
package/src/dream/runner.mjs
CHANGED
|
@@ -38,6 +38,8 @@ import { existsSync, mkdirSync, appendFileSync, readFileSync } from 'node:fs';
|
|
|
38
38
|
import { join, dirname } from 'node:path';
|
|
39
39
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
40
40
|
import { isOnCooldown, markCompleted } from './cooldown.js';
|
|
41
|
+
import { shouldRunNow } from './state-file.js';
|
|
42
|
+
import { runStages } from './stage-runner.js';
|
|
41
43
|
|
|
42
44
|
const __filename = fileURLToPath(import.meta.url);
|
|
43
45
|
const __dirname = dirname(__filename);
|
|
@@ -91,13 +93,24 @@ function log(line) {
|
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
// ---------------------------------------------------------------------------
|
|
94
|
-
//
|
|
96
|
+
// Idle gate (M4 — replaces legacy 4h cooldown with min_idle_minutes
|
|
97
|
+
// gate, default 30 min, override via IJFW_DREAM_MIN_IDLE_MIN).
|
|
98
|
+
// Legacy cooldown.markCompleted() is still called as a final stage so
|
|
99
|
+
// downstream code reading the old marker keeps working.
|
|
95
100
|
// ---------------------------------------------------------------------------
|
|
96
101
|
|
|
97
|
-
|
|
98
|
-
|
|
102
|
+
const MIN_IDLE_MIN = Number(process.env.IJFW_DREAM_MIN_IDLE_MIN || 30);
|
|
103
|
+
if (!shouldRunNow(opts.projectRoot, { min_idle_minutes: MIN_IDLE_MIN })) {
|
|
104
|
+
log(`skip: idle gate <${MIN_IDLE_MIN}min since last run`);
|
|
99
105
|
process.exit(0);
|
|
100
106
|
}
|
|
107
|
+
// Legacy cooldown is now informational only — the new idle gate above is
|
|
108
|
+
// strictly stricter (30 min) than the old 4h. We still consult it as a
|
|
109
|
+
// belt-and-braces signal, but ONLY for visibility in the log; it does not
|
|
110
|
+
// block the run.
|
|
111
|
+
if (isOnCooldown(stateDir)) {
|
|
112
|
+
log(`note: legacy 4h cooldown also active (host=${opts.host}, reason=${opts.reason}) — proceeding under idle gate`);
|
|
113
|
+
}
|
|
101
114
|
|
|
102
115
|
log(`start: host=${opts.host}, reason=${opts.reason}, project=${opts.projectRoot}`);
|
|
103
116
|
|
|
@@ -351,14 +364,37 @@ function safeJournalSummary() {
|
|
|
351
364
|
|
|
352
365
|
(async () => {
|
|
353
366
|
try {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
await
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
367
|
+
// M4 hardening: lift each step into a stage-runner stage so a failure
|
|
368
|
+
// in one stage logs + continues; downstream stages still execute.
|
|
369
|
+
// State file records each stage's status independently.
|
|
370
|
+
const summary = await runStages(opts.projectRoot, [
|
|
371
|
+
{
|
|
372
|
+
name: 'journal_summary',
|
|
373
|
+
run: async () => {
|
|
374
|
+
const s = safeJournalSummary();
|
|
375
|
+
log(`journal: ${s.entries} entries across ${s.sessions} sessions`);
|
|
376
|
+
return s;
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
name: 'tier_promotion',
|
|
381
|
+
run: async () => {
|
|
382
|
+
const out = await runTierPromotion();
|
|
383
|
+
return out || { skipped: 'no-op' };
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
name: 'mark_completed_legacy',
|
|
388
|
+
run: async () => {
|
|
389
|
+
// Preserve the legacy 4h cooldown marker so any downstream code
|
|
390
|
+
// still reading .dream-state.json continues to work.
|
|
391
|
+
const ok = markCompleted(stateDir);
|
|
392
|
+
log(`mark-completed: ${ok ? 'ok' : 'failed (non-fatal)'}`);
|
|
393
|
+
return { ok };
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
]);
|
|
397
|
+
log(`end: completed=${summary.completed.length} failed=${summary.failed.length}`);
|
|
362
398
|
} catch (err) {
|
|
363
399
|
// Defensive: any unexpected throw lands in the log but never
|
|
364
400
|
// surfaces a non-zero exit to the parent hook.
|