@deksden-com/dd-flow-cli 0.9.0-beta.18 → 0.9.0-beta.21

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 CHANGED
@@ -1,5 +1,23 @@
1
1
  # @deksden-com/dd-flow-cli
2
2
 
3
+ ## 0.9.0-beta.21
4
+
5
+ ### Patch Changes
6
+
7
+ - Validate every selected engine artifact and execute its pinned snapshot even when its version matches the router. Retain a new recovery interruption before root acknowledgement.
8
+
9
+ ## 0.9.0-beta.20
10
+
11
+ ### Patch Changes
12
+
13
+ - Verify recovery payloads and workspace drift before resuming, preserve staged Git changes, publish snapshots through a temporary directory, and allow explicit recovery restoration into fresh targets. Fence RUN mutations and late asynchronous Work completion while recovery is active.
14
+
15
+ ## 0.9.0-beta.19
16
+
17
+ ### Patch Changes
18
+
19
+ - Require a fresh trusted hook receipt from the physical Work owner before terminal stage or Work lifecycle commands can mutate a vNext RUN. Record terminal native-child observations at their originating Stage so unbound delegation is retained as a diagnostic instead of contaminating a later fan-out wave.
20
+
3
21
  ## 0.9.0-beta.18
4
22
 
5
23
  ### Patch Changes
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "cli_package": "@deksden-com/dd-flow-cli",
3
- "cli_version": "0.9.0-beta.18",
4
- "cli_commit": "5624d92e8e96732db6a81f1cf86fb5342f38401f",
5
- "built_at": "2026-09-06T03:05:30.379Z",
3
+ "cli_version": "0.9.0-beta.21",
4
+ "cli_commit": "2350e9cbeddba31e283d27384163b2bb44690d46",
5
+ "built_at": "2026-09-06T23:59:54.226Z",
6
6
  "built_with_canon": {
7
- "version": "4.0.5",
8
- "commit": "272579e9035b88772a0f017c9bec09f877c17552",
7
+ "version": "4.0.6",
8
+ "commit": "2aafb30d23e3f646b736b7ead25438cab97b16bf",
9
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",
package/dist/cli/help.js CHANGED
@@ -155,8 +155,9 @@ Usage:
155
155
  dd-flow run status <RUN-ID|RUN-short-id> --project-root <root> --json
156
156
  dd-flow run list --project-root <root> --json
157
157
  dd-flow run prepare-vnext-specify --project-root <root> --slug <slug> --json
158
- dd-flow run snapshot create <RUN-ID|RUN-short-id> (--stage-entry <stage>|--candidate|--incomplete) --project-root <root> --output <directory> --json
159
- dd-flow run snapshot restore --snapshot <directory> --project-root <prepared-root> --json
158
+ dd-flow run recovery inspect|begin|seal|capture|resume <RUN-ID|RUN-short-id> --project-root <root> --json
159
+ dd-flow run snapshot create <RUN-ID|RUN-short-id> (--stage-entry <stage>|--candidate|--incomplete|--recovery-id <RCV-ID>) --project-root <root> --output <directory> --json
160
+ dd-flow run snapshot restore --snapshot <directory> --project-root <prepared-root> [--recovery-id <RCV-ID>] --json
160
161
  dd-flow run snapshot bootstrap create --project-root <root> --output <directory> --json
161
162
  dd-flow run snapshot bootstrap restore --snapshot <directory> --project-root <prepared-root> --json
162
163
  dd-flow run timeline <RUN-ID|RUN-short-id> --project-root <root> --json
@@ -170,7 +171,7 @@ Usage:
170
171
 
171
172
  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. Use \`dd-flow stat usage --run …\` to reread registered local Codex transcript counters and tool-call facts and refresh the current usage projection; unavailable data is explicit rather than zero.
172
173
 
173
- run prepare-vnext-specify allocates an otherwise unstarted vNext RUN for the canonical SPECIFY entry checkpoint. run snapshot is the eval-only checkpoint boundary: it snapshots one quiescent RUN, its dedicated DD_FLOW_HOME, and the exact project tree including its Git repository, then restores them into a fresh dedicated runtime. It is not a general runtime backup command and fails closed for a shared runtime.
174
+ run prepare-vnext-specify allocates an otherwise unstarted vNext RUN for the canonical SPECIFY entry checkpoint. run recovery makes an interrupted RUN read-only, requires an explicit settled-tree receipt, captures immutable recovery evidence, then explicitly returns delegated Work to the ready graph. run snapshot is the eval-only checkpoint boundary: it snapshots one quiescent RUN, its dedicated DD_FLOW_HOME, and the exact project tree including its Git repository, then restores them into a fresh dedicated runtime. It is not a general runtime backup command and fails closed for a shared runtime.
174
175
 
175
176
  Examples:
176
177
  dd-flow run start --project-root "$PWD" --flow-kind mb_sdlc --subject-type protocol --subject-id PRT-001-demo --slug demo --json
@@ -192,6 +193,7 @@ Usage:
192
193
  dd-flow stage fanout status <RUN-ID|RUN-short-id> --project-root <root> --stage <name> --json
193
194
  dd-flow stage fanout dispatch <RUN-ID|RUN-short-id> --project-root <root> --stage <name> --json
194
195
  dd-flow stage fanout reconcile <RUN-ID> --project-root <root> --stage <name> --observations-file <native-observations.json> --json
196
+ dd-flow stage native observe <RUN-ID> --project-root <root> --stage <name> --observations-file <native-observations.json> --json
195
197
  dd-flow stage finish <RUN-ID|RUN-short-id> --project-root <root> --stage <name> [--semantic-file <RUN-local-json>] --json
196
198
  dd-flow stage finish <RUN-ID|RUN-short-id> --project-root <root> --stage specify (--result-file <RUN-local-specify.json>|--result-stdin) --outcome <specified|failed|cancelled> --json
197
199
 
@@ -41,14 +41,15 @@ import { addVnextCodeRepair, finishVnextCode, startVnextCode } from "../services
41
41
  import { addVnextCodeReviewRepair, finishVnextCodeReview, isVnextCodeReviewRun, startVnextCodeReview } from "../services/vnext-code-review.js";
42
42
  import { applyVnextMerge, finishVnextMerge, getVnextMergeRequest, isVnextMergeRun, repairVnextMerge, routeVnextMergeRequest, startVnextMerge } from "../services/vnext-merge.js";
43
43
  import { mergeServerStatus, serveMergeRequests, stopMergeServer } from "../services/merge-server.js";
44
- import { getVnextFanoutStatus, reconcileVnextFanout } from "../services/vnext-fanout.js";
44
+ import { getVnextFanoutStatus, observeVnextNativeChildren, reconcileVnextFanout } from "../services/vnext-fanout.js";
45
45
  import { pauseStageForUser, resumeStageAfterUser } from "../services/stage-pause.js";
46
46
  import { blockStageForRuntime, unblockStageAfterRuntimeRepair } from "../services/stage-blocker.js";
47
47
  import { classifyCliOperation, requiresExplicitUpgradeAuthorization } from "../services/cli-operation-classifier.js";
48
48
  import { renderWorkerPrompt } from "../services/prompts.js";
49
49
  import { finishStage, startStage } from "../services/stage-lifecycle.js";
50
- import { addWorkBatch, cancelWork, deleteWork, failWork, finishWork, listWorks, mutateWorkDeps, retryWork, showWork, startWork } from "../services/work-registry.js";
50
+ import { addWorkBatch, assertStageLifecycleOwner, assertWorkLifecycleOwner, cancelWork, deleteWork, failWork, finishWork, listWorks, mutateWorkDeps, retryWork, showWork, startWork } from "../services/work-registry.js";
51
51
  import { createEvalBootstrapSnapshot, createEvalRunSnapshot, prepareVnextSpecifyRun, restoreEvalBootstrapSnapshot, restoreEvalRunSnapshot } from "../services/eval-snapshots.js";
52
+ import { acceptRunRecovery, beginRunRecovery, inspectRunRecovery, recordRecoveryCapture, resumeRunRecovery, sealRunRecovery } from "../services/run-recovery.js";
52
53
  import { loadExternalStageContext } from "../services/stage-context.js";
53
54
  import { stopManagedProcess, confirmManagedProcess, finishManagedProcess, heartbeatManagedProcess, managedProcessStatus, reconcileExpiredManagedProcesses, registerManagedProcess } from "../services/managed-processes.js";
54
55
  import { startRuntimeService } from "../services/runtime-service.js";
@@ -670,6 +671,11 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
670
671
  }
671
672
  throw new AppError("usage", "Use: dd-flow stage fanout <status|dispatch|reconcile> <RUN> --stage <stage> --project-root <path>; reconcile requires --observations-file <native-observations.json>", 2);
672
673
  }
674
+ if (family === "stage" && command === "native") {
675
+ if (requiredPosition(parsed, 0, "native action") !== "observe")
676
+ throw new AppError("usage", "Use: dd-flow stage native observe <RUN> --stage <stage> --observations-file <native-observations.json> --project-root <path>", 2);
677
+ return observeVnextNativeChildren(context, { projectRoot: requiredScopeProjectRoot(scopeProjectRoot, "stage native observe"), runId: requiredPosition(parsed, 1, "run-id"), stage: requiredOption(parsed, "stage"), observations: JSON.parse(fs.readFileSync(requiredOption(parsed, "observations-file"), "utf8")) });
678
+ }
673
679
  if (family === "run" && command === "capacity") {
674
680
  if (requiredPosition(parsed, 0, "capacity action") !== "record")
675
681
  throw new AppError("usage", "Use: dd-flow run capacity record <RUN> --available-slots <0..15> --project-root <path>", 2);
@@ -757,19 +763,27 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
757
763
  return deleteWork(context, requiredPosition(parsed, 0, "work-id"));
758
764
  if (family === "work" && command === "start") {
759
765
  const hookEventId = optionalOption(parsed, "hook-event-id");
766
+ const recoveryId = optionalOption(parsed, "recovery-id");
760
767
  const projectRoot = requiredScopeProjectRoot(scopeProjectRoot, "work start");
761
- return startWork(context, requiredPosition(parsed, 0, "work-id"), { ...(hookEventId ? { hookEventId } : {}), projectRoot });
768
+ return startWork(context, requiredPosition(parsed, 0, "work-id"), { ...(hookEventId ? { hookEventId } : {}), ...(recoveryId ? { recoveryId } : {}), projectRoot });
762
769
  }
763
770
  if (family === "work" && command === "finish") {
764
771
  const resultFile = optionalOption(parsed, "result-file");
765
772
  const resultStdin = hasOption(parsed, "result-stdin");
766
773
  if (Boolean(resultFile) === resultStdin)
767
774
  throw new AppError("usage", "work finish requires exactly one of --result-file or --result-stdin", 2);
775
+ const workId = requiredPosition(parsed, 0, "work-id");
776
+ const projectRoot = requiredScopeProjectRoot(scopeProjectRoot, "work finish");
777
+ assertWorkLifecycleOwner(context, { workId, projectRoot, operation: "work_finish", ...(optionalOption(parsed, "hook-event-id") ? { hookEventId: optionalOption(parsed, "hook-event-id") } : {}) });
768
778
  const result = resultFile ? fs.readFileSync(path.resolve(resultFile), "utf8") : await readStdin(io.stdin);
769
- return await finishWork(context, requiredPosition(parsed, 0, "work-id"), result, commandProgress(io, output));
779
+ return await finishWork(context, workId, result, commandProgress(io, output));
780
+ }
781
+ if (family === "work" && command === "fail") {
782
+ const workId = requiredPosition(parsed, 0, "work-id");
783
+ const projectRoot = requiredScopeProjectRoot(scopeProjectRoot, "work fail");
784
+ assertWorkLifecycleOwner(context, { workId, projectRoot, operation: "work_fail", ...(optionalOption(parsed, "hook-event-id") ? { hookEventId: optionalOption(parsed, "hook-event-id") } : {}) });
785
+ return failWork(context, workId, requiredOption(parsed, "reason"));
770
786
  }
771
- if (family === "work" && command === "fail")
772
- return failWork(context, requiredPosition(parsed, 0, "work-id"), requiredOption(parsed, "reason"));
773
787
  if (family === "work" && command === "cancel")
774
788
  return cancelWork(context, requiredPosition(parsed, 0, "work-id"), requiredOption(parsed, "reason"));
775
789
  if (family === "work" && command === "retry")
@@ -882,8 +896,17 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
882
896
  const resultStdin = hasOption(parsed, "result-stdin");
883
897
  const retryCheckId = optionalOption(parsed, "retry-check");
884
898
  const retryReason = optionalOption(parsed, "reason");
899
+ const hookEventId = optionalOption(parsed, "hook-event-id");
885
900
  if (retryCheckId && !retryReason?.trim())
886
901
  throw new AppError("usage", "--retry-check requires --reason explaining the recovered environment", 2);
902
+ const protectedLifecycle = (stage === "specify" && isVnextSpecifyRun(context, { projectRoot, runId }))
903
+ || (stage === "protocolize" && isVnextProtocolizeRun(context, { projectRoot, runId }))
904
+ || (stage === "plan" && isVnextPlanRun(context, { projectRoot, runId }))
905
+ || (stage === "plan-review" && isVnextPlanReviewRun(context, { projectRoot, runId }))
906
+ || (stage === "code" && isVnextPlanRun(context, { projectRoot, runId }))
907
+ || (stage === "code-review" && isVnextCodeReviewRun(context, { projectRoot, runId }));
908
+ if (protectedLifecycle)
909
+ assertStageLifecycleOwner(context, { projectRoot, runId, stage, operation: "stage_finish", ...(hookEventId ? { hookEventId } : {}) });
887
910
  if (stage === "specify" && isVnextSpecifyRun(context, { projectRoot, runId })) {
888
911
  const outcome = optionalOption(parsed, "outcome");
889
912
  if (!outcome || !["specified", "failed", "cancelled"].includes(outcome)) {
@@ -949,11 +972,23 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
949
972
  if (family === "stage" && command === "pause") {
950
973
  if (!hasOption(parsed, "question-stdin"))
951
974
  throw new AppError("usage", "stage pause requires --question-stdin", 2);
975
+ const projectRoot = requiredOption(parsed, "project-root");
976
+ const runId = requiredPosition(parsed, 0, "run-id");
977
+ const stage = requiredOption(parsed, "stage");
978
+ const workId = requiredOption(parsed, "work");
979
+ const protectedLifecycle = (stage === "specify" && isVnextSpecifyRun(context, { projectRoot, runId }))
980
+ || (stage === "protocolize" && isVnextProtocolizeRun(context, { projectRoot, runId }))
981
+ || (stage === "plan" && isVnextPlanRun(context, { projectRoot, runId }))
982
+ || (stage === "plan-review" && isVnextPlanReviewRun(context, { projectRoot, runId }))
983
+ || (stage === "code" && isVnextPlanRun(context, { projectRoot, runId }))
984
+ || (stage === "code-review" && isVnextCodeReviewRun(context, { projectRoot, runId }));
985
+ if (protectedLifecycle)
986
+ assertStageLifecycleOwner(context, { projectRoot, runId, stage, workId, operation: "stage_pause", ...(optionalOption(parsed, "hook-event-id") ? { hookEventId: optionalOption(parsed, "hook-event-id") } : {}) });
952
987
  return pauseStageForUser(context, {
953
- projectRoot: requiredOption(parsed, "project-root"),
954
- runId: requiredPosition(parsed, 0, "run-id"),
955
- stage: requiredOption(parsed, "stage"),
956
- workId: requiredOption(parsed, "work"),
988
+ projectRoot,
989
+ runId,
990
+ stage,
991
+ workId,
957
992
  question: await readStdin(io.stdin)
958
993
  });
959
994
  }
@@ -1429,6 +1464,24 @@ function dispatchSession(context, command, parsed) {
1429
1464
  throw new AppError("usage", `Unknown session command: ${command ?? "<empty>"}`, 2);
1430
1465
  }
1431
1466
  function dispatchRun(context, command, parsed) {
1467
+ if (command === "recovery") {
1468
+ const action = requiredPosition(parsed, 0, "recovery action");
1469
+ const projectRoot = requiredOption(parsed, "project-root");
1470
+ const runId = requiredPosition(parsed, 1, "run-id");
1471
+ if (action === "inspect")
1472
+ return inspectRunRecovery(context, { projectRoot, runId });
1473
+ if (action === "begin")
1474
+ return beginRunRecovery(context, { projectRoot, runId, interruption: requiredOption(parsed, "interruption-json") });
1475
+ if (action === "seal")
1476
+ return sealRunRecovery(context, { projectRoot, runId, recoveryId: requiredOption(parsed, "recovery-id"), settlement: requiredOption(parsed, "settlement-json") });
1477
+ if (action === "capture")
1478
+ return recordRecoveryCapture(context, { projectRoot, runId, recoveryId: requiredOption(parsed, "recovery-id"), capturePath: requiredOption(parsed, "capture-path") });
1479
+ if (action === "resume")
1480
+ return resumeRunRecovery(context, { projectRoot, runId, recoveryId: requiredOption(parsed, "recovery-id"), daemonId: requiredOption(parsed, "daemon-id"), harness: requiredOption(parsed, "harness"), nativeSessionId: requiredOption(parsed, "native-session-id") });
1481
+ if (action === "accept")
1482
+ return acceptRunRecovery(context, { projectRoot, runId, recoveryId: requiredOption(parsed, "recovery-id"), ...(optionalOption(parsed, "hook-event-id") ? { hookEventId: optionalOption(parsed, "hook-event-id") } : {}) });
1483
+ throw new AppError("usage", "Use: dd-flow run recovery inspect|begin|seal|capture|resume|accept", 2);
1484
+ }
1432
1485
  if (command === "snapshot") {
1433
1486
  const action = requiredPosition(parsed, 0, "snapshot action");
1434
1487
  if (action === "bootstrap") {
@@ -1448,9 +1501,10 @@ function dispatchRun(context, command, parsed) {
1448
1501
  if (action === "create") {
1449
1502
  const candidate = parsed.options.has("candidate");
1450
1503
  const incomplete = parsed.options.has("incomplete");
1504
+ const recoveryId = optionalOption(parsed, "recovery-id");
1451
1505
  const stageEntry = optionalOption(parsed, "stage-entry");
1452
- if (Number(candidate) + Number(incomplete) + Number(stageEntry !== undefined) !== 1)
1453
- throw new AppError("usage", "Use exactly one of --candidate, --incomplete or --stage-entry", 2);
1506
+ if (Number(candidate) + Number(incomplete) + Number(stageEntry !== undefined) + Number(recoveryId !== undefined) !== 1)
1507
+ throw new AppError("usage", "Use exactly one of --candidate, --incomplete, --recovery-id or --stage-entry", 2);
1454
1508
  const input = {
1455
1509
  projectRoot: requiredOption(parsed, "project-root"),
1456
1510
  runId: requiredPosition(parsed, 1, "run-id"),
@@ -1458,6 +1512,8 @@ function dispatchRun(context, command, parsed) {
1458
1512
  };
1459
1513
  if (incomplete)
1460
1514
  return createEvalRunSnapshot(context, { ...input, incomplete: true });
1515
+ if (recoveryId)
1516
+ return createEvalRunSnapshot(context, { ...input, recoveryId });
1461
1517
  return candidate
1462
1518
  ? createEvalRunSnapshot(context, { ...input, candidate: true })
1463
1519
  : createEvalRunSnapshot(context, { ...input, stageEntry: stageEntry });
@@ -1465,6 +1521,7 @@ function dispatchRun(context, command, parsed) {
1465
1521
  if (action === "restore")
1466
1522
  return restoreEvalRunSnapshot(context, {
1467
1523
  snapshot: requiredOption(parsed, "snapshot"),
1524
+ ...(optionalOption(parsed, "recovery-id") ? { recoveryId: requiredOption(parsed, "recovery-id") } : {}),
1468
1525
  projectRoot: requiredOption(parsed, "project-root")
1469
1526
  });
1470
1527
  throw new AppError("usage", "Use: dd-flow run snapshot bootstrap create|restore | dd-flow run snapshot create <RUN> (--stage-entry <stage>|--candidate) --project-root <path> --output <directory> | dd-flow run snapshot restore --snapshot <directory> --project-root <path>", 2);
@@ -19,6 +19,8 @@ export function getCmuxStatus(context, input) {
19
19
  }
20
20
  export function renderDashboard(context, input) {
21
21
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
22
+ if (projectRecoveryGuarded(context, project.id))
23
+ throw new AppError("run_recovery_guarded", "Project dashboard is frozen while its RUN is retained for recovery", 1);
22
24
  const format = parseDashboardFormat(input.format);
23
25
  if (format === "html") {
24
26
  if (input.protocol) {
@@ -170,6 +172,10 @@ export function refreshAllDashboards(context, input = {}) {
170
172
  }
171
173
  export function autoRefreshDashboards(context, input) {
172
174
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
175
+ // Hooks and usage ingestion continue to record receipts after capture.
176
+ // Their derived timestamp must not mutate the captured workspace.
177
+ if (projectRecoveryGuarded(context, project.id))
178
+ return { ok: true, skipped: true, reason: "run_recovery_guarded" };
173
179
  const config = readProjectConfig(context, project.id);
174
180
  if (!config.dashboard.auto_refresh) {
175
181
  return { ok: true, skipped: true, reason: "dashboard_auto_refresh_disabled" };
@@ -196,6 +202,9 @@ export function autoRefreshDashboards(context, input) {
196
202
  }
197
203
  return result;
198
204
  }
205
+ function projectRecoveryGuarded(context, projectId) {
206
+ return Boolean(context.db.get("SELECT 1 FROM run_recovery_guards WHERE project_id = ? AND status IN ('draining', 'sealed', 'resuming') LIMIT 1", [projectId]));
207
+ }
199
208
  export function dashboardSummary(context, projectId, projectRoot) {
200
209
  const config = readProjectConfig(context, projectId);
201
210
  return {
@@ -174,7 +174,9 @@ export function routeArgsThroughEngine(context, args, io, stdin, env, resolvedPr
174
174
  if (!selection.selected)
175
175
  return Promise.resolve(null);
176
176
  const current = getCliBuildInfo();
177
- if (selection.selected.package_name === current.package_name && selection.selected.package_version === current.version) {
177
+ const currentRoot = findPackageRoot(path.dirname(sourceFile));
178
+ if (selection.selected.package_name === current.package_name && selection.selected.package_version === current.version
179
+ && currentRoot !== null && fs.realpathSync(selection.selected.snapshot_root) === fs.realpathSync(currentRoot)) {
178
180
  return Promise.resolve({ routed: true, exitCode: 0, inProcess: true });
179
181
  }
180
182
  const entrypoint = resolveManifestEntrypoint(selection.selected);
@@ -383,11 +385,7 @@ export function selectEngine(context, input, options = {}, classification = inpu
383
385
  const manifests = readInstalledManifests(engineStoreRoot(context.ddFlowHome))
384
386
  .filter((manifest) => manifest.package_name === packageName)
385
387
  .filter((manifest) => requiredRange !== null && satisfiesRange(manifest.package_version, requiredRange))
386
- .filter((manifest) => options.allowCurrentInProcess
387
- && manifest.package_name === build.package_name
388
- && manifest.package_version === build.version
389
- ? true
390
- : manifestHealthy(manifest))
388
+ .filter((manifest) => manifestHealthy(manifest))
391
389
  .sort((a, b) => {
392
390
  const versionOrder = compareSemverStrings(b.package_version, a.package_version);
393
391
  if (versionOrder !== 0)