@deksden-com/dd-flow-cli 0.4.2 → 0.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/CHANGELOG.md +27 -4
- package/dist/build-info.json +6 -6
- package/dist/cli/help.js +14 -3
- package/dist/cli/run-cli.js +32 -16
- package/dist/domain/flow-contract.js +81 -2
- 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 +150 -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 +31 -0
- package/dist/schemas/stage-report.schema.json +20 -0
- package/dist/schemas/stage-start-response.schema.json +29 -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/dashboard.js +51 -26
- package/dist/services/engines.js +84 -18
- package/dist/services/hooks.js +80 -246
- package/dist/services/memory-permissions.js +77 -69
- 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 +77 -11
- package/dist/services/runs.js +95 -61
- package/dist/services/schema-validation.js +168 -7
- package/dist/services/sessions.js +132 -68
- package/dist/services/stage-lifecycle.js +572 -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,11 @@
|
|
|
1
1
|
# @deksden-com/dd-flow-cli
|
|
2
2
|
|
|
3
|
+
## 0.5.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
|
|
3
9
|
## 0.4.2
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
|
@@ -8,19 +14,36 @@
|
|
|
8
14
|
|
|
9
15
|
## 0.4.1
|
|
10
16
|
|
|
17
|
+
> Release-accounting correction: the entries below were already present in
|
|
18
|
+
> the published `0.4.1` tarball built from `6f85c04`, but their pending
|
|
19
|
+
> Changesets had not been consumed.
|
|
20
|
+
|
|
21
|
+
### Minor Changes
|
|
22
|
+
|
|
23
|
+
- Add `dd-flow prompt render` for deterministic RUN-local worker prompts,
|
|
24
|
+
including validated semantic context and checked `run://` reads.
|
|
25
|
+
|
|
11
26
|
### Patch Changes
|
|
12
27
|
|
|
13
28
|
- 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
29
|
|
|
15
30
|
- Include production dependencies in installed engine snapshots, so a routed CLI works outside the source checkout.
|
|
16
31
|
|
|
17
|
-
|
|
32
|
+
- Expand `dd-flow run timeline` with stage durations, session roster, usage
|
|
33
|
+
coverage, artifact references and repeatable `--hide` controls.
|
|
18
34
|
|
|
19
|
-
|
|
35
|
+
- Update canonical Memory Bank resolution for the `mb-upgrade` `05-review`
|
|
36
|
+
stage instead of requiring a separate top-level `mb-upgrade-review.md`.
|
|
37
|
+
|
|
38
|
+
- Make typed `PRT-*` and `RUN-*` identity project-scoped and monotonic across
|
|
39
|
+
registered projects, migrate legacy global keys without renumbering, and
|
|
40
|
+
ignore legacy date-based protocol ids during allocation.
|
|
20
41
|
|
|
21
|
-
-
|
|
42
|
+
- Add source-aware flow-flag snapshots, provenance revisions, grouped session
|
|
43
|
+
coverage and privacy-safe timing/usage projections.
|
|
22
44
|
|
|
23
|
-
-
|
|
45
|
+
- Validate the complete flow-contract catalog before mutable RUN/protocol
|
|
46
|
+
starts and expose exact read-only diagnostics for invalid catalogs.
|
|
24
47
|
|
|
25
48
|
## 0.4.0
|
|
26
49
|
|
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.5.0",
|
|
4
|
+
"cli_commit": "64b75acbb2e62c9e80eb29c501f5f3481f86cf20",
|
|
5
|
+
"built_at": "2026-08-11T10:20:58.455Z",
|
|
6
6
|
"built_with_canon": {
|
|
7
|
-
"version": "
|
|
8
|
-
"commit": "
|
|
9
|
-
"flow_contract": "dd-flow-canonical-2026-
|
|
7
|
+
"version": "3.0.0",
|
|
8
|
+
"commit": "1e8c39078ed7a34adde93315a9cdcf2bbfc15374",
|
|
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.
|
|
@@ -150,13 +151,23 @@ 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 finish <RUN-ID|RUN-short-id> --project-root <root> --stage <name> [--status done|blocked|failed] [--semantic-file <RUN-local-json>] --json
|
|
169
|
+
|
|
170
|
+
stage start attaches the stage, archives an earlier attempt when needed and writes one RUN-local stage-prompt.md. stage finish derives a bounded report and always generates validated JSON, Markdown, template-backed HTML and a durable protocol-summary projection. Raw intake and telemetry remain RUN-local.`
|
|
160
171
|
],
|
|
161
172
|
[
|
|
162
173
|
"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,28 @@ 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
|
+
return startStage(context, {
|
|
545
|
+
projectRoot: requiredOption(parsed, "project-root"),
|
|
546
|
+
runId: requiredPosition(parsed, 0, "run-id"),
|
|
547
|
+
stage: requiredOption(parsed, "stage"),
|
|
548
|
+
...(dir ? { dir } : {})
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
if (family === "stage" && command === "finish") {
|
|
552
|
+
const dir = optionalOption(parsed, "dir");
|
|
553
|
+
const status = optionalOption(parsed, "status");
|
|
554
|
+
const semanticFile = optionalOption(parsed, "semantic-file") ?? optionalOption(parsed, "data");
|
|
555
|
+
return finishStage(context, {
|
|
556
|
+
projectRoot: requiredOption(parsed, "project-root"),
|
|
557
|
+
runId: requiredPosition(parsed, 0, "run-id"),
|
|
558
|
+
stage: requiredOption(parsed, "stage"),
|
|
559
|
+
...(dir ? { dir } : {}),
|
|
560
|
+
...(status ? { status } : {}),
|
|
561
|
+
...(semanticFile ? { semanticFile } : {})
|
|
562
|
+
});
|
|
563
|
+
}
|
|
548
564
|
if (family === "lane") {
|
|
549
565
|
return dispatchLane(context, command, parsed);
|
|
550
566
|
}
|
|
@@ -1313,7 +1329,7 @@ function dispatchCodexHook(context, parsed, stdin) {
|
|
|
1313
1329
|
if (action === "handle") {
|
|
1314
1330
|
return handleCodexHook(context, {
|
|
1315
1331
|
event: requiredOption(parsed, "event"),
|
|
1316
|
-
projectRoot:
|
|
1332
|
+
projectRoot: optionalOption(parsed, "project-root"),
|
|
1317
1333
|
stdin
|
|
1318
1334
|
});
|
|
1319
1335
|
}
|
|
@@ -1591,9 +1607,6 @@ function projectRootForMutation(context, args, result, resolvedScope) {
|
|
|
1591
1607
|
const transitionParsed = parseArgs([command ?? "", ...rest]);
|
|
1592
1608
|
return protocolProjectRoot(context, requiredPosition(transitionParsed, 0, "protocol-id"));
|
|
1593
1609
|
}
|
|
1594
|
-
if (family === "plan" && command === "set") {
|
|
1595
|
-
return protocolProjectRoot(context, requiredPosition(parsed, 0, "protocol-id"));
|
|
1596
|
-
}
|
|
1597
1610
|
if (family === "plan" && command === "item") {
|
|
1598
1611
|
return protocolProjectRoot(context, requiredPosition(parsed, 1, "protocol-id"));
|
|
1599
1612
|
}
|
|
@@ -1624,11 +1637,14 @@ function projectRootForMutation(context, args, result, resolvedScope) {
|
|
|
1624
1637
|
if (family === "run" && ["start", "attach-stage", "complete-stage", "complete", "flags"].includes(command ?? "")) {
|
|
1625
1638
|
return requiredOption(parsed, "project-root");
|
|
1626
1639
|
}
|
|
1640
|
+
if (family === "stage" && ["start", "finish"].includes(command ?? "")) {
|
|
1641
|
+
return requiredOption(parsed, "project-root");
|
|
1642
|
+
}
|
|
1627
1643
|
if (family === "worktree" && ["create", "bootstrap", "close"].includes(command ?? "")) {
|
|
1628
1644
|
return protocolProjectRoot(context, requiredOption(parsed, "protocol-id"));
|
|
1629
1645
|
}
|
|
1630
1646
|
if (family === "codex" && command === "hook" && requiredPosition(parsed, 0, "codex hook action") === "handle") {
|
|
1631
|
-
return
|
|
1647
|
+
return optionalOption(parsed, "project-root");
|
|
1632
1648
|
}
|
|
1633
1649
|
if (family === "codex" && command === "hooks") {
|
|
1634
1650
|
const action = requiredPosition(parsed, 0, "codex hooks action");
|
|
@@ -1671,7 +1687,7 @@ function isScopedProtocolMutation(family, command) {
|
|
|
1671
1687
|
if (family === "protocol")
|
|
1672
1688
|
return ["transition", "sync-from-run", "ready-for-merge", "cancel"].includes(command ?? "");
|
|
1673
1689
|
if (family === "plan")
|
|
1674
|
-
return command === "
|
|
1690
|
+
return command === "item";
|
|
1675
1691
|
if (family === "worktree")
|
|
1676
1692
|
return ["create", "bootstrap", "close"].includes(command ?? "");
|
|
1677
1693
|
if (family === "merge-queue")
|
|
@@ -360,9 +360,12 @@ function numberValue(value) {
|
|
|
360
360
|
return typeof value === "number" && Number.isInteger(value) ? value : null;
|
|
361
361
|
}
|
|
362
362
|
export function normalizeFlowContract(value) {
|
|
363
|
-
const id = requireString(value.id, "/id");
|
|
364
363
|
const version = requireNumber(value.version, "/version");
|
|
365
|
-
|
|
364
|
+
const id = requireString(value.id, "/id");
|
|
365
|
+
if (version === 4 || version === 5) {
|
|
366
|
+
return normalizeCanonicalRuntimeContract(value, id, version);
|
|
367
|
+
}
|
|
368
|
+
if (version > 5)
|
|
366
369
|
fail("/version", `unsupported flow-contract version: ${version}`);
|
|
367
370
|
if (version === 3 && !/^dd-flow-canonical-[0-9]{4}-[0-9]{2}$/.test(id)) {
|
|
368
371
|
fail("/id", "must identify a canonical version 3 flow contract");
|
|
@@ -396,6 +399,82 @@ export function normalizeFlowContract(value) {
|
|
|
396
399
|
...(flowFlags ? { flow_flags: flowFlags } : {})
|
|
397
400
|
};
|
|
398
401
|
}
|
|
402
|
+
/**
|
|
403
|
+
* Canonical 2026-08 deliberately owns runtime mechanics rather than the
|
|
404
|
+
* protocol transition graph. Keep the latter as a CLI-owned derived adapter
|
|
405
|
+
* so existing protocol services can make the same guarded decisions while
|
|
406
|
+
* the project file remains the canonical v4/v5 contract.
|
|
407
|
+
*/
|
|
408
|
+
function normalizeCanonicalRuntimeContract(value, id, version) {
|
|
409
|
+
if (id !== "dd-flow-canonical-2026-08") {
|
|
410
|
+
fail("/id", "must identify the canonical 2026-08 runtime contract");
|
|
411
|
+
}
|
|
412
|
+
const stages = {
|
|
413
|
+
registered: { terminal: false },
|
|
414
|
+
priming: { terminal: false },
|
|
415
|
+
specify: { terminal: false },
|
|
416
|
+
plan: { terminal: false },
|
|
417
|
+
interactive: { terminal: false },
|
|
418
|
+
consolidation: { terminal: false },
|
|
419
|
+
hardening: { terminal: false },
|
|
420
|
+
implementation: { terminal: false },
|
|
421
|
+
readiness: { terminal: false },
|
|
422
|
+
ready_for_merge: { terminal: false },
|
|
423
|
+
queued_for_merge: { terminal: false },
|
|
424
|
+
integration: { terminal: false },
|
|
425
|
+
cancelled: { terminal: true },
|
|
426
|
+
closed: { terminal: true },
|
|
427
|
+
blocked: { terminal: false },
|
|
428
|
+
waiting_for_user: { terminal: false }
|
|
429
|
+
};
|
|
430
|
+
const legacyAliases = {
|
|
431
|
+
f: "priming",
|
|
432
|
+
prime_intake: "specify",
|
|
433
|
+
g0: "specify",
|
|
434
|
+
m1: "readiness",
|
|
435
|
+
m2: "integration",
|
|
436
|
+
prime: "priming"
|
|
437
|
+
};
|
|
438
|
+
const transitions = {
|
|
439
|
+
registered: ["priming", "specify", "interactive", "blocked", "waiting_for_user"],
|
|
440
|
+
priming: ["specify", "interactive", "closed", "blocked", "waiting_for_user"],
|
|
441
|
+
specify: ["plan", "implementation", "blocked", "waiting_for_user"],
|
|
442
|
+
plan: ["implementation", "blocked", "waiting_for_user"],
|
|
443
|
+
interactive: ["consolidation", "blocked", "waiting_for_user"],
|
|
444
|
+
consolidation: ["hardening", "blocked", "waiting_for_user"],
|
|
445
|
+
hardening: ["readiness", "blocked", "waiting_for_user"],
|
|
446
|
+
implementation: ["readiness", "blocked", "waiting_for_user"],
|
|
447
|
+
readiness: ["ready_for_merge", "closed", "blocked", "waiting_for_user"],
|
|
448
|
+
ready_for_merge: ["queued_for_merge", "integration", "closed", "blocked", "waiting_for_user"],
|
|
449
|
+
queued_for_merge: ["integration", "blocked", "waiting_for_user"],
|
|
450
|
+
integration: ["closed", "blocked", "waiting_for_user"],
|
|
451
|
+
cancelled: [],
|
|
452
|
+
closed: [],
|
|
453
|
+
blocked: ["specify", "plan", "interactive", "consolidation", "hardening", "implementation", "readiness", "ready_for_merge", "closed"],
|
|
454
|
+
waiting_for_user: ["specify", "plan", "interactive", "consolidation", "hardening", "implementation", "readiness", "ready_for_merge", "blocked", "closed"]
|
|
455
|
+
};
|
|
456
|
+
const entrypoints = recordValue(value.stage_lifecycle);
|
|
457
|
+
const autoPolicy = recordValue(value.validation);
|
|
458
|
+
return {
|
|
459
|
+
id,
|
|
460
|
+
version,
|
|
461
|
+
capabilities: [
|
|
462
|
+
"flow_run_snapshot.v1",
|
|
463
|
+
"session_coverage_units.v1",
|
|
464
|
+
"privacy_safe_observability.v1",
|
|
465
|
+
...(version === 5 ? ["canonical_protocol_plan.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"]) {
|
|
@@ -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;
|
|
@@ -124,10 +124,10 @@
|
|
|
124
124
|
"run": {
|
|
125
125
|
"type": "object",
|
|
126
126
|
"additionalProperties": true,
|
|
127
|
-
"required": ["run_id", "
|
|
127
|
+
"required": ["run_id", "run_state"],
|
|
128
128
|
"properties": {
|
|
129
129
|
"run_id": { "type": "string", "pattern": "^RUN-[0-9]{3}-[a-z0-9]+(?:-[a-z0-9]+)*$" },
|
|
130
|
-
"
|
|
130
|
+
"run_state": { "type": "string", "minLength": 1 }
|
|
131
131
|
}
|
|
132
132
|
},
|
|
133
133
|
"stage": {
|