@deksden-com/dd-flow-cli 0.4.2 → 0.6.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/CHANGELOG.md +33 -4
- package/README.md +2 -2
- package/dist/build-info.json +6 -6
- package/dist/cli/help.js +16 -4
- package/dist/cli/run-cli.js +42 -17
- package/dist/domain/flow-contract.js +82 -3
- package/dist/domain/session-coverage.js +88 -0
- package/dist/domain/validation.js +56 -28
- package/dist/protocol/local-files.js +1 -16
- package/dist/schemas/code-stage-report.schema.json +2 -2
- package/dist/schemas/flow-contract.schema.json +152 -94
- package/dist/schemas/flow-run.schema.json +129 -23
- package/dist/schemas/mb-upgrade-review-data.schema.json +2 -2
- package/dist/schemas/memorybank-permissions-preflight.schema.json +13 -73
- package/dist/schemas/merge-stage-report.schema.json +2 -2
- package/dist/schemas/plan-stage-report.schema.json +38 -335
- package/dist/schemas/project-flow-pack-manifest.schema.json +4 -4
- package/dist/schemas/protocol-plan.schema.json +197 -0
- package/dist/schemas/release-impact.schema.json +9 -5
- package/dist/schemas/session-usage.schema.json +16 -0
- package/dist/schemas/stage-finish-input.schema.json +20 -0
- package/dist/schemas/stage-prompt.schema.json +35 -0
- package/dist/schemas/stage-report.schema.json +20 -0
- package/dist/schemas/stage-start-response.schema.json +30 -0
- package/dist/schemas/timeline-event.schema.json +29 -0
- package/dist/schemas/worktrunk-workspace.schema.json +19 -0
- package/dist/services/branch-context.js +9 -4
- package/dist/services/cleanup.js +77 -0
- package/dist/services/dashboard.js +51 -26
- package/dist/services/engines.js +84 -18
- package/dist/services/hooks.js +101 -250
- package/dist/services/memory-permissions.js +77 -69
- package/dist/services/migrations.js +1 -1
- package/dist/services/plan-runtime.js +124 -0
- package/dist/services/plans.js +22 -84
- package/dist/services/projects.js +2 -1
- package/dist/services/prompts.js +26 -21
- package/dist/services/protocols.js +29 -25
- package/dist/services/run-projection.js +93 -13
- package/dist/services/runs.js +128 -61
- package/dist/services/schema-validation.js +168 -7
- package/dist/services/sessions.js +97 -73
- package/dist/services/stage-lifecycle.js +737 -0
- package/dist/services/tooling.js +285 -0
- package/dist/services/usage.js +183 -30
- package/dist/services/version-status.js +1 -1
- package/dist/services/worktrees.js +88 -39
- package/dist/storage/database.js +72 -30
- package/dist/storage/paths.js +0 -9
- package/package.json +14 -13
- package/tools/worktrunk-manifest.json +34 -0
- package/dist/schemas/flow-run-index-v3.schema.json +0 -203
- package/dist/schemas/flow-run-index.schema.json +0 -175
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @deksden-com/dd-flow-cli
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 47cd270: Add SPC-006 bootstrap stage start/context packets and sealed stage finish, while repairing Memory Bank upgrade stage routing, Codex hook event identity, PATH-stable per-home hook installation, canonical Codex session registration, session coverage projections, terminal orchestrator closure, and safe reconciliation of runs whose legacy authority and workspace are gone.
|
|
8
|
+
|
|
9
|
+
## 0.5.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 43bf9c2: Cut over runtime execution to the canonical SQLite-backed flow contract with a single PreToolUse binding, canonical plan progress, resumable worker coverage, and validated `stage-report.json`, `stage-report.md`, and `stage-report.html` artifacts.
|
|
14
|
+
|
|
3
15
|
## 0.4.2
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
|
@@ -8,19 +20,36 @@
|
|
|
8
20
|
|
|
9
21
|
## 0.4.1
|
|
10
22
|
|
|
23
|
+
> Release-accounting correction: the entries below were already present in
|
|
24
|
+
> the published `0.4.1` tarball built from `6f85c04`, but their pending
|
|
25
|
+
> Changesets had not been consumed.
|
|
26
|
+
|
|
27
|
+
### Minor Changes
|
|
28
|
+
|
|
29
|
+
- Add `dd-flow prompt render` for deterministic RUN-local worker prompts,
|
|
30
|
+
including validated semantic context and checked `run://` reads.
|
|
31
|
+
|
|
11
32
|
### Patch Changes
|
|
12
33
|
|
|
13
34
|
- Preserve `--project-root` through engine routing, compatibility preflight, protocol services, nested lookups, merge-queue commands, and post-mutation dashboard refresh. Duplicate rootless protocol ids fail closed without side effects, while successful mutations remain successful when an optional dashboard projection fails.
|
|
14
35
|
|
|
15
36
|
- Include production dependencies in installed engine snapshots, so a routed CLI works outside the source checkout.
|
|
16
37
|
|
|
17
|
-
|
|
38
|
+
- Expand `dd-flow run timeline` with stage durations, session roster, usage
|
|
39
|
+
coverage, artifact references and repeatable `--hide` controls.
|
|
18
40
|
|
|
19
|
-
|
|
41
|
+
- Update canonical Memory Bank resolution for the `mb-upgrade` `05-review`
|
|
42
|
+
stage instead of requiring a separate top-level `mb-upgrade-review.md`.
|
|
43
|
+
|
|
44
|
+
- Make typed `PRT-*` and `RUN-*` identity project-scoped and monotonic across
|
|
45
|
+
registered projects, migrate legacy global keys without renumbering, and
|
|
46
|
+
ignore legacy date-based protocol ids during allocation.
|
|
20
47
|
|
|
21
|
-
-
|
|
48
|
+
- Add source-aware flow-flag snapshots, provenance revisions, grouped session
|
|
49
|
+
coverage and privacy-safe timing/usage projections.
|
|
22
50
|
|
|
23
|
-
-
|
|
51
|
+
- Validate the complete flow-contract catalog before mutable RUN/protocol
|
|
52
|
+
starts and expose exact read-only diagnostics for invalid catalogs.
|
|
24
53
|
|
|
25
54
|
## 0.4.0
|
|
26
55
|
|
package/README.md
CHANGED
|
@@ -75,10 +75,10 @@ dd-flow protocol register HANDSHAKE-001 --project-root "$PWD" --json
|
|
|
75
75
|
dd-flow protocol status PRT-HANDSHAKE-001 --project-root "$PWD" --json
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
Inspect the canonical plan and update its SQLite progress projection:
|
|
79
79
|
|
|
80
80
|
```bash
|
|
81
|
-
dd-flow plan
|
|
81
|
+
dd-flow plan status PRT-HANDSHAKE-001 --project-root "$PWD" --json
|
|
82
82
|
dd-flow plan item start PRT-HANDSHAKE-001 P1 --project-root "$PWD" --json
|
|
83
83
|
dd-flow plan item done PRT-HANDSHAKE-001 P1 --project-root "$PWD" --summary "Implemented" --evidence "pnpm test" --json
|
|
84
84
|
```
|
package/dist/build-info.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"cli_package": "@deksden-com/dd-flow-cli",
|
|
3
|
-
"cli_version": "0.
|
|
4
|
-
"cli_commit": "
|
|
5
|
-
"built_at": "2026-08-
|
|
3
|
+
"cli_version": "0.6.0",
|
|
4
|
+
"cli_commit": "22cf9c8f5293168dae76212c9a4b74b90a1efeab",
|
|
5
|
+
"built_at": "2026-08-12T05:09:56.163Z",
|
|
6
6
|
"built_with_canon": {
|
|
7
|
-
"version": "
|
|
8
|
-
"commit": "
|
|
9
|
-
"flow_contract": "dd-flow-canonical-2026-
|
|
7
|
+
"version": "3.1.0",
|
|
8
|
+
"commit": "c201167b896a039ff4881d6d816b9735fba2f799",
|
|
9
|
+
"flow_contract": "dd-flow-canonical-2026-08",
|
|
10
10
|
"repo_root": "/Users/deksden/Documents/_Projects/dd-memorybank",
|
|
11
11
|
"memorybank_root": "/Users/deksden/Documents/_Projects/dd-memorybank/.memory-bank",
|
|
12
12
|
"flow_root": "/Users/deksden/Documents/_Projects/dd-memorybank/.memory-bank/dd-flow",
|
package/dist/cli/help.js
CHANGED
|
@@ -21,8 +21,9 @@ Core commands:
|
|
|
21
21
|
migration impact/plan/report/verify Assess release impact and prepare mb-upgrade migration evidence.
|
|
22
22
|
cleanup scan/apply Detect and repair stale local runtime state.
|
|
23
23
|
run start/status/list/timeline/usage/flags Track concrete flow executions, timing, source-aware usage and RUN-local flow flags.
|
|
24
|
+
stage start/finish Run the canonical two-command stage lifecycle and generate evidence.
|
|
24
25
|
id next Preview next typed ids for protocol or run allocation.
|
|
25
|
-
plan
|
|
26
|
+
plan status/item Inspect canonical plans and update SQLite progress.
|
|
26
27
|
prompt render Render one bounded, RUN-local worker launch prompt.
|
|
27
28
|
lane workspace/lock/status Manage shared workspace lanes and leases.
|
|
28
29
|
merge status/one-shot/bundle Inspect, claim, or complete merge work from the current session.
|
|
@@ -142,7 +143,7 @@ Usage:
|
|
|
142
143
|
dd-flow id next --type protocol|run --project-root <root> --slug <slug> --json
|
|
143
144
|
dd-flow run status <RUN-ID|RUN-short-id> --project-root <root> --json
|
|
144
145
|
dd-flow run list --project-root <root> --json
|
|
145
|
-
dd-flow run timeline <RUN-ID|RUN-short-id> --project-root <root>
|
|
146
|
+
dd-flow run timeline <RUN-ID|RUN-short-id> --project-root <root> --json
|
|
146
147
|
dd-flow run usage <RUN-ID|RUN-short-id> --project-root <root> [--group-by session|role|stage|aspect|plan-item] --json
|
|
147
148
|
dd-flow run flags status <RUN-ID|RUN-short-id> --project-root <root> --json
|
|
148
149
|
dd-flow run flags revise <RUN-ID|RUN-short-id> --project-root <root> --expected-revision <n> --idempotency-key <key> [--preset <name>] [--flag key=value] [--flag key=value] [--allow-downgrade --reason <text>] --json
|
|
@@ -150,13 +151,24 @@ Usage:
|
|
|
150
151
|
dd-flow run complete-stage <RUN-ID|RUN-short-id> --project-root <root> --stage <name> --status <status> [--stage-report <path>] [--data <path>] [--data-schema-id <id>] [--report <path>] [--alias <path>] --json
|
|
151
152
|
dd-flow run complete <RUN-ID|RUN-short-id> --project-root <root> --status done|blocked|cancelled|failed [--verdict <text>] [--next-action <text>] --json
|
|
152
153
|
|
|
153
|
-
RUN-* is the execution envelope for one concrete flow launch. Semantic truth remains in protocol, experiment, DEF, scenario, evidence, and Memory Bank documents. New run artifacts are stored under DD_FLOW_HOME/projects/<PRJ-ID-slug>/runs/<RUN-ID-slug>/ with
|
|
154
|
+
RUN-* is the execution envelope for one concrete flow launch. Semantic truth remains in protocol, experiment, DEF, scenario, evidence, and Memory Bank documents. New run artifacts are stored under DD_FLOW_HOME/projects/<PRJ-ID-slug>/runs/<RUN-ID-slug>/ with one run.json projection and an append-only timeline.jsonl. Timeline is the compact full run report: summary is always present, while --hide may suppress bulky sections. The run usage command reconciles registered local Codex transcript counters before reporting; unavailable data is explicit rather than zero.
|
|
154
155
|
|
|
155
156
|
Examples:
|
|
156
157
|
dd-flow run start --project-root "$PWD" --flow-kind mb_sdlc --subject-type protocol --subject-id PRT-001-demo --slug demo --json
|
|
157
158
|
dd-flow id next --type run --project-root "$PWD" --slug demo --json
|
|
158
159
|
dd-flow run attach-stage RUN-001 --project-root "$PWD" --stage plan --dir 01-plan --status running --json
|
|
159
|
-
dd-flow run complete-stage RUN-001 --project-root "$PWD" --stage plan --status done --data 01-plan/stage-report.json --stage-report 01-plan/stage-report.html --report 01-plan/report.md --json`
|
|
160
|
+
dd-flow run complete-stage RUN-001 --project-root "$PWD" --stage plan --status done --data 01-plan/stage-report.json --stage-report 01-plan/stage-report.html --report 01-plan/stage-report.md --json`
|
|
161
|
+
],
|
|
162
|
+
[
|
|
163
|
+
"stage",
|
|
164
|
+
`dd-flow stage - execute one bounded stage lifecycle
|
|
165
|
+
|
|
166
|
+
Usage:
|
|
167
|
+
dd-flow stage start <RUN-ID|RUN-short-id> --project-root <root> --stage <name> [--dir <NN-stage-slug>] --json
|
|
168
|
+
dd-flow stage start --bootstrap --project-root <root> --stage specify --subject <safe-subject> [--intake-file <path>] --json
|
|
169
|
+
dd-flow stage finish <RUN-ID|RUN-short-id> --project-root <root> --stage <name> [--outcome done|blocked|failed] [--semantic-file <RUN-local-json>] --json
|
|
170
|
+
|
|
171
|
+
stage start is the worker's first practical flow command. It performs deterministic setup and returns a ready-to-use, stage-specific work packet with trusted runtime facts, compatibility and permission preflight, aliases, write boundary and exact finish command. The same packet is written to RUN-local stage-prompt.md and stage-prompt.json. --bootstrap creates the initial protocol/RUN envelope and copies the supplied intake into the RUN home before starting specify. stage finish defaults to @stage/stage-input.json, accepts semantic --outcome and always generates validated JSON, Markdown, template-backed HTML and a durable protocol-summary projection. Raw intake and telemetry remain RUN-local.`
|
|
160
172
|
],
|
|
161
173
|
[
|
|
162
174
|
"id",
|
package/dist/cli/run-cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import { AppError, isAppError } from "../shared/errors.js";
|
|
|
5
5
|
import { writeJson } from "../shared/json.js";
|
|
6
6
|
import { archiveProject, getProjectStatus, migrateProjectIds, registerProject, resolveProject } from "../services/projects.js";
|
|
7
7
|
import { cancelProtocol, getProtocolBlockers, getProtocolStatus, getReadyProtocols, implementProtocol, readyForMerge, registerProtocol, requireProtocol, syncProtocolFromRun, transitionProtocol } from "../services/protocols.js";
|
|
8
|
-
import { blockPlanItem, completePlanItem, getPlanStatus,
|
|
8
|
+
import { blockPlanItem, completePlanItem, getPlanStatus, skipPlanItem, startPlanItem } from "../services/plans.js";
|
|
9
9
|
import { cancelMergeQueueJob, claimNextMergeJob, claimMergeBundle, completeMergeBundle, completeMergeJob, failMergeBundle, failMergeJob, getMergeBundleStatus, getMergeQueueStatus, noteMergeJob, waitNextMergeJob } from "../services/merge-queue.js";
|
|
10
10
|
import { getMergeWorkerStatus, oneShotMergeClaim, startMergeWorker, stopProjectMergeWorker } from "../services/merge-worker.js";
|
|
11
11
|
import { cleanupApply, cleanupScan } from "../services/cleanup.js";
|
|
@@ -31,6 +31,7 @@ import { resolveProjectRoot } from "../storage/paths.js";
|
|
|
31
31
|
import { doctorEngines, engineInfo, installCurrentEngine, listEngines, resolveEngine, resolveOperationProjectRoot, routeArgsThroughEngine } from "../services/engines.js";
|
|
32
32
|
import { preflightCliCompatibility } from "../services/compatibility-preflight.js";
|
|
33
33
|
import { renderWorkerPrompt } from "../services/prompts.js";
|
|
34
|
+
import { finishStage, startStage } from "../services/stage-lifecycle.js";
|
|
34
35
|
const defaultIo = {
|
|
35
36
|
stdout: process.stdout,
|
|
36
37
|
stderr: process.stderr,
|
|
@@ -151,7 +152,8 @@ function progressForCommand(args) {
|
|
|
151
152
|
family === "merge-worker" ||
|
|
152
153
|
(family === "protocol" && command === "cancel") ||
|
|
153
154
|
(family === "project" && command === "archive") ||
|
|
154
|
-
family === "run"
|
|
155
|
+
family === "run" ||
|
|
156
|
+
family === "stage") {
|
|
155
157
|
return [
|
|
156
158
|
{ phase: "start", message: `Starting ${family} ${command}`, at: "" },
|
|
157
159
|
{ phase: "done", message: `${family} ${command} finished`, at: "" }
|
|
@@ -518,14 +520,6 @@ async function dispatch(args, context, io, scopeProjectRoot = null) {
|
|
|
518
520
|
...(reason ? { reason } : {})
|
|
519
521
|
});
|
|
520
522
|
}
|
|
521
|
-
if (family === "plan" && command === "set") {
|
|
522
|
-
const protocolId = requiredPosition(parsed, 0, "protocol-id");
|
|
523
|
-
return setProtocolPlan(context, {
|
|
524
|
-
protocolId,
|
|
525
|
-
projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "plan set"),
|
|
526
|
-
file: requiredOption(parsed, "file")
|
|
527
|
-
});
|
|
528
|
-
}
|
|
529
523
|
if (family === "plan" && command === "status") {
|
|
530
524
|
const protocolId = requiredPosition(parsed, 0, "protocol-id");
|
|
531
525
|
return getPlanStatus(context, { protocolId, projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "plan status") });
|
|
@@ -545,6 +539,33 @@ async function dispatch(args, context, io, scopeProjectRoot = null) {
|
|
|
545
539
|
...(workspaceRoot ? { workspaceRoot } : {})
|
|
546
540
|
});
|
|
547
541
|
}
|
|
542
|
+
if (family === "stage" && command === "start") {
|
|
543
|
+
const dir = optionalOption(parsed, "dir");
|
|
544
|
+
const bootstrap = hasOption(parsed, "bootstrap");
|
|
545
|
+
const subject = optionalOption(parsed, "subject");
|
|
546
|
+
const intakeFile = optionalOption(parsed, "intake-file");
|
|
547
|
+
const sessionId = optionalOption(parsed, "session-id");
|
|
548
|
+
return startStage(context, {
|
|
549
|
+
projectRoot: requiredOption(parsed, "project-root"),
|
|
550
|
+
...(bootstrap ? { bootstrap: { subject: subject ?? requiredOption(parsed, "subject"), ...(intakeFile ? { intakeFile } : {}) } } : { runId: requiredPosition(parsed, 0, "run-id") }),
|
|
551
|
+
stage: requiredOption(parsed, "stage"),
|
|
552
|
+
...(sessionId ? { sessionId } : {}),
|
|
553
|
+
...(dir ? { dir } : {})
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
if (family === "stage" && command === "finish") {
|
|
557
|
+
const dir = optionalOption(parsed, "dir");
|
|
558
|
+
const outcome = optionalOption(parsed, "outcome") ?? optionalOption(parsed, "status");
|
|
559
|
+
const semanticFile = optionalOption(parsed, "semantic-file") ?? optionalOption(parsed, "data");
|
|
560
|
+
return finishStage(context, {
|
|
561
|
+
projectRoot: requiredOption(parsed, "project-root"),
|
|
562
|
+
runId: requiredPosition(parsed, 0, "run-id"),
|
|
563
|
+
stage: requiredOption(parsed, "stage"),
|
|
564
|
+
...(dir ? { dir } : {}),
|
|
565
|
+
...(outcome ? { outcome } : {}),
|
|
566
|
+
...(semanticFile ? { semanticFile } : {})
|
|
567
|
+
});
|
|
568
|
+
}
|
|
548
569
|
if (family === "lane") {
|
|
549
570
|
return dispatchLane(context, command, parsed);
|
|
550
571
|
}
|
|
@@ -1311,11 +1332,15 @@ function dispatchCodexHome(context, parsed) {
|
|
|
1311
1332
|
function dispatchCodexHook(context, parsed, stdin) {
|
|
1312
1333
|
const action = requiredPosition(parsed, 0, "codex hook action");
|
|
1313
1334
|
if (action === "handle") {
|
|
1314
|
-
|
|
1335
|
+
const result = handleCodexHook(context, {
|
|
1315
1336
|
event: requiredOption(parsed, "event"),
|
|
1316
|
-
projectRoot:
|
|
1337
|
+
projectRoot: optionalOption(parsed, "project-root"),
|
|
1317
1338
|
stdin
|
|
1318
1339
|
});
|
|
1340
|
+
if (result && typeof result === "object" && !Array.isArray(result) && "hookSpecificOutput" in result) {
|
|
1341
|
+
return { hookSpecificOutput: result.hookSpecificOutput };
|
|
1342
|
+
}
|
|
1343
|
+
return {};
|
|
1319
1344
|
}
|
|
1320
1345
|
throw new AppError("usage", `Unknown codex hook action: ${action}`, 2);
|
|
1321
1346
|
}
|
|
@@ -1591,9 +1616,6 @@ function projectRootForMutation(context, args, result, resolvedScope) {
|
|
|
1591
1616
|
const transitionParsed = parseArgs([command ?? "", ...rest]);
|
|
1592
1617
|
return protocolProjectRoot(context, requiredPosition(transitionParsed, 0, "protocol-id"));
|
|
1593
1618
|
}
|
|
1594
|
-
if (family === "plan" && command === "set") {
|
|
1595
|
-
return protocolProjectRoot(context, requiredPosition(parsed, 0, "protocol-id"));
|
|
1596
|
-
}
|
|
1597
1619
|
if (family === "plan" && command === "item") {
|
|
1598
1620
|
return protocolProjectRoot(context, requiredPosition(parsed, 1, "protocol-id"));
|
|
1599
1621
|
}
|
|
@@ -1624,11 +1646,14 @@ function projectRootForMutation(context, args, result, resolvedScope) {
|
|
|
1624
1646
|
if (family === "run" && ["start", "attach-stage", "complete-stage", "complete", "flags"].includes(command ?? "")) {
|
|
1625
1647
|
return requiredOption(parsed, "project-root");
|
|
1626
1648
|
}
|
|
1649
|
+
if (family === "stage" && ["start", "finish"].includes(command ?? "")) {
|
|
1650
|
+
return requiredOption(parsed, "project-root");
|
|
1651
|
+
}
|
|
1627
1652
|
if (family === "worktree" && ["create", "bootstrap", "close"].includes(command ?? "")) {
|
|
1628
1653
|
return protocolProjectRoot(context, requiredOption(parsed, "protocol-id"));
|
|
1629
1654
|
}
|
|
1630
1655
|
if (family === "codex" && command === "hook" && requiredPosition(parsed, 0, "codex hook action") === "handle") {
|
|
1631
|
-
return
|
|
1656
|
+
return optionalOption(parsed, "project-root");
|
|
1632
1657
|
}
|
|
1633
1658
|
if (family === "codex" && command === "hooks") {
|
|
1634
1659
|
const action = requiredPosition(parsed, 0, "codex hooks action");
|
|
@@ -1671,7 +1696,7 @@ function isScopedProtocolMutation(family, command) {
|
|
|
1671
1696
|
if (family === "protocol")
|
|
1672
1697
|
return ["transition", "sync-from-run", "ready-for-merge", "cancel"].includes(command ?? "");
|
|
1673
1698
|
if (family === "plan")
|
|
1674
|
-
return command === "
|
|
1699
|
+
return command === "item";
|
|
1675
1700
|
if (family === "worktree")
|
|
1676
1701
|
return ["create", "bootstrap", "close"].includes(command ?? "");
|
|
1677
1702
|
if (family === "merge-queue")
|
|
@@ -332,7 +332,6 @@ function flagStrength(key, value, definition) {
|
|
|
332
332
|
"subagents.route": ["self_check", "grouped_subagent", "focused_subagent", "mixed"],
|
|
333
333
|
"subagents.grouping": ["off", "allowlisted"],
|
|
334
334
|
"subagents.pool_fallback": ["one", "runtime"],
|
|
335
|
-
"observability.detail": ["compact", "normal", "full"],
|
|
336
335
|
"knowledge.extract": ["skip", "conditional", "required"],
|
|
337
336
|
"knowledge.promote": ["skip", "conditional", "required"],
|
|
338
337
|
"workspace.bootstrap.mode": ["not_required", "revalidate", "required"],
|
|
@@ -360,9 +359,12 @@ function numberValue(value) {
|
|
|
360
359
|
return typeof value === "number" && Number.isInteger(value) ? value : null;
|
|
361
360
|
}
|
|
362
361
|
export function normalizeFlowContract(value) {
|
|
363
|
-
const id = requireString(value.id, "/id");
|
|
364
362
|
const version = requireNumber(value.version, "/version");
|
|
365
|
-
|
|
363
|
+
const id = requireString(value.id, "/id");
|
|
364
|
+
if (version === 4 || version === 5 || version === 6) {
|
|
365
|
+
return normalizeCanonicalRuntimeContract(value, id, version);
|
|
366
|
+
}
|
|
367
|
+
if (version > 6)
|
|
366
368
|
fail("/version", `unsupported flow-contract version: ${version}`);
|
|
367
369
|
if (version === 3 && !/^dd-flow-canonical-[0-9]{4}-[0-9]{2}$/.test(id)) {
|
|
368
370
|
fail("/id", "must identify a canonical version 3 flow contract");
|
|
@@ -396,6 +398,83 @@ export function normalizeFlowContract(value) {
|
|
|
396
398
|
...(flowFlags ? { flow_flags: flowFlags } : {})
|
|
397
399
|
};
|
|
398
400
|
}
|
|
401
|
+
/**
|
|
402
|
+
* Canonical 2026-08 deliberately owns runtime mechanics rather than the
|
|
403
|
+
* protocol transition graph. Keep the latter as a CLI-owned derived adapter
|
|
404
|
+
* so existing protocol services can make the same guarded decisions while
|
|
405
|
+
* the project file remains the canonical v4/v5 contract.
|
|
406
|
+
*/
|
|
407
|
+
function normalizeCanonicalRuntimeContract(value, id, version) {
|
|
408
|
+
if (id !== "dd-flow-canonical-2026-08") {
|
|
409
|
+
fail("/id", "must identify the canonical 2026-08 runtime contract");
|
|
410
|
+
}
|
|
411
|
+
const stages = {
|
|
412
|
+
registered: { terminal: false },
|
|
413
|
+
priming: { terminal: false },
|
|
414
|
+
specify: { terminal: false },
|
|
415
|
+
plan: { terminal: false },
|
|
416
|
+
interactive: { terminal: false },
|
|
417
|
+
consolidation: { terminal: false },
|
|
418
|
+
hardening: { terminal: false },
|
|
419
|
+
implementation: { terminal: false },
|
|
420
|
+
readiness: { terminal: false },
|
|
421
|
+
ready_for_merge: { terminal: false },
|
|
422
|
+
queued_for_merge: { terminal: false },
|
|
423
|
+
integration: { terminal: false },
|
|
424
|
+
cancelled: { terminal: true },
|
|
425
|
+
closed: { terminal: true },
|
|
426
|
+
blocked: { terminal: false },
|
|
427
|
+
waiting_for_user: { terminal: false }
|
|
428
|
+
};
|
|
429
|
+
const legacyAliases = {
|
|
430
|
+
f: "priming",
|
|
431
|
+
prime_intake: "specify",
|
|
432
|
+
g0: "specify",
|
|
433
|
+
m1: "readiness",
|
|
434
|
+
m2: "integration",
|
|
435
|
+
prime: "priming"
|
|
436
|
+
};
|
|
437
|
+
const transitions = {
|
|
438
|
+
registered: ["priming", "specify", "interactive", "blocked", "waiting_for_user"],
|
|
439
|
+
priming: ["specify", "interactive", "closed", "blocked", "waiting_for_user"],
|
|
440
|
+
specify: ["plan", "implementation", "blocked", "waiting_for_user"],
|
|
441
|
+
plan: ["implementation", "blocked", "waiting_for_user"],
|
|
442
|
+
interactive: ["consolidation", "blocked", "waiting_for_user"],
|
|
443
|
+
consolidation: ["hardening", "blocked", "waiting_for_user"],
|
|
444
|
+
hardening: ["readiness", "blocked", "waiting_for_user"],
|
|
445
|
+
implementation: ["readiness", "blocked", "waiting_for_user"],
|
|
446
|
+
readiness: ["ready_for_merge", "closed", "blocked", "waiting_for_user"],
|
|
447
|
+
ready_for_merge: ["queued_for_merge", "integration", "closed", "blocked", "waiting_for_user"],
|
|
448
|
+
queued_for_merge: ["integration", "blocked", "waiting_for_user"],
|
|
449
|
+
integration: ["closed", "blocked", "waiting_for_user"],
|
|
450
|
+
cancelled: [],
|
|
451
|
+
closed: [],
|
|
452
|
+
blocked: ["specify", "plan", "interactive", "consolidation", "hardening", "implementation", "readiness", "ready_for_merge", "closed"],
|
|
453
|
+
waiting_for_user: ["specify", "plan", "interactive", "consolidation", "hardening", "implementation", "readiness", "ready_for_merge", "blocked", "closed"]
|
|
454
|
+
};
|
|
455
|
+
const entrypoints = recordValue(value.stage_lifecycle);
|
|
456
|
+
const autoPolicy = recordValue(value.validation);
|
|
457
|
+
return {
|
|
458
|
+
id,
|
|
459
|
+
version,
|
|
460
|
+
capabilities: [
|
|
461
|
+
"flow_run_snapshot.v1",
|
|
462
|
+
"session_coverage_units.v1",
|
|
463
|
+
"privacy_safe_observability.v1",
|
|
464
|
+
...(version >= 5 ? ["canonical_protocol_plan.v1"] : []),
|
|
465
|
+
...(version >= 6 ? ["stage_bootstrap_context_packet.v1"] : [])
|
|
466
|
+
],
|
|
467
|
+
stages,
|
|
468
|
+
transitions,
|
|
469
|
+
readiness: { command: "ready-for-merge", allowed_from: ["readiness", "ready_for_merge"], target_stage: "ready_for_merge" },
|
|
470
|
+
merge_queue: { complete: { target_stage: "closed", next_action: "none" } },
|
|
471
|
+
defaults: { registered_next_action: "prime_or_protocol" },
|
|
472
|
+
legacy_aliases: legacyAliases,
|
|
473
|
+
...(entrypoints ? { entrypoints } : {}),
|
|
474
|
+
...(autoPolicy ? { auto_policy: autoPolicy } : {}),
|
|
475
|
+
route_aliases: { none: "no_plan", short: "compact_plan", full_plan: "full_plan" }
|
|
476
|
+
};
|
|
477
|
+
}
|
|
399
478
|
function findProjectFlowContractPath(projectRoot) {
|
|
400
479
|
const basePath = path.join(projectRoot, ".memory-bank", "dd-flow", "flow-contract");
|
|
401
480
|
for (const extension of [".json", ".yaml", ".yml"]) {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export function reconcileSessionCoverageRows(sessions) {
|
|
2
|
+
const sessionCounts = countValues(sessions.map((session) => session.session_id));
|
|
3
|
+
const duplicateSessionIds = duplicates(sessionCounts);
|
|
4
|
+
const invalidSessionIds = [];
|
|
5
|
+
const parsed = sessions.map((session) => {
|
|
6
|
+
try {
|
|
7
|
+
return { session, units: normalizeCoverageUnits(JSON.parse(session.coverage_units_json || "[]")) };
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
invalidSessionIds.push(session.session_id);
|
|
11
|
+
return { session, units: [] };
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
const orchestrators = parsed.filter(({ session }) => session.session_kind === "orchestrator");
|
|
15
|
+
const workers = parsed.filter(({ session }) => session.session_kind !== "orchestrator");
|
|
16
|
+
const expectedCounts = countValues(orchestrators.flatMap(({ units }) => units.map((unit) => unit.unit_id)));
|
|
17
|
+
const expectedUnitIds = [...expectedCounts.keys()].sort();
|
|
18
|
+
const duplicateExpectedUnitIds = duplicates(expectedCounts);
|
|
19
|
+
const observedOwners = new Map();
|
|
20
|
+
for (const { session, units } of workers) {
|
|
21
|
+
for (const unit of units) {
|
|
22
|
+
const owners = observedOwners.get(unit.unit_id) ?? [];
|
|
23
|
+
owners.push(session.session_id);
|
|
24
|
+
observedOwners.set(unit.unit_id, owners);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const observedUnitIds = [...observedOwners.keys()].sort();
|
|
28
|
+
const missingUnitIds = expectedUnitIds.filter((unitId) => !observedOwners.has(unitId));
|
|
29
|
+
const duplicateUnitIds = [...observedOwners.entries()]
|
|
30
|
+
.filter(([, owners]) => new Set(owners).size > 1)
|
|
31
|
+
.map(([id]) => id)
|
|
32
|
+
.sort();
|
|
33
|
+
const diagnostics = [];
|
|
34
|
+
if (orchestrators.length === 0)
|
|
35
|
+
diagnostics.push("orchestrator_session_missing");
|
|
36
|
+
if (orchestrators.length > 1)
|
|
37
|
+
diagnostics.push("multiple_orchestrator_sessions");
|
|
38
|
+
if (workers.length > 0 && expectedUnitIds.length === 0)
|
|
39
|
+
diagnostics.push("expected_worker_units_missing");
|
|
40
|
+
if (missingUnitIds.length > 0)
|
|
41
|
+
diagnostics.push("expected_worker_units_unobserved");
|
|
42
|
+
if (duplicateUnitIds.length > 0 || duplicateExpectedUnitIds.length > 0)
|
|
43
|
+
diagnostics.push("duplicate_coverage_binding");
|
|
44
|
+
if (duplicateSessionIds.length > 0)
|
|
45
|
+
diagnostics.push("duplicate_session_binding");
|
|
46
|
+
if (invalidSessionIds.length > 0)
|
|
47
|
+
diagnostics.push("invalid_coverage_units");
|
|
48
|
+
const unavailable = orchestrators.length === 0 || invalidSessionIds.length > 0;
|
|
49
|
+
const partial = orchestrators.length !== 1
|
|
50
|
+
|| (workers.length > 0 && expectedUnitIds.length === 0)
|
|
51
|
+
|| missingUnitIds.length > 0
|
|
52
|
+
|| duplicateUnitIds.length > 0
|
|
53
|
+
|| duplicateExpectedUnitIds.length > 0
|
|
54
|
+
|| duplicateSessionIds.length > 0;
|
|
55
|
+
return {
|
|
56
|
+
status: unavailable ? "unavailable" : partial ? "partial" : "complete",
|
|
57
|
+
expected_unit_ids: expectedUnitIds,
|
|
58
|
+
observed_unit_ids: observedUnitIds,
|
|
59
|
+
missing_unit_ids: missingUnitIds,
|
|
60
|
+
duplicate_unit_ids: duplicateUnitIds,
|
|
61
|
+
duplicate_expected_unit_ids: duplicateExpectedUnitIds,
|
|
62
|
+
duplicate_session_ids: duplicateSessionIds,
|
|
63
|
+
orchestrator_session_ids: orchestrators.map(({ session }) => session.session_id).sort(),
|
|
64
|
+
invalid_session_ids: [...new Set(invalidSessionIds)].sort(),
|
|
65
|
+
diagnostics
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function normalizeCoverageUnits(value) {
|
|
69
|
+
if (!Array.isArray(value))
|
|
70
|
+
throw new Error("coverage_units must be an array");
|
|
71
|
+
return value.map((item) => {
|
|
72
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
73
|
+
throw new Error("coverage unit must be an object");
|
|
74
|
+
const unitId = item.unit_id;
|
|
75
|
+
if (typeof unitId !== "string" || unitId.length === 0)
|
|
76
|
+
throw new Error("coverage unit requires unit_id");
|
|
77
|
+
return { unit_id: unitId };
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function countValues(values) {
|
|
81
|
+
const counts = new Map();
|
|
82
|
+
for (const value of values)
|
|
83
|
+
counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
84
|
+
return counts;
|
|
85
|
+
}
|
|
86
|
+
function duplicates(counts) {
|
|
87
|
+
return [...counts.entries()].filter(([, count]) => count > 1).map(([id]) => id).sort();
|
|
88
|
+
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { Ajv } from "ajv/dist/ajv.js";
|
|
1
5
|
import { AppError } from "../shared/errors.js";
|
|
2
|
-
import {
|
|
6
|
+
import { isAllowedStage } from "./contracts.js";
|
|
3
7
|
import { normalizeStage } from "./flow-contract.js";
|
|
8
|
+
let validateCanonicalPlan;
|
|
4
9
|
export function requireStage(value, contract) {
|
|
5
10
|
if (!isAllowedStage(value, contract)) {
|
|
6
11
|
throw new AppError("validation", `Unknown protocol stage: ${value}`, 2);
|
|
@@ -8,77 +13,100 @@ export function requireStage(value, contract) {
|
|
|
8
13
|
return contract ? normalizeStage(value, contract) : value;
|
|
9
14
|
}
|
|
10
15
|
export function validatePlan(input, expectedProtocolId) {
|
|
11
|
-
|
|
12
|
-
if (!plan || typeof plan !== "object" || Array.isArray(plan)) {
|
|
16
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
13
17
|
throw new AppError("validation", "Plan must be a JSON object", 2);
|
|
14
18
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
requireString(plan.title, "title");
|
|
19
|
+
validateCanonicalPlanStructure(input);
|
|
20
|
+
const plan = input;
|
|
21
|
+
const errors = [];
|
|
19
22
|
if (expectedProtocolId && plan.protocol_id !== expectedProtocolId) {
|
|
20
|
-
|
|
23
|
+
errors.push({
|
|
24
|
+
path: "/protocol_id",
|
|
25
|
+
message: "Plan protocol_id does not match command protocol id",
|
|
21
26
|
expectedProtocolId,
|
|
22
27
|
actualProtocolId: plan.protocol_id
|
|
23
28
|
});
|
|
24
29
|
}
|
|
25
30
|
if (!Array.isArray(plan.items)) {
|
|
26
|
-
|
|
31
|
+
errors.push({ path: "/items", message: "Plan items must be an array" });
|
|
32
|
+
throw new AppError("validation", "Plan validation failed", 2, { errors });
|
|
27
33
|
}
|
|
28
34
|
const ids = new Set();
|
|
29
|
-
const items =
|
|
35
|
+
const items = [];
|
|
36
|
+
plan.items.forEach((item, index) => {
|
|
37
|
+
try {
|
|
38
|
+
items.push(validatePlanItem(item, index));
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
errors.push(validationIssue(error, `/items/${index}`));
|
|
42
|
+
}
|
|
43
|
+
});
|
|
30
44
|
for (const item of items) {
|
|
31
45
|
if (ids.has(item.id)) {
|
|
32
|
-
|
|
46
|
+
errors.push({ path: "/items", message: `Duplicate plan item id: ${item.id}` });
|
|
33
47
|
}
|
|
34
48
|
ids.add(item.id);
|
|
35
49
|
}
|
|
36
50
|
for (const item of items) {
|
|
37
51
|
for (const dep of item.depends_on) {
|
|
38
52
|
if (!ids.has(dep)) {
|
|
39
|
-
|
|
53
|
+
errors.push({ path: `/items/${item.id}/depends_on`, message: `Plan item ${item.id} depends on missing item ${dep}` });
|
|
40
54
|
}
|
|
41
55
|
}
|
|
42
56
|
}
|
|
57
|
+
if (errors.length > 0) {
|
|
58
|
+
throw new AppError("validation", "Plan validation failed", 2, { errors });
|
|
59
|
+
}
|
|
43
60
|
return { ...plan, items };
|
|
44
61
|
}
|
|
45
|
-
function
|
|
62
|
+
function validationIssue(error, path) {
|
|
63
|
+
return {
|
|
64
|
+
path,
|
|
65
|
+
message: error instanceof AppError ? error.message : String(error)
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export function validatePlanItem(input, index) {
|
|
46
69
|
const item = input;
|
|
47
70
|
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
48
71
|
throw new AppError("validation", `Plan item ${index} must be an object`, 2);
|
|
49
72
|
}
|
|
50
73
|
const id = requireString(item.id, `items[${index}].id`);
|
|
51
74
|
const kind = requireString(item.kind, `items[${index}].kind`);
|
|
52
|
-
if (!allowedPlanKinds.includes(kind)) {
|
|
53
|
-
throw new AppError("validation", `Invalid plan item kind for ${id}: ${kind}`, 2);
|
|
54
|
-
}
|
|
55
|
-
const status = requireString(item.status, `items[${index}].status`);
|
|
56
|
-
if (!allowedPlanStatuses.includes(status)) {
|
|
57
|
-
throw new AppError("validation", `Invalid plan item status for ${id}: ${status}`, 2);
|
|
58
|
-
}
|
|
59
75
|
if (!Array.isArray(item.depends_on) || item.depends_on.some((dep) => typeof dep !== "string")) {
|
|
60
76
|
throw new AppError("validation", `Plan item ${id} depends_on must be a string array`, 2);
|
|
61
77
|
}
|
|
62
|
-
|
|
63
|
-
throw new AppError("validation", `Plan item ${id} evidence must be a string array`, 2);
|
|
64
|
-
}
|
|
78
|
+
const targetStage = requireString(item.target_stage, `items[${index}].target_stage`);
|
|
65
79
|
return {
|
|
80
|
+
...item,
|
|
66
81
|
id,
|
|
67
|
-
kind
|
|
82
|
+
kind,
|
|
68
83
|
title: requireString(item.title, `items[${index}].title`),
|
|
69
|
-
status:
|
|
84
|
+
status: "pending",
|
|
70
85
|
depends_on: item.depends_on,
|
|
71
|
-
stage:
|
|
86
|
+
stage: targetStage === "code" ? "implementation" : targetStage,
|
|
87
|
+
target_stage: targetStage,
|
|
72
88
|
required: requireBoolean(item.required, `items[${index}].required`),
|
|
73
89
|
owner: requireString(item.owner, `items[${index}].owner`),
|
|
74
|
-
summary:
|
|
75
|
-
evidence:
|
|
90
|
+
summary: requireString(item.summary, `items[${index}].summary`),
|
|
91
|
+
evidence: [],
|
|
76
92
|
...(item.semantic_spine ? { semantic_spine: validateSemanticSpine(item.semantic_spine, id) } : {}),
|
|
77
93
|
...(item.execution_context ? { execution_context: validateExecutionContext(item.execution_context, id) } : {}),
|
|
78
94
|
...(typeof item.block_reason === "string" ? { block_reason: item.block_reason } : {}),
|
|
79
95
|
...(typeof item.user_required === "boolean" ? { user_required: item.user_required } : {})
|
|
80
96
|
};
|
|
81
97
|
}
|
|
98
|
+
function validateCanonicalPlanStructure(input) {
|
|
99
|
+
const validate = validateCanonicalPlan ??= new Ajv({ allErrors: true, strict: false, validateFormats: false }).compile(JSON.parse(fs.readFileSync(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "schemas", "protocol-plan.schema.json"), "utf8")));
|
|
100
|
+
if (validate(input))
|
|
101
|
+
return;
|
|
102
|
+
throw new AppError("validation", "Plan validation failed", 2, {
|
|
103
|
+
errors: (validate.errors ?? []).map((error) => ({
|
|
104
|
+
path: error.instancePath || "/",
|
|
105
|
+
message: error.message ?? "schema validation failed",
|
|
106
|
+
keyword: error.keyword
|
|
107
|
+
}))
|
|
108
|
+
});
|
|
109
|
+
}
|
|
82
110
|
function validateExecutionContext(input, itemId) {
|
|
83
111
|
const context = requireRecord(input, `Plan item ${itemId} execution_context`);
|
|
84
112
|
return {
|
|
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { schemaVersion } from "../domain/contracts.js";
|
|
4
4
|
import { loadProjectFlowContract } from "../domain/flow-contract.js";
|
|
5
|
-
import { ensureDir,
|
|
5
|
+
import { ensureDir, protocolDir, stateJsonPath } from "../storage/paths.js";
|
|
6
6
|
export function ensureProtocolFiles(input) {
|
|
7
7
|
const storageRoot = input.storageRoot ?? input.projectRoot;
|
|
8
8
|
const dir = protocolDir(storageRoot, input.protocolId);
|
|
@@ -120,21 +120,6 @@ export function readStateFile(file, protocolId) {
|
|
|
120
120
|
export function writeState(file, state) {
|
|
121
121
|
fs.writeFileSync(file, `${JSON.stringify(state, null, 2)}\n`);
|
|
122
122
|
}
|
|
123
|
-
export function readPlan(projectRoot, protocolId) {
|
|
124
|
-
return readPlanFile(planJsonPath(projectRoot, protocolId));
|
|
125
|
-
}
|
|
126
|
-
export function readPlanFile(file) {
|
|
127
|
-
if (!fs.existsSync(file)) {
|
|
128
|
-
return undefined;
|
|
129
|
-
}
|
|
130
|
-
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
131
|
-
}
|
|
132
|
-
export function writePlan(projectRoot, protocolId, plan) {
|
|
133
|
-
writePlanFile(planJsonPath(projectRoot, protocolId), plan);
|
|
134
|
-
}
|
|
135
|
-
export function writePlanFile(file, plan) {
|
|
136
|
-
fs.writeFileSync(file, `${JSON.stringify(plan, null, 2)}\n`);
|
|
137
|
-
}
|
|
138
123
|
function readStateIfExists(file) {
|
|
139
124
|
if (!fs.existsSync(file)) {
|
|
140
125
|
return undefined;
|