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

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 (73) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/dist/build-info.json +5 -5
  3. package/dist/cli/help.js +42 -1
  4. package/dist/cli/run-cli.js +147 -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 +379 -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/hooks.js +5 -5
  34. package/dist/services/managed-daemon-binding.js +38 -0
  35. package/dist/services/managed-processes.js +54 -7
  36. package/dist/services/merge-server.js +53 -30
  37. package/dist/services/native-daemon-history.js +49 -0
  38. package/dist/services/native-session-control.js +31 -0
  39. package/dist/services/protocols.js +9 -0
  40. package/dist/services/recovery-snapshot-database.js +103 -0
  41. package/dist/services/run-control-receipt.js +56 -0
  42. package/dist/services/run-control-worker.js +297 -0
  43. package/dist/services/run-control.js +752 -0
  44. package/dist/services/run-controller-adapter.js +79 -0
  45. package/dist/services/run-controller-capture.js +117 -0
  46. package/dist/services/run-controller-process.js +178 -0
  47. package/dist/services/run-controller-recovery.js +219 -0
  48. package/dist/services/run-controller-state.js +31 -0
  49. package/dist/services/run-controller.js +539 -0
  50. package/dist/services/run-recovery-runtime.js +69 -0
  51. package/dist/services/run-recovery.js +33 -9
  52. package/dist/services/runs.js +65 -13
  53. package/dist/services/runtime-budget.js +362 -0
  54. package/dist/services/runtime-scope-capture.js +50 -0
  55. package/dist/services/runtime-scope-control.js +431 -0
  56. package/dist/services/runtime-scope-resume.js +543 -0
  57. package/dist/services/runtime-scope-stop.js +95 -0
  58. package/dist/services/runtime-scope-worker.js +165 -0
  59. package/dist/services/runtime-service.js +4 -0
  60. package/dist/services/vnext-code-review.js +1 -1
  61. package/dist/services/vnext-code.js +1 -1
  62. package/dist/services/vnext-execution-profile.js +2 -35
  63. package/dist/services/vnext-fanout.js +23 -7
  64. package/dist/services/vnext-plan-review.js +12 -9
  65. package/dist/services/vnext-plan.js +1 -1
  66. package/dist/services/vnext-protocolize.js +10 -4
  67. package/dist/services/vnext-specify.js +12 -5
  68. package/dist/services/work-registry.js +26 -8
  69. package/dist/services/workspace-bootstrap.js +10 -1
  70. package/dist/storage/database.js +160 -13
  71. package/dist/storage/writer-contract.js +76 -0
  72. package/dist/storage/writer-migration.js +102 -0
  73. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # @deksden-com/dd-flow-cli
2
2
 
3
+ ## 0.9.0-beta.40
4
+
5
+ ### Patch Changes
6
+
7
+ - 2b4b714: Admit ordinary managed HITL answers through their exact generation-bound native
8
+ prompt, preserving pause, Session, raw-answer and prompt integrity. Keep accepted
9
+ answer intent separate from physical delivery so stop drains native operations
10
+ and recovery can deliver an unanswered pause without replaying completed work.
11
+ - c5eeb6f: Capture owned Codex home configuration and evidence without copying shared host
12
+ history or ephemeral helper links. Reuse the provisioned shared-entry contract
13
+ and retain strict rejection of unrelated external recovery artifact links.
14
+
15
+ ## 0.9.0-beta.39
16
+
17
+ ### Patch Changes
18
+
19
+ - 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.
20
+
21
+ 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.
22
+
23
+ Discover native Codex children from spawn receipts and release terminal failed provider turns only after a fresh settled-tree observation.
24
+
3
25
  ## 0.9.0-beta.38
4
26
 
5
27
  ### Patch Changes
@@ -1037,7 +1059,6 @@ record` command with the bounded 15-probe contract.
1037
1059
  - 16f68a3: Add router-native engine snapshot commands and routed project-command dispatch.
1038
1060
 
1039
1061
  Also include the runtime/dashboard compatibility wave needed by the current Memory Bank canon:
1040
-
1041
1062
  - target-based dashboard `open`/`refresh` commands and related-command help;
1042
1063
  - persisted FIFO lane waiters and merge wait-acquire specialization;
1043
1064
  - `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.40",
4
+ "cli_commit": "652524c6f370aa17c75474de5ebb7b3b9bf0b3a2",
5
+ "built_at": "2026-09-09T17:31:05.981Z",
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,17 @@ 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 { controllerHitlAdmission } from "../services/run-controller-adapter.js";
63
+ import { controlLifecycleReply, runControlStatus } from "../services/run-control.js";
64
+ import { requestRunControl, serveRunControl } from "../services/run-control-worker.js";
56
65
  import { startRuntimeService } from "../services/runtime-service.js";
57
66
  const defaultIo = {
58
67
  stdout: process.stdout,
@@ -159,6 +168,7 @@ function resolveProjectRootBeforeRouting(args, env) {
159
168
  function databaseModeForCommand(args, classification) {
160
169
  const [family, command] = args;
161
170
  if (family === "engine" ||
171
+ family === "storage" && command === "migrate-writer" ||
162
172
  family === "version" ||
163
173
  family === "schema" ||
164
174
  family === "canon" && command === "resolve" ||
@@ -510,6 +520,13 @@ function shortSha(value) {
510
520
  async function dispatch(args, context, io, scopeProjectRoot = null, classification = classifyCliOperation(args, context.env), output = parseOutputOptions(args)) {
511
521
  const [family, command, ...rest] = args;
512
522
  const parsed = parseArgs(rest);
523
+ if (family === "storage" && command === "migrate-writer") {
524
+ if (parsed.positional.length || [...parsed.options.keys()].some(key => !["database", "backup", "offline"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
525
+ throw new AppError("usage", "Usage: dd-flow storage migrate-writer --database ABSOLUTE_DB --backup NEW_ABSOLUTE_FILE --offline", 2);
526
+ if (parsed.options.get("offline")?.[0] !== flagValue)
527
+ throw new AppError("usage", "Writer migration requires --offline after draining all local and external runtime owners", 2);
528
+ return { ok: true, ...migrateStoreWriter(requiredOption(parsed, "database"), requiredOption(parsed, "backup")) };
529
+ }
513
530
  if (family === "engine") {
514
531
  return dispatchEngine(context, command, parsed, classification);
515
532
  }
@@ -898,6 +915,7 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
898
915
  const projectRoot = requiredOption(parsed, "project-root");
899
916
  const runId = requiredPosition(parsed, 0, "run-id");
900
917
  const stage = requiredOption(parsed, "stage");
918
+ const controlledReply = (result) => controlLifecycleReply(context, { projectRoot, runId }, result);
901
919
  const semanticFile = optionalOption(parsed, "semantic-file") ?? optionalOption(parsed, "data");
902
920
  const resultFile = optionalOption(parsed, "result-file") ?? semanticFile;
903
921
  const resultStdin = hasOption(parsed, "result-stdin");
@@ -925,56 +943,56 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
925
943
  if (!resultFile && !resultStdin) {
926
944
  throw new AppError("usage", "vNext SPECIFY finish requires --result-file or --result-stdin", 2);
927
945
  }
928
- return finishVnextSpecifyStage(context, {
946
+ return controlledReply(await finishVnextSpecifyStage(context, {
929
947
  projectRoot,
930
948
  runId,
931
949
  ...(resultFile ? { resultFile } : { resultJson: await readStdin(io.stdin) }),
932
950
  outcome: outcome
933
- });
951
+ }));
934
952
  }
935
953
  if (stage === "protocolize" && isVnextProtocolizeRun(context, { projectRoot, runId })) {
936
954
  if (!resultFile || resultStdin || hasOption(parsed, "outcome")) {
937
955
  throw new AppError("usage", "vNext PROTOCOLIZE requires exactly --result-file", 2);
938
956
  }
939
- return finishVnextProtocolize(context, { projectRoot, runId, resultFile });
957
+ return controlledReply(await finishVnextProtocolize(context, { projectRoot, runId, resultFile }));
940
958
  }
941
959
  if (stage === "plan" && isVnextPlanRun(context, { projectRoot, runId })) {
942
960
  if (optionalOption(parsed, "code-work-batch-file"))
943
961
  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 });
962
+ return controlledReply(await finishVnextPlan(context, { projectRoot, runId }));
945
963
  }
946
964
  if (stage === "plan-review" && isVnextPlanReviewRun(context, { projectRoot, runId })) {
947
965
  if (resultStdin || hasOption(parsed, "outcome"))
948
966
  throw new AppError("usage", "vNext PLAN-REVIEW uses --decision-file", 2);
949
967
  const decisionFile = optionalOption(parsed, "decision-file");
950
- return finishVnextPlanReview(context, { projectRoot, runId, ...(decisionFile ? { decisionFile } : {}) });
968
+ return controlledReply(await finishVnextPlanReview(context, { projectRoot, runId, ...(decisionFile ? { decisionFile } : {}) }));
951
969
  }
952
970
  if (stage === "code" && isVnextPlanRun(context, { projectRoot, runId })) {
953
971
  if (semanticFile || resultFile || resultStdin || hasOption(parsed, "outcome")) {
954
972
  throw new AppError("usage", "vNext CODE finish accepts only --verification-file", 2);
955
973
  }
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) });
974
+ 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
975
  }
958
976
  if (stage === "code-review" && isVnextCodeReviewRun(context, { projectRoot, runId })) {
959
977
  if (resultStdin || semanticFile || resultFile || hasOption(parsed, "outcome"))
960
978
  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) });
979
+ 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
980
  }
963
981
  if (stage === "merge" && isVnextMergeRun(context, { projectRoot, runId })) {
964
982
  if (resultStdin || semanticFile || resultFile || hasOption(parsed, "outcome"))
965
983
  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) });
984
+ return controlledReply(await finishVnextMerge(context, { projectRoot, runId, requestId: requiredOption(parsed, "request"), workId: requiredOption(parsed, "work"), ...(retryCheckId ? { retryCheckId, retryReason: retryReason } : {}), progress: commandProgress(io, output) }));
967
985
  }
968
986
  if (hasOption(parsed, "outcome") || hasOption(parsed, "result-file") || resultStdin) {
969
987
  throw new AppError("usage", "--outcome, --result-file and --result-stdin are only available for vNext SPECIFY", 2);
970
988
  }
971
- return finishStage(context, {
989
+ return controlledReply(await finishStage(context, {
972
990
  projectRoot,
973
991
  runId,
974
992
  stage,
975
993
  ...(dir ? { dir } : {}),
976
994
  ...(semanticFile ? { semanticFile } : {})
977
- });
995
+ }));
978
996
  }
979
997
  if (family === "stage" && command === "pause") {
980
998
  if (!hasOption(parsed, "question-stdin"))
@@ -1056,6 +1074,43 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
1056
1074
  return dispatchSession(context, command, parsed);
1057
1075
  }
1058
1076
  if (family === "run") {
1077
+ if (command === "control") {
1078
+ const action = requiredPosition(parsed, 0, "control action");
1079
+ const scope = { projectRoot: requiredOption(parsed, "project-root"), runId: requiredOption(parsed, "run") };
1080
+ if (action === "status")
1081
+ return runControlStatus(context, scope);
1082
+ if (action === "resume") {
1083
+ if (["force", "grace-ms", "escalate"].some(option => hasOption(parsed, option)))
1084
+ throw new AppError("usage", "Resume does not accept stop or pause escalation options", 2);
1085
+ return await resumeRunController(context, { ...scope, requestId: requiredOption(parsed, "request-id"), controlId: requiredOption(parsed, "from"), ...(hasOption(parsed, "wait-ms") ? { waitMs: Number(requiredOption(parsed, "wait-ms")) } : {}) });
1086
+ }
1087
+ if (action === "serve")
1088
+ return await serveRunControl(context, { ...scope, controlId: requiredOption(parsed, "control-id") });
1089
+ if (action === "pause" || action === "stop")
1090
+ return await requestRunControl(context, {
1091
+ ...scope, action, requestId: requiredOption(parsed, "request-id"),
1092
+ ...(hasOption(parsed, "force") ? { force: true } : {}),
1093
+ ...(hasOption(parsed, "grace-ms") ? { graceMs: Number(requiredOption(parsed, "grace-ms")) } : {}),
1094
+ ...(hasOption(parsed, "escalate") ? { escalate: requiredOption(parsed, "escalate") } : {}),
1095
+ ...(hasOption(parsed, "wait-ms") ? { waitMs: Number(requiredOption(parsed, "wait-ms")) } : {})
1096
+ });
1097
+ throw new AppError("usage", "Use: dd-flow run control pause|stop|resume|status", 2);
1098
+ }
1099
+ if (command === "drive") {
1100
+ const action = requiredPosition(parsed, 0, "drive action");
1101
+ const scope = { projectRoot: requiredOption(parsed, "project-root"), runId: requiredOption(parsed, "run") };
1102
+ if (action === "status")
1103
+ return runControllerStatus(context, { ...scope, ...(optionalOption(parsed, "after") ? { after: Number(optionalOption(parsed, "after")) } : {}) });
1104
+ if (action === "launch")
1105
+ 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") } : {}) });
1106
+ if (action === "serve")
1107
+ return await serveRunController(context, { ...scope, controllerId: requiredOption(parsed, "controller-id") });
1108
+ if (action === "answer")
1109
+ return answerRunController(context, { ...scope, requestId: requiredOption(parsed, "request-id"), pauseId: requiredOption(parsed, "pause-id"), answerFile: requiredOption(parsed, "answer-file") });
1110
+ if (action === "context")
1111
+ 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") });
1112
+ throw new AppError("usage", "Use: dd-flow run drive launch|status|answer|context", 2);
1113
+ }
1059
1114
  return dispatchRun(context, command, parsed);
1060
1115
  }
1061
1116
  if (family === "stat") {
@@ -1528,13 +1583,14 @@ function dispatchRun(context, command, parsed) {
1528
1583
  if (action === "restore")
1529
1584
  return restoreEvalRunSnapshot(context, {
1530
1585
  snapshot: requiredOption(parsed, "snapshot"),
1586
+ executionRoutingFile: optionalOption(parsed, "execution-routing-file"),
1531
1587
  ...(optionalOption(parsed, "recovery-id") ? { recoveryId: requiredOption(parsed, "recovery-id") } : {}),
1532
1588
  projectRoot: requiredOption(parsed, "project-root")
1533
1589
  });
1534
1590
  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
1591
  }
1536
1592
  if (command === "prepare-vnext-specify") {
1537
- return prepareVnextSpecifyRun(context, { projectRoot: requiredOption(parsed, "project-root"), slug: requiredOption(parsed, "slug") });
1593
+ return prepareVnextSpecifyRun(context, { projectRoot: requiredOption(parsed, "project-root"), slug: requiredOption(parsed, "slug"), executionRoutingFile: optionalOption(parsed, "execution-routing-file") });
1538
1594
  }
1539
1595
  if (command === "start") {
1540
1596
  if (optionalOption(parsed, "flow-kind") === "mb-upgrade") {
@@ -1551,6 +1607,7 @@ function dispatchRun(context, command, parsed) {
1551
1607
  nextAction: optionalOption(parsed, "next-action"),
1552
1608
  preset: optionalOption(parsed, "preset"),
1553
1609
  taskProfile: readOptionalJsonFile(optionalOption(parsed, "task-profile-file")),
1610
+ executionRoutingFile: optionalOption(parsed, "execution-routing-file"),
1554
1611
  ...(protocolOverrides ? { protocolOverrides } : {}),
1555
1612
  runOverrides: parseFlowFlagOverrides(allOptions(parsed, "flag"))
1556
1613
  });
@@ -1666,22 +1723,64 @@ function dispatchRun(context, command, parsed) {
1666
1723
  throw new AppError("usage", `Unknown run command: ${command ?? "<empty>"}`, 2);
1667
1724
  }
1668
1725
  async function dispatchRuntime(context, command, parsed, progress) {
1726
+ if (command === "scope") {
1727
+ if (parsed.positional.length === 1 && parsed.positional[0] === "resume") {
1728
+ if ([...parsed.options.keys()].some(key => !["scope-id", "request-id", "generation", "capture-key"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
1729
+ throw new AppError("usage", "Use runtime scope resume --scope-id ID --request-id ID --generation N --capture-key SHA256", 2);
1730
+ 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") }) };
1731
+ }
1732
+ if (parsed.positional.length === 1 && parsed.positional[0] === "control") {
1733
+ if ([...parsed.options.keys()].some(key => !["scope-id", "request-id", "mode", "manifest-json"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
1734
+ throw new AppError("usage", "Use runtime scope control --scope-id ID --request-id ID --mode pause|stop [--manifest-json JSON]", 2);
1735
+ const mode = requiredOption(parsed, "mode");
1736
+ if (mode !== "pause" && mode !== "stop")
1737
+ throw new AppError("usage", "Scope control mode must be pause or stop", 2);
1738
+ 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) };
1739
+ }
1740
+ if (parsed.positional.length === 1 && parsed.positional[0] === "serve") {
1741
+ if ([...parsed.options.keys()].some(key => !["scope-id", "generation"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
1742
+ throw new AppError("usage", "Invalid internal scope worker arguments", 2);
1743
+ return { ok: true, ...await serveRuntimeScopeControl(context, requiredOption(parsed, "scope-id"), Number(requiredOption(parsed, "generation"))) };
1744
+ }
1745
+ if (parsed.positional.length === 1 && parsed.positional[0] === "stop") {
1746
+ if ([...parsed.options.keys()].some(key => !["scope-id", "request-id"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
1747
+ throw new AppError("usage", "Usage: dd-flow runtime scope stop --scope-id EVAL_ID --request-id ID", 2);
1748
+ return { ok: true, ...await stopRuntimeScope(context, requiredOption(parsed, "scope-id"), requiredOption(parsed, "request-id")) };
1749
+ }
1750
+ if (parsed.positional.length === 1 && parsed.positional[0] === "status") {
1751
+ if ([...parsed.options.keys()].some(key => key !== "scope-id") || [...parsed.options.values()].some(values => values.length !== 1))
1752
+ throw new AppError("usage", "Usage: dd-flow runtime scope status --scope-id EVAL_ID", 2);
1753
+ return { ok: true, ...runtimeBudgetStatus(context, requiredOption(parsed, "scope-id")) };
1754
+ }
1755
+ 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))
1756
+ throw new AppError("usage", "Usage: dd-flow runtime scope fence --scope-id EVAL_ID --request-id ID", 2);
1757
+ return { ok: true, ...fenceRuntimeBudget(context, requiredOption(parsed, "scope-id"), requiredOption(parsed, "request-id")) };
1758
+ }
1669
1759
  if (command !== "process")
1670
- throw new AppError("usage", "Usage: dd-flow runtime process <start|stop|register|confirm|heartbeat|finish|status|reconcile>", 2);
1760
+ throw new AppError("usage", "Usage: dd-flow runtime <scope|process>; see dd-flow runtime scope --help", 2);
1671
1761
  const action = requiredPosition(parsed, 0, "runtime process action");
1762
+ if (action === "check-admission")
1763
+ return { ok: true, ...admitRuntimeCheck(context, requiredOption(parsed, "id"), requiredOption(parsed, "lease-token")) };
1672
1764
  if (action === "start")
1673
1765
  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
1766
  if (action === "stop")
1675
1767
  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
1768
  if (action === "register") {
1677
1769
  const leaseMs = optionalPositiveNumber(parsed, "lease-ms", true);
1770
+ const budget = parseRuntimeBudget(optionalOption(parsed, "budget-json"));
1771
+ const projectId = optionalOption(parsed, "project-id") ?? null, runId = optionalOption(parsed, "run") ?? null;
1772
+ const guard = projectId && runId ? recoveryGuard(context, projectId, runId) : null;
1773
+ const run = projectId && runId ? context.db.get("SELECT id FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]) : null;
1678
1774
  const process = registerManagedProcess(context, {
1679
1775
  kind: requiredOption(parsed, "kind"), ownerId: requiredOption(parsed, "owner"),
1776
+ uniqueActiveOperation: requiredOption(parsed, "kind") === "eval-observer",
1777
+ ...(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
1778
  ownerPid: optionalPositiveNumber(parsed, "owner-pid", true),
1681
- projectId: optionalOption(parsed, "project-id") ?? null, runId: optionalOption(parsed, "run") ?? null,
1779
+ projectId, runId,
1682
1780
  workId: optionalOption(parsed, "work") ?? null, checkId: optionalOption(parsed, "check") ?? null,
1683
1781
  operationId: optionalOption(parsed, "operation") ?? null, stdoutPath: optionalOption(parsed, "stdout") ?? null,
1684
- stderrPath: optionalOption(parsed, "stderr") ?? null, ...(leaseMs ? { leaseMs } : {})
1782
+ stderrPath: optionalOption(parsed, "stderr") ?? null, ...(leaseMs ? { leaseMs } : {}),
1783
+ 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
1784
  });
1686
1785
  return { ok: true, process };
1687
1786
  }
@@ -1694,6 +1793,37 @@ async function dispatchRuntime(context, command, parsed, progress) {
1694
1793
  const leaseMs = optionalPositiveNumber(parsed, "lease-ms", true);
1695
1794
  return { ok: heartbeatManagedProcess(context, { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), ...(leaseMs ? { leaseMs } : {}) }) };
1696
1795
  }
1796
+ if (action === "admission") {
1797
+ const record = managedProcessStatus(context).find(process => process.id === requiredOption(parsed, "id"));
1798
+ 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))
1799
+ throw new AppError("process_ownership_unknown", "Productive dispatch requires its current confirmed runtime owner", 1);
1800
+ const metadata = JSON.parse(record.metadata_json);
1801
+ if (metadata.state_dir !== requiredOption(parsed, "state-dir"))
1802
+ throw new AppError("process_ownership_unknown", "Runtime admission belongs to another state directory", 1);
1803
+ if (record.project_id && record.run_id) {
1804
+ if (managedProcessRuntimeHome(record, context) !== context.ddFlowHome)
1805
+ throw new AppError("process_runtime_scope_mismatch", "Runtime admission belongs to another or unproven flow home", 1);
1806
+ 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") });
1807
+ if (!recovering) {
1808
+ if (metadata.run_admitted !== true || !Number.isSafeInteger(metadata.run_generation))
1809
+ throw new AppError("run_recovery_guarded", "Runtime was not admitted before the RUN fence; reconcile its launch", 1);
1810
+ assertRunMutationAllowed(context, record.project_id, record.run_id, metadata.run_generation);
1811
+ }
1812
+ const run = context.db.get("SELECT status FROM runs WHERE project_id = ? AND id = ?", [record.project_id, record.run_id]);
1813
+ 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")]);
1814
+ const acceptedHitlPrompt = ["paused", "waiting_for_user"].includes(run?.status ?? "") && controllerHitlAdmission(context, record, { stateDir: requiredOption(parsed, "state-dir"), operationId: optionalOption(parsed, "operation-id"), operation: optionalOption(parsed, "operation"), nativeSessionId: optionalOption(parsed, "native-session-id"), promptSha256: optionalOption(parsed, "prompt-sha256") });
1815
+ if (!run || run.status !== "running" && !retainedHitlPrompt && !acceptedHitlPrompt && !["judge", "probe"].includes(metadata.role ?? ""))
1816
+ throw new AppError("invalid_run_state", "Subject dispatch requires a running RUN or its retained recovery/HITL prompt", 1);
1817
+ }
1818
+ 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") });
1819
+ return { ok: true, ...budget, process_id: record.id, generation: metadata.run_generation ?? null };
1820
+ }
1821
+ if (action === "release-turn") {
1822
+ const record = managedProcessStatus(context).find(process => process.id === requiredOption(parsed, "id"));
1823
+ if (!record || record.lease_token !== requiredOption(parsed, "lease-token") || record.owner_id !== requiredOption(parsed, "owner") || managedProcessRuntimeHome(record, context) !== context.ddFlowHome)
1824
+ throw new AppError("process_ownership_unknown", "Budget release requires its retained physical owner", 1);
1825
+ return releaseProviderTurns(context, record, { daemonId: requiredOption(parsed, "daemon-id"), operationId: optionalOption(parsed, "operation-id"), observationFile: optionalOption(parsed, "observation-file") });
1826
+ }
1697
1827
  if (action === "finish") {
1698
1828
  const state = requiredOption(parsed, "state");
1699
1829
  if (state !== "stopped" && state !== "failed")
@@ -1704,7 +1834,7 @@ async function dispatchRuntime(context, command, parsed, progress) {
1704
1834
  return { ok: true, processes: managedProcessStatus(context) };
1705
1835
  if (action === "reconcile")
1706
1836
  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);
1837
+ throw new AppError("usage", "Usage: dd-flow runtime process <start|stop|register|confirm|heartbeat|admission|finish|status|reconcile>", 2);
1708
1838
  }
1709
1839
  function dispatchStat(context, command, parsed) {
1710
1840
  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); }