@deksden-com/dd-flow-cli 0.9.0-beta.137 → 0.9.0-beta.138

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 (260) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +9 -0
  3. package/dist/build-info.json +6 -6
  4. package/dist/cli/argument-values.js +5 -5
  5. package/dist/cli/command-inputs.js +89 -20
  6. package/dist/cli/command-metadata.js +197 -0
  7. package/dist/cli/command-option-descriptions.js +191 -0
  8. package/dist/cli/completion-cli.js +70 -0
  9. package/dist/cli/completion.js +57 -0
  10. package/dist/cli/error-output.js +52 -0
  11. package/dist/cli/executor-onboarding.js +76 -0
  12. package/dist/cli/flows.js +29 -0
  13. package/dist/cli/harnesses.js +84 -0
  14. package/dist/cli/help.js +240 -6
  15. package/dist/cli/input-preparation.js +54 -5
  16. package/dist/cli/memory-flow-admission.js +44 -0
  17. package/dist/cli/memory-init-invocation.js +78 -0
  18. package/dist/cli/memory-init.js +240 -0
  19. package/dist/cli/memory-storage-admission.js +53 -0
  20. package/dist/cli/models.js +39 -0
  21. package/dist/cli/native-hook-ingress.js +1 -1
  22. package/dist/cli/operator-profiles.js +61 -0
  23. package/dist/cli/output-options.js +35 -0
  24. package/dist/cli/response-file.js +71 -0
  25. package/dist/cli/run-cli.js +283 -160
  26. package/dist/cli/setup.js +149 -0
  27. package/dist/cli.js +54 -2
  28. package/dist/domain/stage-handler-identities.js +20 -0
  29. package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
  30. package/dist/harness-runtime/lib/daemon-operations.mjs +4 -3
  31. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +4 -2
  32. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +11 -5
  33. package/dist/harness-runtime/lib/dd-codex.mjs +22 -10
  34. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +2 -1
  35. package/dist/harness-runtime/lib/dd-droid.mjs +2 -1
  36. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +4 -2
  37. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +6 -4
  38. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +9 -9
  39. package/dist/harness-runtime/lib/delegation-instructions.mjs +5 -3
  40. package/dist/harness-runtime/lib/lifecycle-command.mjs +1 -0
  41. package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -0
  42. package/dist/harness-runtime/lib/managed-daemon.mjs +50 -15
  43. package/dist/harness-runtime/lib/managed-flow-env.mjs +15 -0
  44. package/dist/harness-runtime/lib/native-hook-command.mjs +1 -1
  45. package/dist/harness-runtime/lib/native-request-proof.d.mts +2 -0
  46. package/dist/harness-runtime/lib/native-request-proof.mjs +65 -0
  47. package/dist/harness-runtime/lib/opencode-plugin.mjs +2 -0
  48. package/dist/harness-runtime/lib/operation-progress.mjs +3 -1
  49. package/dist/runtime/context.js +1 -1
  50. package/dist/schemas/discovery-result.schema.json +184 -0
  51. package/dist/schemas/execution-bundle.schema.json +205 -0
  52. package/dist/schemas/execution-settings.schema.json +162 -0
  53. package/dist/schemas/flow-descriptor.schema.json +54 -0
  54. package/dist/schemas/flow-run.schema.json +22 -0
  55. package/dist/schemas/harness-catalog.schema.json +80 -0
  56. package/dist/schemas/mb-init-compose-result.schema.json +457 -0
  57. package/dist/schemas/mb-init-discover-result.schema.json +500 -0
  58. package/dist/schemas/mb-init-finish-result.schema.json +422 -0
  59. package/dist/schemas/mb-init-preflight-result.schema.json +367 -0
  60. package/dist/schemas/mb-init-review-result.schema.json +396 -0
  61. package/dist/schemas/mb-init-verify-result.schema.json +391 -0
  62. package/dist/schemas/memory-bootstrap-receipt-v2.schema.json +67 -0
  63. package/dist/schemas/memory-bootstrap-receipt.schema.json +354 -0
  64. package/dist/schemas/memory-composition-work-result.schema.json +38 -0
  65. package/dist/schemas/memory-repair-work-result.schema.json +49 -0
  66. package/dist/schemas/memory-review-correction-plan.schema.json +30 -0
  67. package/dist/schemas/memory-review-work-result.schema.json +43 -0
  68. package/dist/schemas/model-catalog.schema.json +153 -0
  69. package/dist/schemas/profile-settings.schema.json +122 -0
  70. package/dist/schemas/release-manifest.schema.json +291 -0
  71. package/dist/schemas/run-engine-binding.schema.json +1 -1
  72. package/dist/services/check-runner-evidence.js +51 -4
  73. package/dist/services/cli-operation-classifier.js +8 -47
  74. package/dist/services/cli-upgrade-allowlist.js +27 -0
  75. package/dist/services/code-checks.js +387 -34
  76. package/dist/services/controller-execution.js +88 -0
  77. package/dist/services/controller-fanout.js +71 -12
  78. package/dist/services/dashboard.js +8 -2
  79. package/dist/services/descriptor-coordinator-session.js +35 -0
  80. package/dist/services/descriptor-root-work.js +53 -0
  81. package/dist/services/descriptor-run-settlement.js +105 -0
  82. package/dist/services/descriptor-run-start.js +449 -0
  83. package/dist/services/descriptor-stage-coordinator.js +197 -0
  84. package/dist/services/descriptor-stage-settlement.js +222 -0
  85. package/dist/services/descriptor-stage-start.js +139 -0
  86. package/dist/services/discovery/agy-discovery.js +2 -0
  87. package/dist/services/discovery/codex-discovery.js +2 -0
  88. package/dist/services/discovery/droid-discovery.js +2 -0
  89. package/dist/services/discovery/grok-discovery.js +2 -0
  90. package/dist/services/discovery/opencode-discovery.js +2 -0
  91. package/dist/services/discovery/static.js +31 -0
  92. package/dist/services/discovery/zcode-discovery.js +2 -0
  93. package/dist/services/engines.js +39 -22
  94. package/dist/services/eval-snapshots.js +83 -25
  95. package/dist/services/execution-bundles.js +263 -0
  96. package/dist/services/execution-policy.js +20 -16
  97. package/dist/services/executor-onboarding.js +56 -0
  98. package/dist/services/external-work-launch.js +91 -11
  99. package/dist/services/flow-catalog.js +21 -0
  100. package/dist/services/flow-descriptors.js +82 -0
  101. package/dist/services/frozen-engine.js +242 -0
  102. package/dist/services/generic-execution.js +164 -0
  103. package/dist/services/harness-adapter.js +28 -2
  104. package/dist/services/harness-discovery.js +29 -0
  105. package/dist/services/harness-doctor.js +75 -0
  106. package/dist/services/harness-probe-effects.js +51 -0
  107. package/dist/services/harness-version-probe.js +100 -0
  108. package/dist/services/hooks.js +24 -6
  109. package/dist/services/hybrid-contracts.js +483 -0
  110. package/dist/services/ids.js +29 -1
  111. package/dist/services/installation-lock.js +95 -0
  112. package/dist/services/installed-flow-source.js +183 -0
  113. package/dist/services/lifecycle-invocations.js +257 -78
  114. package/dist/services/memory-accepted-composition.js +77 -0
  115. package/dist/services/memory-baseline-preservation.js +65 -0
  116. package/dist/services/memory-completed-correction-wave.js +320 -0
  117. package/dist/services/memory-compose-handler.js +258 -0
  118. package/dist/services/memory-composition-acceptance.js +199 -0
  119. package/dist/services/memory-composition-graph.js +134 -0
  120. package/dist/services/memory-composition-registration.js +146 -0
  121. package/dist/services/memory-composition-work-custody.js +151 -0
  122. package/dist/services/memory-composition-work-results.js +96 -0
  123. package/dist/services/memory-corrected-review-acceptance.js +129 -0
  124. package/dist/services/memory-corrected-review-batch.js +30 -0
  125. package/dist/services/memory-corrected-review-findings.js +106 -0
  126. package/dist/services/memory-corrected-review-inputs.js +210 -0
  127. package/dist/services/memory-corrected-review-projection.js +78 -0
  128. package/dist/services/memory-corrected-review-receipt.js +170 -0
  129. package/dist/services/memory-corrected-review-registration.js +191 -0
  130. package/dist/services/memory-correction-check-receipt.js +219 -0
  131. package/dist/services/memory-correction-input-owner.js +6 -0
  132. package/dist/services/memory-discover-handler.js +104 -0
  133. package/dist/services/memory-finish-evidence.js +435 -0
  134. package/dist/services/memory-finish-handler.js +215 -0
  135. package/dist/services/memory-finish-integration.js +280 -0
  136. package/dist/services/memory-finish-readiness.js +122 -0
  137. package/dist/services/memory-git-integration.js +151 -0
  138. package/dist/services/memory-historical-corrected-review.js +206 -0
  139. package/dist/services/memory-historical-original-review.js +130 -0
  140. package/dist/services/memory-historical-repair-registration.js +99 -0
  141. package/dist/services/memory-historical-second-review.js +128 -0
  142. package/dist/services/memory-init-bootstrap-readiness.js +210 -0
  143. package/dist/services/memory-init-contexts.js +89 -0
  144. package/dist/services/memory-init-controller-start.js +84 -0
  145. package/dist/services/memory-init-engine-release.js +223 -0
  146. package/dist/services/memory-init-git-policy.js +136 -0
  147. package/dist/services/memory-init-lint-capability.js +185 -0
  148. package/dist/services/memory-init-native-capacity.js +241 -0
  149. package/dist/services/memory-init-preflight.js +312 -0
  150. package/dist/services/memory-init-project-bootstrap.js +599 -0
  151. package/dist/services/memory-init-request-candidate.js +157 -0
  152. package/dist/services/memory-init-request-input.js +66 -0
  153. package/dist/services/memory-init-resource-storage.js +125 -0
  154. package/dist/services/memory-init-run-allocation.js +81 -0
  155. package/dist/services/memory-init-run-bundle.js +98 -0
  156. package/dist/services/memory-init-start.js +175 -0
  157. package/dist/services/memory-init-startup-custody.js +80 -0
  158. package/dist/services/memory-init-startup-input.js +240 -0
  159. package/dist/services/memory-init-storage.js +81 -0
  160. package/dist/services/memory-init-stores.js +359 -0
  161. package/dist/services/memory-init-worktrunk-capability.js +172 -0
  162. package/dist/services/memory-layer-publication.js +611 -0
  163. package/dist/services/memory-layer.js +156 -0
  164. package/dist/services/memory-lint-config.js +85 -0
  165. package/dist/services/memory-native-review-copy.js +165 -0
  166. package/dist/services/memory-normal-flow-entry.js +75 -0
  167. package/dist/services/memory-ordinary-defaults.js +112 -0
  168. package/dist/services/memory-preflight-bootstrap-custody.js +115 -0
  169. package/dist/services/memory-preflight-handler.js +259 -0
  170. package/dist/services/memory-repair-current-view.js +299 -0
  171. package/dist/services/memory-repair-document-delta.js +451 -0
  172. package/dist/services/memory-repair-history.js +491 -0
  173. package/dist/services/memory-repair-physical-binding.js +312 -0
  174. package/dist/services/memory-repair-publication-files.js +152 -0
  175. package/dist/services/memory-repair-publication-intent.js +27 -0
  176. package/dist/services/memory-repair-publication-plan.js +160 -0
  177. package/dist/services/memory-repair-publication-source.js +225 -0
  178. package/dist/services/memory-repair-publication.js +794 -0
  179. package/dist/services/memory-repair-registration.js +239 -0
  180. package/dist/services/memory-repair-work-baseline.js +413 -0
  181. package/dist/services/memory-repair-work-completion.js +97 -0
  182. package/dist/services/memory-repair-work-custody.js +73 -0
  183. package/dist/services/memory-repair-work-results.js +89 -0
  184. package/dist/services/memory-repair-workspace.js +252 -0
  185. package/dist/services/memory-repair-writer-copy.js +735 -0
  186. package/dist/services/memory-result-binding.js +173 -0
  187. package/dist/services/memory-retained-review-authority.js +129 -0
  188. package/dist/services/memory-retained-review-results.js +70 -0
  189. package/dist/services/memory-retained-second-baseline.js +102 -0
  190. package/dist/services/memory-retained-second-repairs.js +156 -0
  191. package/dist/services/memory-review-acceptance.js +214 -0
  192. package/dist/services/memory-review-correction-plan.js +171 -0
  193. package/dist/services/memory-review-handler.js +585 -0
  194. package/dist/services/memory-review-inputs.js +200 -0
  195. package/dist/services/memory-review-receipt.js +180 -0
  196. package/dist/services/memory-review-registration.js +108 -0
  197. package/dist/services/memory-review-work-custody.js +124 -0
  198. package/dist/services/memory-review-work-input.js +72 -0
  199. package/dist/services/memory-review-work-results.js +73 -0
  200. package/dist/services/memory-second-corrected-review-inputs.js +165 -0
  201. package/dist/services/memory-second-correction-plan.js +76 -0
  202. package/dist/services/memory-second-repair-baseline.js +91 -0
  203. package/dist/services/memory-second-repair-launch.js +614 -0
  204. package/dist/services/memory-second-repair-registration.js +88 -0
  205. package/dist/services/memory-second-repair-source.js +187 -0
  206. package/dist/services/memory-second-review-acceptance.js +115 -0
  207. package/dist/services/memory-source-observations.js +117 -0
  208. package/dist/services/memory-stage-dispatch.js +28 -0
  209. package/dist/services/memory-stage-results.js +199 -0
  210. package/dist/services/memory-verification-acceptance.js +135 -0
  211. package/dist/services/memory-verification-checks.js +252 -0
  212. package/dist/services/memory-verification-receipt.js +159 -0
  213. package/dist/services/memory-verify-handler.js +135 -0
  214. package/dist/services/memory-work-result-preparation.js +63 -0
  215. package/dist/services/model-catalog.js +109 -0
  216. package/dist/services/model-discovery.js +46 -0
  217. package/dist/services/operator-profiles.js +236 -0
  218. package/dist/services/projects.js +17 -2
  219. package/dist/services/recovery-snapshot-database.js +416 -1
  220. package/dist/services/release-archive-producer.js +52 -0
  221. package/dist/services/release-archive.js +143 -0
  222. package/dist/services/release-cache.js +142 -0
  223. package/dist/services/release-compatibility.js +18 -0
  224. package/dist/services/release-download.js +67 -0
  225. package/dist/services/release-envelope.js +104 -0
  226. package/dist/services/release-manifest.js +45 -0
  227. package/dist/services/release-payload.js +114 -0
  228. package/dist/services/release-producer.js +63 -0
  229. package/dist/services/release-provenance.js +160 -0
  230. package/dist/services/release-registry.js +66 -0
  231. package/dist/services/release-source.js +194 -0
  232. package/dist/services/repair-intents.js +1 -1
  233. package/dist/services/review-copy.js +10 -2
  234. package/dist/services/run-control.js +80 -9
  235. package/dist/services/run-controller-capture.js +10 -3
  236. package/dist/services/run-controller-process.js +21 -0
  237. package/dist/services/run-controller-recovery.js +3 -1
  238. package/dist/services/run-controller-state.js +1 -1
  239. package/dist/services/run-controller.js +101 -40
  240. package/dist/services/run-recovery-guard.js +2 -0
  241. package/dist/services/run-recovery.js +145 -5
  242. package/dist/services/run-start-custody.js +177 -0
  243. package/dist/services/runs.js +319 -25
  244. package/dist/services/setup.js +377 -0
  245. package/dist/services/stage-context.js +9 -1
  246. package/dist/services/stage-handlers.js +74 -0
  247. package/dist/services/stage-report-renderer.js +63 -0
  248. package/dist/services/stage-work-graph.js +17 -0
  249. package/dist/services/vnext-code.js +3 -3
  250. package/dist/services/vnext-execution-profile.js +2 -2
  251. package/dist/services/vnext-fanout.js +70 -9
  252. package/dist/services/vnext-merge.js +8 -3
  253. package/dist/services/vnext-plan-review.js +6 -4
  254. package/dist/services/work-registry.js +616 -71
  255. package/dist/services/workspace-files.js +1 -1
  256. package/dist/storage/database.js +84 -6
  257. package/dist/storage/writer-contract.js +11 -7
  258. package/package.json +13 -3
  259. package/tools/create-canon-release.mjs +33 -0
  260. package/tools/produce-hybrid-release.mjs +41 -0
@@ -0,0 +1,149 @@
1
+ import { prepareCommandRoute } from "./command-inputs.js";
2
+ import path from "node:path";
3
+ import { parseOutputOptions } from "./output-options.js";
4
+ import { resolveDdFlowHome } from "../storage/paths.js";
5
+ import { readSetupStatus, installAuthenticatedRelease, readRetainedSetupExecutor, hasDeferredSetupOnboarding, readSetupOnboarding, commitSetupOnboarding } from "../services/setup.js";
6
+ import { readTrustedReleaseProducers, readReleaseBundle } from "../services/release-source.js";
7
+ import { createReleaseCompatibility, installedCatalogVersions } from "../services/release-compatibility.js";
8
+ import { getCliVersionReport } from "../services/build-info.js";
9
+ import { readOperatorProfiles, readProfileCreationIntent } from "../services/operator-profiles.js";
10
+ import { onboardExecutor, selectOnboardingExecutor } from "../services/executor-onboarding.js";
11
+ import { askExecutorChoice } from "./executor-onboarding.js";
12
+ import { requiredOption, optionalOption } from "./argument-values.js";
13
+ import { AppError, errorRecord } from "../shared/errors.js";
14
+ import { hybridDigest } from "../services/hybrid-contracts.js";
15
+ import { prepareResponseDestination, writeResponseFile } from "./response-file.js";
16
+ import { readRegistryRelease } from "../services/release-registry.js";
17
+ import { writePreparationError } from "./error-output.js";
18
+ import { publishSetup } from "../harness-runtime/lib/setup-progress.mjs";
19
+ /** Lightweight global diagnostic ingress; never creates a runtime context. */
20
+ export async function runSetupCli(args, env, io = { stdout: process.stdout, stderr: process.stderr, stdin: process.stdin }) {
21
+ let json = false;
22
+ try {
23
+ const output = parseOutputOptions(args);
24
+ json = output.json;
25
+ if (output.responseFile)
26
+ prepareResponseDestination(output.responseFile);
27
+ const route = prepareCommandRoute(output.args);
28
+ const humanProgress = !output.json && !output.progressJsonl;
29
+ const reportHumanProgress = (message) => {
30
+ // Diagnostic delivery cannot turn a completed installation into failure.
31
+ try {
32
+ io.stderr.write(`setup: ${message}\n`);
33
+ }
34
+ catch { /* Best-effort human progress only. */ }
35
+ };
36
+ const home = resolveDdFlowHome(env);
37
+ let result;
38
+ if (route.key === "setup status")
39
+ result = { ok: true, action: "setup_status", observed_cli: getCliVersionReport().cli, ...readSetupStatus(home) };
40
+ else if (route.key === "setup" || route.key === "setup update") {
41
+ const parsed = route.parsed;
42
+ const source = optionalOption(parsed, "canon-source"), version = optionalOption(parsed, "canon-version");
43
+ const registryFile = optionalOption(parsed, "release-registry");
44
+ if (source && (version || registryFile))
45
+ throw new AppError("usage", "--canon-source is exclusive with --canon-version and --release-registry; see dd-flow setup --help", 2, { effect: "no_effect" });
46
+ const channel = (optionalOption(parsed, "channel") ?? "stable");
47
+ const selection = source ? null : readRegistryRelease(registryFile ?? env.DD_FLOW_RELEASE_REGISTRY ?? path.join(home, "release-registry.json"), channel, version);
48
+ const profile = optionalOption(parsed, "profile"), skip = parsed.options.has("skip-agent-setup");
49
+ const intentFile = optionalOption(parsed, "save-profile-intent");
50
+ if (intentFile && skip)
51
+ throw new AppError("usage", "--save-profile-intent conflicts with --skip-agent-setup", 2, { effect: "no_effect" });
52
+ let profileCreation = intentFile ? readProfileCreationIntent(intentFile) : undefined;
53
+ const stdin = io.stdin ?? process.stdin;
54
+ const nonInteractive = parsed.options.has("non-interactive") || output.json || output.progressJsonl
55
+ || !stdin.isTTY || !io.stderr.isTTY;
56
+ const profileFile = optionalOption(parsed, "profile-file");
57
+ const requestId = requiredOption(parsed, "request-id");
58
+ const retained = profile === undefined && profileFile === undefined && !intentFile && !skip ? readRetainedSetupExecutor(home, requestId) : null;
59
+ if (retained?.profileCreation)
60
+ profileCreation = retained.profileCreation;
61
+ const choice = { ...(profile !== undefined ? { profileId: profile } : {}),
62
+ ...(profileFile !== undefined ? { profileFile } : {}),
63
+ ...(retained?.executor ? { profile: retained.executor } : profileCreation && profile === undefined && profileFile === undefined ? { profile: profileCreation.profile } : {}),
64
+ skip: skip || retained !== null && retained.executor === null };
65
+ let deferred = hasDeferredSetupOnboarding(home, requestId);
66
+ let selected;
67
+ if (!deferred) {
68
+ try {
69
+ selected = selectOnboardingExecutor(choice, () => readOperatorProfiles(home), { allowSkip: true });
70
+ }
71
+ catch (error) {
72
+ if (!(error instanceof AppError) || error.code !== "agent_profile_missing" || nonInteractive
73
+ || profile !== undefined || profileFile !== undefined || intentFile || skip)
74
+ throw error;
75
+ deferred = true;
76
+ }
77
+ }
78
+ else if (profile !== undefined || profileFile !== undefined || intentFile || skip) {
79
+ // Explicit continuation inputs still fail before downloading or replaying.
80
+ selected = selectOnboardingExecutor(choice, () => readOperatorProfiles(home), { allowSkip: true });
81
+ }
82
+ const observedCli = getCliVersionReport().cli;
83
+ const compatibility = createReleaseCompatibility({ channel,
84
+ catalogVersions: installedCatalogVersions, routerVersion: observedCli.version });
85
+ const producers = readTrustedReleaseProducers(requiredOption(parsed, "trust-file"));
86
+ const bundle = await readReleaseBundle(source ?? selection.envelope_url, producers, compatibility, {
87
+ ...(selection ? { expectedVersion: selection.version } : {}),
88
+ ...((output.progressJsonl || humanProgress) ? { onProgress: (progress) => {
89
+ if (output.progressJsonl)
90
+ io.stdout.write(`${JSON.stringify({ type: "setup_download_progress", request_id: requestId, ...progress })}\n`);
91
+ else if (progress.received_bytes === progress.expected_bytes)
92
+ reportHumanProgress(`${progress.artifact} archive received (${progress.received_bytes} bytes); integrity validation follows`);
93
+ } } : {})
94
+ });
95
+ if (selection && bundle.authenticated.manifest.version !== selection.version)
96
+ throw new AppError("release_registry", "Authenticated release differs from selected registry version", 1, { reason: "selected_version_mismatch", phase: "release_selection", effect: "no_effect", help: "setup" });
97
+ const installed = installAuthenticatedRelease(home, { requestId, action: route.key === "setup update" ? "update" : "install",
98
+ envelope: bundle.envelope, trustedProducers: producers, compatibility, canonArchive: bundle.canonArchive, engineArchive: bundle.engineArchive,
99
+ ...(deferred ? { deferredOnboarding: true } : { executor: selected.profile, ...(profileCreation ? { profileCreation } : {}) }),
100
+ onCheckpoint: phase => {
101
+ publishSetup("dependency_check_completed", `release-setup:${phase}`);
102
+ if (output.progressJsonl)
103
+ io.stdout.write(`${JSON.stringify({ type: "setup_progress", request_id: requestId, phase })}\n`);
104
+ else if (humanProgress)
105
+ reportHumanProgress({ prepared: "verified release request retained", canon_cached: "canon cache verified", engine_cached: "engine cache verified",
106
+ profiles_saved: "profile settings saved", committed: "installation committed; runtime qualification remains unknown" }[phase]);
107
+ } });
108
+ if (humanProgress && installed.replayed)
109
+ reportHumanProgress("exact request replay verified; retained selection was not reactivated");
110
+ if (deferred) {
111
+ const saved = readSetupOnboarding(home, requestId);
112
+ const explicit = profile !== undefined || profileFile !== undefined || intentFile !== undefined || skip;
113
+ const deferredChoice = saved && !explicit ? { ...(saved.executor ? { profile: saved.executor } : { skip: true }),
114
+ ...(saved.profileCreation ? { profileCreation: saved.profileCreation } : {}) } : choice;
115
+ try {
116
+ selected ??= await onboardExecutor(deferredChoice, () => readOperatorProfiles(home), { allowSkip: true, nonInteractive }, settings => askExecutorChoice(settings, { stdin, stderr: io.stderr }));
117
+ const consent = commitSetupOnboarding(home, requestId, { executor: selected.profile,
118
+ ...(profileCreation ?? selected.profileCreation ? { profileCreation: profileCreation ?? selected.profileCreation } : {}) });
119
+ installed.executor = consent.executor;
120
+ if (humanProgress)
121
+ reportHumanProgress("executor onboarding committed after installation");
122
+ }
123
+ catch (error) {
124
+ if (error instanceof AppError) {
125
+ error.details.installation = "committed";
126
+ error.details.request_id = requestId;
127
+ error.details.next_action = "Replay the same setup request to resume executor onboarding; installation remains retained";
128
+ }
129
+ throw error;
130
+ }
131
+ }
132
+ result = { ...installed, action: route.key === "setup update" ? "setup_update" : "setup_install", observed_cli: observedCli,
133
+ executor: installed.executor ? { profile_id: installed.executor.id, revision: hybridDigest(installed.executor), readiness: "unknown_requires_qualification" } : { profile_id: null, readiness: "not_configured" },
134
+ ...readSetupStatus(home) };
135
+ }
136
+ else
137
+ throw new AppError("usage", "Unknown setup command; see dd-flow setup --help", 2, { effect: "no_effect" });
138
+ io.stdout.write(`${JSON.stringify(writeResponseFile(result, output), null, json ? undefined : 2)}\n`);
139
+ return 0;
140
+ }
141
+ catch (error) {
142
+ const record = errorRecord(error);
143
+ writePreparationError(io, args, record);
144
+ if (!json && error instanceof AppError && error.details.installation === "committed") {
145
+ io.stderr.write("setup: installation remains committed; replay the same request to resume executor onboarding\n");
146
+ }
147
+ return record.exitCode;
148
+ }
149
+ }
package/dist/cli.js CHANGED
@@ -1,14 +1,66 @@
1
1
  #!/usr/bin/env node
2
2
  import { isNativeHookIngress } from "./cli/native-hook-ingress.js";
3
3
  import { isRuntimeMaintenanceIngress, runRuntimeMaintenanceIngress } from "./cli/runtime-maintenance-ingress.js";
4
+ import { resolveHelp } from "./cli/help.js";
5
+ import { errorRecord } from "./shared/errors.js";
6
+ import { writePreparationError } from "./cli/error-output.js";
4
7
  import { publishSetup } from './harness-runtime/lib/setup-progress.mjs';
5
8
  const args = process.argv.slice(2);
6
- const managedHook = Boolean(process.env.DD_FLOW_CODEX_STATE_DIR) && args.slice(0, 3).join(" ") === "codex hook handle" && !args.includes("--help") && !args.includes("-h");
9
+ let help;
10
+ try {
11
+ help = resolveHelp(args);
12
+ }
13
+ catch (error) {
14
+ const record = errorRecord(error);
15
+ writePreparationError({ stderr: process.stderr }, args, record);
16
+ process.exit(record.exitCode);
17
+ }
18
+ if (help) {
19
+ process.stdout.write(`${help.json ? JSON.stringify(help.data) : help.text}\n`);
20
+ process.exit(0);
21
+ }
22
+ if (args[0] === "completion") {
23
+ const { runCompletionCli } = await import("./cli/completion-cli.js");
24
+ process.exit(runCompletionCli(args));
25
+ }
26
+ if (args[0] === "profiles") {
27
+ const { runOperatorProfilesCli } = await import("./cli/operator-profiles.js");
28
+ process.exit(runOperatorProfilesCli(args, process.env));
29
+ }
30
+ if (args[0] === "setup") {
31
+ const { runSetupCli } = await import("./cli/setup.js");
32
+ process.exit(await runSetupCli(args, process.env));
33
+ }
34
+ if (args[0] === "harnesses") {
35
+ const { runHarnessesCli } = await import("./cli/harnesses.js");
36
+ process.exit(await runHarnessesCli(args, process.env));
37
+ }
38
+ if (args[0] === "models") {
39
+ const { runModelsCli } = await import("./cli/models.js");
40
+ process.exit(runModelsCli(args));
41
+ }
42
+ if (args[0] === "flow" && ["list", "show"].includes(args[1] ?? "")) {
43
+ const { runFlowCatalogCli } = await import("./cli/flows.js");
44
+ process.exit(runFlowCatalogCli(args));
45
+ }
46
+ if (args[0] === "init" || args[0] === "flow" && args[1] === "mb-init") {
47
+ const { runMemoryInitCli } = await import("./cli/memory-init.js");
48
+ process.exit(await runMemoryInitCli(args, process.env));
49
+ }
50
+ if (args.slice(0, 3).join(" ") === "runtime storage admit") {
51
+ const { runMemoryStorageAdmissionCli } = await import("./cli/memory-storage-admission.js");
52
+ process.exit(runMemoryStorageAdmissionCli(args));
53
+ }
54
+ if (args.slice(0, 3).join(" ") === "runtime flow admit") {
55
+ const { runMemoryFlowAdmissionCli } = await import("./cli/memory-flow-admission.js");
56
+ process.exit(await runMemoryFlowAdmissionCli(args));
57
+ }
58
+ const managedHook = Boolean(process.env.DD_FLOW_CODEX_STATE_DIR) && args.slice(0, 3).join(" ") === "codex hook handle";
7
59
  const exitCode = isRuntimeMaintenanceIngress(args, process.env)
8
60
  ? await runRuntimeMaintenanceIngress(args)
9
61
  : managedHook
10
62
  ? await (await import("./cli/hook-ingress.js")).runHookIngress(args)
11
- : isNativeHookIngress(args) && !args.includes("--help") && !args.includes("-h")
63
+ : isNativeHookIngress(args)
12
64
  ? await (await import("./cli/native-hook-ingress.js")).runNativeHookIngress(args)
13
65
  : await (async () => {
14
66
  publishSetup('ingress_entered');
@@ -0,0 +1,20 @@
1
+ /** Engine-owned identities shared by inspection and executable registration.
2
+ * Keep this module dependency-free: catalog inspection must not load handlers.
3
+ */
4
+ export const sdlcHandlerIdentities = Object.freeze({
5
+ specify: Object.freeze({ stage: "specify", entryId: "sdlc-specify-entry", finishId: "sdlc-specify-finish" }),
6
+ protocolize: Object.freeze({ stage: "protocolize", entryId: "sdlc-protocolize-entry", finishId: "sdlc-protocolize-finish" }),
7
+ plan: Object.freeze({ stage: "plan", entryId: "sdlc-plan-entry", finishId: "sdlc-plan-finish" }),
8
+ "plan-review": Object.freeze({ stage: "plan-review", entryId: "sdlc-plan-review-entry", finishId: "sdlc-plan-review-finish" }),
9
+ code: Object.freeze({ stage: "code", entryId: "sdlc-code-entry", finishId: "sdlc-code-finish" }),
10
+ "code-review": Object.freeze({ stage: "code-review", entryId: "sdlc-code-review-entry", finishId: "sdlc-code-review-finish" }),
11
+ merge: Object.freeze({ stage: "merge", entryId: "sdlc-merge-entry", finishId: "sdlc-merge-finish" })
12
+ });
13
+ export const memoryInitHandlerIdentities = Object.freeze({
14
+ preflight: Object.freeze({ stage: "preflight", entryId: "memory-stage-entry", finishId: "memory-stage-finish" }),
15
+ discover: Object.freeze({ stage: "discover", entryId: "memory-stage-entry", finishId: "memory-stage-finish" }),
16
+ compose: Object.freeze({ stage: "compose", entryId: "memory-stage-entry", finishId: "memory-stage-finish" }),
17
+ verify: Object.freeze({ stage: "verify", entryId: "memory-stage-entry", finishId: "memory-stage-finish" }),
18
+ review: Object.freeze({ stage: "review", entryId: "memory-stage-entry", finishId: "memory-stage-finish" }),
19
+ finish: Object.freeze({ stage: "finish", entryId: "memory-stage-entry", finishId: "memory-stage-finish" })
20
+ });
@@ -0,0 +1 @@
1
+ export function inspectDaemonOperation(root: string, id: string): Promise<Record<string, unknown>>;
@@ -8,6 +8,7 @@ import { errorRecord, isObservationLoss, isProvenShellCorrection, reportedError
8
8
  import { assertDaemonOwnership, budgetObservationScope, releaseBudgetObservation, completeBudgetOperation } from "./managed-daemon.mjs";
9
9
  import { readModelObservations, modelAttribution } from "./model-observations.mjs";
10
10
  import { publishOperationProgress, publishOperationWaiting, withOperationProgress } from './operation-progress.mjs';
11
+ import { prepareDaemonRequest } from "./native-request-proof.mjs";
11
12
 
12
13
  const productive = new Set(["session.create", "session.prompt", "session.start", "session.fork", "session.resume"]);
13
14
  const controls = new Map();
@@ -301,7 +302,7 @@ export async function durableDaemonStart(options, action) {
301
302
  // scope can reuse this exact adapter launch after retiring the old daemon.
302
303
  const launch = { schema_id: "dd-flow/native-launch@1", executable: options.entryPath ?? null, args: process.argv.slice(2), cwd: process.cwd(), runtime_owner: process.env.DD_FLOW_RUNTIME_OWNER ?? null,
303
304
  env: Object.fromEntries(["CODEX_HOME", "DD_FLOW_CONFIG_HOME", "DD_FLOW_HOME", "DD_FLOW_BIN", "DD_FLOW_RESOURCE_HOME"].map(key => [key, process.env[key] ?? null])) };
304
- const request = { id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation: "daemon.start", params: { ...options, runtimeOwner: process.env.DD_FLOW_RUNTIME_OWNER ?? null, resourceHome: process.env.DD_FLOW_RESOURCE_HOME ?? null, launch } };
305
+ const request = prepareDaemonRequest({ id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation: "daemon.start", params: { ...options, runtimeOwner: process.env.DD_FLOW_RUNTIME_OWNER ?? null, resourceHome: process.env.DD_FLOW_RESOURCE_HOME ?? null, launch } });
305
306
  return await journalDispatch(root, request, () => action(options));
306
307
  }
307
308
 
@@ -314,13 +315,13 @@ async function journalDispatch(root, request, action) {
314
315
  const daemon = await read(path.join(root, "daemon.json"));
315
316
  const startup = request.operation === "daemon.start";
316
317
  const daemonId = daemon?.daemon_id ?? daemon?.config?.daemonId ?? null;
317
- await writeJsonAtomic(temporary, { schema_id: "dd-flow/daemon-operation@1", operation_id: request.id, operation: request.operation, session_id: request.params?.sessionId ?? null, daemon_id: startup ? null : daemonId, owner_id: daemon?.config?.runtime_owner?.owner_id ?? null, generation: dispatchGenerations.get(root) ?? 0, ...(startup ? { predecessor_daemon_id: daemonId, launch: request.params?.launch } : {}), params_sha256: fingerprint, requested_at: new Date().toISOString() });
318
+ await writeJsonAtomic(temporary, { schema_id: "dd-flow/daemon-operation@1", operation_id: request.id, operation: request.operation, session_id: request.params?.sessionId ?? null, daemon_id: startup ? null : daemonId, owner_id: daemon?.config?.runtime_owner?.owner_id ?? null, generation: dispatchGenerations.get(root) ?? 0, ...(startup ? { predecessor_daemon_id: daemonId, launch: request.params?.launch } : {}), params_sha256: fingerprint, ...(request.invocation_sha256 ? { invocation_sha256: request.invocation_sha256 } : {}), requested_at: new Date().toISOString() });
318
319
  try {
319
320
  await link(temporary, path.join(directory, "requested.json"));
320
321
  } catch (error) {
321
322
  if (error.code !== "EEXIST") throw error;
322
323
  const saved = await inspectDaemonOperation(root, request.id);
323
- if (saved.params_sha256 !== fingerprint) throw Object.assign(new Error("operation id was reused with different parameters"), { code: "operation_conflict", details: { operation_id: request.id } });
324
+ if (saved.params_sha256 !== fingerprint || (saved.invocation_sha256 ?? null) !== (request.invocation_sha256 ?? null)) throw Object.assign(new Error("operation id was reused with different parameters or invocation"), { code: "operation_conflict", details: { operation_id: request.id } });
324
325
  if (saved.state === "completed") return saved.result;
325
326
  if (["failed", "interrupted"].includes(saved.state)) throw reportedError(saved.error, "provider operation failed");
326
327
  throw Object.assign(new Error("provider operation has no confirmed terminal result; inspect the existing operation instead of resending it"), { code: "operation_observation_lost", details: { operation_id: request.id, receipt_directory: directory } });
@@ -1,6 +1,8 @@
1
+ import { managedFlowEnv } from './managed-flow-env.mjs';
1
2
  import { SETUP_STARTUP_MS, startupObserver, observationDetails } from './setup-progress.mjs';
2
3
  import { callDaemonObserved } from './daemon-observation.mjs';
3
4
  import { operationProgressReporter } from './operation-progress.mjs';
5
+ import { prepareDaemonRequest } from "./native-request-proof.mjs";
4
6
  import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
5
7
  import { errorRecord } from "./operation-errors.mjs";
6
8
  import { ToolObservations, agyToolObservation, agyTranscriptObservations } from "./tool-observations.mjs";
@@ -58,7 +60,7 @@ async function writeJson(file, value) { const temporary = `${file}.${process.pid
58
60
  async function readState(dir) { try { return JSON.parse(await readFile(locations(dir).state, "utf8")); } catch (error) { if (error.code === "ENOENT") return null; throw error; } }
59
61
  async function removeSocket(paths) { try { if (!(await lstat(paths.socket)).isSocket()) throw new DaemonError("unsafe_socket", "refusing to replace a non-socket daemon path"); await unlink(paths.socket); } catch (error) { if (error.code !== "ENOENT") throw error; } }
60
62
  const shell = value => `'${String(value).replaceAll("'", `'"'"'`)}'`;
61
- export function sanitizedEnv(config) { const env = { ...process.env, TMPDIR: config.temporary, DD_AGY_HOOK_NODE: process.execPath, DD_AGY_HOOK_ENTRY: config.entryPath, DD_AGY_STATE_DIR: config.stateDir, DD_AGY_DAEMON_ID: config.daemonId }; for (const key of ["GEMINI_API_KEY","GOOGLE_API_KEY","ANTHROPIC_API_KEY","OPENAI_API_KEY","DD_FLOW_DAEMON_ID"]) env[key] = undefined; if (config.ddFlowHome && config.daemonId) env.DD_FLOW_DAEMON_ID = config.daemonId; return env; }
63
+ export function sanitizedEnv(config) { const env = { ...process.env, TMPDIR: config.temporary, DD_AGY_HOOK_NODE: process.execPath, DD_AGY_HOOK_ENTRY: config.entryPath, DD_AGY_STATE_DIR: config.stateDir, DD_AGY_DAEMON_ID: config.daemonId }; for (const key of ["GEMINI_API_KEY","GOOGLE_API_KEY","ANTHROPIC_API_KEY","OPENAI_API_KEY"]) env[key] = undefined; return { ...env, ...managedFlowEnv(config) }; }
62
64
  const errorPayload = errorRecord;
63
65
  async function forwardUsage(config, sessionId, result, tools, descendants) {
64
66
  if (!config.ddFlowBin || !config.projectRoot) return;
@@ -164,7 +166,7 @@ export async function qualifyAgyHooks(projectRoot, { stateDir = null, install =
164
166
  }
165
167
 
166
168
  export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 30_000, requestId = null) {
167
- const request = { schema_id: REQUEST_SCHEMA, id: requestId ?? process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params };
169
+ const request = prepareDaemonRequest({ schema_id: REQUEST_SCHEMA, id: requestId ?? process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params });
168
170
  return await callDaemonObserved({ stateDir, socket: locations(stateDir).socket, request, responseSchema: RESPONSE_SCHEMA, harness: 'agy', timeoutMs });
169
171
  }
170
172
 
@@ -1,6 +1,8 @@
1
+ import { managedFlowEnv } from './managed-flow-env.mjs';
1
2
  import { SETUP_STARTUP_MS, startupObserver, observationDetails } from './setup-progress.mjs';
2
3
  import { callDaemonObserved } from './daemon-observation.mjs';
3
4
  import { admitNativeRequestBuffer } from "./native-streams.mjs";
5
+ import { prepareDaemonRequest } from "./native-request-proof.mjs";
4
6
  import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
5
7
  import { createHash, randomUUID } from "node:crypto";
6
8
  import { chmod, lstat, mkdir, open, readFile, rename, unlink, writeFile } from "node:fs/promises";
@@ -12,9 +14,11 @@ import path from "node:path";
12
14
  import { spawn } from "node:child_process";
13
15
  import { waitForSettlement } from "./session-settlement.mjs";
14
16
  import { errorRecord } from "./operation-errors.mjs";
15
- import { CodexBridge, cancelSessionWithBridge, createSessionWithBridge, inspectSessionWithBridge, promptSessionWithBridge, startSessionWithBridge } from "./dd-codex.mjs";
17
+ import { CODEX_REQUEST_TIMEOUT_MS, LATE_RESPONSE_GRACE_MS, CodexBridge, cancelSessionWithBridge, createSessionWithBridge, inspectSessionWithBridge, promptSessionWithBridge, startSessionWithBridge } from "./dd-codex.mjs";
16
18
  import { assertDaemonOwnership, prepareRuntimeOwner, cleanupFailedDaemonStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, startManagedBridge, stopProcessGroup } from "./managed-daemon.mjs";
17
19
 
20
+ export function codexBridgeEnv(config, daemonId) { return { ...config.env, ...managedFlowEnv(config, daemonId) }; }
21
+
18
22
  const requestSchema = "dd-codex/daemon-request@1";
19
23
  const responseSchema = "dd-codex/daemon-response@1";
20
24
  const stateSchema = "dd-codex/daemon-state@1";
@@ -72,13 +76,15 @@ export async function cancelCodexTree(bridge, options, { remember = () => {}, ac
72
76
  return { harness: "codex-desktop", provider_session_id: options.sessionId, adapter_session_id: options.sessionId, cancelled: true, settled: true, tree_session_ids: [...sessions], cleanup_errors: settlement.cleanup_errors, tree_observations: settlement.observations.map(item => ({ provider_session_id: item.provider_session_id, status: item.status, settled: item.settled })) };
73
77
  }
74
78
 
75
- export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 30_000) {
76
- const request = { schema_id: requestSchema, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params };
79
+ export async function callDaemon(stateDir, operation, params = {}, timeoutMs = operation === "session.create" ? params.timeoutMs ?? CODEX_REQUEST_TIMEOUT_MS + LATE_RESPONSE_GRACE_MS : 30_000) {
80
+ const request = prepareDaemonRequest({ schema_id: requestSchema, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params });
77
81
  return await callDaemonObserved({ stateDir, socket: locations(stateDir).socket, request, responseSchema: responseSchema, harness: 'codex', timeoutMs });
78
82
  }
79
83
 
80
84
  export async function startDaemon(options) {
81
- const paths = locations(options.stateDir); const cwd = absolute(options.cwd, "--cwd"); const config = { cwd, journal: absolute(options.journal, "--journal"), bin: options.bin ?? null, ddFlowBin: options.ddFlowBin ? absolute(options.ddFlowBin, "--dd-flow-bin") : null, ddFlowHome: options.ddFlowHome ? absolute(options.ddFlowHome, "--dd-flow-home") : null, projectRoot: options.projectRoot ? absolute(options.projectRoot, "--project-root") : cwd, resourceHome: process.env.DD_FLOW_RESOURCE_HOME ? absolute(process.env.DD_FLOW_RESOURCE_HOME, "DD_FLOW_RESOURCE_HOME") : null, env: options.ddFlowHome ? { DD_FLOW_HOME: absolute(options.ddFlowHome, "--dd-flow-home"), ...(options.ddFlowBin ? { DD_FLOW_BIN: absolute(options.ddFlowBin, "--dd-flow-bin") } : {}) } : {} };
85
+ if (!options.noFlow && Boolean(options.ddFlowHome) !== Boolean(options.ddFlowBin)) throw Object.assign(new Error("--dd-flow-home and --dd-flow-bin must be provided together"), { code: "flow_context_incomplete" });
86
+ if (options.noFlow) options = { ...options, ddFlowHome: undefined, ddFlowBin: undefined };
87
+ const paths = locations(options.stateDir); const cwd = absolute(options.cwd, "--cwd"); const config = { cwd, journal: absolute(options.journal, "--journal"), bin: options.bin ?? null, ddFlowBin: options.ddFlowBin ? absolute(options.ddFlowBin, "--dd-flow-bin") : null, ddFlowHome: options.ddFlowHome ? absolute(options.ddFlowHome, "--dd-flow-home") : null, projectRoot: options.projectRoot ? absolute(options.projectRoot, "--project-root") : cwd, resourceHome: !options.noFlow && process.env.DD_FLOW_RESOURCE_HOME ? absolute(process.env.DD_FLOW_RESOURCE_HOME, "DD_FLOW_RESOURCE_HOME") : null, env: options.ddFlowHome ? { DD_FLOW_HOME: absolute(options.ddFlowHome, "--dd-flow-home"), ...(options.ddFlowBin ? { DD_FLOW_BIN: absolute(options.ddFlowBin, "--dd-flow-bin") } : {}) } : {} };
82
88
  await prepareRuntimeOwner(config, paths.dir);
83
89
  await mkdir(paths.dir, { recursive: true, mode: 0o700 }); await chmod(paths.dir, 0o700);
84
90
  let previous = await readState(paths.state); assertDaemonRestartable(previous);
@@ -95,7 +101,7 @@ export async function stopDaemon(options) { const timeoutMs = options.timeoutMs
95
101
 
96
102
  export async function serveDaemon(stateDir) {
97
103
  const paths = locations(stateDir); const state = await readState(paths.state); if (!state || state.schema_id !== stateSchema) throw Object.assign(new Error("daemon state missing"), { code: "daemon_state_missing" });
98
- const bridge = new CodexBridge({ ...state.config, env: { ...state.config.env, CODEX_HOME: process.env.CODEX_HOME, DD_FLOW_DAEMON_ID: state.daemon_id, DD_FLOW_CODEX_STATE_DIR: paths.dir } }); await startManagedBridge({ config: state.config, state, bridge, kind: "codex-provider", publish: snapshot => writeState(paths.state, snapshot) }); const sessions = new Set(state.sessions ?? []); let active = null; let server; let persist = Promise.resolve();
104
+ const bridge = new CodexBridge({ ...state.config, env: { ...codexBridgeEnv(state.config, state.daemon_id), CODEX_HOME: process.env.CODEX_HOME, DD_FLOW_CODEX_STATE_DIR: paths.dir } }); await startManagedBridge({ config: state.config, state, bridge, kind: "codex-provider", publish: snapshot => writeState(paths.state, snapshot) }); const sessions = new Set(state.sessions ?? []); let active = null; let server; let persist = Promise.resolve();
99
105
  scheduleDaemonProcessHeartbeat(state.config, state.resource_process);
100
106
  const save = (patch = {}) => { Object.assign(state, patch, { pid: process.pid, updated_at: new Date().toISOString(), sessions: [...sessions] }); const snapshot = structuredClone(state); persist = persist.catch(() => {}).then(() => writeState(paths.state, snapshot)); return persist; };
101
107
  const dispatch = async (operation, params, operationId) => {
@@ -13,7 +13,8 @@ import { readNativeLines, streamNativeDiagnostics, nativeObservationError } from
13
13
  import { operationProgressReporter } from "./operation-progress.mjs";
14
14
  import { publishSetup } from './setup-progress.mjs';
15
15
 
16
- const LATE_RESPONSE_GRACE_MS = 5_000;
16
+ export const CODEX_REQUEST_TIMEOUT_MS = 120_000;
17
+ export const LATE_RESPONSE_GRACE_MS = 5_000;
17
18
  const CODEX_SESSION_CONFIG = Object.freeze({
18
19
  bypass_hook_trust: true,
19
20
  "features.plugins": false,
@@ -409,7 +410,7 @@ export class CodexBridge {
409
410
  this.send({ jsonrpc: "2.0", id: message.id, error });
410
411
  }
411
412
 
412
- request(method, params = {}, timeoutMs = this.options.timeoutMs ?? 120_000) {
413
+ request(method, params = {}, timeoutMs = this.options.timeoutMs ?? CODEX_REQUEST_TIMEOUT_MS) {
413
414
  const turnFloor = method === "turn/start" ? new Set([this.currentTurns?.get(params.threadId), ...[...this.turns].filter(([, value]) => value.threadId === params.threadId).map(([id]) => id)].filter(Boolean)) : null;
414
415
  if (method === "turn/start" && params.threadId) {
415
416
  // Only one productive root Turn is admitted by the daemon fence. Drop
@@ -576,7 +577,8 @@ export async function promptSessionWithBridge(bridge, options) {
576
577
  const started = await bridge.request("turn/start", { threadId: sessionId, input: [{ type: "text", text: prompt }], cwd, model: options.model ?? null, effort: options.reasoning ?? null, approvalPolicy: "never", sandboxPolicy: { type: "dangerFullAccess" } }, 30_000);
577
578
  const turn = started?.turn ?? started; const turnId = turn?.id ?? null;
578
579
  let completed;
579
- try { completed = await waitForTurn(bridge, sessionId, turnId, options.livenessTimeoutMs ?? options.timeoutMs ?? 1_800_000, options, cwd, clock); }
580
+ try { completed = isTerminalTurnStatus(turn?.status) ? terminalTurn({ turn }, sessionId, turnId, bridge)
581
+ : await waitForTurn(bridge, sessionId, turnId, options.livenessTimeoutMs ?? options.timeoutMs ?? 1_800_000, options, cwd, clock, turn?.status); }
580
582
  catch (error) {
581
583
  const retained = bridge.turns?.get(turnId);
582
584
  if (retained?.value?.threadId === sessionId && retained.value.turn?.id === turnId && retained.value.turn.status === "completed") {
@@ -624,11 +626,20 @@ async function ensureThreadLoaded(bridge, sessionId, options, cwd) {
624
626
  }, 30_000);
625
627
  }
626
628
 
627
- async function waitForTurn(bridge, sessionId, turnId, timeoutMs, options, cwd, clock = new ObservationClock({ timeoutMs, onGap: gap => bridge.journal?.write("observation_gap", { ...gap, session_id: sessionId, turn_id: turnId }) })) {
629
+ async function waitForTurn(bridge, sessionId, turnId, timeoutMs, options, cwd, clock = new ObservationClock({ timeoutMs, onGap: gap => bridge.journal?.write("observation_gap", { ...gap, session_id: sessionId, turn_id: turnId }) }), startStatus) {
628
630
  // app-server can briefly expose the new Turn as `interrupted` between the
629
631
  // turn/start response and its turn/started notification. Give that snapshot
630
632
  // one event-loop grace window before treating failure as authoritative.
631
633
  const failureNotBefore = Date.now() + 1_000;
634
+ const reconcileSnapshot = async (status, threadStatus) => {
635
+ if (!isAuthoritativeTerminal(bridge, turnId, status, threadStatus, failureNotBefore)) return false;
636
+ if (status === "completed" || startStatus !== "inProgress" || bridge.turns.get(turnId)?.status === "completed") return true;
637
+ // A provisional failure can contradict the accepted start before its
638
+ // delayed notification arrives. Recheck this Turn, never start another.
639
+ const current = await readTurn(bridge, sessionId, turnId, true);
640
+ return bridge.turns.get(turnId)?.status === "completed" || current?.turn?.status === status
641
+ && isAuthoritativeTerminal(bridge, turnId, status, current.threadStatus, failureNotBefore);
642
+ };
632
643
  let nextStoredReadAt = Date.now();
633
644
  while (true) {
634
645
  bridge.assertHookHealthy?.(sessionId);
@@ -636,20 +647,20 @@ async function waitForTurn(bridge, sessionId, turnId, timeoutMs, options, cwd, c
636
647
  const expired = clock.sample(bridge.activityObservations?.get(sessionId) ?? bridge.activity?.get(sessionId));
637
648
  if (expired || Date.now() >= nextStoredReadAt) {
638
649
  const stored = await readTurn(bridge, sessionId, turnId, false);
639
- if (stored?.turn && isAuthoritativeTerminal(bridge, turnId, stored.turn.status, stored.threadStatus, failureNotBefore)) return terminalTurn({ turn: stored.turn }, sessionId, turnId, bridge);
650
+ if (stored?.turn && await reconcileSnapshot(stored.turn.status, stored.threadStatus)) return terminalTurn({ turn: stored.turn }, sessionId, turnId, bridge);
640
651
  // A loaded app-server can retain a stale `active` Thread state after a
641
652
  // provider-side interruption. The persisted turn page is independent of
642
653
  // that live subscription and is the compact authority for its terminal
643
654
  // status; unlike `includeTurns`, it never hydrates the whole history.
644
655
  const persisted = await readPersistedTurn(bridge, sessionId, turnId);
645
- if (persisted && isAuthoritativeTerminal(bridge, turnId, persisted.status, stored?.threadStatus, failureNotBefore)) return terminalTurn({ turn: persisted }, sessionId, turnId, bridge);
656
+ if (persisted && await reconcileSnapshot(persisted.status, stored?.threadStatus)) return terminalTurn({ turn: persisted }, sessionId, turnId, bridge);
646
657
  // Desktop may unload an idle Thread immediately after an interrupted
647
658
  // Turn. Reload it once before polling again: otherwise the compact
648
659
  // status remains `notLoaded` forever and hides the terminal Turn.
649
660
  if (stored?.threadStatus === "notLoaded") {
650
661
  await ensureThreadLoaded(bridge, sessionId, options, cwd);
651
662
  const hydrated = await readTurn(bridge, sessionId, turnId, true);
652
- if (hydrated?.turn && isAuthoritativeTerminal(bridge, turnId, hydrated.turn.status, hydrated.threadStatus, failureNotBefore)) return terminalTurn({ turn: hydrated.turn }, sessionId, turnId, bridge);
663
+ if (hydrated?.turn && await reconcileSnapshot(hydrated.turn.status, hydrated.threadStatus)) return terminalTurn({ turn: hydrated.turn }, sessionId, turnId, bridge);
653
664
  }
654
665
  // Some app-server versions correctly omit turns for a compact read.
655
666
  // Once the thread is idle, hydrate exactly once to recover the terminal
@@ -660,7 +671,7 @@ async function waitForTurn(bridge, sessionId, turnId, timeoutMs, options, cwd, c
660
671
  // an inProgress Turn after emitting the final agent message; treating
661
672
  // that stale Turn as terminal incorrectly aborts an otherwise usable
662
673
  // worker session.
663
- if (hydrated?.turn && isAuthoritativeTerminal(bridge, turnId, hydrated.turn.status, hydrated.threadStatus, failureNotBefore)) return terminalTurn({ turn: hydrated.turn }, sessionId, turnId, bridge);
674
+ if (hydrated?.turn && await reconcileSnapshot(hydrated.turn.status, hydrated.threadStatus)) return terminalTurn({ turn: hydrated.turn }, sessionId, turnId, bridge);
664
675
  }
665
676
  // Some app-server builds emit a final agent message but omit both the
666
677
  // terminal event and the compact turn record. Wait briefly, hydrate once,
@@ -671,8 +682,9 @@ async function waitForTurn(bridge, sessionId, turnId, timeoutMs, options, cwd, c
671
682
  // app-server reports the Thread idle.
672
683
  if (final?.turnId === turnId && stored?.threadStatus === "idle" && Date.now() - final.completedAt >= 1_000) {
673
684
  const hydrated = await readTurn(bridge, sessionId, turnId, true);
674
- if (hydrated?.turn && isAuthoritativeTerminal(bridge, turnId, hydrated.turn.status, hydrated.threadStatus, failureNotBefore)) return terminalTurn({ turn: hydrated.turn }, sessionId, turnId, bridge);
675
- return terminalTurn({ turn: { id: turnId, status: "completed", synthetic: true, final_message: final.item } }, sessionId, turnId, bridge);
685
+ if (hydrated?.turn && await reconcileSnapshot(hydrated.turn.status, hydrated.threadStatus)) return terminalTurn({ turn: hydrated.turn }, sessionId, turnId, bridge);
686
+ if (hydrated?.threadStatus === "idle" && !isTerminalTurnStatus(hydrated.turn?.status))
687
+ return terminalTurn({ turn: { id: turnId, status: "completed", synthetic: true, final_message: final.item } }, sessionId, turnId, bridge);
676
688
  }
677
689
  nextStoredReadAt = Date.now() < failureNotBefore ? failureNotBefore : Date.now() + 15_000;
678
690
  }
@@ -1,6 +1,7 @@
1
1
  import { SETUP_STARTUP_MS, startupObserver, observationDetails } from './setup-progress.mjs';
2
2
  import { callDaemonObserved } from './daemon-observation.mjs';
3
3
  import { admitNativeRequestBuffer, nativeObservationError } from "./native-streams.mjs";
4
+ import { prepareDaemonRequest } from "./native-request-proof.mjs";
4
5
  import { createHash, randomUUID } from "node:crypto";
5
6
  import { chmod, lstat, mkdir, open, unlink, realpath } from "node:fs/promises";
6
7
  import { spawn } from "node:child_process";
@@ -16,7 +17,7 @@ import { prepareRuntimeOwner, cleanupFailedStart, confirmDaemonProcess, confirmD
16
17
  const REQUEST = "dd-droid/daemon-request@1", RESPONSE = "dd-droid/daemon-response@1";
17
18
  async function removeSocket(file) { try { if (!(await lstat(file)).isSocket()) throw new DroidError("unsafe_socket", "Refusing to replace non-socket daemon path"); await unlink(file); } catch (error) { if (error.code !== "ENOENT") throw error; } }
18
19
  export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 30_000, operationId = process.env.DD_EVAL_OPERATION_ID ?? randomUUID()) {
19
- const request = { schema_id: REQUEST, id: operationId, operation, params };
20
+ const request = prepareDaemonRequest({ schema_id: REQUEST, id: operationId, operation, params });
20
21
  return await callDaemonObserved({ stateDir, socket: droidPaths(stateDir).socket, request, responseSchema: RESPONSE, harness: 'droid', timeoutMs });
21
22
  }
22
23
  function sameConfig(a, b) { return ["cwd", "bin", "model", "reasoning", "mode", "provider", "ddFlowBin", "ddFlowHome", "resourceHome", "projectRoot", "noFlow","inactivityMs"].every(key => a?.[key] === b[key]); }
@@ -1,3 +1,4 @@
1
+ import { managedFlowEnv } from './managed-flow-env.mjs';
1
2
  import { processSnapshot } from "./process-snapshot.mjs";
2
3
  import { ToolObservations, droidToolObservations, journalToolObservations } from "./tool-observations.mjs";
3
4
  export { processSnapshot } from "./process-snapshot.mjs";
@@ -44,7 +45,7 @@ export function droidEnvironment(config) {
44
45
  // This adapter deliberately uses the operator's existing Factory login.
45
46
  // Undefined is a tombstone: downstream env overlays must not resurrect keys.
46
47
  for (const key of Object.keys(env)) if (key.endsWith("API_KEY")) env[key] = undefined;
47
- return env;
48
+ return { ...env, ...managedFlowEnv(config) };
48
49
  }
49
50
  export async function prepareDroidHome(config) {
50
51
  const paths = droidPaths(config.stateDir);
@@ -1,7 +1,9 @@
1
+ import { managedFlowEnv } from './managed-flow-env.mjs';
1
2
  import { SETUP_STARTUP_MS, startupObserver, observationDetails } from './setup-progress.mjs';
2
3
  import { callDaemonObserved } from './daemon-observation.mjs';
3
4
  import { canonicalNativeOperation } from './adapter-timeouts.mjs';
4
5
  import { admitNativeRequestBuffer } from "./native-streams.mjs";
6
+ import { prepareDaemonRequest } from "./native-request-proof.mjs";
5
7
  import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
6
8
  import { errorRecord, isProvenShellCorrection } from "./operation-errors.mjs";
7
9
  import { ToolObservations, acpToolObservation } from "./tool-observations.mjs";
@@ -39,7 +41,7 @@ const errorPayload = errorRecord;
39
41
  function shellQuote(value) { return `'${String(value).replace(/'/g, "'\\''")}'`; }
40
42
 
41
43
  function executable(command, args = []) { const resolved = command ?? process.env.DD_GROK_BIN ?? "grok"; return /\.[cm]?js$/.test(resolved) ? { command: process.execPath, args: [resolved, ...args] } : { command: resolved, args }; }
42
- export function isolatedEnv(config) { const env = { ...process.env, HOME: config.grokHome, GROK_HOME: config.grokHome, ...(config.ddFlowHome ? { DD_FLOW_HOME: config.ddFlowHome } : {}), ...(config.resourceHome ? { DD_FLOW_RESOURCE_HOME: config.resourceHome } : {}), ...(config.authPath ? { GROK_AUTH_PATH: config.authPath } : {}) }; env.DD_FLOW_DAEMON_ID = undefined; if (config.ddFlowHome && config.daemonId) env.DD_FLOW_DAEMON_ID = config.daemonId; return env; }
44
+ export function isolatedEnv(config) { return { ...process.env, HOME: config.grokHome, GROK_HOME: config.grokHome, ...(config.authPath ? { GROK_AUTH_PATH: config.authPath } : {}), ...managedFlowEnv(config) }; }
43
45
  function within(candidate, root) { const resolved = path.resolve(candidate); const parent = path.resolve(root); return resolved === parent || resolved.startsWith(`${parent}${path.sep}`); }
44
46
 
45
47
  async function writeIsolatedConfig(paths) {
@@ -151,7 +153,7 @@ async function materializeSessionArchive(paths, archiveDir) {
151
153
 
152
154
  export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 30_000) {
153
155
  operation = canonicalNativeOperation('grok', operation);
154
- const request = { schema_id: REQUEST_SCHEMA, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params };
156
+ const request = prepareDaemonRequest({ schema_id: REQUEST_SCHEMA, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params });
155
157
  return await callDaemonObserved({ stateDir, socket: locations(stateDir).socket, request, responseSchema: RESPONSE_SCHEMA, harness: 'grok', timeoutMs });
156
158
  }
157
159
 
@@ -1,6 +1,8 @@
1
+ import { managedFlowEnv } from './managed-flow-env.mjs';
1
2
  import { SETUP_STARTUP_MS, startupObserver, observationDetails } from './setup-progress.mjs';
2
3
  import { callDaemonObserved } from './daemon-observation.mjs';
3
4
  import { admitNativeRequestBuffer } from "./native-streams.mjs";
5
+ import { prepareDaemonRequest } from "./native-request-proof.mjs";
4
6
  import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
5
7
  import { openCodeToolObservation, journalToolObservations } from "./tool-observations.mjs";
6
8
  import { ObservationClock } from "./observation-clock.mjs";
@@ -40,13 +42,13 @@ const writeJson = writeJsonAtomic;
40
42
  async function readState(dir) { try { return JSON.parse(await readFile(locations(dir).state, "utf8")); } catch (error) { if (error.code === "ENOENT") return null; throw error; } }
41
43
  async function removeSocket(paths) { try { if (!(await lstat(paths.socket)).isSocket()) throw new DaemonError("unsafe_socket", "refusing to replace non-socket daemon path"); await unlink(paths.socket); } catch (error) { if (error.code !== "ENOENT") throw error; } }
42
44
  function executable(bin, args = []) { return /\.[cm]?js$/.test(bin) ? { command: process.execPath, args: [bin, ...args] } : { command: bin, args }; }
43
- function isolatedEnv(config) {
45
+ export function isolatedEnv(config) {
44
46
  const env = { ...process.env };
45
47
  for (const name of ["OPENCODE_CONFIG", "OPENCODE_CONFIG_CONTENT", "OPENCODE_PASSWORD", "OPENCODE_PTY_HANDOFF", "OPENCODE_SERVER", "OPENCODE_SIMULATE"]) delete env[name];
46
48
  return { ...env, XDG_DATA_HOME: config.dataHome, XDG_CONFIG_HOME: config.configHome, XDG_CACHE_HOME: config.cacheHome,
47
49
  XDG_STATE_HOME: config.stateHome, OPENCODE_DB: config.database, OPENCODE_CONFIG_DIR: config.adapterDir,
48
50
  OPENCODE_CONFIG_PROJECT_DISABLE: "1", OPENCODE_SERVER_PASSWORD: config.password,
49
- ...(config.ddFlowHome ? { DD_FLOW_HOME: config.ddFlowHome } : {}), ...(config.resourceHome ? { DD_FLOW_RESOURCE_HOME: config.resourceHome } : {}),
51
+ ...managedFlowEnv(config),
50
52
  DD_OPENCODE_PLUGIN_SHA256: config.pluginSha256 ?? "none" };
51
53
  }
52
54
  export async function run(bin, args, config, options = {}) {
@@ -63,7 +65,7 @@ const errorPayload = errorRecord;
63
65
 
64
66
  async function writePlugin(paths, config) {
65
67
  const directory = path.join(paths.config, "opencode", "plugins"); await mkdir(directory, { recursive: true, mode: 0o700 });
66
- const source = `import { makeOpenCodePlugin } from ${JSON.stringify(new URL("./opencode-plugin.mjs", import.meta.url).href)};\nexport default makeOpenCodePlugin(${JSON.stringify({ daemonId: config.daemonId, version: config.version, ddFlowBin: config.ddFlowBin, nodeBin: process.execPath, ddFlowHome: config.ddFlowHome, projectRoot: config.projectRoot })});\n`;
68
+ const source = `import { makeOpenCodePlugin } from ${JSON.stringify(new URL("./opencode-plugin.mjs", import.meta.url).href)};\nexport default makeOpenCodePlugin(${JSON.stringify({ daemonId: config.daemonId, version: config.version, ddFlowBin: config.ddFlowBin, nodeBin: process.execPath, ddFlowHome: config.ddFlowHome, projectRoot: config.projectRoot, resourceHome: config.resourceHome })});\n`;
67
69
  const checksum = createHash("sha256").update(source).digest("hex"); await writeFile(path.join(directory, "dd-flow.js"), source, { mode: 0o600 }); return checksum;
68
70
  }
69
71
  async function prepare(paths, config, authSource) {
@@ -136,7 +138,7 @@ async function importArchive(config, client) {
136
138
  export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 30_000) {
137
139
  const state = await readState(stateDir);
138
140
  if (state && state.schema_id !== STATE_SCHEMA) throw new DaemonError("legacy_opencode_state", "Use archive/opencode-v1-final-beta134 for v1 states; the v2 adapter cannot dispatch to a legacy daemon", false, { effect: "no_effect" });
139
- const request = { schema_id: REQUEST_SCHEMA, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params };
141
+ const request = prepareDaemonRequest({ schema_id: REQUEST_SCHEMA, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params });
140
142
  return await callDaemonObserved({ stateDir, socket: locations(stateDir).socket, request, responseSchema: RESPONSE_SCHEMA, harness: 'opencode', timeoutMs });
141
143
  }
142
144
  function sameConfig(state, config) { return ["cwd","journal","bin","provider","model","variant","agent","projectRoot","ddFlowBin","ddFlowHome","sessionArchive","resourceHome","noFlow","configSource","authSource"].every(key => (state.config?.[key] ?? null) === (config[key] ?? null)); }