@deksden-com/dd-flow-cli 0.9.0-beta.17 → 0.9.0-beta.19

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,17 @@
1
1
  # @deksden-com/dd-flow-cli
2
2
 
3
+ ## 0.9.0-beta.19
4
+
5
+ ### Patch Changes
6
+
7
+ - 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.
8
+
9
+ ## 0.9.0-beta.18
10
+
11
+ ### Patch Changes
12
+
13
+ - Add Factory Droid harness routing, native lifecycle receipts and physical Session usage ingestion. Preserve child identity and reject conflicting hook replays or inclusive Droid usage to prevent incorrect Work binding and double counting.
14
+
3
15
  ## 0.9.0-beta.17
4
16
 
5
17
  ### Patch Changes
package/README.md CHANGED
@@ -17,6 +17,27 @@ Never use a broad `pkill`/`killall`. A failed owned stop retains resources.
17
17
 
18
18
  It does not replace Memory Bank prompts and does not make product, design, merge, or verification judgments. Prompts own intent, route selection, planning depth, evidence meaning, and semantic readiness. The CLI owns explicit local state: projects, protocols, Codex session bindings, transitions, worktree records, lanes, locks, merge queue jobs, hook records, and audit events.
19
19
 
20
+ ## Factory Droid harness
21
+
22
+ Configure `droid-cli` in `~/.dd-flow/harnesses.json` with absolute
23
+ `adapter_command` (`dd-droid`) and `runtime_command` (`droid`) paths. Agent
24
+ profiles use the `droid` alias; the adapter receives `--droid-bin`.
25
+
26
+ The controlled adapter sends `dd-flow/droid-tool-event@1` JSON to
27
+ `dd-flow droid event handle --project-root <root> --json`. Events include a
28
+ stable `event_id`, `daemon_id`, `phase` (`before` or `after`), native Session
29
+ identity and parent, absolute Session directory, `Execute` input, observed
30
+ profile and transcript path. Before a lifecycle invocation, the returned
31
+ `hookSpecificOutput.updatedInput` adds its one-use `--hook-event-id` receipt.
32
+ The event ID belongs to the native hook invocation and is reused only for
33
+ transport retries. Session success is established by the Work contract.
34
+
35
+ `dd-flow droid usage ingest --project-root <root> --json` accepts cumulative
36
+ per-Session counters with explicit `usage_scope: physical_session` and
37
+ `completeness: complete|partial`. Inclusive root counters are rejected;
38
+ partial windows remain unavailable in RUN totals until complete evidence
39
+ arrives. Keep inclusive provider totals in adapter evidence for reconciliation.
40
+
20
41
  ## Runtime Model
21
42
 
22
43
  Root local database:
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "cli_package": "@deksden-com/dd-flow-cli",
3
- "cli_version": "0.9.0-beta.17",
4
- "cli_commit": "162dd994d19c0d454a2ad6b7b62abd70c3e848d6",
5
- "built_at": "2026-09-06T00:16:19.328Z",
3
+ "cli_version": "0.9.0-beta.19",
4
+ "cli_commit": "02648aa2db37e9332089ca941b06f5735b563496",
5
+ "built_at": "2026-09-06T13:33:22.164Z",
6
6
  "built_with_canon": {
7
- "version": "4.0.4",
8
- "commit": "ced59c19d0a4062146a1dc4ccb977e10284a2fe4",
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
@@ -38,6 +38,7 @@ Core commands:
38
38
  codex home/hooks/hook Manage Codex homes, hooks, and hook events.
39
39
  zcode event/usage Ingest trusted zcode-acp lifecycle and usage evidence.
40
40
  grok event/usage Ingest trusted Grok Build lifecycle and usage evidence.
41
+ droid event/usage Ingest trusted Factory Droid lifecycle and physical usage evidence.
41
42
  opencode event/usage Ingest trusted OpenCode lifecycle and usage evidence.
42
43
  agy event/usage Ingest trusted Antigravity CLI lifecycle and usage evidence.
43
44
  worktree plan/create/status Record Worktrunk-backed feature worktrees.
@@ -154,7 +155,8 @@ Usage:
154
155
  dd-flow run status <RUN-ID|RUN-short-id> --project-root <root> --json
155
156
  dd-flow run list --project-root <root> --json
156
157
  dd-flow run prepare-vnext-specify --project-root <root> --slug <slug> --json
157
- dd-flow run snapshot create <RUN-ID|RUN-short-id> (--stage-entry <stage>|--candidate|--incomplete) --project-root <root> --output <directory> --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
158
160
  dd-flow run snapshot restore --snapshot <directory> --project-root <prepared-root> --json
159
161
  dd-flow run snapshot bootstrap create --project-root <root> --output <directory> --json
160
162
  dd-flow run snapshot bootstrap restore --snapshot <directory> --project-root <prepared-root> --json
@@ -169,7 +171,7 @@ Usage:
169
171
 
170
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.
171
173
 
172
- 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.
173
175
 
174
176
  Examples:
175
177
  dd-flow run start --project-root "$PWD" --flow-kind mb_sdlc --subject-type protocol --subject-id PRT-001-demo --slug demo --json
@@ -191,6 +193,7 @@ Usage:
191
193
  dd-flow stage fanout status <RUN-ID|RUN-short-id> --project-root <root> --stage <name> --json
192
194
  dd-flow stage fanout dispatch <RUN-ID|RUN-short-id> --project-root <root> --stage <name> --json
193
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
194
197
  dd-flow stage finish <RUN-ID|RUN-short-id> --project-root <root> --stage <name> [--semantic-file <RUN-local-json>] --json
195
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
196
199
 
@@ -549,6 +552,17 @@ Usage:
549
552
 
550
553
  Both payloads are read from stdin. The event adapter retains lifecycle identity;
551
554
  the usage adapter retains cumulative provider counters for RUN accounting.`
555
+ ],
556
+ [
557
+ "droid event",
558
+ `dd-flow droid event - ingest a controlled Factory Droid Execute hook
559
+
560
+ Usage:
561
+ dd-flow droid event handle --project-root <root> --json
562
+ dd-flow droid usage ingest --project-root <root> --json
563
+
564
+ Reads trusted adapter JSON from stdin. Usage requires physical_session scope and explicit completeness.
565
+ `
552
566
  ],
553
567
  [
554
568
  "opencode event",
@@ -10,7 +10,7 @@ import { blockPlanItem, completePlanItem, getPlanStatus, skipPlanItem, startPlan
10
10
  import { cancelMergeQueueJob, claimNextMergeJob, claimMergeJob, claimMergeBundle, completeMergeBundle, completeMergeJob, failMergeBundle, failMergeJob, getMergeBundleStatus, getMergeQueueStatus, noteMergeJob, waitNextMergeJob } from "../services/merge-queue.js";
11
11
  import { getMergeWorkerStatus, oneShotMergeClaim, startMergeWorker, stopProjectMergeWorker } from "../services/merge-worker.js";
12
12
  import { cleanupApply, cleanupScan } from "../services/cleanup.js";
13
- import { getCodexHomeStatus, getCodexHooksStatus, handleCodexHook, handleAgyEvent, handleGrokEvent, handleOpenCodeEvent, handleZcodeEvent, isolatedDdFlowHomeFromHookStdin, claimBootstrapHookEvent, findRecentMatchingHookEvent, stageStartMatchKey, initCodexHome, installCodexHooks, planCodexHome, printCodexHomeEnv, printCodexHooks, removeCodexHome, removeCodexHooks } from "../services/hooks.js";
13
+ import { getCodexHomeStatus, getCodexHooksStatus, handleCodexHook, handleAgyEvent, handleDroidEvent, handleGrokEvent, handleOpenCodeEvent, handleZcodeEvent, isolatedDdFlowHomeFromHookStdin, claimBootstrapHookEvent, findRecentMatchingHookEvent, stageStartMatchKey, initCodexHome, installCodexHooks, planCodexHome, printCodexHomeEnv, printCodexHooks, removeCodexHome, removeCodexHooks } from "../services/hooks.js";
14
14
  import { bootstrapWorktree, closeWorktree, createWorktreeRecord, getWorktreeStatus, planWorktree } from "../services/worktrees.js";
15
15
  import { acquireLaneLock, cancelLaneWaiter, checkLaneWorkspace, getLaneWaiters, getLaneStatus, heartbeatLaneLock, releaseLaneLock, setLaneWorkspace, waitAcquireLaneLock, waitForLaneLock } from "../services/lanes.js";
16
16
  import { getProjectConfigStatus, setProjectConfigValue } from "../services/config.js";
@@ -26,7 +26,7 @@ import { attachFlowRunStage, completeFlowRun, completeFlowRunStage, getFlowRunSe
26
26
  import { previewNextEntityId } from "../services/ids.js";
27
27
  import { validateSchema } from "../services/schema-validation.js";
28
28
  import { preflightMemoryPermissions } from "../services/memory-permissions.js";
29
- import { ingestAgyUsage, ingestGrokUsage, ingestOpenCodeUsage, ingestZcodeUsage } from "../services/usage.js";
29
+ import { ingestDroidUsage, ingestAgyUsage, ingestGrokUsage, ingestOpenCodeUsage, ingestZcodeUsage } from "../services/usage.js";
30
30
  import { applyMigration, assessMigrationImpact, assertUpgradeRunStorageReady, planMigration, verifyMigrationReport } from "../services/migrations.js";
31
31
  import { publishProjectSummary } from "../services/project-summary.js";
32
32
  import { requireProjectByRoot } from "../services/projects.js";
@@ -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 { 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);
@@ -765,11 +771,18 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
765
771
  const resultStdin = hasOption(parsed, "result-stdin");
766
772
  if (Boolean(resultFile) === resultStdin)
767
773
  throw new AppError("usage", "work finish requires exactly one of --result-file or --result-stdin", 2);
774
+ const workId = requiredPosition(parsed, 0, "work-id");
775
+ const projectRoot = requiredScopeProjectRoot(scopeProjectRoot, "work finish");
776
+ assertWorkLifecycleOwner(context, { workId, projectRoot, operation: "work_finish", ...(optionalOption(parsed, "hook-event-id") ? { hookEventId: optionalOption(parsed, "hook-event-id") } : {}) });
768
777
  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));
778
+ return await finishWork(context, workId, result, commandProgress(io, output));
779
+ }
780
+ if (family === "work" && command === "fail") {
781
+ const workId = requiredPosition(parsed, 0, "work-id");
782
+ const projectRoot = requiredScopeProjectRoot(scopeProjectRoot, "work fail");
783
+ assertWorkLifecycleOwner(context, { workId, projectRoot, operation: "work_fail", ...(optionalOption(parsed, "hook-event-id") ? { hookEventId: optionalOption(parsed, "hook-event-id") } : {}) });
784
+ return failWork(context, workId, requiredOption(parsed, "reason"));
770
785
  }
771
- if (family === "work" && command === "fail")
772
- return failWork(context, requiredPosition(parsed, 0, "work-id"), requiredOption(parsed, "reason"));
773
786
  if (family === "work" && command === "cancel")
774
787
  return cancelWork(context, requiredPosition(parsed, 0, "work-id"), requiredOption(parsed, "reason"));
775
788
  if (family === "work" && command === "retry")
@@ -882,8 +895,17 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
882
895
  const resultStdin = hasOption(parsed, "result-stdin");
883
896
  const retryCheckId = optionalOption(parsed, "retry-check");
884
897
  const retryReason = optionalOption(parsed, "reason");
898
+ const hookEventId = optionalOption(parsed, "hook-event-id");
885
899
  if (retryCheckId && !retryReason?.trim())
886
900
  throw new AppError("usage", "--retry-check requires --reason explaining the recovered environment", 2);
901
+ const protectedLifecycle = (stage === "specify" && isVnextSpecifyRun(context, { projectRoot, runId }))
902
+ || (stage === "protocolize" && isVnextProtocolizeRun(context, { projectRoot, runId }))
903
+ || (stage === "plan" && isVnextPlanRun(context, { projectRoot, runId }))
904
+ || (stage === "plan-review" && isVnextPlanReviewRun(context, { projectRoot, runId }))
905
+ || (stage === "code" && isVnextPlanRun(context, { projectRoot, runId }))
906
+ || (stage === "code-review" && isVnextCodeReviewRun(context, { projectRoot, runId }));
907
+ if (protectedLifecycle)
908
+ assertStageLifecycleOwner(context, { projectRoot, runId, stage, operation: "stage_finish", ...(hookEventId ? { hookEventId } : {}) });
887
909
  if (stage === "specify" && isVnextSpecifyRun(context, { projectRoot, runId })) {
888
910
  const outcome = optionalOption(parsed, "outcome");
889
911
  if (!outcome || !["specified", "failed", "cancelled"].includes(outcome)) {
@@ -949,11 +971,23 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
949
971
  if (family === "stage" && command === "pause") {
950
972
  if (!hasOption(parsed, "question-stdin"))
951
973
  throw new AppError("usage", "stage pause requires --question-stdin", 2);
974
+ const projectRoot = requiredOption(parsed, "project-root");
975
+ const runId = requiredPosition(parsed, 0, "run-id");
976
+ const stage = requiredOption(parsed, "stage");
977
+ const workId = requiredOption(parsed, "work");
978
+ const protectedLifecycle = (stage === "specify" && isVnextSpecifyRun(context, { projectRoot, runId }))
979
+ || (stage === "protocolize" && isVnextProtocolizeRun(context, { projectRoot, runId }))
980
+ || (stage === "plan" && isVnextPlanRun(context, { projectRoot, runId }))
981
+ || (stage === "plan-review" && isVnextPlanReviewRun(context, { projectRoot, runId }))
982
+ || (stage === "code" && isVnextPlanRun(context, { projectRoot, runId }))
983
+ || (stage === "code-review" && isVnextCodeReviewRun(context, { projectRoot, runId }));
984
+ if (protectedLifecycle)
985
+ assertStageLifecycleOwner(context, { projectRoot, runId, stage, workId, operation: "stage_pause", ...(optionalOption(parsed, "hook-event-id") ? { hookEventId: optionalOption(parsed, "hook-event-id") } : {}) });
952
986
  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"),
987
+ projectRoot,
988
+ runId,
989
+ stage,
990
+ workId,
957
991
  question: await readStdin(io.stdin)
958
992
  });
959
993
  }
@@ -1070,6 +1104,16 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
1070
1104
  throw new AppError("usage", "Unknown opencode usage action", 2);
1071
1105
  return ingestOpenCodeUsage(context, { projectRoot: requiredOption(parsed, "project-root"), stdin: await readStdin(io.stdin) });
1072
1106
  }
1107
+ if (family === "droid" && command === "event") {
1108
+ if (requiredPosition(parsed, 0, "droid event action") !== "handle")
1109
+ throw new AppError("usage", "Unknown droid event action", 2);
1110
+ return handleDroidEvent(context, { projectRoot: requiredOption(parsed, "project-root"), stdin: await readStdin(io.stdin) });
1111
+ }
1112
+ if (family === "droid" && command === "usage") {
1113
+ if (requiredPosition(parsed, 0, "droid usage action") !== "ingest")
1114
+ throw new AppError("usage", "Unknown droid usage action", 2);
1115
+ return ingestDroidUsage(context, { projectRoot: requiredOption(parsed, "project-root"), stdin: await readStdin(io.stdin) });
1116
+ }
1073
1117
  if (family === "agy" && command === "event")
1074
1118
  return dispatchAgyEvent(context, parsed, await readStdin(io.stdin));
1075
1119
  if (family === "agy" && command === "usage") {
@@ -1419,6 +1463,22 @@ function dispatchSession(context, command, parsed) {
1419
1463
  throw new AppError("usage", `Unknown session command: ${command ?? "<empty>"}`, 2);
1420
1464
  }
1421
1465
  function dispatchRun(context, command, parsed) {
1466
+ if (command === "recovery") {
1467
+ const action = requiredPosition(parsed, 0, "recovery action");
1468
+ const projectRoot = requiredOption(parsed, "project-root");
1469
+ const runId = requiredPosition(parsed, 1, "run-id");
1470
+ if (action === "inspect")
1471
+ return inspectRunRecovery(context, { projectRoot, runId });
1472
+ if (action === "begin")
1473
+ return beginRunRecovery(context, { projectRoot, runId, interruption: requiredOption(parsed, "interruption-json") });
1474
+ if (action === "seal")
1475
+ return sealRunRecovery(context, { projectRoot, runId, recoveryId: requiredOption(parsed, "recovery-id"), settlement: requiredOption(parsed, "settlement-json") });
1476
+ if (action === "capture")
1477
+ return recordRecoveryCapture(context, { projectRoot, runId, recoveryId: requiredOption(parsed, "recovery-id"), capturePath: requiredOption(parsed, "capture-path") });
1478
+ if (action === "resume")
1479
+ return resumeRunRecovery(context, { projectRoot, runId, recoveryId: requiredOption(parsed, "recovery-id") });
1480
+ throw new AppError("usage", "Use: dd-flow run recovery inspect|begin|seal|capture|resume", 2);
1481
+ }
1422
1482
  if (command === "snapshot") {
1423
1483
  const action = requiredPosition(parsed, 0, "snapshot action");
1424
1484
  if (action === "bootstrap") {
@@ -1438,9 +1498,10 @@ function dispatchRun(context, command, parsed) {
1438
1498
  if (action === "create") {
1439
1499
  const candidate = parsed.options.has("candidate");
1440
1500
  const incomplete = parsed.options.has("incomplete");
1501
+ const recoveryId = optionalOption(parsed, "recovery-id");
1441
1502
  const stageEntry = optionalOption(parsed, "stage-entry");
1442
- if (Number(candidate) + Number(incomplete) + Number(stageEntry !== undefined) !== 1)
1443
- throw new AppError("usage", "Use exactly one of --candidate, --incomplete or --stage-entry", 2);
1503
+ if (Number(candidate) + Number(incomplete) + Number(stageEntry !== undefined) + Number(recoveryId !== undefined) !== 1)
1504
+ throw new AppError("usage", "Use exactly one of --candidate, --incomplete, --recovery-id or --stage-entry", 2);
1444
1505
  const input = {
1445
1506
  projectRoot: requiredOption(parsed, "project-root"),
1446
1507
  runId: requiredPosition(parsed, 1, "run-id"),
@@ -1448,6 +1509,8 @@ function dispatchRun(context, command, parsed) {
1448
1509
  };
1449
1510
  if (incomplete)
1450
1511
  return createEvalRunSnapshot(context, { ...input, incomplete: true });
1512
+ if (recoveryId)
1513
+ return createEvalRunSnapshot(context, { ...input, recoveryId });
1451
1514
  return candidate
1452
1515
  ? createEvalRunSnapshot(context, { ...input, candidate: true })
1453
1516
  : createEvalRunSnapshot(context, { ...input, stageEntry: stageEntry });
@@ -2372,7 +2435,7 @@ function projectRootForMutation(context, args, result, resolvedScope) {
2372
2435
  if (family === "opencode" && ((command === "event" && requiredPosition(parsed, 0, "opencode event action") === "handle") || command === "usage")) {
2373
2436
  return requiredOption(parsed, "project-root");
2374
2437
  }
2375
- if (family === "agy" && ((command === "event" && requiredPosition(parsed, 0, "agy event action") === "handle") || command === "usage"))
2438
+ if (["agy", "droid"].includes(family ?? "") && ((command === "event" && requiredPosition(parsed, 0, `${family} event action`) === "handle") || command === "usage"))
2376
2439
  return requiredOption(parsed, "project-root");
2377
2440
  if (family === "codex" && command === "hooks") {
2378
2441
  const action = requiredPosition(parsed, 0, "codex hooks action");
@@ -7,7 +7,7 @@
7
7
  "properties": {
8
8
  "schema_id": {"const": "dd-flow/agent-profile@1"},
9
9
  "id": {"type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*$"},
10
- "harness": {"enum": ["codex", "zcode", "agy", "opencode", "grok"]},
10
+ "harness": {"enum": ["codex", "zcode", "agy", "opencode", "grok", "droid"]},
11
11
  "provider": {"type": "string", "minLength": 1},
12
12
  "model": {"type": "string", "minLength": 1},
13
13
  "reasoning": {"type": "string", "minLength": 1},
@@ -106,6 +106,8 @@ function isRouterNative(args) {
106
106
  || (first === "zcode" && args[1] === "usage" && args[2] === "ingest")
107
107
  || (first === "grok" && args[1] === "event" && args[2] === "handle")
108
108
  || (first === "grok" && args[1] === "usage" && args[2] === "ingest")
109
+ || (first === "droid" && args[1] === "event" && args[2] === "handle")
110
+ || (first === "droid" && args[1] === "usage" && args[2] === "ingest")
109
111
  || (first === "opencode" && args[1] === "event" && args[2] === "handle")
110
112
  || (first === "opencode" && args[1] === "usage" && args[2] === "ingest")
111
113
  || (first === "agy" && args[1] === "event" && args[2] === "handle")
@@ -55,7 +55,13 @@ export function createEvalRunSnapshot(context, input) {
55
55
  const project = requireProjectByRoot(context, projectRoot);
56
56
  const status = getFlowRunStatus(context, { projectRoot, runId: input.runId });
57
57
  assertDedicatedHome(context, project.id, input.runId);
58
- if (input.incomplete) { /* Forensic evidence is not an accepted or restorable boundary. */ }
58
+ const recovery = typeof input.recoveryId === "string";
59
+ if (recovery) {
60
+ const guard = context.db.get("SELECT status, recovery_id FROM run_recovery_guards WHERE project_id = ? AND run_id = ? AND recovery_id = ?", [project.id, input.runId, input.recoveryId]);
61
+ if (!guard || guard.status !== "sealed")
62
+ throw new AppError("recovery_invalid_state", "Recovery snapshots require the matching sealed recovery guard", 1, { run_id: input.runId, recovery_id: input.recoveryId ?? null, status: guard?.status ?? null });
63
+ }
64
+ if (input.incomplete || recovery) { /* Recovery captures only after the runner seals all writers. */ }
59
65
  else if (input.candidate)
60
66
  assertTerminalCandidate(status.index.stage_runs ?? []);
61
67
  else if (input.stageEntry)
@@ -66,7 +72,7 @@ export function createEvalRunSnapshot(context, input) {
66
72
  const activeChildren = input.candidate
67
73
  ? context.db.get("SELECT COUNT(*) AS count FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id IS NOT NULL AND status IN ('created', 'running', 'paused')", [project.id, input.runId])?.count ?? 0
68
74
  : context.db.get("SELECT COUNT(*) AS count FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id IS NOT NULL AND (status IN ('running', 'paused') OR (status = 'created' AND (? IS NULL OR COALESCE(result_schema, '') NOT LIKE ?)))", [project.id, input.runId, allowedCreatedResultSchema, allowedCreatedResultSchema ? `${allowedCreatedResultSchema}@%` : ""])?.count ?? 0;
69
- if (!input.incomplete && activeChildren > 0)
75
+ if (!input.incomplete && !recovery && activeChildren > 0)
70
76
  throw new AppError("snapshot_not_quiescent", `${input.candidate ? "Candidate" : "Stage-entry"} snapshot requires no active child Work`, 1, { run_id: input.runId, active_children: activeChildren });
71
77
  const output = path.resolve(input.output);
72
78
  if (fs.existsSync(output))
@@ -103,9 +109,9 @@ export function createEvalRunSnapshot(context, input) {
103
109
  project_id: project.id,
104
110
  project_root: projectRoot,
105
111
  dd_flow_home: context.ddFlowHome,
106
- purpose: input.incomplete ? "incomplete" : input.candidate ? "candidate" : "stage_entry",
107
- stage_entry: input.incomplete || input.candidate ? null : input.stageEntry,
108
- ...(input.incomplete ? { consistency: "sqlite_read_snapshot_files_copied_nonatomically", active_child_work_count: activeChildren, restorable: false } : {}),
112
+ purpose: input.incomplete ? "incomplete" : recovery ? "recovery" : input.candidate ? "candidate" : "stage_entry",
113
+ stage_entry: input.incomplete || recovery || input.candidate ? null : input.stageEntry,
114
+ ...(input.incomplete ? { consistency: "sqlite_read_snapshot_files_copied_nonatomically", active_child_work_count: activeChildren, restorable: false } : recovery ? { recovery_id: input.recoveryId, consistency: "sealed_writer_barrier_required", active_child_work_count: activeChildren, restorable: "same-runtime-or-explicit-restore" } : {}),
109
115
  created_at: context.now(),
110
116
  runtime_sha256: treeHash(path.join(output, "runtime")),
111
117
  project_sha256: treeHash(path.join(output, "project")),
@@ -129,6 +135,8 @@ export function restoreEvalRunSnapshot(context, input) {
129
135
  const manifest = readManifest(snapshot);
130
136
  if (manifest.purpose === "incomplete")
131
137
  throw new AppError("snapshot_incomplete_not_restorable", "Incomplete snapshot is forensic evidence, not a stage-entry fixture", 1, { snapshot });
138
+ if (manifest.purpose === "recovery")
139
+ throw new AppError("snapshot_recovery_requires_explicit_restore", "Recovery snapshots require the recovery runner; they are not a generic stage-entry fixture", 1, { snapshot });
132
140
  if (manifest.purpose === "candidate")
133
141
  throw new AppError("snapshot_candidate_not_restorable", "A terminal candidate snapshot is evidence, not a stage-entry fixture", 1, { snapshot });
134
142
  if (!manifest.stage_entry)
@@ -383,7 +391,7 @@ function readManifest(snapshot) {
383
391
  if (!fs.existsSync(file))
384
392
  throw new AppError("snapshot_missing", "Snapshot manifest is missing", 1, { snapshot });
385
393
  const value = JSON.parse(fs.readFileSync(file, "utf8"));
386
- if (value.schema_id !== "dd-flow/eval-run-snapshot@5" || !value.run_id || !value.project_root || !value.dd_flow_home || typeof value.purpose !== "string" || !["stage_entry", "candidate", "incomplete"].includes(value.purpose) || (value.purpose === "stage_entry" && !value.stage_entry) || (value.purpose !== "stage_entry" && value.stage_entry !== null) || !value.runtime_sha256 || !value.project_sha256 || !value.project_git || !value.workspace)
394
+ if (value.schema_id !== "dd-flow/eval-run-snapshot@5" || !value.run_id || !value.project_root || !value.dd_flow_home || typeof value.purpose !== "string" || !["stage_entry", "candidate", "incomplete", "recovery"].includes(value.purpose) || (value.purpose === "stage_entry" && !value.stage_entry) || (value.purpose !== "stage_entry" && value.stage_entry !== null) || !value.runtime_sha256 || !value.project_sha256 || !value.project_git || !value.workspace)
387
395
  throw new AppError("snapshot_invalid", "Snapshot manifest is invalid", 1, { snapshot });
388
396
  if (!value.project_git.branch || !value.project_git.head || !value.project_git.bundle_sha256)
389
397
  throw new AppError("snapshot_invalid", "Snapshot project Git metadata is incomplete", 1, { snapshot });
@@ -10,6 +10,7 @@ export function harnessKey(value) {
10
10
  zcode: "zcode-acp", "zcode-acp": "zcode-acp",
11
11
  grok: "grok-acp", "grok-acp": "grok-acp",
12
12
  agy: "antigravity-cli", "antigravity-cli": "antigravity-cli",
13
+ droid: "droid-cli", "droid-cli": "droid-cli",
13
14
  opencode: "opencode-server", "opencode-server": "opencode-server"
14
15
  };
15
16
  const resolved = keys[value];
@@ -60,6 +61,7 @@ export function harnessRuntimeArguments(ddFlowHome, harness) {
60
61
  "zcode-acp": "--zcode-acp-bin",
61
62
  "grok-acp": "--grok-bin",
62
63
  "antigravity-cli": "--agy-bin",
64
+ "droid-cli": "--droid-bin",
63
65
  "opencode-server": "--opencode-bin"
64
66
  };
65
67
  return [option[key], runtime];