@deksden-com/dd-flow-cli 0.9.0-beta.13 → 0.9.0-beta.134

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 (204) hide show
  1. package/CHANGELOG.md +687 -0
  2. package/README.md +101 -1
  3. package/dist/build-info.json +10 -10
  4. package/dist/cli/argument-values.js +38 -0
  5. package/dist/cli/command-inputs.js +306 -0
  6. package/dist/cli/help.js +88 -12
  7. package/dist/cli/hook-ingress.js +78 -0
  8. package/dist/cli/input-preparation.js +116 -0
  9. package/dist/cli/native-hook-ingress.js +80 -0
  10. package/dist/cli/run-cli.js +1465 -450
  11. package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
  12. package/dist/cli/runtime-maintenance-ingress.js +52 -0
  13. package/dist/cli/runtime-process.js +153 -0
  14. package/dist/cli.js +17 -2
  15. package/dist/domain/stage-catalog.js +1 -1
  16. package/dist/harness-runtime/DD-ZCODE.md +99 -0
  17. package/dist/harness-runtime/bin/dd-agy.mjs +56 -0
  18. package/dist/harness-runtime/bin/dd-codex.mjs +27 -0
  19. package/dist/harness-runtime/bin/dd-droid.mjs +34 -0
  20. package/dist/harness-runtime/bin/dd-grok.mjs +40 -0
  21. package/dist/harness-runtime/bin/dd-opencode.mjs +22 -0
  22. package/dist/harness-runtime/bin/dd-zcode.mjs +87 -0
  23. package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
  24. package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
  25. package/dist/harness-runtime/lib/agy-hooks.d.mts +1 -0
  26. package/dist/harness-runtime/lib/agy-hooks.mjs +9 -0
  27. package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +10 -0
  28. package/dist/harness-runtime/lib/codex-capacity-policy.mjs +89 -0
  29. package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
  30. package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
  31. package/dist/harness-runtime/lib/daemon-operations.mjs +370 -0
  32. package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +6 -0
  33. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +693 -0
  34. package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
  35. package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
  36. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +190 -0
  37. package/dist/harness-runtime/lib/dd-codex.d.mts +1 -0
  38. package/dist/harness-runtime/lib/dd-codex.mjs +805 -0
  39. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +128 -0
  40. package/dist/harness-runtime/lib/dd-droid.mjs +561 -0
  41. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +490 -0
  42. package/dist/harness-runtime/lib/dd-grok.mjs +177 -0
  43. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +255 -0
  44. package/dist/harness-runtime/lib/dd-opencode.mjs +128 -0
  45. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +703 -0
  46. package/dist/harness-runtime/lib/dd-zcode.mjs +1128 -0
  47. package/dist/harness-runtime/lib/delegation-instructions.d.mts +13 -0
  48. package/dist/harness-runtime/lib/delegation-instructions.mjs +144 -0
  49. package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
  50. package/dist/harness-runtime/lib/driver-recovery.mjs +139 -0
  51. package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
  52. package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
  53. package/dist/harness-runtime/lib/lease-renewal.mjs +36 -0
  54. package/dist/harness-runtime/lib/lifecycle-command.d.mts +20 -0
  55. package/dist/harness-runtime/lib/lifecycle-command.mjs +272 -0
  56. package/dist/harness-runtime/lib/lifecycle-contract.d.mts +10 -0
  57. package/dist/harness-runtime/lib/lifecycle-contract.mjs +19 -0
  58. package/dist/harness-runtime/lib/managed-daemon.d.mts +12 -0
  59. package/dist/harness-runtime/lib/managed-daemon.mjs +619 -0
  60. package/dist/harness-runtime/lib/model-observations.mjs +101 -0
  61. package/dist/harness-runtime/lib/native-children.d.mts +14 -0
  62. package/dist/harness-runtime/lib/native-children.mjs +94 -0
  63. package/dist/harness-runtime/lib/native-hook-command.d.mts +7 -0
  64. package/dist/harness-runtime/lib/native-hook-command.mjs +251 -0
  65. package/dist/harness-runtime/lib/native-streams.mjs +45 -0
  66. package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
  67. package/dist/harness-runtime/lib/observation-clock.mjs +92 -0
  68. package/dist/harness-runtime/lib/operation-context.mjs +5 -0
  69. package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
  70. package/dist/harness-runtime/lib/operation-errors.mjs +116 -0
  71. package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
  72. package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
  73. package/dist/harness-runtime/lib/process-json.d.mts +5 -0
  74. package/dist/harness-runtime/lib/process-json.mjs +201 -0
  75. package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
  76. package/dist/harness-runtime/lib/runner-events.mjs +200 -0
  77. package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
  78. package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
  79. package/dist/harness-runtime/lib/setup-progress.d.mts +9 -0
  80. package/dist/harness-runtime/lib/setup-progress.mjs +51 -0
  81. package/dist/harness-runtime/lib/tool-observations.d.mts +21 -0
  82. package/dist/harness-runtime/lib/tool-observations.mjs +228 -0
  83. package/dist/harness-runtime/lib/zcode-usage.mjs +55 -0
  84. package/dist/protocol/local-files.js +18 -9
  85. package/dist/runtime/context.js +8 -4
  86. package/dist/schemas/agent-profile.schema.json +1 -1
  87. package/dist/schemas/code-review-result.schema.json +1 -1
  88. package/dist/schemas/code-verification.schema.json +7 -5
  89. package/dist/schemas/code-work-batch.schema.json +2 -2
  90. package/dist/schemas/flow-run.schema.json +2 -0
  91. package/dist/schemas/plan-review-decision.schema.json +3 -3
  92. package/dist/schemas/plan-review-result.schema.json +2 -2
  93. package/dist/schemas/run-control-receipt.schema.json +118 -0
  94. package/dist/schemas/run-control-request.schema.json +36 -0
  95. package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
  96. package/dist/services/branch-context.js +3 -3
  97. package/dist/services/canon.js +11 -8
  98. package/dist/services/check-runner-evidence.js +121 -0
  99. package/dist/services/cleanup.js +133 -40
  100. package/dist/services/cli-operation-classifier.js +22 -24
  101. package/dist/services/code-checks.js +851 -109
  102. package/dist/services/codex-capacity.js +367 -0
  103. package/dist/services/codex-hook-delivery.js +32 -0
  104. package/dist/services/command-context.js +80 -0
  105. package/dist/services/compatibility-preflight.js +13 -3
  106. package/dist/services/config.js +11 -8
  107. package/dist/services/continuation-outcome.js +20 -0
  108. package/dist/services/controller-fanout.js +136 -0
  109. package/dist/services/dashboard.js +135 -56
  110. package/dist/services/engines.js +245 -119
  111. package/dist/services/eval-snapshots.js +1146 -105
  112. package/dist/services/execution-policy.js +181 -0
  113. package/dist/services/external-work-launch.js +230 -0
  114. package/dist/services/harness-adapter.js +329 -36
  115. package/dist/services/harness-config.js +2 -0
  116. package/dist/services/hooks.js +753 -325
  117. package/dist/services/lanes.js +78 -65
  118. package/dist/services/lifecycle-command.js +1 -288
  119. package/dist/services/lifecycle-contract.js +1 -0
  120. package/dist/services/lifecycle-invocations.js +1828 -0
  121. package/dist/services/managed-daemon-binding.js +38 -0
  122. package/dist/services/managed-lease-monitor.js +133 -0
  123. package/dist/services/managed-processes.js +409 -51
  124. package/dist/services/merge-queue.js +228 -149
  125. package/dist/services/merge-server.js +163 -35
  126. package/dist/services/migrations.js +37 -16
  127. package/dist/services/native-daemon-history.js +49 -0
  128. package/dist/services/native-session-control.js +39 -0
  129. package/dist/services/plan-runtime.js +26 -11
  130. package/dist/services/plans.js +28 -25
  131. package/dist/services/projects.js +20 -15
  132. package/dist/services/prompts.js +28 -17
  133. package/dist/services/protocols.js +163 -87
  134. package/dist/services/recovery-observation-budget.js +121 -0
  135. package/dist/services/recovery-snapshot-database.js +145 -0
  136. package/dist/services/repair-intents.js +42 -0
  137. package/dist/services/review-copy.js +157 -0
  138. package/dist/services/review-deferral.js +23 -0
  139. package/dist/services/run-control-receipt.js +56 -0
  140. package/dist/services/run-control-worker.js +411 -0
  141. package/dist/services/run-control.js +985 -0
  142. package/dist/services/run-controller-adapter.js +399 -0
  143. package/dist/services/run-controller-capture.js +179 -0
  144. package/dist/services/run-controller-process.js +187 -0
  145. package/dist/services/run-controller-recovery.js +289 -0
  146. package/dist/services/run-controller-state.js +97 -0
  147. package/dist/services/run-controller.js +1035 -0
  148. package/dist/services/run-engine-bindings.js +1 -1
  149. package/dist/services/run-fork.js +147 -0
  150. package/dist/services/run-observations.js +152 -0
  151. package/dist/services/run-projection.js +22 -64
  152. package/dist/services/run-recovery-guard.js +75 -0
  153. package/dist/services/run-recovery-runtime.js +112 -0
  154. package/dist/services/run-recovery.js +349 -0
  155. package/dist/services/runs.js +558 -217
  156. package/dist/services/runtime-budget.js +430 -0
  157. package/dist/services/runtime-command.js +43 -0
  158. package/dist/services/runtime-scope-capture.js +52 -0
  159. package/dist/services/runtime-scope-control.js +468 -0
  160. package/dist/services/runtime-scope-resume.js +599 -0
  161. package/dist/services/runtime-scope-stop.js +180 -0
  162. package/dist/services/runtime-scope-worker.js +287 -0
  163. package/dist/services/runtime-service.js +96 -0
  164. package/dist/services/schema-validation.js +10 -8
  165. package/dist/services/scope-writer-cleanup.js +27 -0
  166. package/dist/services/sessions.js +35 -69
  167. package/dist/services/stage-blocker.js +17 -7
  168. package/dist/services/stage-context.js +185 -19
  169. package/dist/services/stage-lifecycle.js +98 -107
  170. package/dist/services/stage-pause.js +89 -75
  171. package/dist/services/stage-report-renderer.js +158 -6
  172. package/dist/services/stage-work-graph.js +35 -0
  173. package/dist/services/tooling.js +32 -23
  174. package/dist/services/usage.js +49 -101
  175. package/dist/services/verification-matrix.js +374 -0
  176. package/dist/services/vnext-code-review.js +222 -87
  177. package/dist/services/vnext-code.js +324 -251
  178. package/dist/services/vnext-contracts.js +10 -1
  179. package/dist/services/vnext-execution-profile.js +79 -4
  180. package/dist/services/vnext-fanout.js +182 -17
  181. package/dist/services/vnext-merge.js +335 -113
  182. package/dist/services/vnext-plan-review.js +276 -126
  183. package/dist/services/vnext-plan.js +227 -102
  184. package/dist/services/vnext-protocolize.js +335 -82
  185. package/dist/services/vnext-specify.js +119 -81
  186. package/dist/services/work-registry.js +1083 -194
  187. package/dist/services/workspace-bootstrap.js +87 -0
  188. package/dist/services/workspace-files.js +85 -0
  189. package/dist/services/worktrees.js +98 -44
  190. package/dist/shared/entity-references.js +8 -0
  191. package/dist/shared/errors.js +17 -0
  192. package/dist/storage/agent-output.js +26 -0
  193. package/dist/storage/database.js +785 -37
  194. package/dist/storage/foreign-key-transaction.js +39 -0
  195. package/dist/storage/immutable-input.js +45 -0
  196. package/dist/storage/import-guard.js +102 -0
  197. package/dist/storage/native-cleanup.js +52 -0
  198. package/dist/storage/paths.js +17 -4
  199. package/dist/storage/projection-lock.js +73 -0
  200. package/dist/storage/work-references.js +23 -0
  201. package/dist/storage/writer-contract.js +77 -0
  202. package/dist/storage/writer-migration.js +85 -0
  203. package/package.json +30 -13
  204. package/tools/repair-paused-run-status.mjs +84 -0
@@ -1,59 +1,352 @@
1
1
  import { spawn } from "node:child_process";
2
- import { AppError } from "../shared/errors.js";
3
- /** Runs one harness command with the lifecycle guarantees shared by agent services. */
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
+ import { AppError, errorRecord } from "../shared/errors.js";
6
+ import { stopProcessGroup } from "../harness-runtime/lib/managed-daemon.mjs";
7
+ import { recoveryObservationBudget, recoveryRpcTimeout, withRecoveryObservation } from "./recovery-observation-budget.js";
8
+ import { ObservationClock, validateObservationDuration } from "../harness-runtime/lib/observation-clock.mjs";
9
+ import { canonicalNativeOperation, nativeOperationWait } from "../harness-runtime/lib/adapter-timeouts.mjs";
10
+ import { readOperationProgress, readOperationRecord, readOperationWaiting, suspendOperationClock } from "../harness-runtime/lib/operation-progress.mjs";
11
+ import { recoverDaemonReply } from "../harness-runtime/lib/daemon-observation.mjs";
12
+ import { isObservationLoss } from "../harness-runtime/lib/operation-errors.mjs";
13
+ const maxAdapterOutputBytes = 1_000_000;
14
+ /** Native hooks need a real executable, not a PATH-dependent shell expression. */
15
+ export function prepareHarnessFlowExecutable(context, stateDir) {
16
+ const configured = context.env.DD_FLOW_BIN;
17
+ if (configured !== undefined) {
18
+ if (!path.isAbsolute(configured) || !fs.existsSync(configured))
19
+ throw new AppError("harness_runtime_missing", "DD_FLOW_BIN must identify an existing absolute executable", 2);
20
+ if (path.basename(configured) === "dd-flow")
21
+ return configured;
22
+ }
23
+ const isolated = path.join(context.ddFlowHome, "bin", "dd-flow");
24
+ if (!configured && fs.existsSync(isolated))
25
+ return isolated;
26
+ const entry = fileURLToPath(new URL("../cli.js", import.meta.url));
27
+ if (!configured && !fs.existsSync(entry))
28
+ throw new AppError("harness_runtime_unbuilt", "Build the CLI or supply its pinned DD_FLOW_BIN before launching a harness", 1);
29
+ // The trusted lifecycle parser recognizes this basename, including for a
30
+ // custom pinned executable. Do not broaden that parser to arbitrary scripts.
31
+ const executable = path.join(stateDir, "dd-flow");
32
+ const dispatch = configured
33
+ ? `const { spawnSync } = await import("node:child_process");\nconst child = spawnSync(${JSON.stringify(configured)}, process.argv.slice(2), { stdio: "inherit", env: process.env });\nif (child.error) throw child.error;\nif (child.signal) process.kill(process.pid, child.signal);\nprocess.exit(child.status ?? 1);\n`
34
+ : `await import(${JSON.stringify(pathToFileURL(entry).href)});\n`;
35
+ const contents = `#!${process.execPath}\nprocess.env.DD_FLOW_HOME = ${JSON.stringify(context.ddFlowHome)};\n${dispatch}`;
36
+ try {
37
+ fs.writeFileSync(executable, contents, { flag: "wx", mode: 0o700 });
38
+ }
39
+ catch (error) {
40
+ if (error.code !== "EEXIST" || fs.readFileSync(executable, "utf8") !== contents)
41
+ throw new AppError("harness_runtime_binding_conflict", "Launch runtime executable already has different content", 1, { path: executable });
42
+ }
43
+ return executable;
44
+ }
4
45
  export function runHarnessAdapter(input) {
5
- const timeoutMs = input.timeoutMs ?? 120_000;
46
+ const index = input.args.indexOf("--state-dir"), stateDir = index >= 0 ? input.args[index + 1] : undefined;
47
+ const operation = canonicalNativeOperation(input.harness ?? "", `${input.args[0]}.${input.args[1]}`);
48
+ const boundOperation = ["session.create", "session.fork", "session.start", "session.resume"].includes(operation);
49
+ const binding = { operation, ...(input.runtimeOwner ? { owner_id: input.runtimeOwner.owner_id } : {}) };
50
+ let retainedOutcome;
51
+ const onOutcome = (value) => {
52
+ const fingerprint = JSON.stringify(value);
53
+ if (fingerprint !== retainedOutcome) {
54
+ input.onNativeOutcome?.(value);
55
+ retainedOutcome = fingerprint;
56
+ }
57
+ };
58
+ const dispatched = runAdapterTransport(input, stateDir, binding);
59
+ return dispatched.catch(async (error) => {
60
+ if (!stateDir || !boundOperation || input.signal?.aborted || !(isObservationLoss(error) || isObservationLoss(error?.details?.cause)))
61
+ throw error;
62
+ try {
63
+ return await recoverDaemonReply(stateDir, input.operationId, { timeoutMs: recoveryRpcTimeout(30_000), ...(input.signal ? { signal: input.signal } : {}), ...(error?.observationClock ? { observationClock: error.observationClock } : {}), binding, onOutcome,
64
+ // Reconcile only original bookkeeping. Work/MERGE share this path and
65
+ // must not replay creation merely because its release receipt is pending.
66
+ onPendingSettlement: async (retained) => {
67
+ input.signal?.throwIfAborted();
68
+ await withRecoveryObservation(recoveryObservationBudget(retained), () => runAdapterTransport({ ...input, args: ["daemon", "operation", "settle", "--state-dir", stateDir, "--operation-id", input.operationId, "--json"], operationId: `${input.operationId}:settlement`, timeoutMs: recoveryRpcTimeout(30_000) }, stateDir, {}), true);
69
+ }
70
+ });
71
+ }
72
+ catch (recovery) {
73
+ if (recovery && typeof recovery === "object")
74
+ Object.assign(recovery, { cause: error });
75
+ throw recovery;
76
+ }
77
+ });
78
+ }
79
+ function runAdapterTransport(input, stateDir, binding = {}) {
80
+ // `null` is deliberate: a productive Turn has no adapter-wall-clock limit.
81
+ // Preserve it instead of treating it like an omitted short control request.
82
+ if (input.timeoutMs != null)
83
+ validateObservationDuration(input.timeoutMs);
84
+ const operation = canonicalNativeOperation(input.harness ?? "", `${input.args[0]}.${input.args[1]}`);
85
+ const productive = input.harness !== undefined && nativeOperationWait(input.harness, operation) === "native-work";
86
+ const timeoutMs = recoveryRpcTimeout(productive ? null : input.timeoutMs === undefined ? 120_000 : input.timeoutMs);
87
+ if (timeoutMs !== null)
88
+ validateObservationDuration(timeoutMs);
89
+ if (!input.operationId.trim())
90
+ throw new AppError("operation_id_required", "Harness calls require a scoped operation id", 2);
91
+ // The current adapter transport uses this name; its identity is runtime-owned,
92
+ // never the operation or trusted daemon identity of the calling coordinator.
93
+ const env = { ...input.env, DD_EVAL_OPERATION_ID: input.operationId };
94
+ delete env.DD_FLOW_DAEMON_ID;
95
+ delete env.DD_FLOW_SCOPE_CONTROL_TOKEN;
96
+ delete env.DD_FLOW_RUNTIME_OWNER;
97
+ if (input.runtimeOwner) {
98
+ if (!path.isAbsolute(input.executable) || input.runtimeOwner.adapter_executable !== undefined && input.runtimeOwner.adapter_executable !== input.executable)
99
+ throw new AppError("harness_runtime_binding_conflict", "Owned harness call must use its frozen absolute adapter executable", 1);
100
+ env.DD_FLOW_RUNTIME_OWNER = JSON.stringify({ ...input.runtimeOwner, adapter_executable: input.executable });
101
+ }
6
102
  return new Promise((resolve, reject) => {
103
+ if (input.signal?.aborted)
104
+ return reject(new AppError("harness_adapter_aborted", "Harness adapter call was cancelled before dispatch", 1));
7
105
  const detached = process.platform !== "win32";
8
- const child = spawn(input.executable, input.args, { env: input.env, detached, stdio: ["ignore", "pipe", "pipe"] });
106
+ const child = spawn(input.executable, input.args, { env, ...(input.cwd ? { cwd: input.cwd } : {}), detached, stdio: ["ignore", "pipe", "pipe"] });
9
107
  let stdout = "";
10
108
  let stderr = "";
109
+ let outputTooLarge = false;
11
110
  let timedOut = false;
12
- let killTimer;
13
- const terminate = (signal) => { try {
14
- if (detached && child.pid)
15
- process.kill(-child.pid, signal);
16
- else
17
- child.kill(signal);
18
- }
19
- catch { /* The process already settled. */ } };
20
- const heartbeat = input.progress && input.progressMessage ? setInterval(() => input.progress?.(`${input.progressMessage}; waiting for adapter completion`), 15_000) : undefined;
21
- const timer = setTimeout(() => { timedOut = true; terminate("SIGTERM"); killTimer = setTimeout(() => terminate("SIGKILL"), 5_000); }, timeoutMs);
22
- const evidence = (value) => input.onEvidence?.({ executable: input.executable, args: input.args, ...value, at: new Date().toISOString() });
23
- child.stdout.setEncoding("utf8").on("data", (chunk) => { stdout += chunk; });
24
- child.stderr.setEncoding("utf8").on("data", (chunk) => { stderr += chunk; });
25
- child.once("error", (error) => { clearTimeout(timer); if (heartbeat)
26
- clearInterval(heartbeat); if (killTimer)
27
- clearTimeout(killTimer); evidence({ status: null, error: error.message }); reject(new AppError("merge_adapter_failed", "Harness adapter call failed", 1, { executable: input.executable, args: input.args.slice(0, 3), error: error.message })); });
28
- child.once("close", (status, signal) => {
29
- clearTimeout(timer);
111
+ let aborted = false;
112
+ let cleanup;
113
+ let pendingDiagnostic = "", droppingDiagnostic = false, progressCursor = 0;
114
+ let prettyDiagnostic = "", retainedError;
115
+ const phases = new Set();
116
+ let operationSequence = 0;
117
+ let diagnosticReads = Promise.resolve();
118
+ const clock = timeoutMs === null ? null : new ObservationClock({ timeoutMs });
119
+ clock?.sample(progressCursor);
120
+ let settled = false;
121
+ let callbackError;
122
+ let spawnError;
123
+ const stopTransport = () => {
124
+ cleanup ??= stopProcessGroup(child, 5_000, { detached }).catch(error => {
125
+ if (timer)
126
+ clearInterval(timer);
127
+ if (heartbeat)
128
+ clearInterval(heartbeat);
129
+ input.signal?.removeEventListener("abort", abort);
130
+ const details = { cleanup_error: errorRecord(error) };
131
+ if (callbackError instanceof Error) {
132
+ Object.assign(callbackError, { cleanup_error: details.cleanup_error, details: { ...callbackError.details, ...details } });
133
+ reject(callbackError);
134
+ }
135
+ else
136
+ reject(new AppError(outputTooLarge ? "harness_adapter_output_limit" : timedOut ? "harness_adapter_timeout" : aborted ? "harness_adapter_aborted" : "harness_adapter_cleanup_unconfirmed", "Adapter transport cleanup could not be confirmed", 1, details));
137
+ });
138
+ };
139
+ const deliver = (callback) => {
140
+ try {
141
+ const result = callback();
142
+ if (result && typeof result.then === "function") {
143
+ void Promise.resolve(result).catch(() => { });
144
+ throw new AppError("progress_callback_async", "Harness callbacks must be synchronous", 1);
145
+ }
146
+ }
147
+ catch (error) {
148
+ callbackError ??= error;
149
+ if (!settled)
150
+ stopTransport();
151
+ }
152
+ };
153
+ const heartbeat = input.progress && input.progressMessage ? setInterval(() => deliver(() => input.progress?.(`${input.progressMessage}; waiting for adapter completion`)), 15_000) : undefined;
154
+ let observing = false;
155
+ const timer = timeoutMs === null ? undefined : setInterval(async () => {
156
+ if (settled || cleanup)
157
+ return;
158
+ if (observing)
159
+ return;
160
+ observing = true;
161
+ // Helper phase progress must never replenish its owner's independent
162
+ // physical-settlement episode. ALS retains that existing budget here.
163
+ try {
164
+ recoveryRpcTimeout(timeoutMs);
165
+ }
166
+ catch (error) {
167
+ callbackError ??= error;
168
+ stopTransport();
169
+ observing = false;
170
+ return;
171
+ }
172
+ try {
173
+ const waiting = stateDir && await readOperationWaiting(stateDir, input.operationId, binding);
174
+ if (settled || cleanup)
175
+ return;
176
+ if (waiting)
177
+ suspendOperationClock(clock);
178
+ else if (clock.sample(progressCursor)) {
179
+ timedOut = true;
180
+ stopTransport();
181
+ }
182
+ }
183
+ catch (error) {
184
+ callbackError ??= error;
185
+ stopTransport();
186
+ }
187
+ finally {
188
+ observing = false;
189
+ }
190
+ }, Math.max(1, Math.min(1000, timeoutMs / 4)));
191
+ const abort = () => { aborted = true; stopTransport(); };
192
+ if (input.signal?.aborted)
193
+ abort();
194
+ else
195
+ input.signal?.addEventListener("abort", abort, { once: true });
196
+ const evidence = (value) => input.onEvidence?.({ operation_id: input.operationId, executable: input.executable, args: input.args, ...value, at: new Date().toISOString() });
197
+ const append = (current, chunk) => {
198
+ if (Buffer.byteLength(current) + Buffer.byteLength(chunk) > maxAdapterOutputBytes) {
199
+ outputTooLarge = true;
200
+ stopTransport();
201
+ return current;
202
+ }
203
+ return current + chunk;
204
+ };
205
+ const observeDiagnostic = (line) => {
206
+ if (prettyDiagnostic === null) {
207
+ if (line.trimEnd() === "}")
208
+ prettyDiagnostic = "";
209
+ return;
210
+ }
211
+ const candidate = prettyDiagnostic ? `${prettyDiagnostic}\n${line}` : line;
212
+ if (candidate.length > 64_000) {
213
+ prettyDiagnostic = null;
214
+ return;
215
+ }
216
+ let value;
217
+ try {
218
+ value = JSON.parse(candidate);
219
+ prettyDiagnostic = "";
220
+ }
221
+ catch {
222
+ prettyDiagnostic = prettyDiagnostic || line.trimStart().startsWith("{") ? candidate : "";
223
+ return;
224
+ }
225
+ if (!value || typeof value !== "object" || Array.isArray(value))
226
+ return;
227
+ if (value.kind === "operation_progress" && stateDir && value.operation_id === input.operationId) {
228
+ // The diagnostic is only a wake-up: durable, bound evidence owns liveness.
229
+ diagnosticReads = diagnosticReads.then(async () => {
230
+ const record = await readOperationProgress(stateDir, input.operationId, binding);
231
+ if (!record || record.sequence <= operationSequence)
232
+ return;
233
+ const requested = await readOperationRecord(stateDir, input.operationId, "requested.json");
234
+ if (!requested)
235
+ throw new AppError("operation_progress_invalid", "Operation request disappeared while observing progress", 1);
236
+ for (const key of ["daemon_id", "params_sha256", "generation"])
237
+ binding[key] ??= requested[key] ?? null;
238
+ operationSequence = record.sequence;
239
+ clock?.sample(++progressCursor, Date.parse(record.observed_at));
240
+ }).catch(error => { callbackError ??= error; if (!settled)
241
+ stopTransport(); });
242
+ return;
243
+ }
244
+ const cause = adapterCause(candidate);
245
+ if (typeof cause.code === "string")
246
+ retainedError = cause;
247
+ if (clock && !(stateDir && ["session.create", "session.fork", "session.start", "session.resume", "session.prompt"].includes(operation))
248
+ && ["native_hook_phase", "process_maintenance_phase"].includes(String(value.kind))
249
+ && typeof value.phase === "string" && /^[a-z_]{1,48}$/.test(value.phase)
250
+ && !["heartbeat", "poll", "elapsed", "wait", "waiting", "status"].includes(value.phase)) {
251
+ const marker = JSON.stringify([value.kind, value.phase, value.operation_id ?? value.process_id]);
252
+ if (!phases.has(marker)) {
253
+ phases.add(marker);
254
+ clock.sample(++progressCursor);
255
+ }
256
+ }
257
+ };
258
+ child.stdout.setEncoding("utf8").on("data", (chunk) => { stdout = append(stdout, chunk); });
259
+ // Diagnostics have no lifetime quota. Stream to the existing evidence sink
260
+ // and carry only a bounded tail in the disposable RPC receipt.
261
+ child.stderr.setEncoding("utf8").on("data", (chunk) => {
262
+ stderr = (stderr + chunk).slice(-64_000);
263
+ for (const piece of chunk.split(/(?<=\n)/)) {
264
+ if (!droppingDiagnostic)
265
+ pendingDiagnostic += piece;
266
+ if (pendingDiagnostic.length > 64_000) {
267
+ prettyDiagnostic = prettyDiagnostic || pendingDiagnostic.trimStart().startsWith("{") ? null : "";
268
+ pendingDiagnostic = "";
269
+ droppingDiagnostic = true;
270
+ }
271
+ if (!piece.endsWith("\n"))
272
+ continue;
273
+ if (!droppingDiagnostic)
274
+ observeDiagnostic(pendingDiagnostic);
275
+ pendingDiagnostic = "";
276
+ droppingDiagnostic = false;
277
+ }
278
+ if (input.onEvidence)
279
+ deliver(() => evidence({ type: "adapter_diagnostic", stderr: chunk }));
280
+ });
281
+ child.once("error", (error) => { spawnError = error; });
282
+ child.once("close", async (status, signal) => {
283
+ if (settled)
284
+ return;
285
+ settled = true;
286
+ if (timer)
287
+ clearInterval(timer);
288
+ input.signal?.removeEventListener("abort", abort);
30
289
  if (heartbeat)
31
290
  clearInterval(heartbeat);
32
- if (killTimer)
33
- clearTimeout(killTimer);
34
- evidence({ status, signal, timed_out: timedOut, stdout, stderr });
291
+ await cleanup;
292
+ if (!droppingDiagnostic && pendingDiagnostic)
293
+ observeDiagnostic(pendingDiagnostic);
294
+ await diagnosticReads;
295
+ const progressFailure = callbackError;
296
+ deliver(() => evidence({ status, signal, timed_out: timedOut, aborted, stdout, stderr, ...(spawnError ? { error: spawnError.message } : {}) }));
297
+ if (spawnError)
298
+ return reject(new AppError("harness_adapter_failed", "Harness adapter call failed", 1, { cause: adapterCause(spawnError.message), ...(callbackError ? { evidence_error: String(callbackError) } : {}) }));
299
+ if (outputTooLarge)
300
+ return reject(new AppError("harness_adapter_output_limit", "Harness adapter exceeded the bounded reply limit", 1, { executable: input.executable, args: input.args.slice(0, 3), max_output_bytes: maxAdapterOutputBytes }));
35
301
  if (timedOut)
36
- return reject(new AppError("merge_adapter_timeout", "Harness adapter call timed out", 1, { executable: input.executable, args: input.args.slice(0, 3), timeout_ms: timeoutMs }));
302
+ return reject(Object.assign(new AppError("harness_adapter_timeout", "Harness adapter call timed out", 1, { executable: input.executable, args: input.args.slice(0, 3), timeout_ms: timeoutMs }), { observationClock: clock }));
303
+ if (aborted)
304
+ return reject(new AppError("harness_adapter_aborted", "Harness adapter call was cancelled", 1, { executable: input.executable, args: input.args.slice(0, 3) }));
37
305
  if (status !== 0)
38
- return reject(new AppError("merge_adapter_failed", "Harness adapter call failed", 1, { executable: input.executable, args: input.args.slice(0, 3), status, signal, stderr }));
306
+ return reject(progressFailure ?? new AppError("harness_adapter_failed", "Harness adapter call failed", 1, { executable: input.executable, args: input.args.slice(0, 3), status, signal, stderr, cause: adapterCause(stdout, stderr, retainedError ? JSON.stringify(retainedError) : ""), ...(callbackError ? { evidence_error: String(callbackError) } : {}) }));
307
+ if (callbackError)
308
+ return reject(callbackError);
39
309
  try {
40
- resolve(JSON.parse(stdout));
310
+ const reply = JSON.parse(stdout);
311
+ if (!reply || typeof reply !== "object" || Array.isArray(reply))
312
+ throw new Error("not object");
313
+ const value = reply;
314
+ if (value.ok === false)
315
+ return reject(new AppError("harness_adapter_failed", "Harness adapter reported a runtime failure", 1, { cause: adapterCause(stdout) }));
316
+ resolve(reply);
41
317
  }
42
318
  catch {
43
- reject(new AppError("merge_adapter_invalid", "Harness adapter returned invalid JSON", 1, { executable: input.executable, stdout }));
319
+ reject(new AppError("harness_adapter_invalid", "Harness adapter returned one invalid JSON object", 1, { executable: input.executable }));
44
320
  }
45
321
  });
46
322
  });
47
323
  }
324
+ function adapterCause(...outputs) {
325
+ for (const output of outputs)
326
+ for (const line of [output.trim(), ...output.trim().split("\n").reverse()]) {
327
+ try {
328
+ const value = JSON.parse(line);
329
+ const nested = value.error && typeof value.error === "object" && !Array.isArray(value.error) ? value.error : null;
330
+ const code = typeof nested?.code === "string" ? nested.code : typeof value.code === "string" ? value.code : null;
331
+ const message = typeof nested?.message === "string" ? nested.message : typeof value.error === "string" ? value.error : typeof value.message === "string" ? value.message : code;
332
+ if (code)
333
+ return errorRecord({ ...(nested ?? value), code, message });
334
+ }
335
+ catch { /* Adapter diagnostics need not be JSON. */ }
336
+ }
337
+ return { message: outputs.map(output => output.trim()).filter(Boolean).join("\n") || "adapter process failed" };
338
+ }
48
339
  export function adapterSessionId(value) {
49
340
  if (!value || typeof value !== "object")
50
341
  return null;
51
- for (const [key, item] of Object.entries(value)) {
52
- if (["session_id", "sessionId", "provider_session_id", "providerSessionId", "adapter_session_id", "adapterSessionId", "thread_id", "threadId", "conversation_id"].includes(key) && typeof item === "string")
53
- return item;
54
- const nested = adapterSessionId(item);
55
- if (nested)
56
- return nested;
57
- }
342
+ const reply = value;
343
+ for (const key of ["provider_session_id", "adapter_session_id", "session_id"])
344
+ if (typeof reply[key] === "string" && reply[key].trim())
345
+ return reply[key];
346
+ const session = reply.session;
347
+ if (session && typeof session === "object" && !Array.isArray(session))
348
+ for (const key of ["provider_session_id", "session_id"])
349
+ if (typeof session[key] === "string" && String(session[key]).trim())
350
+ return session[key];
58
351
  return null;
59
352
  }
@@ -10,6 +10,7 @@ export function harnessKey(value) {
10
10
  zcode: "zcode-acp", "zcode-acp": "zcode-acp",
11
11
  grok: "grok-acp", "grok-acp": "grok-acp",
12
12
  agy: "antigravity-cli", "antigravity-cli": "antigravity-cli",
13
+ droid: "droid-cli", "droid-cli": "droid-cli",
13
14
  opencode: "opencode-server", "opencode-server": "opencode-server"
14
15
  };
15
16
  const resolved = keys[value];
@@ -60,6 +61,7 @@ export function harnessRuntimeArguments(ddFlowHome, harness) {
60
61
  "zcode-acp": "--zcode-acp-bin",
61
62
  "grok-acp": "--grok-bin",
62
63
  "antigravity-cli": "--agy-bin",
64
+ "droid-cli": "--droid-bin",
63
65
  "opencode-server": "--opencode-bin"
64
66
  };
65
67
  return [option[key], runtime];