@deksden-com/dd-flow-cli 0.9.0-beta.38 → 0.9.0-beta.39

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 (72) hide show
  1. package/CHANGELOG.md +10 -1
  2. package/dist/build-info.json +5 -5
  3. package/dist/cli/help.js +42 -1
  4. package/dist/cli/run-cli.js +145 -17
  5. package/dist/harness-runtime/bin/dd-agy.mjs +3 -2
  6. package/dist/harness-runtime/bin/dd-codex.mjs +4 -3
  7. package/dist/harness-runtime/bin/dd-droid.mjs +3 -2
  8. package/dist/harness-runtime/bin/dd-grok.mjs +4 -3
  9. package/dist/harness-runtime/bin/dd-opencode.mjs +4 -3
  10. package/dist/harness-runtime/bin/dd-zcode.mjs +4 -3
  11. package/dist/harness-runtime/lib/daemon-operations.mjs +48 -9
  12. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +49 -12
  13. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +57 -16
  14. package/dist/harness-runtime/lib/dd-codex.mjs +10 -2
  15. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +3 -2
  16. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +12 -6
  17. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +4 -3
  18. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +8 -5
  19. package/dist/harness-runtime/lib/managed-daemon.mjs +125 -6
  20. package/dist/harness-runtime/lib/process-json.mjs +1 -1
  21. package/dist/harness-runtime/lib/session-settlement.mjs +4 -2
  22. package/dist/schemas/run-control-receipt.schema.json +99 -0
  23. package/dist/schemas/run-control-request.schema.json +36 -0
  24. package/dist/services/cleanup.js +7 -4
  25. package/dist/services/cli-operation-classifier.js +3 -1
  26. package/dist/services/code-checks.js +33 -7
  27. package/dist/services/controller-fanout.js +105 -0
  28. package/dist/services/engines.js +2 -0
  29. package/dist/services/eval-snapshots.js +347 -36
  30. package/dist/services/execution-policy.js +122 -6
  31. package/dist/services/external-work-launch.js +33 -12
  32. package/dist/services/harness-adapter.js +49 -2
  33. package/dist/services/managed-daemon-binding.js +38 -0
  34. package/dist/services/managed-processes.js +54 -7
  35. package/dist/services/merge-server.js +53 -30
  36. package/dist/services/native-daemon-history.js +49 -0
  37. package/dist/services/native-session-control.js +31 -0
  38. package/dist/services/protocols.js +9 -0
  39. package/dist/services/recovery-snapshot-database.js +103 -0
  40. package/dist/services/run-control-receipt.js +56 -0
  41. package/dist/services/run-control-worker.js +297 -0
  42. package/dist/services/run-control.js +752 -0
  43. package/dist/services/run-controller-adapter.js +44 -0
  44. package/dist/services/run-controller-capture.js +117 -0
  45. package/dist/services/run-controller-process.js +178 -0
  46. package/dist/services/run-controller-recovery.js +219 -0
  47. package/dist/services/run-controller-state.js +31 -0
  48. package/dist/services/run-controller.js +538 -0
  49. package/dist/services/run-recovery-runtime.js +69 -0
  50. package/dist/services/run-recovery.js +33 -9
  51. package/dist/services/runs.js +65 -13
  52. package/dist/services/runtime-budget.js +362 -0
  53. package/dist/services/runtime-scope-capture.js +50 -0
  54. package/dist/services/runtime-scope-control.js +431 -0
  55. package/dist/services/runtime-scope-resume.js +543 -0
  56. package/dist/services/runtime-scope-stop.js +95 -0
  57. package/dist/services/runtime-scope-worker.js +165 -0
  58. package/dist/services/runtime-service.js +4 -0
  59. package/dist/services/vnext-code-review.js +1 -1
  60. package/dist/services/vnext-code.js +1 -1
  61. package/dist/services/vnext-execution-profile.js +2 -35
  62. package/dist/services/vnext-fanout.js +23 -7
  63. package/dist/services/vnext-plan-review.js +12 -9
  64. package/dist/services/vnext-plan.js +1 -1
  65. package/dist/services/vnext-protocolize.js +10 -4
  66. package/dist/services/vnext-specify.js +12 -5
  67. package/dist/services/work-registry.js +26 -8
  68. package/dist/services/workspace-bootstrap.js +10 -1
  69. package/dist/storage/database.js +160 -13
  70. package/dist/storage/writer-contract.js +76 -0
  71. package/dist/storage/writer-migration.js +102 -0
  72. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # @deksden-com/dd-flow-cli
2
2
 
3
+ ## 0.9.0-beta.39
4
+
5
+ ### Patch Changes
6
+
7
+ - 803d2d0: Own managed Stage/Work execution, immutable boundary captures, mixed-profile handoffs and operator pause/stop/resume in the shared CLI runtime. Fence productive admission durably, reconcile exact native/process generations, preserve interrupted and unknown outcomes, and resume retained sessions without replaying completed work.
8
+
9
+ Control experiment-wide Subject, Judge and probe ownership through one retained resource registry, with detached control/recovery workers and explicit physical drain and release evidence. Require guarded shared-store migration and current runtime bindings; remove dependence on eval-owned productive adapters.
10
+
11
+ Discover native Codex children from spawn receipts and release terminal failed provider turns only after a fresh settled-tree observation.
12
+
3
13
  ## 0.9.0-beta.38
4
14
 
5
15
  ### Patch Changes
@@ -1037,7 +1047,6 @@ record` command with the bounded 15-probe contract.
1037
1047
  - 16f68a3: Add router-native engine snapshot commands and routed project-command dispatch.
1038
1048
 
1039
1049
  Also include the runtime/dashboard compatibility wave needed by the current Memory Bank canon:
1040
-
1041
1050
  - target-based dashboard `open`/`refresh` commands and related-command help;
1042
1051
  - persisted FIFO lane waiters and merge wait-acquire specialization;
1043
1052
  - `migration plan/report/verify` evidence helpers for controlled `mb-upgrade` runtime migrations;
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "cli_package": "@deksden-com/dd-flow-cli",
3
- "cli_version": "0.9.0-beta.38",
4
- "cli_commit": "e1d723e620ee5aeec961c2bee1de1e72fcfb71a9",
5
- "built_at": "2026-09-08T03:57:03.109Z",
3
+ "cli_version": "0.9.0-beta.39",
4
+ "cli_commit": "adabaee64d3de604908351aa1ee048208c1dd249",
5
+ "built_at": "2026-09-09T16:08:31.531Z",
6
6
  "built_with_canon": {
7
- "version": "4.0.6",
8
- "commit": "c6fc50cb3b5526ea0162ee4454d1ee36f17be2da",
7
+ "version": "4.1.0",
8
+ "commit": "ef349bf47cba1c987468e51d73a0dbadbd48dc1f",
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
@@ -1,4 +1,36 @@
1
1
  const helpPages = new Map([
2
+ ["runtime scope", `Usage:
3
+ dd-flow runtime scope fence --scope-id EVAL_ID --request-id ID --json
4
+ dd-flow runtime scope status --scope-id EVAL_ID --json
5
+ dd-flow runtime scope stop --scope-id EVAL_ID --request-id ID --json
6
+ dd-flow runtime scope control --scope-id EVAL_ID --request-id ID --mode pause|stop [--manifest-json JSON] --json
7
+ dd-flow runtime scope resume --scope-id EVAL_ID --request-id ID --generation N --capture-key SHA256 --json
8
+
9
+ Use the EVAL manifest's retained DD_FLOW_RESOURCE_HOME. Commands select exactly
10
+ one scope across its registered runtime homes; neighboring scopes are untouched.
11
+ fence durably blocks new provider turns and owned baseline checks. It does not
12
+ stop existing work. status reports retained ownership, not proof of settlement.
13
+ stop fences first and requests owned daemon-tree and baseline-process cleanup.
14
+ Inspect settled and each node's evidence: ok alone does not prove cleanup.
15
+ Unproven ownership or cleanup remains pending; repeat stop to observe settlement.
16
+ The fence/stop route is terminal cancellation and has no resume command.
17
+ control records recoverable scope intent and starts a detached drain owner.
18
+ The EVAL resolver supplies a runtime-scope-manifest@1 declaration with the source
19
+ manifest path, SHA-256 and execution IDs, including queued executions. Declaration
20
+ changes are rejected. resume accepts the exact captured generation and starts
21
+ detached recovery preparation. It does not yet release scope admission: inspect
22
+ dispatch_blocked and pending reasons; accepted preparation is not resumed work.
23
+ RUN pause/stop/resume uses dd-flow run control.`],
24
+ ["storage", `Usage:
25
+ dd-flow storage migrate-writer --database ABSOLUTE_DB --backup NEW_ABSOLUTE_FILE --offline --json
26
+
27
+ Drain all local and external runtime owners before using --offline. This command
28
+ migrates one existing database, not a whole runtime or retained provider Session.
29
+ It refuses unresolved ownership rows, preserves a verified WAL-safe backup, and
30
+ fences unaware old writers. It does not initialize the default DD_FLOW_HOME.
31
+ The backup path must be new; existing backups are never overwritten. An exact
32
+ retry of a committed migration returns its receipt after verifying the backup.
33
+ If no committed receipt exists, retain the failed backup and choose a new path.`],
2
34
  [
3
35
  "",
4
36
  `dd-flow - mechanical control layer for dd-flow workflows
@@ -33,6 +65,8 @@ Core commands:
33
65
  session register/status/stop Register flow sessions and stop workers.
34
66
  dashboard data/render/open/refresh Render, refresh, open, and inspect project/global dashboards.
35
67
  schema validate Validate canonical dd-flow JSON data contracts.
68
+ storage migrate-writer Explicitly back up and fence a drained SQLite store.
69
+ runtime scope fence/status/stop Fence, inspect, or cancel one retained EVAL scope.
36
70
  memory permissions preflight Check Memory Bank write/read permissions.
37
71
  integration cmux status Inspect optional cmux integration.
38
72
  codex home/hooks/hook Manage Codex homes, hooks, and hook events.
@@ -153,8 +187,11 @@ Usage:
153
187
  dd-flow run start --project-root <root> [--workspace-root <checkout>] --flow-kind <kind> --subject-type <type> --subject-id <id> --slug <slug> [--preset compact|normal|full] [--task-profile-file <file>] [--protocol-override-file <file>] [--flag key=value] [--next-action <text>] --json
154
188
  dd-flow id next --type protocol|run --project-root <root> --slug <slug> --json
155
189
  dd-flow run status <RUN-ID|RUN-short-id> --project-root <root> --json
190
+ dd-flow run control status --run <RUN-ID> --project-root <root> --json
191
+ dd-flow run drive launch --run <RUN-ID> --project-root <root> --request-id <id> --context-file <file> --context-sha256 <sha256> [--stop-after <stage>] [--capture-root <directory>] --json
192
+ dd-flow run drive status --run <RUN-ID> --project-root <root> [--after <sequence>] --json
156
193
  dd-flow run list --project-root <root> --json
157
- dd-flow run prepare-vnext-specify --project-root <root> --slug <slug> --json
194
+ dd-flow run prepare-vnext-specify --project-root <root> --slug <slug> [--execution-routing-file <file>] --json
158
195
  dd-flow run recovery inspect|begin|seal|capture|resume <RUN-ID|RUN-short-id> --project-root <root> --json
159
196
  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
197
  dd-flow run snapshot restore --snapshot <directory> --project-root <prepared-root> [--recovery-id <RCV-ID>] --json
@@ -171,8 +208,12 @@ Usage:
171
208
 
172
209
  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.
173
210
 
211
+ run start and prepare-vnext-specify accept --execution-routing-file for vNext RUNs: dd-flow/execution-routing@1 with an execution object selects the complete routing policy from existing agent-profiles. RUN creation freezes the input and selected profiles without changing project settings.
212
+ run snapshot restore also accepts --execution-routing-file for a new stage-entry experiment. Its fresh DD_FLOW_HOME may contain only agent-profiles and the empty database; selected profiles are read before import, previous RUN policy is archived, and existing Work overrides must resolve against the selected frozen profiles. Recovery imports reject this option.
174
213
  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.
175
214
 
215
+ Managed drive is not yet native-provider qualified. With --capture-root, its detached controller fences RUN writers and captures each completed Stage before dispatching its successor. The capture root must be outside the runtime, project and workspace. Status events expose boundary_captured receipts; capturing means writers are still fenced, and capture_failed retains the fence for operator reconciliation. External snapshot creation is rejected while a managed controller owns a live RUN.
216
+
176
217
  Examples:
177
218
  dd-flow run start --project-root "$PWD" --flow-kind mb_sdlc --subject-type protocol --subject-id PRT-001-demo --slug demo --json
178
219
  dd-flow id next --type run --project-root "$PWD" --slug demo --json
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { createContext, createRouterContext } from "../runtime/context.js";
4
+ import { migrateStoreWriter } from "../storage/writer-migration.js";
4
5
  import { helpForArgs } from "./help.js";
5
6
  import { AppError, isAppError } from "../shared/errors.js";
6
7
  import { writeJson } from "../shared/json.js";
@@ -50,9 +51,16 @@ import { finishStage, startStage } from "../services/stage-lifecycle.js";
50
51
  import { addWorkBatch, assertStageLifecycleOwner, assertWorkLifecycleOwner, cancelWork, deleteWork, failWork, finishWork, listWorks, mutateWorkDeps, retryWork, showWork, startWork } from "../services/work-registry.js";
51
52
  import { launchExternalWork } from "../services/external-work-launch.js";
52
53
  import { createEvalBootstrapSnapshot, createEvalRunSnapshot, prepareVnextSpecifyRun, restoreEvalBootstrapSnapshot, restoreEvalRunSnapshot } from "../services/eval-snapshots.js";
53
- import { acceptRunRecovery, beginRunRecovery, inspectRunRecovery, recordRecoveryCapture, resumeRunRecovery, sealRunRecovery } from "../services/run-recovery.js";
54
+ import { acceptRunRecovery, assertRunMutationAllowed, beginRunRecovery, inspectRunRecovery, recordRecoveryCapture, recoveryGuard, resumeRunRecovery, sealRunRecovery } from "../services/run-recovery.js";
55
+ import { recoveryRuntimeAdmission } from "../services/run-recovery-runtime.js";
54
56
  import { loadExternalStageContext } from "../services/stage-context.js";
55
- import { stopManagedProcess, confirmManagedProcess, finishManagedProcess, heartbeatManagedProcess, managedProcessStatus, reconcileExpiredManagedProcesses, registerManagedProcess } from "../services/managed-processes.js";
57
+ import { stopManagedProcess, confirmManagedProcess, finishManagedProcess, heartbeatManagedProcess, managedProcessRuntimeHome, managedProcessStatus, processIsAlive, reconcileExpiredManagedProcesses, registerManagedProcess } from "../services/managed-processes.js";
58
+ import { fenceRuntimeBudget, runtimeBudgetStatus, admitRuntimeCheck, parseRuntimeBudget, reserveProviderTurn, releaseProviderTurns } from "../services/runtime-budget.js";
59
+ import { stopRuntimeScope } from "../services/runtime-scope-stop.js";
60
+ import { launchRuntimeScopeControl, launchRuntimeScopeResume, serveRuntimeScopeControl } from "../services/runtime-scope-worker.js";
61
+ import { launchRunController, resumeRunController, serveRunController, runControllerStatus, answerRunController, supplyRunControllerContext } from "../services/run-controller.js";
62
+ import { controlLifecycleReply, runControlStatus } from "../services/run-control.js";
63
+ import { requestRunControl, serveRunControl } from "../services/run-control-worker.js";
56
64
  import { startRuntimeService } from "../services/runtime-service.js";
57
65
  const defaultIo = {
58
66
  stdout: process.stdout,
@@ -159,6 +167,7 @@ function resolveProjectRootBeforeRouting(args, env) {
159
167
  function databaseModeForCommand(args, classification) {
160
168
  const [family, command] = args;
161
169
  if (family === "engine" ||
170
+ family === "storage" && command === "migrate-writer" ||
162
171
  family === "version" ||
163
172
  family === "schema" ||
164
173
  family === "canon" && command === "resolve" ||
@@ -510,6 +519,13 @@ function shortSha(value) {
510
519
  async function dispatch(args, context, io, scopeProjectRoot = null, classification = classifyCliOperation(args, context.env), output = parseOutputOptions(args)) {
511
520
  const [family, command, ...rest] = args;
512
521
  const parsed = parseArgs(rest);
522
+ if (family === "storage" && command === "migrate-writer") {
523
+ if (parsed.positional.length || [...parsed.options.keys()].some(key => !["database", "backup", "offline"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
524
+ throw new AppError("usage", "Usage: dd-flow storage migrate-writer --database ABSOLUTE_DB --backup NEW_ABSOLUTE_FILE --offline", 2);
525
+ if (parsed.options.get("offline")?.[0] !== flagValue)
526
+ throw new AppError("usage", "Writer migration requires --offline after draining all local and external runtime owners", 2);
527
+ return { ok: true, ...migrateStoreWriter(requiredOption(parsed, "database"), requiredOption(parsed, "backup")) };
528
+ }
513
529
  if (family === "engine") {
514
530
  return dispatchEngine(context, command, parsed, classification);
515
531
  }
@@ -898,6 +914,7 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
898
914
  const projectRoot = requiredOption(parsed, "project-root");
899
915
  const runId = requiredPosition(parsed, 0, "run-id");
900
916
  const stage = requiredOption(parsed, "stage");
917
+ const controlledReply = (result) => controlLifecycleReply(context, { projectRoot, runId }, result);
901
918
  const semanticFile = optionalOption(parsed, "semantic-file") ?? optionalOption(parsed, "data");
902
919
  const resultFile = optionalOption(parsed, "result-file") ?? semanticFile;
903
920
  const resultStdin = hasOption(parsed, "result-stdin");
@@ -925,56 +942,56 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
925
942
  if (!resultFile && !resultStdin) {
926
943
  throw new AppError("usage", "vNext SPECIFY finish requires --result-file or --result-stdin", 2);
927
944
  }
928
- return finishVnextSpecifyStage(context, {
945
+ return controlledReply(await finishVnextSpecifyStage(context, {
929
946
  projectRoot,
930
947
  runId,
931
948
  ...(resultFile ? { resultFile } : { resultJson: await readStdin(io.stdin) }),
932
949
  outcome: outcome
933
- });
950
+ }));
934
951
  }
935
952
  if (stage === "protocolize" && isVnextProtocolizeRun(context, { projectRoot, runId })) {
936
953
  if (!resultFile || resultStdin || hasOption(parsed, "outcome")) {
937
954
  throw new AppError("usage", "vNext PROTOCOLIZE requires exactly --result-file", 2);
938
955
  }
939
- return finishVnextProtocolize(context, { projectRoot, runId, resultFile });
956
+ return controlledReply(await finishVnextProtocolize(context, { projectRoot, runId, resultFile }));
940
957
  }
941
958
  if (stage === "plan" && isVnextPlanRun(context, { projectRoot, runId })) {
942
959
  if (optionalOption(parsed, "code-work-batch-file"))
943
960
  throw new AppError("usage", "vNext PLAN derives its CODE batch from accepted semantic plans; do not supply a batch file", 2);
944
- return finishVnextPlan(context, { projectRoot, runId });
961
+ return controlledReply(await finishVnextPlan(context, { projectRoot, runId }));
945
962
  }
946
963
  if (stage === "plan-review" && isVnextPlanReviewRun(context, { projectRoot, runId })) {
947
964
  if (resultStdin || hasOption(parsed, "outcome"))
948
965
  throw new AppError("usage", "vNext PLAN-REVIEW uses --decision-file", 2);
949
966
  const decisionFile = optionalOption(parsed, "decision-file");
950
- return finishVnextPlanReview(context, { projectRoot, runId, ...(decisionFile ? { decisionFile } : {}) });
967
+ return controlledReply(await finishVnextPlanReview(context, { projectRoot, runId, ...(decisionFile ? { decisionFile } : {}) }));
951
968
  }
952
969
  if (stage === "code" && isVnextPlanRun(context, { projectRoot, runId })) {
953
970
  if (semanticFile || resultFile || resultStdin || hasOption(parsed, "outcome")) {
954
971
  throw new AppError("usage", "vNext CODE finish accepts only --verification-file", 2);
955
972
  }
956
- return await finishVnextCode(context, { projectRoot, runId, verificationFile: optionalOption(parsed, "verification-file") ?? path.join("<missing>", "code-verification.json"), ...(retryCheckId ? { retryCheckId, retryReason: retryReason } : {}), progress: commandProgress(io, output) });
973
+ return controlledReply(await finishVnextCode(context, { projectRoot, runId, verificationFile: optionalOption(parsed, "verification-file") ?? path.join("<missing>", "code-verification.json"), ...(retryCheckId ? { retryCheckId, retryReason: retryReason } : {}), progress: commandProgress(io, output) }));
957
974
  }
958
975
  if (stage === "code-review" && isVnextCodeReviewRun(context, { projectRoot, runId })) {
959
976
  if (resultStdin || semanticFile || resultFile || hasOption(parsed, "outcome"))
960
977
  throw new AppError("usage", "vNext CODE-REVIEW uses --decision-file", 2);
961
- return await finishVnextCodeReview(context, { projectRoot, runId, ...(optionalOption(parsed, "decision-file") ? { decisionFile: optionalOption(parsed, "decision-file") } : {}), ...(retryCheckId ? { retryCheckId, retryReason: retryReason } : {}), progress: commandProgress(io, output) });
978
+ return controlledReply(await finishVnextCodeReview(context, { projectRoot, runId, ...(optionalOption(parsed, "decision-file") ? { decisionFile: optionalOption(parsed, "decision-file") } : {}), ...(retryCheckId ? { retryCheckId, retryReason: retryReason } : {}), progress: commandProgress(io, output) }));
962
979
  }
963
980
  if (stage === "merge" && isVnextMergeRun(context, { projectRoot, runId })) {
964
981
  if (resultStdin || semanticFile || resultFile || hasOption(parsed, "outcome"))
965
982
  throw new AppError("usage", "vNext MERGE uses the Work result path returned by stage start", 2);
966
- return await finishVnextMerge(context, { projectRoot, runId, requestId: requiredOption(parsed, "request"), workId: requiredOption(parsed, "work"), ...(retryCheckId ? { retryCheckId, retryReason: retryReason } : {}), progress: commandProgress(io, output) });
983
+ return controlledReply(await finishVnextMerge(context, { projectRoot, runId, requestId: requiredOption(parsed, "request"), workId: requiredOption(parsed, "work"), ...(retryCheckId ? { retryCheckId, retryReason: retryReason } : {}), progress: commandProgress(io, output) }));
967
984
  }
968
985
  if (hasOption(parsed, "outcome") || hasOption(parsed, "result-file") || resultStdin) {
969
986
  throw new AppError("usage", "--outcome, --result-file and --result-stdin are only available for vNext SPECIFY", 2);
970
987
  }
971
- return finishStage(context, {
988
+ return controlledReply(await finishStage(context, {
972
989
  projectRoot,
973
990
  runId,
974
991
  stage,
975
992
  ...(dir ? { dir } : {}),
976
993
  ...(semanticFile ? { semanticFile } : {})
977
- });
994
+ }));
978
995
  }
979
996
  if (family === "stage" && command === "pause") {
980
997
  if (!hasOption(parsed, "question-stdin"))
@@ -1056,6 +1073,43 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
1056
1073
  return dispatchSession(context, command, parsed);
1057
1074
  }
1058
1075
  if (family === "run") {
1076
+ if (command === "control") {
1077
+ const action = requiredPosition(parsed, 0, "control action");
1078
+ const scope = { projectRoot: requiredOption(parsed, "project-root"), runId: requiredOption(parsed, "run") };
1079
+ if (action === "status")
1080
+ return runControlStatus(context, scope);
1081
+ if (action === "resume") {
1082
+ if (["force", "grace-ms", "escalate"].some(option => hasOption(parsed, option)))
1083
+ throw new AppError("usage", "Resume does not accept stop or pause escalation options", 2);
1084
+ return await resumeRunController(context, { ...scope, requestId: requiredOption(parsed, "request-id"), controlId: requiredOption(parsed, "from"), ...(hasOption(parsed, "wait-ms") ? { waitMs: Number(requiredOption(parsed, "wait-ms")) } : {}) });
1085
+ }
1086
+ if (action === "serve")
1087
+ return await serveRunControl(context, { ...scope, controlId: requiredOption(parsed, "control-id") });
1088
+ if (action === "pause" || action === "stop")
1089
+ return await requestRunControl(context, {
1090
+ ...scope, action, requestId: requiredOption(parsed, "request-id"),
1091
+ ...(hasOption(parsed, "force") ? { force: true } : {}),
1092
+ ...(hasOption(parsed, "grace-ms") ? { graceMs: Number(requiredOption(parsed, "grace-ms")) } : {}),
1093
+ ...(hasOption(parsed, "escalate") ? { escalate: requiredOption(parsed, "escalate") } : {}),
1094
+ ...(hasOption(parsed, "wait-ms") ? { waitMs: Number(requiredOption(parsed, "wait-ms")) } : {})
1095
+ });
1096
+ throw new AppError("usage", "Use: dd-flow run control pause|stop|resume|status", 2);
1097
+ }
1098
+ if (command === "drive") {
1099
+ const action = requiredPosition(parsed, 0, "drive action");
1100
+ const scope = { projectRoot: requiredOption(parsed, "project-root"), runId: requiredOption(parsed, "run") };
1101
+ if (action === "status")
1102
+ return runControllerStatus(context, { ...scope, ...(optionalOption(parsed, "after") ? { after: Number(optionalOption(parsed, "after")) } : {}) });
1103
+ if (action === "launch")
1104
+ return await launchRunController(context, { ...scope, requestId: requiredOption(parsed, "request-id"), contextFile: requiredOption(parsed, "context-file"), contextSha256: requiredOption(parsed, "context-sha256"), ...(optionalOption(parsed, "stop-after") ? { stopAfter: optionalOption(parsed, "stop-after") } : {}), ...(optionalOption(parsed, "capture-root") ? { captureRoot: optionalOption(parsed, "capture-root") } : {}) });
1105
+ if (action === "serve")
1106
+ return await serveRunController(context, { ...scope, controllerId: requiredOption(parsed, "controller-id") });
1107
+ if (action === "answer")
1108
+ return answerRunController(context, { ...scope, requestId: requiredOption(parsed, "request-id"), pauseId: requiredOption(parsed, "pause-id"), answerFile: requiredOption(parsed, "answer-file") });
1109
+ if (action === "context")
1110
+ return supplyRunControllerContext(context, { ...scope, controllerId: requiredOption(parsed, "controller-id"), requestId: requiredOption(parsed, "request-id"), stage: requiredOption(parsed, "stage"), contextFile: requiredOption(parsed, "context-file"), contextSha256: requiredOption(parsed, "context-sha256") });
1111
+ throw new AppError("usage", "Use: dd-flow run drive launch|status|answer|context", 2);
1112
+ }
1059
1113
  return dispatchRun(context, command, parsed);
1060
1114
  }
1061
1115
  if (family === "stat") {
@@ -1528,13 +1582,14 @@ function dispatchRun(context, command, parsed) {
1528
1582
  if (action === "restore")
1529
1583
  return restoreEvalRunSnapshot(context, {
1530
1584
  snapshot: requiredOption(parsed, "snapshot"),
1585
+ executionRoutingFile: optionalOption(parsed, "execution-routing-file"),
1531
1586
  ...(optionalOption(parsed, "recovery-id") ? { recoveryId: requiredOption(parsed, "recovery-id") } : {}),
1532
1587
  projectRoot: requiredOption(parsed, "project-root")
1533
1588
  });
1534
1589
  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);
1535
1590
  }
1536
1591
  if (command === "prepare-vnext-specify") {
1537
- return prepareVnextSpecifyRun(context, { projectRoot: requiredOption(parsed, "project-root"), slug: requiredOption(parsed, "slug") });
1592
+ return prepareVnextSpecifyRun(context, { projectRoot: requiredOption(parsed, "project-root"), slug: requiredOption(parsed, "slug"), executionRoutingFile: optionalOption(parsed, "execution-routing-file") });
1538
1593
  }
1539
1594
  if (command === "start") {
1540
1595
  if (optionalOption(parsed, "flow-kind") === "mb-upgrade") {
@@ -1551,6 +1606,7 @@ function dispatchRun(context, command, parsed) {
1551
1606
  nextAction: optionalOption(parsed, "next-action"),
1552
1607
  preset: optionalOption(parsed, "preset"),
1553
1608
  taskProfile: readOptionalJsonFile(optionalOption(parsed, "task-profile-file")),
1609
+ executionRoutingFile: optionalOption(parsed, "execution-routing-file"),
1554
1610
  ...(protocolOverrides ? { protocolOverrides } : {}),
1555
1611
  runOverrides: parseFlowFlagOverrides(allOptions(parsed, "flag"))
1556
1612
  });
@@ -1666,22 +1722,64 @@ function dispatchRun(context, command, parsed) {
1666
1722
  throw new AppError("usage", `Unknown run command: ${command ?? "<empty>"}`, 2);
1667
1723
  }
1668
1724
  async function dispatchRuntime(context, command, parsed, progress) {
1725
+ if (command === "scope") {
1726
+ if (parsed.positional.length === 1 && parsed.positional[0] === "resume") {
1727
+ if ([...parsed.options.keys()].some(key => !["scope-id", "request-id", "generation", "capture-key"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
1728
+ throw new AppError("usage", "Use runtime scope resume --scope-id ID --request-id ID --generation N --capture-key SHA256", 2);
1729
+ return { ok: true, ...await launchRuntimeScopeResume(context, { scopeId: requiredOption(parsed, "scope-id"), requestId: requiredOption(parsed, "request-id"), generation: Number(requiredOption(parsed, "generation")), captureKey: requiredOption(parsed, "capture-key") }) };
1730
+ }
1731
+ if (parsed.positional.length === 1 && parsed.positional[0] === "control") {
1732
+ if ([...parsed.options.keys()].some(key => !["scope-id", "request-id", "mode", "manifest-json"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
1733
+ throw new AppError("usage", "Use runtime scope control --scope-id ID --request-id ID --mode pause|stop [--manifest-json JSON]", 2);
1734
+ const mode = requiredOption(parsed, "mode");
1735
+ if (mode !== "pause" && mode !== "stop")
1736
+ throw new AppError("usage", "Scope control mode must be pause or stop", 2);
1737
+ return { ok: true, ...await launchRuntimeScopeControl(context, requiredOption(parsed, "scope-id"), requiredOption(parsed, "request-id"), mode, hasOption(parsed, "manifest-json") ? JSON.parse(requiredOption(parsed, "manifest-json")) : undefined) };
1738
+ }
1739
+ if (parsed.positional.length === 1 && parsed.positional[0] === "serve") {
1740
+ if ([...parsed.options.keys()].some(key => !["scope-id", "generation"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
1741
+ throw new AppError("usage", "Invalid internal scope worker arguments", 2);
1742
+ return { ok: true, ...await serveRuntimeScopeControl(context, requiredOption(parsed, "scope-id"), Number(requiredOption(parsed, "generation"))) };
1743
+ }
1744
+ if (parsed.positional.length === 1 && parsed.positional[0] === "stop") {
1745
+ if ([...parsed.options.keys()].some(key => !["scope-id", "request-id"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
1746
+ throw new AppError("usage", "Usage: dd-flow runtime scope stop --scope-id EVAL_ID --request-id ID", 2);
1747
+ return { ok: true, ...await stopRuntimeScope(context, requiredOption(parsed, "scope-id"), requiredOption(parsed, "request-id")) };
1748
+ }
1749
+ if (parsed.positional.length === 1 && parsed.positional[0] === "status") {
1750
+ if ([...parsed.options.keys()].some(key => key !== "scope-id") || [...parsed.options.values()].some(values => values.length !== 1))
1751
+ throw new AppError("usage", "Usage: dd-flow runtime scope status --scope-id EVAL_ID", 2);
1752
+ return { ok: true, ...runtimeBudgetStatus(context, requiredOption(parsed, "scope-id")) };
1753
+ }
1754
+ if (parsed.positional.length !== 1 || parsed.positional[0] !== "fence" || [...parsed.options.keys()].some(key => !["scope-id", "request-id"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
1755
+ throw new AppError("usage", "Usage: dd-flow runtime scope fence --scope-id EVAL_ID --request-id ID", 2);
1756
+ return { ok: true, ...fenceRuntimeBudget(context, requiredOption(parsed, "scope-id"), requiredOption(parsed, "request-id")) };
1757
+ }
1669
1758
  if (command !== "process")
1670
- throw new AppError("usage", "Usage: dd-flow runtime process <start|stop|register|confirm|heartbeat|finish|status|reconcile>", 2);
1759
+ throw new AppError("usage", "Usage: dd-flow runtime <scope|process>; see dd-flow runtime scope --help", 2);
1671
1760
  const action = requiredPosition(parsed, 0, "runtime process action");
1761
+ if (action === "check-admission")
1762
+ return { ok: true, ...admitRuntimeCheck(context, requiredOption(parsed, "id"), requiredOption(parsed, "lease-token")) };
1672
1763
  if (action === "start")
1673
1764
  return await startRuntimeService(context, { projectRoot: requiredOption(parsed, "project-root"), runId: requiredOption(parsed, "run"), command: requiredOption(parsed, "command"), ports: requiredOption(parsed, "ports").split(","), readyPort: requiredOption(parsed, "ready-port"), readyPath: optionalOption(parsed, "ready-path") ?? "/", timeoutMs: optionalPositiveNumber(parsed, "ready-timeout-ms", true) ?? 30_000, progress });
1674
1765
  if (action === "stop")
1675
1766
  return { ok: true, process: await stopManagedProcess(context, { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), ...(optionalPositiveNumber(parsed, "grace-ms", true) ? { graceMs: optionalPositiveNumber(parsed, "grace-ms", true) } : {}) }) };
1676
1767
  if (action === "register") {
1677
1768
  const leaseMs = optionalPositiveNumber(parsed, "lease-ms", true);
1769
+ const budget = parseRuntimeBudget(optionalOption(parsed, "budget-json"));
1770
+ const projectId = optionalOption(parsed, "project-id") ?? null, runId = optionalOption(parsed, "run") ?? null;
1771
+ const guard = projectId && runId ? recoveryGuard(context, projectId, runId) : null;
1772
+ const run = projectId && runId ? context.db.get("SELECT id FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]) : null;
1678
1773
  const process = registerManagedProcess(context, {
1679
1774
  kind: requiredOption(parsed, "kind"), ownerId: requiredOption(parsed, "owner"),
1775
+ uniqueActiveOperation: requiredOption(parsed, "kind") === "eval-observer",
1776
+ ...(requiredOption(parsed, "kind") === "eval-observer" ? { pid: optionalPositiveNumber(parsed, "pid", true) ?? 0, ...(optionalPositiveNumber(parsed, "process-group-id", true) ? { processGroupId: optionalPositiveNumber(parsed, "process-group-id", true) } : {}) } : {}),
1680
1777
  ownerPid: optionalPositiveNumber(parsed, "owner-pid", true),
1681
- projectId: optionalOption(parsed, "project-id") ?? null, runId: optionalOption(parsed, "run") ?? null,
1778
+ projectId, runId,
1682
1779
  workId: optionalOption(parsed, "work") ?? null, checkId: optionalOption(parsed, "check") ?? null,
1683
1780
  operationId: optionalOption(parsed, "operation") ?? null, stdoutPath: optionalOption(parsed, "stdout") ?? null,
1684
- stderrPath: optionalOption(parsed, "stderr") ?? null, ...(leaseMs ? { leaseMs } : {})
1781
+ stderrPath: optionalOption(parsed, "stderr") ?? null, ...(leaseMs ? { leaseMs } : {}),
1782
+ metadata: { ...(budget ? { budget, harness: optionalOption(parsed, "harness") ?? null } : {}), ...(projectId && runId ? { run_generation: guard?.generation ?? 0, run_admitted: Boolean(run) && !["draining", "sealed", "resuming"].includes(guard?.status ?? "") } : {}), ...(optionalOption(parsed, "dispatch-barrier") ? { dispatch_barrier: optionalOption(parsed, "dispatch-barrier") } : {}), ...(optionalOption(parsed, "role") ? { role: optionalOption(parsed, "role") } : {}), ...(optionalOption(parsed, "state-dir") ? { state_dir: optionalOption(parsed, "state-dir") } : {}), ...(optionalOption(parsed, "adapter-executable") ? { adapter_executable: optionalOption(parsed, "adapter-executable") } : {}) }
1685
1783
  });
1686
1784
  return { ok: true, process };
1687
1785
  }
@@ -1694,6 +1792,36 @@ async function dispatchRuntime(context, command, parsed, progress) {
1694
1792
  const leaseMs = optionalPositiveNumber(parsed, "lease-ms", true);
1695
1793
  return { ok: heartbeatManagedProcess(context, { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), ...(leaseMs ? { leaseMs } : {}) }) };
1696
1794
  }
1795
+ if (action === "admission") {
1796
+ const record = managedProcessStatus(context).find(process => process.id === requiredOption(parsed, "id"));
1797
+ if (!record || record.lease_token !== requiredOption(parsed, "lease-token") || record.owner_id !== requiredOption(parsed, "owner") || record.state !== "running" || !record.pid || !record.pid_started_at || !processIsAlive(record))
1798
+ throw new AppError("process_ownership_unknown", "Productive dispatch requires its current confirmed runtime owner", 1);
1799
+ const metadata = JSON.parse(record.metadata_json);
1800
+ if (metadata.state_dir !== requiredOption(parsed, "state-dir"))
1801
+ throw new AppError("process_ownership_unknown", "Runtime admission belongs to another state directory", 1);
1802
+ if (record.project_id && record.run_id) {
1803
+ if (managedProcessRuntimeHome(record, context) !== context.ddFlowHome)
1804
+ throw new AppError("process_runtime_scope_mismatch", "Runtime admission belongs to another or unproven flow home", 1);
1805
+ const recovering = recoveryRuntimeAdmission(context, record, { stateDir: requiredOption(parsed, "state-dir"), daemonId: optionalOption(parsed, "daemon-id"), operationId: optionalOption(parsed, "operation-id"), operation: optionalOption(parsed, "operation"), nativeSessionId: optionalOption(parsed, "native-session-id"), promptSha256: optionalOption(parsed, "prompt-sha256") });
1806
+ if (!recovering) {
1807
+ if (metadata.run_admitted !== true || !Number.isSafeInteger(metadata.run_generation))
1808
+ throw new AppError("run_recovery_guarded", "Runtime was not admitted before the RUN fence; reconcile its launch", 1);
1809
+ assertRunMutationAllowed(context, record.project_id, record.run_id, metadata.run_generation);
1810
+ }
1811
+ const run = context.db.get("SELECT status FROM runs WHERE project_id = ? AND id = ?", [record.project_id, record.run_id]);
1812
+ const retainedHitlPrompt = recovering && ["paused", "waiting_for_user"].includes(run?.status ?? "") && ["session.prompt", "session.resume"].includes(optionalOption(parsed, "operation") ?? "") && context.db.get("SELECT 1 FROM run_recovery_bindings b JOIN run_recovery_guards g ON g.recovery_id = b.recovery_id WHERE g.project_id = ? AND g.run_id = ? AND g.generation = ? AND b.daemon_id = ? AND b.provider_session_id = ?", [record.project_id, record.run_id, metadata.run_generation, optionalOption(parsed, "daemon-id"), optionalOption(parsed, "native-session-id")]);
1813
+ if (!run || run.status !== "running" && !retainedHitlPrompt && !["judge", "probe"].includes(metadata.role ?? ""))
1814
+ throw new AppError("invalid_run_state", "Subject dispatch requires a running RUN or its retained recovery/HITL prompt", 1);
1815
+ }
1816
+ const budget = reserveProviderTurn(context, record, { operationId: optionalOption(parsed, "operation-id") ?? "", operation: optionalOption(parsed, "operation") ?? "", sessionId: optionalOption(parsed, "native-session-id"), daemonId: optionalOption(parsed, "daemon-id") });
1817
+ return { ok: true, ...budget, process_id: record.id, generation: metadata.run_generation ?? null };
1818
+ }
1819
+ if (action === "release-turn") {
1820
+ const record = managedProcessStatus(context).find(process => process.id === requiredOption(parsed, "id"));
1821
+ if (!record || record.lease_token !== requiredOption(parsed, "lease-token") || record.owner_id !== requiredOption(parsed, "owner") || managedProcessRuntimeHome(record, context) !== context.ddFlowHome)
1822
+ throw new AppError("process_ownership_unknown", "Budget release requires its retained physical owner", 1);
1823
+ return releaseProviderTurns(context, record, { daemonId: requiredOption(parsed, "daemon-id"), operationId: optionalOption(parsed, "operation-id"), observationFile: optionalOption(parsed, "observation-file") });
1824
+ }
1697
1825
  if (action === "finish") {
1698
1826
  const state = requiredOption(parsed, "state");
1699
1827
  if (state !== "stopped" && state !== "failed")
@@ -1704,7 +1832,7 @@ async function dispatchRuntime(context, command, parsed, progress) {
1704
1832
  return { ok: true, processes: managedProcessStatus(context) };
1705
1833
  if (action === "reconcile")
1706
1834
  return { ok: true, processes: await reconcileExpiredManagedProcesses(context, requiredOption(parsed, "owner"), optionalPositiveNumber(parsed, "grace-ms", true)) };
1707
- throw new AppError("usage", "Usage: dd-flow runtime process <start|stop|register|confirm|heartbeat|finish|status|reconcile>", 2);
1835
+ throw new AppError("usage", "Usage: dd-flow runtime process <start|stop|register|confirm|heartbeat|admission|finish|status|reconcile>", 2);
1708
1836
  }
1709
1837
  function dispatchStat(context, command, parsed) {
1710
1838
  if (command === "usage") {
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { durableDaemonStart, inspectDaemonOperation } from "../lib/daemon-operations.mjs";
2
3
  import { readFile } from "node:fs/promises";
3
4
  import { spawn } from "node:child_process";
4
5
  import { doctor } from "../lib/dd-agy.mjs";
@@ -22,10 +23,10 @@ async function hook(options) {
22
23
  try {
23
24
  const { positional, options } = parse(process.argv.slice(2)), [family, command] = positional; const common = { journal: options.journal, stateDir: options["state-dir"], cwd: options.cwd, bin: options["agy-bin"], provider: options.provider, model: options.model, reasoning: options.reasoning, mode: options.mode, projectRoot: options["project-root"], ddFlowBin: options["dd-flow-bin"], ddFlowHome: options["dd-flow-home"], noFlow: options["no-flow"] === true, timeoutMs: options.timeout ? Number(options.timeout) * 1000 : undefined }; let result;
24
25
  if (family === "doctor") result = await doctor(common);
25
- else if (family === "daemon" && command === "start") result = await startDaemon({ ...common, sessionId: options["session-id"], entryPath: process.argv[1] });
26
+ else if (family === "daemon" && command === "start") result = await durableDaemonStart({ ...common, sessionId: options["session-id"], entryPath: process.argv[1] }, startDaemon);
26
27
  else if (family === "daemon" && command === "serve") { await serveDaemon(common.stateDir); process.exit(0); }
27
28
  else if (family === "daemon" && command === "status") result = await callDaemon(common.stateDir, "daemon.status");
28
- else if (family === "daemon" && command === "operation") result = await callDaemon(common.stateDir, "operation.inspect", { operationId: options["operation-id"] });
29
+ else if (family === "daemon" && command === "operation") result = await inspectDaemonOperation(common.stateDir, options["operation-id"]);
29
30
  else if (family === "daemon" && command === "stop") result = await stopDaemon({ stateDir: common.stateDir, cancelTree: options["cancel-tree"], timeoutMs: common.timeoutMs });
30
31
  else if (family === "hook" && command === "handle") { process.stdout.write(`${JSON.stringify(await hook(options))}\n`); process.exit(0); }
31
32
  else if (family === "session") { if (command === "fork") throw Object.assign(new Error("Antigravity native headless fork is unsupported; use deterministic_replay"), { code: "agy_headless_fork_unsupported" }); const operation = command === "status" ? "inspect" : command; result = await callDaemon(common.stateDir, `session.${operation}`, { sessionId: options["session-id"], ...(command === "create" || command === "prompt" ? { prompt: await prompt(options) } : {}), tree: options.tree === true }, common.timeoutMs ?? 1_800_000); }
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { durableDaemonStart, inspectDaemonOperation } from "../lib/daemon-operations.mjs";
2
3
  import { cancelSession, createSession, doctor, inspectSession, promptFromFile, promptSession, startSession } from "../lib/dd-codex.mjs";
3
4
  import { callDaemon, serveDaemon, startDaemon, stopDaemon } from "../lib/dd-codex-daemon.mjs";
4
5
 
@@ -7,15 +8,15 @@ function common(options) { return { bin: options["codex-bin"], journal: options.
7
8
  try {
8
9
  const { positional, options } = parse(process.argv.slice(2)); const [family, command] = positional; const shared = common(options); let result;
9
10
  if (family === "doctor") result = await doctor(shared);
10
- else if (family === "daemon" && command === "start") result = await startDaemon({ ...shared, entryPath: process.argv[1] });
11
+ else if (family === "daemon" && command === "start") result = await durableDaemonStart({ ...shared, entryPath: process.argv[1] }, startDaemon);
11
12
  else if (family === "daemon" && command === "serve") { await serveDaemon(shared.stateDir); process.exit(0); }
12
13
  else if (family === "daemon" && command === "status") result = await callDaemon(shared.stateDir, "daemon.status");
13
- else if (family === "daemon" && command === "operation") result = await callDaemon(shared.stateDir, "operation.inspect", { operationId: options["operation-id"] });
14
+ else if (family === "daemon" && command === "operation") result = await inspectDaemonOperation(shared.stateDir, options["operation-id"]);
14
15
  else if (family === "daemon" && command === "stop") result = await stopDaemon({ ...shared, cancelTree: options["cancel-tree"] });
15
16
  else if (family === "session" && command === "create") result = shared.stateDir ? await callDaemon(shared.stateDir, "session.create", shared) : await createSession(shared);
16
17
  else if (family === "session" && command === "prompt") result = shared.stateDir ? await callDaemon(shared.stateDir, "session.prompt", { ...shared, prompt: options["prompt-file"] ? await promptFromFile(options["prompt-file"]) : options.prompt }, shared.timeoutMs ?? 1_800_000) : await promptSession({ ...shared, prompt: options["prompt-file"] ? await promptFromFile(options["prompt-file"]) : options.prompt });
17
18
  else if (family === "session" && command === "start") result = shared.stateDir ? await callDaemon(shared.stateDir, "session.start", { ...shared, prompt: options["prompt-file"] ? await promptFromFile(options["prompt-file"]) : options.prompt }, shared.timeoutMs ?? 30_000) : await startSession({ ...shared, prompt: options["prompt-file"] ? await promptFromFile(options["prompt-file"]) : options.prompt });
18
- else if (family === "session" && ["inspect", "status", "resume"].includes(command)) result = shared.stateDir ? await callDaemon(shared.stateDir, "session.inspect", shared) : await inspectSession(shared);
19
+ else if (family === "session" && ["inspect", "status", "resume"].includes(command)) result = shared.stateDir ? await callDaemon(shared.stateDir, command === "resume" ? "session.resume" : "session.inspect", shared) : await inspectSession(shared);
19
20
  else if (family === "session" && command === "cancel") result = shared.stateDir ? await callDaemon(shared.stateDir, "session.cancel", shared) : await cancelSession(shared);
20
21
  else throw new Error("usage: dd-codex doctor | daemon start|status|stop | session create|start|prompt|inspect|status|resume|cancel [--json]");
21
22
  process.stdout.write(`${JSON.stringify({ ok: true, ...result })}\n`);
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { durableDaemonStart, inspectDaemonOperation } from "../lib/daemon-operations.mjs";
2
3
  import { mkdtemp, readFile, rm } from "node:fs/promises";
3
4
  import { randomUUID } from "node:crypto";
4
5
  import os from "node:os";
@@ -18,10 +19,10 @@ try {
18
19
  const runtime = new DroidRuntime({ ...config, stateDir: dir, home: path.join(dir, "home"), cwd: config.cwd ?? process.cwd(), journal: path.join(dir, "events.jsonl"), daemonId: randomUUID(), ddFlowHome: null, ddFlowBin: null }, {}, async () => {});
19
20
  try { await runtime.start(); result = { harness: "droid-cli", observed_runtime: { droid: await droidVersion(config.bin), factory_protocol: runtime.protocol, dd_harness_contract: DROID_CONTRACT }, models: runtime.models.map(model => model.id ?? model.modelId), capabilities: { native_children: true, tree_cancel: true, selective_child_cancel: false, native_fork: false }, protocol_expected: DROID_PROTOCOL }; }
20
21
  finally { await runtime.closeTree(true); await rm(dir, { recursive: true, force: true }); }
21
- } else if (family === "daemon" && command === "start") result = await startDaemon(config);
22
+ } else if (family === "daemon" && command === "start") result = await durableDaemonStart(config, startDaemon);
22
23
  else if (family === "daemon" && command === "serve") { await serveDaemon(config.stateDir); process.exit(0); }
23
24
  else if (family === "daemon" && command === "status") result = await callDaemon(config.stateDir, "daemon.status");
24
- else if (family === "daemon" && command === "operation") result = await callDaemon(config.stateDir, "operation.inspect", { operationId: options["operation-id"] });
25
+ else if (family === "daemon" && command === "operation") result = await inspectDaemonOperation(config.stateDir, options["operation-id"]);
25
26
  else if (family === "daemon" && command === "stop") result = await stopDaemon({ stateDir: config.stateDir, cancelTree: options["cancel-tree"] });
26
27
  else if (family === "hook" && command === "handle") { const payload = JSON.parse(await input()); const eventId = randomUUID(); result = await callDaemon(config.stateDir, "hook.observe", { payload, eventId }, 25_000, eventId); }
27
28
  else if (family === "session") { if (command === "fork") throw new Error("Droid fork is not qualified for this adapter"); const prompt = options["prompt-file"] ? await readFile(options["prompt-file"], "utf8") : options.prompt; result = await callDaemon(config.stateDir, `session.${command === "status" ? "inspect" : command}`, { sessionId: options["session-id"], ...(prompt ? { prompt } : {}) }); }
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { durableDaemonStart, inspectDaemonOperation } from "../lib/daemon-operations.mjs";
2
3
  import { spawn } from "node:child_process";
3
4
  import { cancelSession, createSession, doctor, forkSession, inspectSession, promptFromFile, promptSession } from "../lib/dd-grok.mjs";
4
5
  import { callDaemon, serveDaemon, startDaemon, stopDaemon } from "../lib/dd-grok-daemon.mjs";
@@ -16,13 +17,13 @@ async function hook(options) {
16
17
  try {
17
18
  const { positional, options } = parse(process.argv.slice(2)); const [family, command] = positional; const shared = common(options); let result;
18
19
  if (family === "doctor") result = await doctor(shared);
19
- else if (family === "daemon" && command === "start") result = await startDaemon({ ...shared, entryPath: process.argv[1] });
20
+ else if (family === "daemon" && command === "start") result = await durableDaemonStart({ ...shared, entryPath: process.argv[1] }, startDaemon);
20
21
  else if (family === "daemon" && command === "serve") { await serveDaemon(shared.stateDir); process.exit(0); }
21
22
  else if (family === "daemon" && command === "status") result = await callDaemon(shared.stateDir, "daemon.status");
22
- else if (family === "daemon" && command === "operation") result = await callDaemon(shared.stateDir, "operation.inspect", { operationId: options["operation-id"] });
23
+ else if (family === "daemon" && command === "operation") result = await inspectDaemonOperation(shared.stateDir, options["operation-id"]);
23
24
  else if (family === "daemon" && command === "stop") result = await stopDaemon({ stateDir: shared.stateDir, cancelTree: options["cancel-tree"], timeoutMs: shared.timeoutMs });
24
25
  else if (family === "hook" && command === "handle") { process.stdout.write(`${JSON.stringify(await hook(options))}\n`); process.exit(0); }
25
- else if (family === "session") { const params = { ...shared, ...(command === "create" || command === "prompt" ? { prompt: options["prompt-file"] ? await promptFromFile(options["prompt-file"]) : options.prompt } : {}), ...(command === "fork" ? { target: JSON.parse(options["target-json"] ?? "null") } : {}), ...(command === "archive" ? { archiveDir: options["archive-dir"] } : {}) }; if (shared.stateDir) result = await callDaemon(shared.stateDir, `session.${["status", "resume"].includes(command) ? "inspect" : command}`, params, shared.timeoutMs ?? 1_800_000); else if (command === "create") result = await createSession(params); else if (command === "prompt") result = await promptSession(params); else if (["inspect", "status", "resume"].includes(command)) result = await inspectSession(params); else if (command === "cancel") result = await cancelSession(params); else if (command === "fork") result = await forkSession(params); else throw new Error(`unknown session command: ${command}`); }
26
+ else if (family === "session") { const params = { ...shared, ...(command === "create" || command === "prompt" ? { prompt: options["prompt-file"] ? await promptFromFile(options["prompt-file"]) : options.prompt } : {}), ...(command === "fork" ? { target: JSON.parse(options["target-json"] ?? "null") } : {}), ...(command === "archive" ? { archiveDir: options["archive-dir"] } : {}) }; if (shared.stateDir) result = await callDaemon(shared.stateDir, `session.${command === "status" ? "inspect" : command}`, params, shared.timeoutMs ?? 1_800_000); else if (command === "create") result = await createSession(params); else if (command === "prompt") result = await promptSession(params); else if (["inspect", "status", "resume"].includes(command)) result = await inspectSession(params); else if (command === "cancel") result = await cancelSession(params); else if (command === "fork") result = await forkSession(params); else throw new Error(`unknown session command: ${command}`); }
26
27
  else throw new Error("usage: dd-grok doctor | daemon start|status|stop | session create|prompt|inspect|status|resume|cancel|fork|archive [options] --json");
27
28
  process.stdout.write(`${JSON.stringify({ ok: true, ...result })}\n`);
28
29
  } catch (error) { if (process.argv[2] === "hook") { process.stdout.write(`${JSON.stringify({ decision: "deny", reason: error.message })}\n`); process.exit(2); } process.stderr.write(`${JSON.stringify({ ok: false, error: error.message, ...(error.code ? { code: error.code, retryable: error.retryable === true, details: error.details } : {}) })}\n`); process.exit(1); }
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { durableDaemonStart, inspectDaemonOperation } from "../lib/daemon-operations.mjs";
2
3
  import { readFile } from "node:fs/promises";
3
4
  import { callDaemon, serveDaemon, startDaemon, stopDaemon } from "../lib/dd-opencode-daemon.mjs";
4
5
 
@@ -6,13 +7,13 @@ function parse(argv) { const positional = [], options = {}; for (let i = 0; i <
6
7
  const prompt = async options => options["prompt-file"] ? await readFile(options["prompt-file"], "utf8") : options.prompt;
7
8
  try {
8
9
  const { positional, options } = parse(process.argv.slice(2)), [family, command] = positional; const common = { journal: options.journal, stateDir: options["state-dir"], sessionId: options["session-id"], cwd: options.cwd, bin: options["opencode-bin"], provider: options.provider, model: options.model, variant: options.variant, agent: options.agent, authPath: options["auth-path"], projectRoot: options["project-root"], ddFlowBin: options["dd-flow-bin"], ddFlowHome: options["dd-flow-home"], noFlow: options["no-flow"] === true, sessionArchive: options["session-archive"], timeoutMs: options.timeout ? Number(options.timeout) * 1000 : undefined }; let result;
9
- if (family === "daemon" && command === "start") result = await startDaemon({ ...common, entryPath: process.argv[1] });
10
+ if (family === "daemon" && command === "start") result = await durableDaemonStart({ ...common, entryPath: process.argv[1] }, startDaemon);
10
11
  else if (family === "daemon" && command === "serve") { await serveDaemon(common.stateDir); process.exit(0); }
11
12
  else if (family === "daemon" && command === "status") result = await callDaemon(common.stateDir, "daemon.status");
12
- else if (family === "daemon" && command === "operation") result = await callDaemon(common.stateDir, "operation.inspect", { operationId: options["operation-id"] });
13
+ else if (family === "daemon" && command === "operation") result = await inspectDaemonOperation(common.stateDir, options["operation-id"]);
13
14
  else if (family === "doctor") result = await callDaemon(common.stateDir, "daemon.doctor");
14
15
  else if (family === "daemon" && command === "stop") result = await stopDaemon({ stateDir: common.stateDir, cancelTree: options["cancel-tree"], timeoutMs: common.timeoutMs });
15
- else if (family === "session") { const operation = ["status","resume"].includes(command) ? "inspect" : command; const params = { sessionId: options["session-id"], title: options.title, messageId: options["message-id"], archiveFile: options["archive-file"], ...(command === "prompt" ? { prompt: await prompt(options) } : {}) }; result = await callDaemon(common.stateDir, `session.${operation}`, params, common.timeoutMs ?? 1_800_000); }
16
+ else if (family === "session") { const operation = command === "status" ? "inspect" : command; const params = { sessionId: options["session-id"], title: options.title, messageId: options["message-id"], archiveFile: options["archive-file"], ...(command === "prompt" ? { prompt: await prompt(options) } : {}) }; result = await callDaemon(common.stateDir, `session.${operation}`, params, common.timeoutMs ?? 1_800_000); }
16
17
  else throw new Error("usage: dd-opencode daemon start|status|stop | session create|prompt|inspect|fork|cancel|archive [options] --json");
17
18
  process.stdout.write(`${JSON.stringify({ ok: true, ...result })}\n`);
18
19
  } catch (error) { process.stderr.write(`${JSON.stringify({ ok: false, error: error.message, ...(error.code ? { code: error.code, retryable: error.retryable === true, details: error.details } : {}) })}\n`); process.exit(1); }
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { durableDaemonStart, inspectDaemonOperation } from "../lib/daemon-operations.mjs";
2
3
  import { cancelChild, cancelSession, createSession, doctor, forkSession, inspectSession, promptFromFile, promptSession } from "../lib/dd-zcode.mjs";
3
4
  import { callDaemon, serveDaemon, startDaemon, stopDaemon } from "../lib/dd-zcode-daemon.mjs";
4
5
 
@@ -33,17 +34,17 @@ try {
33
34
  const answers = options["answers-file"] ? JSON.parse(await promptFromFile(options["answers-file"])) : undefined;
34
35
  const shared = common(options);
35
36
  if (family === "doctor") result = await doctor(shared);
36
- else if (family === "daemon" && command === "start") result = await startDaemon({ ...shared, entryPath: process.argv[1] });
37
+ else if (family === "daemon" && command === "start") result = await durableDaemonStart({ ...shared, entryPath: process.argv[1] }, startDaemon);
37
38
  else if (family === "daemon" && command === "serve") { await serveDaemon(shared.stateDir); process.exit(0); }
38
39
  else if (family === "daemon" && command === "status") result = await callDaemon(shared.stateDir, "daemon.status");
39
- else if (family === "daemon" && command === "operation") result = await callDaemon(shared.stateDir, "operation.inspect", { operationId: options["operation-id"] });
40
+ else if (family === "daemon" && command === "operation") result = await inspectDaemonOperation(shared.stateDir, options["operation-id"]);
40
41
  else if (family === "daemon" && command === "stop") result = await stopDaemon({ stateDir: shared.stateDir, cancelTree: options["cancel-tree"], timeoutMs: shared.timeoutMs });
41
42
  else if (family === "session") {
42
43
  const params = { ...shared, answers, ...(command === "create" || command === "prompt" ? { prompt: options["prompt-file"] ? await promptFromFile(options["prompt-file"]) : options.prompt } : {}), ...(command === "fork" ? { target: JSON.parse(options["target-json"] ?? "null") } : {}) };
43
44
  // promptSessionWithBridge owns the meaningful deadline: it fails when its
44
45
  // ACP Session has been idle for the configured liveness window. A fixed
45
46
  // RPC deadline would wrongly terminate a long but active Turn.
46
- const operation = `session.${["status", "resume"].includes(command) ? "inspect" : command}`;
47
+ const operation = `session.${command === "status" ? "inspect" : command}`;
47
48
  const rpcTimeout = command === "prompt" && shared.timeoutMs === undefined
48
49
  ? null
49
50
  : (shared.timeoutMs ?? 1_800_000);