@deksden-com/dd-flow-cli 0.1.0 → 0.3.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.
Files changed (37) hide show
  1. package/README.md +13 -0
  2. package/dist/build-info.json +15 -0
  3. package/dist/cli/help.js +133 -14
  4. package/dist/cli/run-cli.js +242 -8
  5. package/dist/schemas/archived-flow-manifest.schema.json +112 -0
  6. package/dist/schemas/compatibility.schema.json +26 -0
  7. package/dist/schemas/flow-guidance.schema.json +73 -0
  8. package/dist/schemas/flow-run-index.schema.json +30 -4
  9. package/dist/schemas/global-dashboard-data.schema.json +68 -0
  10. package/dist/schemas/mb-sdlc-review-report.schema.json +242 -0
  11. package/dist/schemas/merge-stage-report.schema.json +61 -1
  12. package/dist/schemas/plan-stage-report.schema.json +255 -0
  13. package/dist/schemas/project-dashboard-data.schema.json +98 -0
  14. package/dist/schemas/project-flow-pack-manifest.schema.json +127 -0
  15. package/dist/schemas/protocol-dashboard-data.schema.json +89 -0
  16. package/dist/schemas/status-report.schema.json +186 -0
  17. package/dist/schemas/version-report.schema.json +22 -0
  18. package/dist/services/build-info.js +114 -0
  19. package/dist/services/canon.js +298 -0
  20. package/dist/services/cleanup.js +14 -1
  21. package/dist/services/config.js +25 -0
  22. package/dist/services/dashboard.js +655 -10
  23. package/dist/services/flow-guidance.js +214 -0
  24. package/dist/services/ids.js +106 -0
  25. package/dist/services/lanes.js +6 -2
  26. package/dist/services/merge-queue.js +68 -4
  27. package/dist/services/merge-worker.js +177 -0
  28. package/dist/services/projects.js +7 -1
  29. package/dist/services/protocols.js +648 -17
  30. package/dist/services/runs.js +98 -21
  31. package/dist/services/schema-validation.js +88 -9
  32. package/dist/services/sessions.js +3 -2
  33. package/dist/services/status.js +306 -0
  34. package/dist/services/version-status.js +284 -0
  35. package/dist/storage/database.js +13 -0
  36. package/dist/storage/paths.js +21 -0
  37. package/package.json +2 -2
package/README.md CHANGED
@@ -41,6 +41,17 @@ The flow contract defines mechanical protocol stages, allowed transitions, the `
41
41
 
42
42
  ## Basic Commands
43
43
 
44
+ Inspect runtime and canonical Memory Bank discovery:
45
+
46
+ ```bash
47
+ dd-flow status --project-root "$PWD" --json
48
+ dd-flow canon resolve --json
49
+ dd-flow canon register --root "$DD_MEMORYBANK" --json
50
+ dd-flow canon status --json
51
+ ```
52
+
53
+ `canon resolve` checks explicit `--root`, then `DD_MEMORYBANK`, then the globally registered value in `DD_FLOW_HOME`, then known nearby `dd-memorybank` checkouts from the current working directory. If two different valid canonical roots are discovered, it fails closed with structured blockers and bootstrap hints. Use `DD_MEMORYBANK` for the local canonical `dd-memorybank` checkout.
54
+
44
55
  Register a project:
45
56
 
46
57
  ```bash
@@ -218,6 +229,8 @@ Defaults:
218
229
  ```text
219
230
  project dashboard: .tasks/dd-flow-dashboard.md
220
231
  global dashboard: ~/.dd-flow/dashboard.md
232
+ project HTML: ~/.dd-flow/projects/<PRJ-ID>/dashboard/project-dashboard.html
233
+ protocol HTML: ~/.dd-flow/projects/<PRJ-ID>/dashboard/protocols/<PRT-ID>.html
221
234
  ```
222
235
 
223
236
  Useful commands:
@@ -0,0 +1,15 @@
1
+ {
2
+ "cli_package": "@deksden-com/dd-flow-cli",
3
+ "cli_version": "0.3.0",
4
+ "cli_commit": "059594c2096e080548d470e789f594e20a1a8ebd",
5
+ "built_at": "2026-06-30T20:32:13.018Z",
6
+ "built_with_canon": {
7
+ "version": "2.8.0",
8
+ "commit": "f549c33bd73d8d18b5cb07d9737b02fe638e61b8",
9
+ "flow_contract": "dd-flow-canonical-2026-06",
10
+ "repo_root": "/Users/deksden/Documents/_Projects/dd-memorybank",
11
+ "memorybank_root": "/Users/deksden/Documents/_Projects/dd-memorybank/.memory-bank",
12
+ "flow_root": "/Users/deksden/Documents/_Projects/dd-memorybank/.memory-bank/dd-flow",
13
+ "layout": "dot_memory_bank"
14
+ }
15
+ }
package/dist/cli/help.js CHANGED
@@ -9,17 +9,23 @@ Usage:
9
9
  dd-flow <command> --help
10
10
 
11
11
  Core commands:
12
+ version Print the installed dd-flow CLI package version.
13
+ status Inspect dd-flow runtime, project and canon roots.
14
+ canon register/status/resolve Register and resolve the canonical Memory Bank.
12
15
  project register/status Register a project and inspect runtime state.
13
16
  project resolve/archive Resolve typed ids and archive stale project roots.
14
17
  project migrate-ids Migrate old project ids.
15
- protocol register/status Register and inspect protocol state.
18
+ protocol register/status/transition Register, inspect, transition and repair protocol state.
16
19
  cleanup scan/apply Detect and repair stale local runtime state.
17
20
  run start/status/list Track concrete flow executions and stage artifacts.
21
+ id next Preview next typed ids for protocol or run allocation.
18
22
  plan set/status/item Attach and update protocol plans.
19
23
  lane workspace/lock/status Manage shared workspace lanes and leases.
24
+ merge status/one-shot Inspect or claim one merge job from the current session.
25
+ merge-worker status/start/stop Manage long-lived project merge workers.
20
26
  merge-queue status/next Claim, wait for, complete, or fail merge jobs.
21
27
  session register/status/stop Register flow sessions and stop workers.
22
- dashboard render/open/refresh Render markdown project/global dashboards.
28
+ dashboard data/render/open/refresh Render markdown or HTML project/global dashboards.
23
29
  schema validate Validate canonical dd-flow JSON data contracts.
24
30
  memory permissions preflight Check Memory Bank write/read permissions.
25
31
  integration cmux status Inspect optional cmux integration.
@@ -28,17 +34,75 @@ Core commands:
28
34
 
29
35
  Examples:
30
36
  dd-flow project register --root "$PWD"
37
+ dd-flow version
38
+ dd-flow status --project-root "$PWD" --json
39
+ dd-flow canon register --root "$DD_MEMORYBANK" --json
40
+ dd-flow canon resolve --json
31
41
  dd-flow project status --root "$PWD" --json
32
42
  dd-flow project resolve PRJ-001 --json
33
43
  dd-flow project archive PRJ-001 --reason "old missing worktree root" --json
34
44
  dd-flow project migrate-ids --root "$PWD" --apply --json
35
- dd-flow run start --project-root "$PWD" --flow-kind coding --subject-type protocol --subject-id PRT-001-demo --slug demo --json
45
+ dd-flow run start --project-root "$PWD" --flow-kind mb_sdlc --subject-type protocol --subject-id PRT-001-demo --slug demo --json
46
+ dd-flow id next --type protocol --project-root "$PWD" --slug demo --json
47
+ dd-flow protocol transition PRT-001-demo --to implementation --payload-file transition.json --json
36
48
  dd-flow lane workspace set --project-root "$PWD" --lane merge --path "$PWD" --branch main --json
37
49
  dd-flow lane lock acquire --project-root "$PWD" --lane merge --worker-id worker-1 --path "$PWD" --ttl 300 --reason "merge worker" --json
50
+ dd-flow merge status --project-root "$PWD" --json
51
+ dd-flow merge-worker start --project-root "$PWD" --worker-id merge-worker --path "$PWD" --json
38
52
 
39
53
  Default output is human-readable. Use --json for hooks, prompts, scripts and other agent automation; JSON mode never mixes human progress text into stdout.
40
54
 
41
55
  project_root is the stable repository identity. workspace_path is the concrete checkout where an agent is about to work.`
56
+ ],
57
+ [
58
+ "version",
59
+ `dd-flow version - print CLI package version
60
+
61
+ Usage:
62
+ dd-flow version
63
+ dd-flow version --json
64
+ dd-flow --version
65
+
66
+ version reports the installed dd-flow CLI package version from package metadata. Use status for richer canon/project compatibility diagnostics.
67
+
68
+ Examples:
69
+ dd-flow version
70
+ dd-flow version --json`
71
+ ],
72
+ [
73
+ "status",
74
+ `dd-flow status - inspect runtime roots and active discovery state
75
+
76
+ Usage:
77
+ dd-flow status [--project-root <root>] --json
78
+ dd-flow status [--root <root>] --json
79
+ dd-flow status [--project-root <root>] [--check-registry] --json
80
+
81
+ status reports DD_FLOW_HOME, the requested or current project root, registered project identity when available, and canonical Memory Bank discovery. It does not require the project to be registered and does not mutate state. In JSON mode canon discovery is embedded as a structured assessment so agents can detect missing or ambiguous canon roots without parsing stderr.
82
+
83
+ By default status does not call the network. Use --check-registry for an explicit cached npm latest check.
84
+
85
+ Examples:
86
+ dd-flow status --project-root "$PWD" --json
87
+ dd-flow status --json`
88
+ ],
89
+ [
90
+ "canon",
91
+ `dd-flow canon - register and resolve the canonical Memory Bank checkout
92
+
93
+ Usage:
94
+ dd-flow canon register --root <canonical-dd-memorybank-root> --json
95
+ dd-flow canon status [--root <canonical-dd-memorybank-root>] --json
96
+ dd-flow canon resolve [--root <canonical-dd-memorybank-root>] --json
97
+
98
+ Discovery order is explicit --root, then DD_MEMORYBANK, then the globally registered runtime value in DD_FLOW_HOME, then known nearby dd-memorybank checkouts from the current working directory. Explicit --root is the only source considered for that command. If multiple different valid canonical checkouts are found, resolve fails closed with blockers so the caller chooses intentionally.
99
+
100
+ A valid canonical root contains dd-flow/README.md, mbb/index.md, protocol/index.md, and canonical-only entrypoints such as dd-flow/mb-init.md, dd-flow/mb-upgrade.md, dd-flow/mb-upgrade-review.md, and dd-flow/mb-distill.md.
101
+
102
+ Examples:
103
+ dd-flow canon register --root "$DD_MEMORYBANK" --json
104
+ dd-flow canon resolve --json
105
+ dd-flow canon status --json`
42
106
  ],
43
107
  [
44
108
  "run",
@@ -46,18 +110,33 @@ project_root is the stable repository identity. workspace_path is the concrete c
46
110
 
47
111
  Usage:
48
112
  dd-flow run start --project-root <root> [--workspace-root <checkout>] --flow-kind <kind> --subject-type <type> --subject-id <id> --slug <slug> [--next-action <text>] --json
113
+ dd-flow id next --type protocol|run --project-root <root> --slug <slug> --json
49
114
  dd-flow run status <RUN-ID|RUN-NNN> --project-root <root> --json
50
115
  dd-flow run list --project-root <root> --json
51
116
  dd-flow run attach-stage <RUN-ID|RUN-NNN> --project-root <root> --stage <name> --dir <NN-stage-slug> --status <status> [--data-schema-id <id>] --json
52
117
  dd-flow run complete-stage <RUN-ID|RUN-NNN> --project-root <root> --stage <name> --status <status> [--stage-report <path>] [--data <path>] [--data-schema-id <id>] [--report <path>] [--alias <path>] --json
53
118
  dd-flow run complete <RUN-ID|RUN-NNN> --project-root <root> --status done|blocked|cancelled|failed [--verdict <text>] [--next-action <text>] --json
54
119
 
55
- RUN-* is the execution envelope for one concrete flow launch. Semantic truth remains in protocol, experiment, DEF, scenario, evidence, and Memory Bank documents. Runtime state is stored under DD_FLOW_HOME/projects/<PRJ-ID-slug>/runtime/runs/<RUN-ID-slug>/, while human-facing artifacts are indexed through <workspace>/.tasks/dd-flow-runs/<RUN-ID-slug>/run-index.json.
120
+ 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 run.json and run-index.json together. Legacy <workspace>/.tasks/dd-flow-runs/<RUN-ID-slug>/ indexes remain readable.
56
121
 
57
122
  Examples:
58
- dd-flow run start --project-root "$PWD" --flow-kind coding --subject-type protocol --subject-id PRT-001-demo --slug demo --json
123
+ dd-flow run start --project-root "$PWD" --flow-kind mb_sdlc --subject-type protocol --subject-id PRT-001-demo --slug demo --json
124
+ dd-flow id next --type run --project-root "$PWD" --slug demo --json
59
125
  dd-flow run attach-stage RUN-001 --project-root "$PWD" --stage plan --dir 01-plan --status running --json
60
126
  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`
127
+ ],
128
+ [
129
+ "id",
130
+ `dd-flow id - preview next typed ids
131
+
132
+ Usage:
133
+ dd-flow id next --type protocol|run --project-root <root> --slug <slug> --json
134
+
135
+ id next is read-only: it scans runtime records and known project files, then returns the next available TYPE-NNN-slug without reserving it. Use run start or protocol register to create durable state.
136
+
137
+ Examples:
138
+ dd-flow id next --type protocol --project-root "$PWD" --slug demo --json
139
+ dd-flow id next --type run --project-root "$PWD" --slug demo --json`
61
140
  ],
62
141
  [
63
142
  "protocol",
@@ -66,10 +145,27 @@ Examples:
66
145
  Usage:
67
146
  dd-flow protocol register <handshake-id> --project-root <root> [--workspace-path <checkout>] --json
68
147
  dd-flow protocol status <protocol-id> --json
148
+ dd-flow protocol ready --project-root <root> --json
149
+ dd-flow protocol blockers <protocol-id> --project-root <root> --json
150
+ dd-flow protocol implement <protocol-id> --project-root <root> [--force --reason <text>] --json
151
+ dd-flow protocol transition <protocol-id> --to <stage> --payload-file <file> [--force --reason <text>] --json
152
+ dd-flow protocol sync-from-run <protocol-id> --run <RUN-ID|RUN-NNN> [--target auto|<stage>] --json
69
153
  dd-flow protocol ready-for-merge <protocol-id> --json
70
154
  dd-flow protocol cancel <protocol-id> --reason <text> [--close-sessions true|false] [--cancel-queue true|false] [--release-locks true|false] [--worktree keep|remove] [--force] --json
71
155
 
156
+ ready, blockers and implement read protocol markdown frontmatter plus runtime state. implement is a preflight/guidance command: it never performs implementation, refuses unresolved blocked_by_protocols unless --force --reason is supplied, refuses terminal protocols and reports the expected next prompt/stage. transition validates the snapshotted flow contract stored in protocol runtime state, writes an audit event and updates lifecycle fields from the payload. --json-file is accepted as a compatibility alias for --payload-file. sync-from-run repairs legacy protocol/run mismatch from concrete RUN evidence instead of requiring manual runtime JSON edits.
157
+
72
158
  The project_root is the stable project identity used for queues, sessions, lanes, and dashboards. Runtime state is stored under DD_FLOW_HOME/projects/<PRJ-ID-slug>/runtime so feature worktree removal does not break status, cancel, or merge finalization. The optional workspace_path records the concrete checkout where the agent works. With --worktree remove, cancel removes the disposable git worktree recorded in runtime state or worktree records; with --force it also force-deletes the local feature branch when present.`
159
+ ],
160
+ [
161
+ "transition",
162
+ `dd-flow transition - compatibility alias for protocol transition
163
+
164
+ Usage:
165
+ dd-flow transition <protocol-id> --to <stage> --payload-file <file> [--force --reason <text>] --json
166
+ dd-flow transition <protocol-id> --to <stage> --json-file <file> [--force --reason <text>] --json
167
+
168
+ Prefer dd-flow protocol transition in new prompts and scripts. This top-level alias remains for older canonical prompts and experiments.`
73
169
  ],
74
170
  [
75
171
  "project",
@@ -132,6 +228,27 @@ Examples:
132
228
  dd-flow lane lock acquire --project-root "$PWD" --lane merge --worker-id worker-1 --path "$PWD" --ttl 300 --reason "merge worker" --json
133
229
  dd-flow lane lock heartbeat --project-root "$PWD" --lane merge --worker-id worker-1 --path "$PWD" --lease-token "$TOKEN" --json
134
230
  dd-flow lane lock release --project-root "$PWD" --lane merge --worker-id worker-1 --path "$PWD" --lease-token "$TOKEN" --reason done --json`
231
+ ],
232
+ [
233
+ "merge",
234
+ `dd-flow merge - current-session merge entrypoint helpers
235
+
236
+ Usage:
237
+ dd-flow merge status --project-root <root> --json
238
+ dd-flow merge one-shot --project-root <root> --worker-id <id> [--path <workspace>] --json
239
+
240
+ merge status reports active merge worker, claimed jobs, active merge lane lock and queue state. merge one-shot is status-only when a long-lived worker, claimed job or active lock exists; otherwise it acquires the merge lane, claims at most one ready job, and returns that job for the shared merge job prompt. If no job is ready, it releases the lock and returns job: null.`
241
+ ],
242
+ [
243
+ "merge-worker",
244
+ `dd-flow merge-worker - manage long-lived project merge workers
245
+
246
+ Usage:
247
+ dd-flow merge-worker status --project-root <root> --json
248
+ dd-flow merge-worker start --project-root <root> --worker-id <id> [--path <workspace>] [--branch <branch>] --json
249
+ dd-flow merge-worker stop --project-root <root> [--worker-id <id>] --reason <text> --json
250
+
251
+ start is idempotent: if a worker, claimed job, or active merge lock already exists it returns status instead of creating a duplicate worker. stop is no-op/status when no worker exists, refuses ambiguous multiple workers unless --worker-id is provided, stops idle workers immediately, and marks busy workers as stop-after-current while preserving the claimed job and lock.`
135
252
  ],
136
253
  [
137
254
  "merge-queue",
@@ -215,16 +332,18 @@ Usage:
215
332
  ],
216
333
  [
217
334
  "dashboard",
218
- `dd-flow dashboard - render or open markdown dashboards
335
+ `dd-flow dashboard - render or open markdown and HTML dashboards
219
336
 
220
337
  Usage:
221
- dd-flow dashboard render --project-root <root> [--output <path>] --json
222
- dd-flow dashboard render-global [--output <path>] --json
223
- dd-flow dashboard open --project-root <root> [--viewer cmux] --json
224
- dd-flow dashboard refresh --project-root <root> [--open auto|true|false] --json
225
- dd-flow dashboard refresh-global [--output <path>] --json
226
-
227
- Dashboards are rendered views of dd-flow state. State-changing commands refresh markdown automatically when dashboard.auto_refresh is enabled. dashboard refresh rewrites project and global markdown when enabled. cmux is used only by explicit open/refresh commands and only according to project config.`
338
+ dd-flow dashboard data --project-root <root> [--protocol <PRT-ID>] --json
339
+ dd-flow dashboard data --global --json
340
+ dd-flow dashboard render --project-root <root> [--format markdown|html] [--protocol <PRT-ID>] [--output <path>] --json
341
+ dd-flow dashboard render-global [--format markdown|html] [--output <path>] --json
342
+ dd-flow dashboard open --project-root <root> [--format markdown|html] [--viewer cmux] --json
343
+ dd-flow dashboard refresh --project-root <root> [--format markdown|html] [--protocol <PRT-ID>] [--open auto|true|false] --json
344
+ dd-flow dashboard refresh-global [--format markdown|html] [--output <path>] --json
345
+
346
+ Dashboards are rendered views of dd-flow state. Markdown remains the compatibility fallback. HTML dashboards write JSON next to self-contained local HTML: global under DD_FLOW_HOME/dashboard/ and project/protocol pages under DD_FLOW_HOME/projects/<PRJ-ID-slug>/dashboard/. State-changing commands still auto-refresh markdown during the transition. cmux is used only by explicit open/refresh commands and only according to project config.`
228
347
  ],
229
348
  [
230
349
  "schema",
@@ -233,7 +352,7 @@ Dashboards are rendered views of dd-flow state. State-changing commands refresh
233
352
  Usage:
234
353
  dd-flow schema validate --schema <name> --file <json-file> [--project-root <root>] [--schema-dir <dir>] --json
235
354
 
236
- Schema names resolve to <name>.schema.json. Lookup order is --schema-dir, then <project-root>/.memory-bank/dd-flow/schemas/, then bundled CLI schemas. If --project-root is omitted, the current working directory is used. JSON mode writes valid results to stdout and structured validation/usage errors to stderr.`
355
+ Schema names resolve to <name>.schema.json. Lookup order is --schema-dir, then <project-root>/.memory-bank/dd-flow/schemas/, then <project-root>/dd-flow/schemas/ for canonical checkouts, then bundled CLI schemas. If --project-root is omitted, the current working directory is used. JSON mode writes valid results to stdout and structured validation/usage errors to stderr.`
237
356
  ],
238
357
  [
239
358
  "schema validate",
@@ -3,17 +3,22 @@ import { helpForArgs } from "./help.js";
3
3
  import { AppError, isAppError } from "../shared/errors.js";
4
4
  import { writeJson } from "../shared/json.js";
5
5
  import { archiveProject, getProjectStatus, migrateProjectIds, registerProject, resolveProject } from "../services/projects.js";
6
- import { cancelProtocol, getProtocolStatus, readyForMerge, registerProtocol, requireProtocol, transitionProtocol } from "../services/protocols.js";
6
+ import { cancelProtocol, getProtocolBlockers, getProtocolStatus, getReadyProtocols, implementProtocol, readyForMerge, registerProtocol, requireProtocol, syncProtocolFromRun, transitionProtocol } from "../services/protocols.js";
7
7
  import { blockPlanItem, completePlanItem, getPlanStatus, setProtocolPlan, skipPlanItem, startPlanItem } from "../services/plans.js";
8
8
  import { cancelMergeQueueJob, claimNextMergeJob, completeMergeJob, failMergeJob, getMergeQueueStatus, noteMergeJob, waitNextMergeJob } from "../services/merge-queue.js";
9
+ import { getMergeWorkerStatus, oneShotMergeClaim, startMergeWorker, stopProjectMergeWorker } from "../services/merge-worker.js";
9
10
  import { cleanupApply, cleanupScan } from "../services/cleanup.js";
10
11
  import { getCodexHomeStatus, getCodexHooksStatus, handleCodexHook, initCodexHome, installCodexHooks, planCodexHome, printCodexHomeEnv, printCodexHooks, removeCodexHome, removeCodexHooks } from "../services/hooks.js";
11
12
  import { bootstrapWorktree, closeWorktree, createWorktreeRecord, getWorktreeStatus, planWorktree } from "../services/worktrees.js";
12
13
  import { acquireLaneLock, checkLaneWorkspace, getLaneStatus, heartbeatLaneLock, releaseLaneLock, setLaneWorkspace, waitForLaneLock } from "../services/lanes.js";
13
14
  import { getProjectConfigStatus, setProjectConfigValue } from "../services/config.js";
14
- import { autoRefreshDashboards, getCmuxStatus, openDashboard, refreshDashboard, refreshGlobalDashboard, renderDashboard, renderGlobalDashboard } from "../services/dashboard.js";
15
+ import { getCanonStatus, registerCanonRoot, resolveCanonRoot } from "../services/canon.js";
16
+ import { getRuntimeStatus } from "../services/status.js";
17
+ import { getCliVersionReport } from "../services/build-info.js";
18
+ import { autoRefreshDashboards, dashboardData, getCmuxStatus, openDashboard, refreshDashboard, refreshGlobalDashboard, renderDashboard, renderGlobalDashboard } from "../services/dashboard.js";
15
19
  import { getFlowSessionStatus, registerFlowSession, stopFlowSession, stopMergeWorker } from "../services/sessions.js";
16
20
  import { attachFlowRunStage, completeFlowRun, completeFlowRunStage, getFlowRunStatus, listFlowRuns, startFlowRun } from "../services/runs.js";
21
+ import { previewNextEntityId } from "../services/ids.js";
17
22
  import { validateSchema } from "../services/schema-validation.js";
18
23
  import { preflightMemoryPermissions } from "../services/memory-permissions.js";
19
24
  import { requireProjectByRoot } from "../services/projects.js";
@@ -31,6 +36,14 @@ export async function runCli(args, io = defaultIo, env = process.env) {
31
36
  if (args.includes("--progress-jsonl")) {
32
37
  throw new AppError("usage", "--progress-jsonl is not implemented yet; use --json for final structured progress", 2);
33
38
  }
39
+ if (output.args.length === 1 && output.args[0] === "--version") {
40
+ const payload = getCliVersionReport();
41
+ if (output.json)
42
+ writeJson(io.stdout, payload);
43
+ else
44
+ io.stdout.write(`dd-flow ${payload.cli.version}\n`);
45
+ return 0;
46
+ }
34
47
  const help = helpForArgs(output.args);
35
48
  if (help) {
36
49
  io.stdout.write(`${help}\n`);
@@ -91,6 +104,8 @@ function progressForCommand(args) {
91
104
  ];
92
105
  }
93
106
  if ((family === "cleanup" && command === "apply") ||
107
+ (family === "merge" && command === "one-shot") ||
108
+ family === "merge-worker" ||
94
109
  (family === "protocol" && command === "cancel") ||
95
110
  (family === "project" && command === "archive") ||
96
111
  family === "run") {
@@ -144,6 +159,13 @@ function renderHumanResult(args, result) {
144
159
  lines.push(`errors: ${errors.length}`);
145
160
  return `${lines.join("\n")}\n`;
146
161
  }
162
+ if (family === "status") {
163
+ return renderStatusHuman(record);
164
+ }
165
+ if (family === "version") {
166
+ const cli = recordObject(record.cli);
167
+ return `dd-flow ${stringValue(cli?.version) ?? "unknown"}\n`;
168
+ }
147
169
  const lines = [`dd-flow ${family ?? ""}${command ? ` ${command}` : ""}: ${ok}`.trim()];
148
170
  const project = record.project && typeof record.project === "object" ? record.project : null;
149
171
  const protocol = record.protocol && typeof record.protocol === "object" ? record.protocol : null;
@@ -168,11 +190,90 @@ function renderHumanResult(args, result) {
168
190
  if (run.run_index_path)
169
191
  lines.push(`run_index: ${String(run.run_index_path)}`);
170
192
  }
193
+ appendFlowGuidanceHuman(lines, record);
194
+ return `${lines.join("\n")}\n`;
195
+ }
196
+ function renderStatusHuman(record) {
197
+ const lines = [`dd-flow status: ${record.ok === false ? "failed" : "ok"}`];
198
+ const cli = recordObject(record.cli);
199
+ const cliBuild = recordObject(cli?.build);
200
+ const builtWithCanon = recordObject(cliBuild?.built_with_canon);
201
+ const cliCompatibility = recordObject(cli?.compatibility);
202
+ const registry = recordObject(cli?.registry);
203
+ const canon = recordObject(record.canon);
204
+ const resolvedCanon = recordObject(canon?.resolved);
205
+ const project = recordObject(record.project);
206
+ const memoryBank = recordObject(project?.memory_bank);
207
+ const flowPack = recordObject(project?.flow_pack);
208
+ const drift = recordObject(project?.drift);
209
+ lines.push(`CLI package: ${stringValue(cli?.package_name) ?? "unknown"} ${stringValue(cli?.version) ?? "unknown"}`);
210
+ if (cliCompatibility) {
211
+ lines.push(`CLI compatibility: ${stringValue(cliCompatibility.verdict) ?? "unknown"} (${stringValue(cliCompatibility.reason) ?? "no_reason"})`);
212
+ if (cliCompatibility.update_command)
213
+ lines.push(`CLI update: ${String(cliCompatibility.update_command)}`);
214
+ }
215
+ if (registry) {
216
+ lines.push(`CLI registry: ${stringValue(registry.latest) ?? "unknown"} (${stringValue(registry.status) ?? "unknown"})`);
217
+ }
218
+ lines.push(`CLI tested with canon: ${stringValue(builtWithCanon?.version) ?? "unknown"} @ ${shortSha(stringValue(builtWithCanon?.commit))}`);
219
+ lines.push(`Active local canon: ${stringValue(resolvedCanon?.root) ?? "unresolved"} ${stringValue(resolvedCanon?.version) ?? "unknown"} @ ${shortSha(stringValue(resolvedCanon?.commit))} (${stringValue(resolvedCanon?.layout) ?? "unknown"})`);
220
+ if (resolvedCanon?.memorybank_root)
221
+ lines.push(`Canon Memory Bank: ${String(resolvedCanon.memorybank_root)}`);
222
+ if (resolvedCanon?.flow_root)
223
+ lines.push(`Canon flow root: ${String(resolvedCanon.flow_root)}`);
224
+ lines.push(`Project: ${stringValue(project?.root) ?? "unknown"}${project?.registered === true ? " (registered)" : " (unregistered)"}`);
225
+ lines.push(`Project Memory Bank: ${stringValue(memoryBank?.version) ?? stringValue(memoryBank?.status) ?? "unknown"}`);
226
+ lines.push(`Project flow pack: ${stringValue(flowPack?.pack_version) ?? stringValue(flowPack?.status) ?? "unknown"} @ ${shortSha(stringValue(flowPack?.source_commit))}`);
227
+ lines.push(`Drift: ${stringValue(drift?.overall) ?? "unknown"}`);
228
+ if (drift?.next_action)
229
+ lines.push(`Next action: ${String(drift.next_action)}`);
230
+ appendFlowGuidanceHuman(lines, record);
171
231
  return `${lines.join("\n")}\n`;
172
232
  }
233
+ function appendFlowGuidanceHuman(lines, record) {
234
+ const guidance = recordObject(record.flow_guidance);
235
+ if (!guidance)
236
+ return;
237
+ const currentStage = stringValue(guidance.current_stage);
238
+ const nextAction = stringValue(guidance.recommended_next_action);
239
+ const prompt = stringValue(guidance.recommended_prompt);
240
+ if (currentStage)
241
+ lines.push(`stage: ${currentStage}`);
242
+ if (nextAction)
243
+ lines.push(`next: ${nextAction}${prompt && prompt !== "none" ? ` (${prompt})` : ""}`);
244
+ const guards = Array.isArray(guidance.guards) ? guidance.guards : [];
245
+ const firstGuard = guards.find((item) => item && typeof item === "object" && !Array.isArray(item));
246
+ if (firstGuard?.id)
247
+ lines.push(`guard: ${String(firstGuard.id)} ${String(firstGuard.status ?? "unknown")}`);
248
+ const missing = Array.isArray(guidance.blocked_if_missing) ? guidance.blocked_if_missing : [];
249
+ if (missing.length > 0)
250
+ lines.push(`missing: ${missing.map(String).join(", ")}`);
251
+ }
252
+ function recordObject(value) {
253
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
254
+ }
255
+ function stringValue(value) {
256
+ return typeof value === "string" && value.length > 0 ? value : null;
257
+ }
258
+ function shortSha(value) {
259
+ return value ? value.slice(0, 12) : "unknown";
260
+ }
173
261
  async function dispatch(args, context, io) {
174
262
  const [family, command, ...rest] = args;
175
263
  const parsed = parseArgs(rest);
264
+ if (family === "status") {
265
+ const rootParsed = parseArgs([command ?? "", ...rest]);
266
+ return getRuntimeStatus(context, {
267
+ projectRoot: optionalOption(rootParsed, "project-root") ?? optionalOption(rootParsed, "root"),
268
+ checkRegistry: hasOption(rootParsed, "check-registry")
269
+ });
270
+ }
271
+ if (family === "version") {
272
+ return getCliVersionReport();
273
+ }
274
+ if (family === "canon") {
275
+ return dispatchCanon(context, command, parsed);
276
+ }
176
277
  if (family === "project" && command === "register") {
177
278
  return registerProject(context, { root: requiredOption(parsed, "root") });
178
279
  }
@@ -209,9 +310,44 @@ async function dispatch(args, context, io) {
209
310
  if (family === "protocol" && command === "status") {
210
311
  return getProtocolStatus(context, { protocolId: requiredPosition(parsed, 0, "protocol-id") });
211
312
  }
313
+ if (family === "protocol" && command === "ready") {
314
+ return getReadyProtocols(context, { projectRoot: requiredOption(parsed, "project-root") });
315
+ }
316
+ if (family === "protocol" && command === "blockers") {
317
+ return getProtocolBlockers(context, {
318
+ protocolId: requiredPosition(parsed, 0, "protocol-id"),
319
+ projectRoot: requiredOption(parsed, "project-root")
320
+ });
321
+ }
322
+ if (family === "protocol" && command === "implement") {
323
+ const reason = optionalOption(parsed, "reason");
324
+ return implementProtocol(context, {
325
+ protocolId: requiredPosition(parsed, 0, "protocol-id"),
326
+ projectRoot: requiredOption(parsed, "project-root"),
327
+ force: hasOption(parsed, "force"),
328
+ ...(reason ? { reason } : {})
329
+ });
330
+ }
212
331
  if (family === "protocol" && command === "ready-for-merge") {
213
332
  return readyForMerge(context, { protocolId: requiredPosition(parsed, 0, "protocol-id") });
214
333
  }
334
+ if (family === "protocol" && command === "transition") {
335
+ const reason = optionalOption(parsed, "reason");
336
+ return transitionProtocol(context, {
337
+ protocolId: requiredPosition(parsed, 0, "protocol-id"),
338
+ to: requiredOption(parsed, "to"),
339
+ jsonFile: requiredOptionAlias(parsed, ["payload-file", "json-file"]),
340
+ force: hasOption(parsed, "force"),
341
+ ...(reason ? { reason } : {})
342
+ });
343
+ }
344
+ if (family === "protocol" && command === "sync-from-run") {
345
+ return syncProtocolFromRun(context, {
346
+ protocolId: requiredPosition(parsed, 0, "protocol-id"),
347
+ runId: requiredOption(parsed, "run"),
348
+ target: optionalOption(parsed, "target") ?? "auto"
349
+ });
350
+ }
215
351
  if (family === "protocol" && command === "cancel") {
216
352
  return cancelProtocol(context, {
217
353
  protocolId: requiredPosition(parsed, 0, "protocol-id"),
@@ -229,7 +365,7 @@ async function dispatch(args, context, io) {
229
365
  return transitionProtocol(context, {
230
366
  protocolId: requiredPosition(transitionParsed, 0, "protocol-id"),
231
367
  to: requiredOption(transitionParsed, "to"),
232
- jsonFile: requiredOption(transitionParsed, "json-file"),
368
+ jsonFile: requiredOptionAlias(transitionParsed, ["payload-file", "json-file"]),
233
369
  force: hasOption(transitionParsed, "force"),
234
370
  ...(reason ? { reason } : {})
235
371
  });
@@ -252,6 +388,12 @@ async function dispatch(args, context, io) {
252
388
  if (family === "merge-queue") {
253
389
  return dispatchMergeQueue(context, command, parsed);
254
390
  }
391
+ if (family === "merge") {
392
+ return dispatchMerge(context, command, parsed);
393
+ }
394
+ if (family === "merge-worker") {
395
+ return dispatchMergeWorker(context, command, parsed);
396
+ }
255
397
  if (family === "cleanup") {
256
398
  return dispatchCleanup(context, command, parsed);
257
399
  }
@@ -261,6 +403,9 @@ async function dispatch(args, context, io) {
261
403
  if (family === "run") {
262
404
  return dispatchRun(context, command, parsed);
263
405
  }
406
+ if (family === "id") {
407
+ return dispatchId(context, command, parsed);
408
+ }
264
409
  if (family === "integration" && command === "cmux") {
265
410
  return dispatchCmux(context, parsed);
266
411
  }
@@ -287,6 +432,18 @@ async function dispatch(args, context, io) {
287
432
  }
288
433
  throw new AppError("usage", `Unknown command: ${args.join(" ") || "<empty>"}`, 2);
289
434
  }
435
+ function dispatchCanon(context, command, parsed) {
436
+ if (command === "register") {
437
+ return registerCanonRoot(context, { root: requiredOption(parsed, "root") });
438
+ }
439
+ if (command === "status") {
440
+ return getCanonStatus(context, { root: optionalOption(parsed, "root") });
441
+ }
442
+ if (command === "resolve") {
443
+ return resolveCanonRoot(context, { explicitRoot: optionalOption(parsed, "root") });
444
+ }
445
+ throw new AppError("usage", `Unknown canon command: ${command ?? "<empty>"}`, 2);
446
+ }
290
447
  function dispatchMergeQueue(context, command, parsed) {
291
448
  if (hasOption(parsed, "session-id")) {
292
449
  throw new AppError("usage", "--session-id is no longer accepted on merge-queue commands; use --worker-id", 2);
@@ -346,6 +503,40 @@ function dispatchMergeQueue(context, command, parsed) {
346
503
  }
347
504
  throw new AppError("usage", `Unknown merge-queue command: ${command ?? "<empty>"}`, 2);
348
505
  }
506
+ function dispatchMerge(context, command, parsed) {
507
+ if (command === "status") {
508
+ return getMergeWorkerStatus(context, { projectRoot: requiredOption(parsed, "project-root") });
509
+ }
510
+ if (command === "one-shot") {
511
+ return oneShotMergeClaim(context, {
512
+ projectRoot: requiredOption(parsed, "project-root"),
513
+ workerId: requiredWorkerId(parsed),
514
+ workspacePath: workspacePathOption(parsed)
515
+ });
516
+ }
517
+ throw new AppError("usage", `Unknown merge command: ${command ?? "<empty>"}`, 2);
518
+ }
519
+ function dispatchMergeWorker(context, command, parsed) {
520
+ if (command === "status") {
521
+ return getMergeWorkerStatus(context, { projectRoot: requiredOption(parsed, "project-root") });
522
+ }
523
+ if (command === "start") {
524
+ return startMergeWorker(context, {
525
+ projectRoot: requiredOption(parsed, "project-root"),
526
+ workerId: requiredWorkerId(parsed),
527
+ workspacePath: workspacePathOption(parsed),
528
+ branch: optionalOption(parsed, "branch")
529
+ });
530
+ }
531
+ if (command === "stop") {
532
+ return stopProjectMergeWorker(context, {
533
+ projectRoot: requiredOption(parsed, "project-root"),
534
+ workerId: optionalOption(parsed, "worker-id"),
535
+ reason: requiredOption(parsed, "reason")
536
+ });
537
+ }
538
+ throw new AppError("usage", `Unknown merge-worker command: ${command ?? "<empty>"}`, 2);
539
+ }
349
540
  function dispatchMemory(command, parsed) {
350
541
  const subcommand = requiredPosition(parsed, 0, "memory subcommand");
351
542
  if (command === "permissions" && subcommand === "preflight") {
@@ -481,6 +672,16 @@ function dispatchRun(context, command, parsed) {
481
672
  }
482
673
  throw new AppError("usage", `Unknown run command: ${command ?? "<empty>"}`, 2);
483
674
  }
675
+ function dispatchId(context, command, parsed) {
676
+ if (command === "next") {
677
+ return previewNextEntityId(context, {
678
+ projectRoot: requiredOption(parsed, "project-root"),
679
+ type: requiredOption(parsed, "type"),
680
+ slug: requiredOption(parsed, "slug")
681
+ });
682
+ }
683
+ throw new AppError("usage", `Unknown id command: ${command ?? "<empty>"}`, 2);
684
+ }
484
685
  function dispatchCmux(context, parsed) {
485
686
  const action = requiredPosition(parsed, 0, "integration cmux action");
486
687
  if (action === "status") {
@@ -489,14 +690,31 @@ function dispatchCmux(context, parsed) {
489
690
  throw new AppError("usage", `Unknown integration cmux action: ${action}`, 2);
490
691
  }
491
692
  function dispatchDashboard(context, command, parsed) {
693
+ if (command === "data") {
694
+ const global = hasOption(parsed, "global");
695
+ return dashboardData(context, {
696
+ global,
697
+ projectRoot: global ? undefined : requiredOption(parsed, "project-root"),
698
+ protocol: optionalOption(parsed, "protocol")
699
+ });
700
+ }
492
701
  if (command === "render") {
493
- return renderDashboard(context, { projectRoot: requiredOption(parsed, "project-root"), output: optionalOption(parsed, "output") });
702
+ return renderDashboard(context, {
703
+ projectRoot: requiredOption(parsed, "project-root"),
704
+ output: optionalOption(parsed, "output"),
705
+ format: optionalOption(parsed, "format"),
706
+ protocol: optionalOption(parsed, "protocol")
707
+ });
494
708
  }
495
709
  if (command === "render-global") {
496
- return renderGlobalDashboard(context, { output: optionalOption(parsed, "output") });
710
+ return renderGlobalDashboard(context, { output: optionalOption(parsed, "output"), format: optionalOption(parsed, "format") });
497
711
  }
498
712
  if (command === "open") {
499
- return openDashboard(context, { projectRoot: requiredOption(parsed, "project-root"), viewer: optionalOption(parsed, "viewer") });
713
+ return openDashboard(context, {
714
+ projectRoot: requiredOption(parsed, "project-root"),
715
+ viewer: optionalOption(parsed, "viewer"),
716
+ format: optionalOption(parsed, "format")
717
+ });
500
718
  }
501
719
  if (command === "refresh") {
502
720
  const open = optionalOption(parsed, "open");
@@ -505,11 +723,13 @@ function dispatchDashboard(context, command, parsed) {
505
723
  }
506
724
  return refreshDashboard(context, {
507
725
  projectRoot: requiredOption(parsed, "project-root"),
508
- open: open
726
+ open: open,
727
+ format: optionalOption(parsed, "format"),
728
+ protocol: optionalOption(parsed, "protocol")
509
729
  });
510
730
  }
511
731
  if (command === "refresh-global") {
512
- return refreshGlobalDashboard(context, { output: optionalOption(parsed, "output") });
732
+ return refreshGlobalDashboard(context, { output: optionalOption(parsed, "output"), format: optionalOption(parsed, "format") });
513
733
  }
514
734
  throw new AppError("usage", `Unknown dashboard command: ${command ?? "<empty>"}`, 2);
515
735
  }
@@ -784,6 +1004,14 @@ function requiredOption(parsed, key) {
784
1004
  }
785
1005
  return value;
786
1006
  }
1007
+ function requiredOptionAlias(parsed, keys) {
1008
+ for (const key of keys) {
1009
+ const value = optionalOption(parsed, key);
1010
+ if (value)
1011
+ return value;
1012
+ }
1013
+ throw new AppError("usage", `Missing required option: ${keys.map((key) => `--${key}`).join(" or ")}`, 2);
1014
+ }
787
1015
  function hasOption(parsed, key) {
788
1016
  return parsed.options.has(key);
789
1017
  }
@@ -882,6 +1110,12 @@ function projectRootForMutation(context, args, result) {
882
1110
  if (family === "merge-queue") {
883
1111
  return projectRootForMergeQueueMutation(context, command, parsed);
884
1112
  }
1113
+ if (family === "merge" && command === "one-shot") {
1114
+ return requiredOption(parsed, "project-root");
1115
+ }
1116
+ if (family === "merge-worker" && ["start", "stop"].includes(command ?? "")) {
1117
+ return requiredOption(parsed, "project-root");
1118
+ }
885
1119
  if (family === "cleanup" && command === "apply") {
886
1120
  return requiredOption(parsed, "project-root");
887
1121
  }