@ijfw/memory-server 1.4.4 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/ijfw-memorize +14 -7
- package/fixtures/team/book.json +6 -6
- package/fixtures/team/business.json +146 -20
- package/fixtures/team/content.json +6 -6
- package/fixtures/team/design.json +148 -20
- package/fixtures/team/mixed.json +206 -27
- package/fixtures/team/research.json +146 -20
- package/fixtures/team/software.json +148 -20
- package/fixtures/truncation-corpus/_generate-corpus.js +367 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/snapshots/v-midO-1-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/snapshots/v-midO-2-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/snapshots/v-midO-3-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/snapshots/v-midO-4-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/snapshots/v-midO-5-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/snapshots/v-noEv-1-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/snapshots/v-noEv-2-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/snapshots/v-noEv-3-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/snapshots/v-noEv-4-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/snapshots/v-noEv-5-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/snapshots/v-errT-1-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/snapshots/v-errT-3-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/snapshots/v-errT-5-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/target/.ijfw/state/workflow.json +1 -0
- package/package.json +6 -3
- package/src/active-extension-writer.js +144 -64
- package/src/api-client.js +43 -5
- package/src/audit-roster.js +80 -5
- package/src/blackboard.js +298 -6
- package/src/cli-run.js +33 -5
- package/src/codex-agents.js +96 -5
- package/src/cost/aggregator.js +39 -9
- package/src/cost/pricing.js +57 -0
- package/src/cost/readers/gemini.js +1 -1
- package/src/cross-audit-chunker.js +189 -0
- package/src/cross-dispatcher.js +124 -21
- package/src/cross-orchestrator-cli.js +754 -159
- package/src/cross-orchestrator.js +1065 -17
- package/src/cross-project-search.js +195 -9
- package/src/dashboard-client-waves.html +304 -0
- package/src/dashboard-client.html +5 -1
- package/src/dashboard-server.js +73 -0
- package/src/deploy-alerts.js +150 -0
- package/src/design/iframe-bridge.js +242 -0
- package/src/design-companion.js +144 -0
- package/src/dispatch/checkpoint-cli.js +97 -0
- package/src/dispatch/colon-syntax.js +81 -1
- package/src/dispatch/extension.js +26 -2
- package/src/dispatch/registry-cli.js +4 -1
- package/src/dispatch/wave-cli.js +201 -6
- package/src/dispatch/worktree-cli.js +40 -0
- package/src/dispatch-planner.js +97 -2
- package/src/dream/runner.mjs +47 -11
- package/src/dream/stage-runner.js +40 -0
- package/src/dream/state-file.js +102 -0
- package/src/extension-installer.js +70 -24
- package/src/extension-quota-tracker.js +4 -2
- package/src/extension-registry.js +289 -35
- package/src/feedback-detector.js +26 -0
- package/src/fs-lock.js +259 -7
- package/src/gate-result.js +95 -1
- package/src/hardware-signer.js +4 -2
- package/src/hero-line.js +86 -5
- package/src/intent-router.js +35 -0
- package/src/lib/a11y-contract.js +117 -0
- package/src/lib/atomic-io.js +29 -8
- package/src/lib/cache-keepalive.js +150 -0
- package/src/lib/jsonl-rotation.js +104 -0
- package/src/lib/lighthouse-pillar.js +121 -0
- package/src/lib/llm-call.js +121 -0
- package/src/lib/playwright-baseline.js +205 -0
- package/src/lib/rekor-bridge.js +221 -0
- package/src/lib/repo-map.js +392 -0
- package/src/lib/shasum-verify.js +164 -0
- package/src/lib/sketches-gc.js +132 -0
- package/src/lib/tmp-suffix.js +62 -0
- package/src/lib/ui-review-runner.js +595 -0
- package/src/lib/uispec-drift.js +301 -0
- package/src/lib/uispec-intake.js +381 -0
- package/src/lib/worktree-guards.js +118 -0
- package/src/lib/worktree-recovery.js +100 -0
- package/src/memory/auto-linker.js +267 -0
- package/src/memory/benchmark.js +498 -0
- package/src/memory/dedup.js +126 -0
- package/src/memory/embedding-cache.js +136 -0
- package/src/memory/fact-extractor.js +168 -0
- package/src/memory/fts5.js +65 -1
- package/src/memory/migration-runner.js +6 -1
- package/src/memory/migrations/004-bitemporal.js +91 -0
- package/src/memory/migrations/005-vector-cache.js +61 -0
- package/src/memory/migrations/006-obsidian-graph.js +46 -0
- package/src/memory/migrations/007-skill-telemetry.js +24 -0
- package/src/memory/migrations/008-write-provenance.js +41 -0
- package/src/memory/migrations/009-obsidian-backfill.js +50 -0
- package/src/memory/obsidian-parser.js +152 -0
- package/src/memory/query-dataview.js +86 -0
- package/src/memory/search.js +46 -15
- package/src/memory/temporal.js +529 -0
- package/src/memory/tokenize.js +10 -0
- package/src/memory-facts-handler.js +37 -0
- package/src/memory-feedback.js +260 -2
- package/src/model-refresh.js +292 -0
- package/src/observability/cost-anomaly.js +166 -0
- package/src/observability/evaluator-checkpoint-contract.js +117 -0
- package/src/observability/trace-id.js +163 -0
- package/src/orchestrator/agents-md-blackboard.js +152 -0
- package/src/orchestrator/checkpoint-contract.md +140 -0
- package/src/orchestrator/debug-trident-trigger.js +374 -0
- package/src/orchestrator/debug-trident.js +570 -0
- package/src/orchestrator/merge-block-aware.js +350 -0
- package/src/orchestrator/plan-checker.js +475 -0
- package/src/orchestrator/post-done-runner.js +277 -0
- package/src/orchestrator/review.js +38 -3
- package/src/orchestrator/skill-telemetry-sink.js +29 -0
- package/src/orchestrator/skill-telemetry.js +37 -0
- package/src/orchestrator/state-events.js +459 -0
- package/src/orchestrator/state-sdk.js +1932 -0
- package/src/orchestrator/status-protocol.js +84 -17
- package/src/orchestrator/subagent-telemetry.js +471 -0
- package/src/orchestrator/termination.js +160 -0
- package/src/orchestrator/verification-gate.js +200 -16
- package/src/orchestrator/wave-state.js +332 -23
- package/src/orchestrator/worktree-provision.js +77 -0
- package/src/override-resolver.js +5 -3
- package/src/override-use-registry.js +111 -5
- package/src/receipts.js +36 -4
- package/src/recovery/checkpoint.js +56 -3
- package/src/recovery/code-fixer.js +961 -0
- package/src/recovery/truncation.js +317 -0
- package/src/redactor.js +75 -6
- package/src/runtime-mediator.js +15 -1
- package/src/sanitizer.js +10 -0
- package/src/search-hybrid.js +139 -0
- package/src/server.js +795 -112
- package/src/swarm/worktree.js +27 -4
- package/src/swarm-config.js +102 -17
- package/src/team/domain-templates/book.json +51 -0
- package/src/team/domain-templates/business.json +44 -0
- package/src/team/domain-templates/content.json +50 -0
- package/src/team/domain-templates/design.json +44 -0
- package/src/team/domain-templates/research.json +44 -0
- package/src/team/domain-templates/software.json +40 -0
- package/src/team/generator.js +440 -3
- package/src/team/modify.js +203 -0
- package/src/team/schemas.js +48 -0
- package/src/update-apply.js +19 -3
- package/src/dashboard-charts.js +0 -239
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* plan-checker.js — v1.5.0-major W12-D C14: pre-dispatch plan validation gate.
|
|
3
|
+
*
|
|
4
|
+
* Pure-function library called by the existing `ijfw_state` MCP tool routing
|
|
5
|
+
* (no new MCP tool — cap is full at 12/12; v1.5.0 T13 absorbed the retired
|
|
6
|
+
* `ijfw_subagent_post_done` tool as the `subagent.post-done` verb). Also
|
|
7
|
+
* surfaced in the `ijfw-plan-check` skill as the deterministic pre-dispatch
|
|
8
|
+
* gate.
|
|
9
|
+
*
|
|
10
|
+
* Distilled from /Users/seandonahoe/.claude/agents/gsd-plan-checker.md — extracts
|
|
11
|
+
* the mechanically-checkable rules (the prose-reasoning ones stay in the skill).
|
|
12
|
+
*
|
|
13
|
+
* No I/O, no network — operates on plan text passed in by caller.
|
|
14
|
+
*
|
|
15
|
+
* v1.5.0 audit-MED-work-M2: this module now optionally composes with
|
|
16
|
+
* `dispatch-planner.js::buildManifest` to surface wave-table file-overlap
|
|
17
|
+
* findings at plan-review time instead of dispatch time. Pass
|
|
18
|
+
* `{ checkWaveOverlap: true }` to opt in. Findings are INFO severity by
|
|
19
|
+
* default (advisory — overlap forces worktree-isolation, not failure).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { parsePlan, buildManifest } from '../dispatch-planner.js';
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Constants
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Literal placeholder tokens that must not appear in a plan handed to execute.
|
|
30
|
+
* Case-insensitive match on word boundaries (except the bracketed/angle forms,
|
|
31
|
+
* which are matched literally).
|
|
32
|
+
*/
|
|
33
|
+
const PLACEHOLDER_PATTERNS = [
|
|
34
|
+
{ regex: /\bTBD\b/g, token: 'TBD' },
|
|
35
|
+
{ regex: /\bFIXME\b/g, token: 'FIXME' },
|
|
36
|
+
{ regex: /\bXXX\b/g, token: 'XXX' },
|
|
37
|
+
{ regex: /\[fill me in\]/gi, token: '[fill me in]' },
|
|
38
|
+
{ regex: /<placeholder>/gi, token: '<placeholder>' },
|
|
39
|
+
{ regex: /\?\?\?/g, token: '???' },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Acceptance-criteria signal — any one of these substrings is enough to satisfy
|
|
44
|
+
* "task has acceptance criteria" (intentionally loose; planners write in prose).
|
|
45
|
+
*/
|
|
46
|
+
const ACCEPTANCE_REGEX = /\b(acceptance|done\s*when|criteria|expected)\b/i;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Empty-step detector: a list item or numbered step whose body (after stripping
|
|
50
|
+
* the marker) is under 20 chars of non-whitespace.
|
|
51
|
+
*/
|
|
52
|
+
const EMPTY_STEP_THRESHOLD = 20;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Test-skip contradictions — if a task says it adds tests AND also says to
|
|
56
|
+
* skip them, that's a BLOCK regardless of strict mode.
|
|
57
|
+
*/
|
|
58
|
+
/* eslint-disable security/detect-unsafe-regex --
|
|
59
|
+
* Matches developer-authored plan markdown on local disk (not network input).
|
|
60
|
+
* Word boundaries + short fixed alternations bound the match — no exponential
|
|
61
|
+
* backtracking risk on any input the planner would actually see.
|
|
62
|
+
*/
|
|
63
|
+
const TEST_ADD_REGEX = /\b(add(?:ing)?|write|writing|create|creating)\s+(?:the\s+)?tests?\b/i;
|
|
64
|
+
const TEST_SKIP_REGEX = /\b(skip\s+(?:the\s+)?tests?|tests?\s+not\s+required|no\s+tests?\s+needed)\b/i;
|
|
65
|
+
/* eslint-enable security/detect-unsafe-regex */
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Finding helpers
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @typedef {Object} Finding
|
|
73
|
+
* @property {'BLOCK'|'HIGH'|'WARN'|'INFO'} severity
|
|
74
|
+
* @property {string} code
|
|
75
|
+
* @property {string} message
|
|
76
|
+
* @property {string} [locationHint]
|
|
77
|
+
*/
|
|
78
|
+
|
|
79
|
+
function mkFinding(severity, code, message, locationHint) {
|
|
80
|
+
const f = { severity, code, message };
|
|
81
|
+
if (locationHint) f.locationHint = locationHint;
|
|
82
|
+
return f;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* v1.5.0 T17 (W1 plan-check hard-BLOCK): the canonical set of severities that
|
|
87
|
+
* structurally REFUSE dispatch when emitted by `validatePlan`.
|
|
88
|
+
*
|
|
89
|
+
* The codebase historically used `BLOCK` (this module) and `HIGH` (newer
|
|
90
|
+
* `termination.js` vocabulary — see `mcp-server/src/orchestrator/termination.js`
|
|
91
|
+
* §"FindingSeverity"). The STATE-SDK contract §7 `phase.plan-check` block + the
|
|
92
|
+
* T16 enforcement matrix both name the dispatch-blocking tier as a "HIGH
|
|
93
|
+
* finding". We treat the two labels as synonyms here so that:
|
|
94
|
+
*
|
|
95
|
+
* (a) legacy callers emitting `BLOCK` keep working unchanged, AND
|
|
96
|
+
* (b) any future rule emitting the canonical `HIGH` label also fails dispatch.
|
|
97
|
+
*
|
|
98
|
+
* This is the single source of truth — `phase.plan-check` in state-sdk.js
|
|
99
|
+
* imports the predicate so the gate cannot drift from `validatePlan`'s output.
|
|
100
|
+
*/
|
|
101
|
+
const HIGH_TIER_SEVERITIES = Object.freeze(new Set(['BLOCK', 'HIGH']));
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @param {Finding} finding
|
|
105
|
+
* @returns {boolean}
|
|
106
|
+
*/
|
|
107
|
+
function isHighFinding(finding) {
|
|
108
|
+
return !!finding && HIGH_TIER_SEVERITIES.has(finding.severity);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// Task-block extraction
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* A "task" can show up in three forms across the planners IJFW supports:
|
|
117
|
+
* 1. `## Task <name>` / `## Task: <name>` (markdown heading)
|
|
118
|
+
* 2. `### Task <name>` (third-level heading)
|
|
119
|
+
* 3. `task_id: <id>` (frontmatter-ish, e.g. inside <task> XML blocks)
|
|
120
|
+
*
|
|
121
|
+
* We extract task blocks by splitting on these boundaries. Each block keeps
|
|
122
|
+
* the heading line + everything up to the next task boundary or EOF.
|
|
123
|
+
*
|
|
124
|
+
* @param {string} planText
|
|
125
|
+
* @returns {Array<{header: string, body: string, lineStart: number}>}
|
|
126
|
+
*/
|
|
127
|
+
function extractTaskBlocks(planText) {
|
|
128
|
+
const lines = planText.split('\n');
|
|
129
|
+
const taskHeaderRegex = /^(?:#{2,3})\s+Task\b[:\s]|^\s*task_id\s*:/i;
|
|
130
|
+
const blocks = [];
|
|
131
|
+
let current = null;
|
|
132
|
+
for (let i = 0; i < lines.length; i++) {
|
|
133
|
+
const line = lines[i];
|
|
134
|
+
if (taskHeaderRegex.test(line)) {
|
|
135
|
+
if (current) blocks.push(current);
|
|
136
|
+
current = { header: line.trim(), body: '', lineStart: i + 1 };
|
|
137
|
+
} else if (current) {
|
|
138
|
+
current.body += line + '\n';
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (current) blocks.push(current);
|
|
142
|
+
return blocks;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Extract all task IDs declared in the plan. Looks for `task_id: X`, an `id:`
|
|
147
|
+
* directly under a `## Task` heading, or `## Task <id>` / `### Task <id>`
|
|
148
|
+
* patterns where <id> is a short alnum/dash token.
|
|
149
|
+
*
|
|
150
|
+
* @param {string} planText
|
|
151
|
+
* @returns {Set<string>}
|
|
152
|
+
*/
|
|
153
|
+
function extractTaskIds(planText) {
|
|
154
|
+
const ids = new Set();
|
|
155
|
+
const lines = planText.split('\n');
|
|
156
|
+
for (let i = 0; i < lines.length; i++) {
|
|
157
|
+
const line = lines[i];
|
|
158
|
+
let m;
|
|
159
|
+
if ((m = line.match(/^\s*task_id\s*:\s*([A-Za-z0-9_\-.]+)/i))) {
|
|
160
|
+
ids.add(m[1]);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if ((m = line.match(/^\s*id\s*:\s*([A-Za-z0-9_\-.]+)/i))) {
|
|
164
|
+
// Only count `id:` lines that appear shortly after a Task heading.
|
|
165
|
+
const lookback = lines.slice(Math.max(0, i - 5), i).join('\n');
|
|
166
|
+
if (/^#{2,3}\s+Task\b/im.test(lookback)) ids.add(m[1]);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if ((m = line.match(/^#{2,3}\s+Task[:\s]+([A-Za-z0-9_\-.]+)/i))) {
|
|
170
|
+
ids.add(m[1]);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return ids;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Extract dependency references from a task body. Matches `depends:` and
|
|
178
|
+
* `blocked-by:` (also `blocked_by:` / `depends_on:` — common variants).
|
|
179
|
+
*
|
|
180
|
+
* @param {string} body
|
|
181
|
+
* @returns {string[]}
|
|
182
|
+
*/
|
|
183
|
+
function extractDependencyRefs(body) {
|
|
184
|
+
const refs = [];
|
|
185
|
+
const lines = body.split('\n');
|
|
186
|
+
for (const line of lines) {
|
|
187
|
+
const m = line.match(/^\s*(?:depends(?:_on)?|blocked[-_]by)\s*:\s*(.+)$/i);
|
|
188
|
+
if (!m) continue;
|
|
189
|
+
// Value may be a single id, comma list, or YAML-style ["a", "b"].
|
|
190
|
+
const raw = m[1].trim().replace(/^\[|\]$/g, '');
|
|
191
|
+
const parts = raw.split(/[,\s]+/).map((p) => p.replace(/^["']|["']$/g, '').trim()).filter(Boolean);
|
|
192
|
+
for (const p of parts) refs.push(p);
|
|
193
|
+
}
|
|
194
|
+
return refs;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Detect "empty step" lines inside a task body — bullet/numbered list items
|
|
199
|
+
* whose payload is short and vague.
|
|
200
|
+
*
|
|
201
|
+
* @param {string} body
|
|
202
|
+
* @param {number} bodyLineOffset 1-indexed line number where the body starts in the plan
|
|
203
|
+
* @returns {Array<{text: string, line: number}>}
|
|
204
|
+
*/
|
|
205
|
+
function findEmptySteps(body, bodyLineOffset) {
|
|
206
|
+
const out = [];
|
|
207
|
+
const lines = body.split('\n');
|
|
208
|
+
for (let i = 0; i < lines.length; i++) {
|
|
209
|
+
const line = lines[i];
|
|
210
|
+
const m = line.match(/^\s*(?:[-*+]|\d+\.)\s+(.+?)\s*$/);
|
|
211
|
+
if (!m) continue;
|
|
212
|
+
const payload = m[1].trim();
|
|
213
|
+
// Strip any leading "implement/do/fix" filler and re-measure to catch
|
|
214
|
+
// "implement the thing" style placeholders.
|
|
215
|
+
// eslint-disable-next-line security/detect-unsafe-regex -- short fixed alternation against developer-authored plan markdown on local disk
|
|
216
|
+
const stripped = payload.replace(/^(?:implement|do|fix|handle)\s+(?:the\s+)?/i, '').trim();
|
|
217
|
+
if (stripped.length < EMPTY_STEP_THRESHOLD) {
|
|
218
|
+
out.push({ text: payload, line: bodyLineOffset + i });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return out;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
// Public API
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Validate a plan text against the C14 pre-dispatch ruleset.
|
|
230
|
+
*
|
|
231
|
+
* Findings carry one of three severities:
|
|
232
|
+
* - BLOCK: dispatch must abort
|
|
233
|
+
* - WARN: dispatch may proceed but the orchestrator should surface the issue
|
|
234
|
+
* - INFO: advisory only
|
|
235
|
+
*
|
|
236
|
+
* In `strict: true` mode, WARNs from the placeholder check get promoted to
|
|
237
|
+
* BLOCK (the rest keep their natural severity — the strict-promotion is
|
|
238
|
+
* scoped to placeholders by design, matching the gsd-plan-checker semantics
|
|
239
|
+
* where placeholder text in a "ready to dispatch" plan is a hard failure).
|
|
240
|
+
*
|
|
241
|
+
* @param {string} planText
|
|
242
|
+
* @param {{strict?: boolean, checkWaveOverlap?: boolean}} [opts]
|
|
243
|
+
* @returns {{ok: boolean, findings: Finding[]}}
|
|
244
|
+
*/
|
|
245
|
+
function validatePlan(planText, opts = {}) {
|
|
246
|
+
const strict = !!opts.strict;
|
|
247
|
+
const checkWaveOverlap = !!opts.checkWaveOverlap;
|
|
248
|
+
/** @type {Finding[]} */
|
|
249
|
+
const findings = [];
|
|
250
|
+
|
|
251
|
+
if (typeof planText !== 'string') {
|
|
252
|
+
return {
|
|
253
|
+
ok: false,
|
|
254
|
+
findings: [mkFinding('BLOCK', 'PC-INPUT', 'planText must be a string')],
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ---- Check 1: placeholders ----------------------------------------------
|
|
259
|
+
for (const { regex, token } of PLACEHOLDER_PATTERNS) {
|
|
260
|
+
// reset lastIndex on every call to be safe with the global flag
|
|
261
|
+
regex.lastIndex = 0;
|
|
262
|
+
const matches = planText.match(regex);
|
|
263
|
+
if (matches && matches.length > 0) {
|
|
264
|
+
const sev = strict ? 'BLOCK' : 'WARN';
|
|
265
|
+
findings.push(
|
|
266
|
+
mkFinding(
|
|
267
|
+
sev,
|
|
268
|
+
'PC-PLACEHOLDER',
|
|
269
|
+
`Found ${matches.length} placeholder token(s) "${token}" — plan is not ready for dispatch`,
|
|
270
|
+
),
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ---- Check 2: completeness (must have ≥1 task) --------------------------
|
|
276
|
+
const taskBlocks = extractTaskBlocks(planText);
|
|
277
|
+
if (taskBlocks.length === 0) {
|
|
278
|
+
findings.push(
|
|
279
|
+
mkFinding(
|
|
280
|
+
'BLOCK',
|
|
281
|
+
'PC-NO-TASKS',
|
|
282
|
+
'Plan contains 0 tasks (expected ≥1 `## Task` / `### Task` / `task_id:` block)',
|
|
283
|
+
),
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ---- Check 3: each task has acceptance criteria -------------------------
|
|
288
|
+
for (const block of taskBlocks) {
|
|
289
|
+
if (!ACCEPTANCE_REGEX.test(block.body) && !ACCEPTANCE_REGEX.test(block.header)) {
|
|
290
|
+
findings.push(
|
|
291
|
+
mkFinding(
|
|
292
|
+
'WARN',
|
|
293
|
+
'PC-NO-ACCEPTANCE',
|
|
294
|
+
`Task block missing acceptance criteria (looked for: acceptance|done when|criteria|expected)`,
|
|
295
|
+
`line ${block.lineStart}: ${block.header}`,
|
|
296
|
+
),
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ---- Check 4: empty / under-specified steps -----------------------------
|
|
302
|
+
for (const block of taskBlocks) {
|
|
303
|
+
// body starts on the line after the header
|
|
304
|
+
const empties = findEmptySteps(block.body, block.lineStart + 1);
|
|
305
|
+
for (const e of empties) {
|
|
306
|
+
findings.push(
|
|
307
|
+
mkFinding(
|
|
308
|
+
'WARN',
|
|
309
|
+
'PC-EMPTY-STEP',
|
|
310
|
+
`Step is too short / vague to be actionable: "${e.text}"`,
|
|
311
|
+
`line ${e.line}`,
|
|
312
|
+
),
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ---- Check 5: dependency sanity (dangling refs) -------------------------
|
|
318
|
+
const declaredIds = extractTaskIds(planText);
|
|
319
|
+
// Build dependency adjacency map keyed by the FIRST task_id we can derive
|
|
320
|
+
// from each block header (mirrors extractTaskIds heuristic). We collect
|
|
321
|
+
// refs first so the cycle pass below can use the same map.
|
|
322
|
+
/** @type {Map<string, string[]>} */
|
|
323
|
+
const depGraph = new Map();
|
|
324
|
+
/** @type {Map<string, string>} */
|
|
325
|
+
const blockHeaderById = new Map();
|
|
326
|
+
/** @type {Map<string, number>} */
|
|
327
|
+
const blockLineById = new Map();
|
|
328
|
+
for (const block of taskBlocks) {
|
|
329
|
+
// Extract the task id this block declares (best-effort; mirrors
|
|
330
|
+
// extractTaskIds). Without an id we can't participate in cycle detection
|
|
331
|
+
// -- skip silently rather than fabricate a synthetic id.
|
|
332
|
+
let blockId = null;
|
|
333
|
+
const idMatch = block.header.match(/^#{2,3}\s+Task[:\s]+([A-Za-z0-9_\-.]+)/i)
|
|
334
|
+
|| block.body.match(/^\s*task_id\s*:\s*([A-Za-z0-9_\-.]+)/im)
|
|
335
|
+
|| block.body.match(/^\s*id\s*:\s*([A-Za-z0-9_\-.]+)/im);
|
|
336
|
+
if (idMatch) blockId = idMatch[1];
|
|
337
|
+
|
|
338
|
+
const refs = extractDependencyRefs(block.body);
|
|
339
|
+
for (const ref of refs) {
|
|
340
|
+
if (!declaredIds.has(ref)) {
|
|
341
|
+
findings.push(
|
|
342
|
+
mkFinding(
|
|
343
|
+
'BLOCK',
|
|
344
|
+
'PC-DANGLING-DEP',
|
|
345
|
+
`Task references unknown dependency "${ref}" (not declared as a task_id in this plan)`,
|
|
346
|
+
`line ${block.lineStart}: ${block.header}`,
|
|
347
|
+
),
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (blockId) {
|
|
352
|
+
depGraph.set(blockId, refs.filter((r) => declaredIds.has(r)));
|
|
353
|
+
blockHeaderById.set(blockId, block.header);
|
|
354
|
+
blockLineById.set(blockId, block.lineStart);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ---- Check 5b: dependency cycles (DFS three-coloring) -------------------
|
|
359
|
+
// v1.5.0 audit-LOW-work-L5: catch dep cycles at plan-review time so they
|
|
360
|
+
// don't deadlock the executor at fan-out. Standard DFS with white/gray/black
|
|
361
|
+
// coloring; a back-edge into a `gray` node is a cycle.
|
|
362
|
+
// BLOCK severity because a cycle is a hard structural break, not a smell.
|
|
363
|
+
{
|
|
364
|
+
const WHITE = 0, GRAY = 1, BLACK = 2;
|
|
365
|
+
const color = new Map();
|
|
366
|
+
for (const id of depGraph.keys()) color.set(id, WHITE);
|
|
367
|
+
/** @type {Set<string>} */
|
|
368
|
+
const cyclesReported = new Set();
|
|
369
|
+
const visit = (id, stack) => {
|
|
370
|
+
color.set(id, GRAY);
|
|
371
|
+
stack.push(id);
|
|
372
|
+
const neighbours = depGraph.get(id) || [];
|
|
373
|
+
for (const n of neighbours) {
|
|
374
|
+
if (!depGraph.has(n)) continue; // dangling — already reported above
|
|
375
|
+
const c = color.get(n) ?? WHITE;
|
|
376
|
+
if (c === GRAY) {
|
|
377
|
+
// Found a back-edge: extract the cycle from the stack.
|
|
378
|
+
const startIdx = stack.indexOf(n);
|
|
379
|
+
const cycle = stack.slice(startIdx).concat(n);
|
|
380
|
+
const key = [...cycle].sort().join('|');
|
|
381
|
+
if (!cyclesReported.has(key)) {
|
|
382
|
+
cyclesReported.add(key);
|
|
383
|
+
findings.push(
|
|
384
|
+
mkFinding(
|
|
385
|
+
'BLOCK',
|
|
386
|
+
'PC-DEP-CYCLE',
|
|
387
|
+
`Dependency cycle detected: ${cycle.join(' -> ')}`,
|
|
388
|
+
blockHeaderById.has(n) ? `line ${blockLineById.get(n)}: ${blockHeaderById.get(n)}` : undefined,
|
|
389
|
+
),
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
} else if (c === WHITE) {
|
|
393
|
+
visit(n, stack);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
stack.pop();
|
|
397
|
+
color.set(id, BLACK);
|
|
398
|
+
};
|
|
399
|
+
for (const id of depGraph.keys()) {
|
|
400
|
+
if ((color.get(id) ?? WHITE) === WHITE) visit(id, []);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ---- Check 6: no test-skip contradiction --------------------------------
|
|
405
|
+
for (const block of taskBlocks) {
|
|
406
|
+
const both = TEST_ADD_REGEX.test(block.body) && TEST_SKIP_REGEX.test(block.body);
|
|
407
|
+
if (both) {
|
|
408
|
+
findings.push(
|
|
409
|
+
mkFinding(
|
|
410
|
+
'BLOCK',
|
|
411
|
+
'PC-TEST-SKIP-CONTRADICTION',
|
|
412
|
+
'Task says to add tests AND to skip tests in the same block — pick one',
|
|
413
|
+
`line ${block.lineStart}: ${block.header}`,
|
|
414
|
+
),
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// ---- Check 7: wave-table file-overlap (M2 composition, opt-in) ---------
|
|
420
|
+
// Runs `buildManifest` on the plan to surface any sub-waves that would be
|
|
421
|
+
// routed to worktree isolation because they declare overlapping `Files:`
|
|
422
|
+
// lines. These are INFO findings, not BLOCK — overlap is RECOVERABLE
|
|
423
|
+
// (dispatch-planner just isolates), but planners benefit from seeing the
|
|
424
|
+
// overlap during review instead of being surprised at dispatch time.
|
|
425
|
+
if (checkWaveOverlap) {
|
|
426
|
+
try {
|
|
427
|
+
const subwaves = parsePlan(planText);
|
|
428
|
+
const manifest = buildManifest(subwaves);
|
|
429
|
+
for (const m of manifest) {
|
|
430
|
+
if (m.mode === 'worktree' && m.overlaps_with && m.overlaps_with.length > 0) {
|
|
431
|
+
findings.push(
|
|
432
|
+
mkFinding(
|
|
433
|
+
'INFO',
|
|
434
|
+
'PC-WAVE-OVERLAP',
|
|
435
|
+
`Sub-wave "${m.id}" overlaps file(s) with peer(s): ${m.overlaps_with.join(', ')} — will be dispatched to worktree isolation`,
|
|
436
|
+
`sub-wave ${m.id} (wave ${m.wave})`,
|
|
437
|
+
),
|
|
438
|
+
);
|
|
439
|
+
} else if (m.mode === 'worktree' && m.reason === 'no-files-declared') {
|
|
440
|
+
findings.push(
|
|
441
|
+
mkFinding(
|
|
442
|
+
'INFO',
|
|
443
|
+
'PC-WAVE-NO-FILES',
|
|
444
|
+
`Sub-wave "${m.id}" declares no Files: line — will default to worktree isolation`,
|
|
445
|
+
`sub-wave ${m.id} (wave ${m.wave})`,
|
|
446
|
+
),
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
} catch {
|
|
451
|
+
// dispatch-planner is best-effort here; a parse failure is not a
|
|
452
|
+
// plan-check failure — fall through silently.
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// v1.5.0 T17 (W1 plan-check hard-BLOCK): a finding in `HIGH_TIER_SEVERITIES`
|
|
457
|
+
// (BLOCK or HIGH — see comment on `HIGH_TIER_SEVERITIES`) is structurally
|
|
458
|
+
// dispatch-blocking. `strict` mode promotes placeholder WARNs to BLOCKs (the
|
|
459
|
+
// historical behaviour); a HIGH-tier finding from any check — strict or not —
|
|
460
|
+
// makes `ok=false` and the `phase.plan-check` verb refuses pre-dispatch.
|
|
461
|
+
const ok = !findings.some(isHighFinding);
|
|
462
|
+
return { ok, findings };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export {
|
|
466
|
+
validatePlan,
|
|
467
|
+
// exported for tests / power users:
|
|
468
|
+
extractTaskBlocks,
|
|
469
|
+
extractTaskIds,
|
|
470
|
+
extractDependencyRefs,
|
|
471
|
+
findEmptySteps,
|
|
472
|
+
PLACEHOLDER_PATTERNS,
|
|
473
|
+
HIGH_TIER_SEVERITIES,
|
|
474
|
+
isHighFinding,
|
|
475
|
+
};
|