@ijfw/memory-server 1.4.3 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fixtures/truncation-corpus/_generate-corpus.js +367 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/snapshots/v-midO-1-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/snapshots/v-midO-2-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/snapshots/v-midO-3-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/snapshots/v-midO-4-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/snapshots/v-midO-5-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/snapshots/v-noEv-1-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/snapshots/v-noEv-2-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/snapshots/v-noEv-3-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/snapshots/v-noEv-4-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/snapshots/v-noEv-5-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/snapshots/v-errT-1-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/snapshots/v-errT-3-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/snapshots/v-errT-5-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/target/.ijfw/state/workflow.json +1 -0
- package/package.json +1 -1
- package/src/active-extension-writer.js +144 -64
- package/src/api-client.js +43 -5
- package/src/audit-roster.js +80 -5
- package/src/blackboard.js +298 -6
- package/src/cli-run.js +33 -5
- package/src/codex-agents.js +96 -5
- package/src/cost/aggregator.js +39 -9
- package/src/cost/pricing.js +57 -0
- package/src/cost/readers/gemini.js +1 -1
- package/src/cross-audit-chunker.js +189 -0
- package/src/cross-dispatcher.js +124 -21
- package/src/cross-orchestrator-cli.js +550 -14
- package/src/cross-orchestrator.js +1171 -10
- package/src/cross-project-search.js +195 -9
- package/src/dashboard-client-planning.html +273 -0
- package/src/dashboard-client-waves.html +304 -0
- package/src/dashboard-client.html +17 -2
- package/src/dashboard-server.js +152 -0
- package/src/deploy-alerts.js +150 -0
- package/src/design/iframe-bridge.js +242 -0
- package/src/design-companion.js +144 -0
- package/src/dispatch/checkpoint-cli.js +97 -0
- package/src/dispatch/colon-syntax.js +81 -1
- package/src/dispatch/extension.js +27 -1
- package/src/dispatch/registry-cli.js +4 -1
- package/src/dispatch/wave-cli.js +323 -0
- package/src/dispatch/worktree-cli.js +40 -0
- package/src/dispatch-planner.js +97 -2
- package/src/dream/runner.mjs +47 -11
- package/src/dream/stage-runner.js +40 -0
- package/src/dream/state-file.js +102 -0
- package/src/extension-installer.js +70 -24
- package/src/extension-quota-tracker.js +4 -2
- package/src/extension-registry.js +289 -35
- package/src/feedback-detector.js +26 -0
- package/src/fs-lock.js +259 -7
- package/src/gate-result.js +95 -1
- package/src/hero-line.js +86 -5
- package/src/intent-router.js +35 -0
- package/src/lib/a11y-contract.js +117 -0
- package/src/lib/atomic-io.js +29 -8
- package/src/lib/cache-keepalive.js +150 -0
- package/src/lib/jsonl-rotation.js +104 -0
- package/src/lib/lighthouse-pillar.js +121 -0
- package/src/lib/llm-call.js +121 -0
- package/src/lib/playwright-baseline.js +205 -0
- package/src/lib/rekor-bridge.js +221 -0
- package/src/lib/repo-map.js +392 -0
- package/src/lib/shasum-verify.js +164 -0
- package/src/lib/sketches-gc.js +132 -0
- package/src/lib/tmp-suffix.js +62 -0
- package/src/lib/ui-review-runner.js +554 -0
- package/src/lib/uispec-drift.js +301 -0
- package/src/lib/uispec-intake.js +381 -0
- package/src/lib/worktree-guards.js +118 -0
- package/src/lib/worktree-recovery.js +100 -0
- package/src/memory/auto-linker.js +152 -0
- package/src/memory/benchmark.js +498 -0
- package/src/memory/dedup.js +126 -0
- package/src/memory/embedding-cache.js +136 -0
- package/src/memory/fact-extractor.js +168 -0
- package/src/memory/fts5.js +65 -1
- package/src/memory/migrations/004-bitemporal.js +91 -0
- package/src/memory/migrations/005-vector-cache.js +61 -0
- package/src/memory/migrations/006-obsidian-graph.js +46 -0
- package/src/memory/migrations/007-skill-telemetry.js +24 -0
- package/src/memory/migrations/008-write-provenance.js +41 -0
- package/src/memory/obsidian-parser.js +91 -0
- package/src/memory/query-dataview.js +86 -0
- package/src/memory/search.js +10 -0
- package/src/memory/temporal.js +529 -0
- package/src/memory/tokenize.js +10 -0
- package/src/memory-facts-handler.js +37 -0
- package/src/memory-feedback.js +260 -2
- package/src/model-refresh.js +292 -0
- package/src/observability/cost-anomaly.js +166 -0
- package/src/observability/evaluator-checkpoint-contract.js +117 -0
- package/src/observability/trace-id.js +163 -0
- package/src/orchestrator/agents-md-blackboard.js +152 -0
- package/src/orchestrator/checkpoint-contract.md +140 -0
- package/src/orchestrator/debug-trident.js +570 -0
- package/src/orchestrator/merge-block-aware.js +350 -0
- package/src/orchestrator/plan-checker.js +475 -0
- package/src/orchestrator/post-done-runner.js +249 -0
- package/src/orchestrator/review.js +136 -0
- package/src/orchestrator/runtime-loop.js +430 -0
- package/src/orchestrator/skill-telemetry-sink.js +29 -0
- package/src/orchestrator/skill-telemetry.js +37 -0
- package/src/orchestrator/state-events.js +459 -0
- package/src/orchestrator/state-sdk.js +1764 -0
- package/src/orchestrator/status-protocol.js +235 -0
- package/src/orchestrator/subagent-telemetry.js +452 -0
- package/src/orchestrator/termination.js +160 -0
- package/src/orchestrator/verification-gate.js +281 -0
- package/src/orchestrator/wave-state.js +564 -0
- package/src/orchestrator/worktree-provision.js +77 -0
- package/src/override-use-registry.js +111 -5
- package/src/receipts.js +36 -4
- package/src/recovery/checkpoint.js +56 -3
- package/src/recovery/code-fixer.js +656 -0
- package/src/recovery/truncation.js +317 -0
- package/src/redactor.js +75 -6
- package/src/runtime-mediator.js +15 -0
- package/src/sanitizer.js +10 -0
- package/src/search-hybrid.js +139 -0
- package/src/server.js +603 -59
- package/src/swarm/worktree.js +27 -4
- package/src/swarm-config.js +113 -12
- package/src/team/domain-templates/book.json +51 -0
- package/src/team/domain-templates/business.json +41 -0
- package/src/team/domain-templates/content.json +50 -0
- package/src/team/domain-templates/design.json +44 -0
- package/src/team/domain-templates/research.json +41 -0
- package/src/team/domain-templates/software.json +40 -0
- package/src/team/generator.js +278 -3
- package/src/team/modify.js +203 -0
- package/src/team/schemas.js +48 -0
- package/src/update-apply.js +19 -3
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dispatch/wave-cli.js — IJFW v1.4.4 / N9 wave-status CLI handlers.
|
|
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
|
+
* - wave-status [<id>|latest]
|
|
10
|
+
* - wave-list
|
|
11
|
+
*
|
|
12
|
+
* Reads via mcp-server/src/orchestrator/wave-state.js (W10-A0). Read-only,
|
|
13
|
+
* snapshot-based per lock-in #31 — no daemon, no subscriptions.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { readdir, stat, readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
|
|
19
|
+
import { readWaveState } from '../orchestrator/wave-state.js';
|
|
20
|
+
import { drainCheckpoints } from '../orchestrator/subagent-telemetry.js';
|
|
21
|
+
|
|
22
|
+
const WAVE_DIR_PREFIX = 'wave-';
|
|
23
|
+
|
|
24
|
+
function tokenize(args) {
|
|
25
|
+
if (Array.isArray(args)) return args.filter((x) => x !== undefined && x !== null);
|
|
26
|
+
if (typeof args !== 'string') return [];
|
|
27
|
+
return args.split(/\s+/).filter(Boolean);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function listWaveEntries(projectRoot) {
|
|
31
|
+
const ijfwDir = join(projectRoot, '.ijfw');
|
|
32
|
+
let entries = [];
|
|
33
|
+
try {
|
|
34
|
+
entries = await readdir(ijfwDir, { withFileTypes: true });
|
|
35
|
+
} catch (err) {
|
|
36
|
+
if (err.code === 'ENOENT') return [];
|
|
37
|
+
throw err;
|
|
38
|
+
}
|
|
39
|
+
const waves = [];
|
|
40
|
+
for (const ent of entries) {
|
|
41
|
+
if (!ent.isDirectory() || !ent.name.startsWith(WAVE_DIR_PREFIX)) continue;
|
|
42
|
+
const id = ent.name.slice(WAVE_DIR_PREFIX.length);
|
|
43
|
+
if (!id) continue;
|
|
44
|
+
const dir = join(ijfwDir, ent.name);
|
|
45
|
+
let mtimeMs = 0;
|
|
46
|
+
try {
|
|
47
|
+
const s = await stat(dir);
|
|
48
|
+
mtimeMs = s.mtimeMs;
|
|
49
|
+
} catch {
|
|
50
|
+
// tolerate vanished dirs
|
|
51
|
+
}
|
|
52
|
+
waves.push({ id, dir, mtimeMs });
|
|
53
|
+
}
|
|
54
|
+
return waves;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function resolveLatestWaveId(projectRoot) {
|
|
58
|
+
const waves = await listWaveEntries(projectRoot);
|
|
59
|
+
if (waves.length === 0) return null;
|
|
60
|
+
waves.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
61
|
+
return waves[0].id;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function renderStateForTerminal({ waveId, frontmatter, body }) {
|
|
65
|
+
const lines = [];
|
|
66
|
+
lines.push(`Wave: ${waveId}`);
|
|
67
|
+
for (const [key, val] of Object.entries(frontmatter || {})) {
|
|
68
|
+
if (key === 'wave_id') continue;
|
|
69
|
+
if (Array.isArray(val)) {
|
|
70
|
+
lines.push(`${key}: [${val.join(', ')}]`);
|
|
71
|
+
} else {
|
|
72
|
+
lines.push(`${key}: ${val}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (body && body.trim()) {
|
|
76
|
+
lines.push('');
|
|
77
|
+
lines.push('--- notes ---');
|
|
78
|
+
lines.push(body.trim());
|
|
79
|
+
}
|
|
80
|
+
return lines.join('\n');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const handlers = {
|
|
84
|
+
'wave-status': async (args, ctx) => {
|
|
85
|
+
const tokens = tokenize(args);
|
|
86
|
+
const projectRoot = (ctx && ctx.projectRoot) || process.cwd();
|
|
87
|
+
let waveId = tokens[0];
|
|
88
|
+
if (!waveId || waveId === 'latest') {
|
|
89
|
+
waveId = await resolveLatestWaveId(projectRoot);
|
|
90
|
+
if (!waveId) {
|
|
91
|
+
return { ok: false, output: 'No waves found in .ijfw/wave-*/' };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const state = await readWaveState(waveId, projectRoot);
|
|
95
|
+
if (!state) {
|
|
96
|
+
return { ok: false, output: `Wave ${waveId} not found` };
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
ok: true,
|
|
100
|
+
output: renderStateForTerminal({
|
|
101
|
+
waveId,
|
|
102
|
+
frontmatter: state.frontmatter,
|
|
103
|
+
body: state.body,
|
|
104
|
+
}),
|
|
105
|
+
};
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
'wave-list': async (_args, ctx) => {
|
|
109
|
+
const projectRoot = (ctx && ctx.projectRoot) || process.cwd();
|
|
110
|
+
const waves = await listWaveEntries(projectRoot);
|
|
111
|
+
if (waves.length === 0) {
|
|
112
|
+
return { ok: true, output: '(no waves)' };
|
|
113
|
+
}
|
|
114
|
+
waves.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
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];
|
|
120
|
+
const status = state?.frontmatter?.status ?? '?';
|
|
121
|
+
const createdAt = state?.frontmatter?.created_at ?? '';
|
|
122
|
+
return `${id}\t${status}\t${createdAt}`;
|
|
123
|
+
});
|
|
124
|
+
return { ok: true, output: rows.join('\n') };
|
|
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
|
+
},
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
export const subcommandHelp = {
|
|
315
|
+
'wave-status': 'wave-status [<id>|latest] — print live state of a wave',
|
|
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)',
|
|
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.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// IJFW v1.5.0 -- per-stage error-isolated dream runner.
|
|
2
|
+
//
|
|
3
|
+
// Wayland's "best-effort staged" pattern: a failure in one stage logs and
|
|
4
|
+
// continues; downstream stages still execute. The state file records each
|
|
5
|
+
// stage's status so a future run (or operator) can see what actually
|
|
6
|
+
// happened, and which stages need re-execution.
|
|
7
|
+
//
|
|
8
|
+
// Lift from sibling project Wayland:
|
|
9
|
+
// crates/wcore-memory/src/consolidate.rs ConsolidationEngine::run
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
markRunStart, markStageStarted, markStageCompleted,
|
|
13
|
+
markStageFailed, markRunCompleted,
|
|
14
|
+
} from './state-file.js';
|
|
15
|
+
|
|
16
|
+
export async function runStages(root, stages) {
|
|
17
|
+
markRunStart(root);
|
|
18
|
+
const completed = [];
|
|
19
|
+
const failed = [];
|
|
20
|
+
for (const stage of stages) {
|
|
21
|
+
if (!stage || typeof stage.name !== 'string' || typeof stage.run !== 'function') {
|
|
22
|
+
failed.push({ name: String(stage?.name), reason: 'invalid_stage_definition' });
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
markStageStarted(root, stage.name);
|
|
26
|
+
try {
|
|
27
|
+
const extras = (await stage.run()) || {};
|
|
28
|
+
markStageCompleted(root, stage.name, extras);
|
|
29
|
+
completed.push({ name: stage.name, extras });
|
|
30
|
+
} catch (e) {
|
|
31
|
+
markStageFailed(root, stage.name, e?.message || String(e));
|
|
32
|
+
failed.push({ name: stage.name, reason: e?.message || String(e) });
|
|
33
|
+
// CONTINUE -- per-stage isolation.
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
markRunCompleted(root);
|
|
37
|
+
return { ok: failed.length === 0, completed, failed };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default { runStages };
|