@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
@@ -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 { admitNativeRequestBuffer } from "./native-streams.mjs";
4
5
  import { ObservationClock } from "./observation-clock.mjs";
6
+ import { prepareDaemonRequest } from "./native-request-proof.mjs";
5
7
  import { spawn } from "node:child_process";
6
8
  import { createHash, randomUUID } from "node:crypto";
7
9
  import { chmod, lstat, mkdir, open, readFile, rename, unlink, writeFile } from "node:fs/promises";
@@ -90,7 +92,7 @@ async function removeSocket(socket, dir) {
90
92
  const errorPayload = errorRecord;
91
93
 
92
94
  export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 30_000) {
93
- const request = { schema_id: REQUEST_SCHEMA, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params };
95
+ const request = prepareDaemonRequest({ schema_id: REQUEST_SCHEMA, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params });
94
96
  return await callDaemonObserved({ stateDir, socket: locations(stateDir).socket, request, responseSchema: RESPONSE_SCHEMA, harness: 'zcode', timeoutMs });
95
97
  }
96
98
 
@@ -110,22 +112,21 @@ function sameConfig(state, config) {
110
112
  }
111
113
 
112
114
  export async function startDaemon(options) {
115
+ if (options.noFlow) options = { ...options, ddFlowHome: undefined, ddFlowBin: undefined, projectRoot: undefined };
113
116
  const paths = locations(options.stateDir);
114
117
  const hasFlowContext = Boolean(options.ddFlowHome || options.projectRoot || options.ddFlowBin);
115
- if (hasFlowContext && (!options.ddFlowHome || !options.projectRoot)) {
116
- throw new DaemonError("flow_context_incomplete", "--dd-flow-home and --project-root must be provided together for ZCode lifecycle forwarding");
118
+ if (hasFlowContext && (!options.ddFlowHome || !options.projectRoot || !options.ddFlowBin)) {
119
+ throw new DaemonError("flow_context_incomplete", "--dd-flow-home, --dd-flow-bin and --project-root must be provided together for ZCode lifecycle forwarding");
117
120
  }
118
121
  const config = {
119
122
  cwd: absolute(options.cwd, "--cwd"),
120
123
  journal: absolute(options.journal, "--journal"),
121
124
  bin: options.bin ?? null,
122
125
  zcodePath: options.zcodePath ?? null,
123
- // The worker itself invokes `dd-flow` from PATH. Use that same executable
124
- // for the trusted event bridge unless a pinned executable was supplied.
125
- ddFlowBin: hasFlowContext ? (options.ddFlowBin ? absolute(options.ddFlowBin, "--dd-flow-bin") : "dd-flow") : null,
126
+ ddFlowBin: hasFlowContext ? absolute(options.ddFlowBin, "--dd-flow-bin") : null,
126
127
  ddFlowHome: options.ddFlowHome ? absolute(options.ddFlowHome, "--dd-flow-home") : null,
127
128
  projectRoot: options.projectRoot ? absolute(options.projectRoot, "--project-root") : null,
128
- resourceHome: process.env.DD_FLOW_RESOURCE_HOME ? absolute(process.env.DD_FLOW_RESOURCE_HOME, "DD_FLOW_RESOURCE_HOME") : null,
129
+ resourceHome: !options.noFlow && process.env.DD_FLOW_RESOURCE_HOME ? absolute(process.env.DD_FLOW_RESOURCE_HOME, "DD_FLOW_RESOURCE_HOME") : null,
129
130
  env: { ...nativeRoutingEnv(), ...(options.ddFlowHome ? { DD_FLOW_HOME: absolute(options.ddFlowHome, "--dd-flow-home"), ...(options.ddFlowBin ? { DD_FLOW_BIN: absolute(options.ddFlowBin, "--dd-flow-bin") } : {}) } : {}) },
130
131
  livenessTimeoutMs: positiveDuration(options.livenessTimeoutMs, "livenessTimeoutMs", DEFAULT_LIVENESS_TIMEOUT_MS),
131
132
  requestTimeoutMs: optionalDuration(options.requestTimeoutMs, "requestTimeoutMs", DEFAULT_REQUEST_TIMEOUT_MS),
@@ -683,8 +684,7 @@ export class DaemonRuntime {
683
684
  }
684
685
 
685
686
  export function zcodeBridgeEnv(config, daemonId) {
686
- return { ...config.env, DD_FLOW_DAEMON_ID: daemonId,
687
- DD_FLOW_RUNTIME_OWNER: config.runtime_owner ? JSON.stringify(config.runtime_owner) : undefined };
687
+ return { ...config.env, ...managedFlowEnv(config, daemonId) };
688
688
  }
689
689
 
690
690
  export async function serveDaemon(stateDir) {
@@ -49,7 +49,7 @@ export function delegationContract(harness) {
49
49
  return `dd-flow/delegation/${key}@${["codex", "opencode"].includes(key) ? 2 : 1}`;
50
50
  }
51
51
 
52
- export const lifecycleCommandCompletionInstruction = "Run runtime-issued lifecycle CLI calls in the foreground, never with a native background or detached-shell option. A shell tool may yield before the lifecycle process exits. Retain and poll its exact process/session handle to completion, including inside a tool wrapper; empty interim output is not a CLI result. Read final stdout or response_file before deciding continuation, and never reissue the command while it is pending.";
52
+ export const lifecycleCommandCompletionInstruction = "Run runtime-issued lifecycle CLI calls in the foreground, never with a native background or detached-shell option. A shell tool may yield before the lifecycle process exits. Retain and poll its exact process/session handle to completion, including inside a tool wrapper; empty interim output is not a CLI result. If a wrapper returns 'Script running with cell ID …', use that wrapper's wait tool to finish the same cell before expecting an inner shell session handle; do not execute the command again. Read final stdout or response_file before deciding continuation, and never reissue the command while it is pending.";
53
53
 
54
54
  export const lifecycleRetryInstruction = `${lifecycleCommandCompletionInstruction} A failure is not permission to replay a lifecycle command. For proven effect=no_effect with recoverable=true and retry_command, correct the input and execute that exact issued successor. A check failure can retain receipts while leaving Work running: correct the evidenced cause first, then use the CLI's exact retry_command for an intentional fresh check attempt; never replay a check merely because Work is unsettled. If a successful CODE finish returns outcome=verification_required, inspect the retained receipts, update its verification file, and use only next.finish_command: the Stage is still running and the previous invocation ID is consumed. If continuation.kind=repair_required, or a successful result has outcome=repair_required and a repair Work ID, end this Turn for the registered repair child; use any retry_command only after that repair settles and verification is refreshed. Unknown effects require reconciliation, never replay. A committed effect without an explicit continuation is not retryable. A native shell launch refusal before the CLI starts permits correcting only that shell-form error and retrying the same exact command. Otherwise report the primary error and stop; never invent IDs, call hooks directly, or run status as a substitute.`;
55
55
 
@@ -71,7 +71,9 @@ export function workerDelegationTask({ workId, startCommand, launchPolicy }) {
71
71
  "Your first technical action must be this exact lifecycle command:",
72
72
  startCommand,
73
73
  "Execute the issued operation and target exactly. Lifecycle invocation IDs are internal runtime authority and are intentionally omitted; never add or invent them. Preserve any explicit native receipt handle that the command does contain.",
74
- "Use the native shell's inherited working directory. Do not set or override a workdir/cwd field for this lifecycle command.",
74
+ startCommand.startsWith("cd '")
75
+ ? "Execute the literal cd shell prefix exactly; it routes this command into its runtime-owned private execution copy. Do not invent native spawn fields. After start, obey the packet's read_only/write scopes and prefix subsequent issued lifecycle commands with the same literal cd route. A review copy never grants permission to write reviewed inputs. The runtime independently checks the CLI process directory."
76
+ : "Use the native shell's inherited working directory. Do not set or override a workdir/cwd field for this lifecycle command.",
75
77
  "Continue only if it succeeds and returns the authoritative Work packet.",
76
78
  lifecycleRetryInstruction,
77
79
  "After a successful start, complete the assigned Work, write its required result, invoke its exact work finish command, then stop.",
@@ -109,7 +111,7 @@ export function renderDelegationInstructions({ harness, stage, capacity, works =
109
111
 
110
112
  export function renderCapacityInstructions({ harness, maximum }) {
111
113
  const adapter = requireAdapter(harness);
112
- if (!Number.isInteger(maximum) || maximum < 1) throw new Error("Invalid native capacity limit");
114
+ if (!Number.isInteger(maximum) || maximum < 1 || maximum > 15) throw new Error("Native capacity limit must be an integer from 1 through 15");
113
115
  return [
114
116
  `Adapter delegation contract: ${delegationContract(harness)}.`,
115
117
  "This is a technical native-subagent capacity qualification, not product work.",
@@ -255,6 +255,7 @@ export function parseCommandArgs(argv) {
255
255
  const positional = [], options = new Map();
256
256
  for (let index = 0; index < argv.length; index++) {
257
257
  const value = argv[index];
258
+ if (value === "--") { positional.push(...argv.slice(index + 1)); break; }
258
259
  if (value?.startsWith("--")) {
259
260
  const equals = value.indexOf("=");
260
261
  if (equals > 2) { const key = value.slice(2, equals); options.set(key, [...(options.get(key) ?? []), value.slice(equals + 1)]); continue; }
@@ -8,5 +8,6 @@ export function confirmDaemonProcess(config: RuntimeConfig, record: ProcessBindi
8
8
  export function assertDaemonOwnership(stateDir: string, dispatch?: Record<string, unknown>, assertCurrent?: () => void): Promise<void>;
9
9
  export function heartbeatDaemonProcess(config: RuntimeConfig, record: ProcessBinding | null | undefined): Promise<void>;
10
10
  export function finishDaemonProcess(config: RuntimeConfig, record: ProcessBinding | null | undefined, state?: string, reason?: string): Promise<void>;
11
+ export function cleanupFailedDaemonStart(config: RuntimeConfig, record: ProcessBinding | null, child: ChildProcess | null, original: Error & { details?: Record<string, unknown> }, readState: () => Promise<Record<string, unknown>>, daemonId: string): Promise<void>;
11
12
  export function completeBudgetOperation(stateDir: string, request: { id: string; operation: string; params?: { sessionId?: string } }, result: unknown, observe?: () => Promise<Record<string, unknown>>): Promise<Record<string, unknown>>;
12
13
  export function stopProcessGroup(child: ChildProcess | null | undefined, graceMs?: number, options?: { detached?: boolean; settlementMs?: number }): Promise<void>;
@@ -61,14 +61,16 @@ export async function prepareRuntimeOwner(config, stateDir, env = process.env) {
61
61
  export async function assertDaemonOwnership(stateDir, dispatch = {}, assertCurrent = () => {}, progress = async () => {}, waiting = async () => {}) {
62
62
  const gate = async () => {
63
63
  assertCurrent();
64
+ let confirmed = false;
64
65
  for (const lease of leases.values()) {
65
66
  if (lease.stateDir !== path.resolve(stateDir)) continue;
66
67
  await reconfirmLease(lease, assertCurrent);
67
68
  assertCurrent();
69
+ confirmed = true;
68
70
  }
71
+ if (confirmed) await progress('ownership_confirmed');
69
72
  };
70
73
  await gate();
71
- await progress('ownership_confirmed');
72
74
  const state = JSON.parse(await readFile(path.join(stateDir, "daemon.json"), "utf8"));
73
75
  const owner = bindRuntimeOwner({ runtime_owner: state.config?.runtime_owner ?? null }, {});
74
76
  if (!owner) return;
@@ -264,7 +266,7 @@ async function executeRuntimeProcess(config, action, options = {}, { timeoutMs,
264
266
  const poisonKey = token => JSON.stringify([path.resolve(resourceHome), options.id, token ?? null]);
265
267
  if (action !== "status" && options.id) {
266
268
  const poison = maintenancePoison.get(poisonKey(options["lease-token"])) ?? maintenancePoison.get(poisonKey(null));
267
- if (poison) throw poison;
269
+ if (poison && !(action === "finish" && poison.finish === JSON.stringify(options))) throw poison.error;
268
270
  }
269
271
  const args = ["runtime", "process", action];
270
272
  for (const [key, value] of Object.entries(options)) if (value !== null && value !== undefined) args.push(`--${key}`, String(value));
@@ -275,9 +277,13 @@ async function executeRuntimeProcess(config, action, options = {}, { timeoutMs,
275
277
  try { output = await runBoundedCommand({ bin: ddFlowBin, args: [...args, "--json"], cwd: config.cwd,
276
278
  env: { ...(config.env ?? {}), DD_FLOW_HOME: ddFlowHome, DD_FLOW_RESOURCE_HOME: resourceHome }, timeoutMs,
277
279
  timeoutCode: action === "heartbeat" ? "process_heartbeat_timeout" : "process_maintenance_timeout", inheritDeadline: false, cleanupWithinBudget: true,
278
- maintenanceProgress: { action, processId: options.id ?? null, startupMs: Math.min(timeoutMs, RENEWAL_POLICY.startupMs), inactivityMs: RENEWAL_POLICY.attemptMs }, signal }); }
280
+ // Storage silence is not loss of authority: the original command/lease
281
+ // deadline remains finite, including slow I/O and multiple bounded reads.
282
+ maintenanceProgress: { action, processId: options.id ?? null, startupMs: Math.min(timeoutMs, RENEWAL_POLICY.startupMs), inactivityMs: timeoutMs }, signal }); }
279
283
  catch (error) {
280
- if (action !== "status" && options.id && error.details?.cleanup_unconfirmed) maintenancePoison.set(poisonKey(options["lease-token"]), error);
284
+ if (action !== "status" && options.id && error.details?.cleanup_unconfirmed) maintenancePoison.set(poisonKey(options["lease-token"]), {
285
+ error, ...(action === "finish" && error.code === "process_maintenance_timeout" ? { finish: JSON.stringify(options) } : {})
286
+ });
281
287
  throw error;
282
288
  }
283
289
  let result;
@@ -393,7 +399,9 @@ export async function heartbeatDaemonProcess(config, record) {
393
399
  if (lease.closing) throw Object.assign(new Error("Managed process is closing"), { code: "process_closing" });
394
400
  if (lease.lost) throw lease.error;
395
401
  if (lease.error && !maintenanceRetryable(lease.error)) throw lease.error;
396
- if (!renewalRemaining(lease.renewal)) throw lease.error;
402
+ if (!renewalRemaining(lease.renewal)) throw lease.error ?? Object.assign(new Error("Managed lease authority has expired before heartbeat dispatch"), {
403
+ code: "process_ownership_unknown", details: { process_id: record.id, reason: "lease_authority_expired" }
404
+ });
397
405
  if (lease.pending) return await lease.pending;
398
406
  lease.pending = (async () => {
399
407
  try {
@@ -425,7 +433,7 @@ export async function finishDaemonProcess(config, process, state = "stopped", re
425
433
  // A predecessor owns its transport fence; finish gets an independent
426
434
  // settlement window only after that flight physically settled.
427
435
  const settlement = createRenewalState(); settlement.uncertainSince = performance.now();
428
- const result = await retryBookkeeping(timeoutMs => runtimeProcess(config, "finish", { id: process.id, "lease-token": process.lease_token, state, reason }, { timeoutMs }), settlement);
436
+ const result = await retryBookkeeping(timeoutMs => runtimeProcess(config, "finish", { id: process.id, "lease-token": process.lease_token, state, reason }, { timeoutMs }), settlement, true);
429
437
  if (result?.ok !== true) throw Object.assign(new Error("managed process finish was not accepted"), { code: "process_lease_lost", details: { process_id: process.id } });
430
438
  leases.delete(key);
431
439
  }
@@ -453,11 +461,16 @@ async function reconfirmLease(lease, assertCurrent) {
453
461
  return;
454
462
  }
455
463
  }
456
- async function retryBookkeeping(action, renewal = createRenewalState()) {
464
+ async function retryBookkeeping(action, renewal = createRenewalState(), repeatFinish = false) {
457
465
  renewal.uncertainSince ??= performance.now();
458
466
  for (;;) {
459
467
  try { return await action(renewalRemaining(renewal)); }
460
- catch (error) { if (!maintenanceRetryable(error) || !renewalFailure(renewal)) throw error; await new Promise(resolve => setTimeout(resolve, Math.min(renewalDelay(renewal), renewalRemaining(renewal)))); }
468
+ catch (error) {
469
+ // Exact finish is an idempotent terminal-state CAS, not productive IO.
470
+ // Its old helper may still commit the same finish; all other writes stay fenced.
471
+ if (!(maintenanceRetryable(error) || repeatFinish && error.code === "process_maintenance_timeout" && error.details?.cleanup_unconfirmed === true) || !renewalFailure(renewal)) throw error;
472
+ await new Promise(resolve => setTimeout(resolve, Math.min(renewalDelay(renewal), renewalRemaining(renewal))));
473
+ }
461
474
  }
462
475
  }
463
476
 
@@ -482,7 +495,10 @@ function requireTimelyMaintenance(renewal, action, receipt, record) {
482
495
  export async function cleanupFailedStart(config, record, child, original) {
483
496
  try {
484
497
  await stopProcessGroup(child);
498
+ const stopped = child?.pid ? { pid: child.pid, pgid: process.platform === "win32" ? null : child.pid,
499
+ settled: true, physical_exit: "ESRCH", observed_at: new Date().toISOString() } : undefined;
485
500
  await finishDaemonProcess(config, record, "failed", original.code ?? "startup_failed");
501
+ return stopped;
486
502
  } catch (cleanup) {
487
503
  original.details = { ...original.details, cleanup_failed: errorRecord(cleanup), process_id: record?.id ?? null, pid: child?.pid ?? null };
488
504
  }
@@ -491,7 +507,7 @@ export async function cleanupFailedStart(config, record, child, original) {
491
507
  /** The provider has its own process group. A failed daemon start must settle
492
508
  * that exact retained process as well as the daemon; PID-only cleanup is unsafe. */
493
509
  export async function cleanupFailedDaemonStart(config, daemonRecord, daemonChild, original, readState, daemonId) {
494
- await cleanupFailedStart(config, daemonRecord, daemonChild, original);
510
+ const stopped = await cleanupFailedStart(config, daemonRecord, daemonChild, original);
495
511
  if (!daemonRecord) return;
496
512
  try {
497
513
  const state = await readState();
@@ -502,6 +518,15 @@ export async function cleanupFailedDaemonStart(config, daemonRecord, daemonChild
502
518
  }
503
519
  const provider = state.provider_process;
504
520
  if (provider) await runtimeProcess(config, "stop", { id: provider.id, "lease-token": provider.lease_token });
521
+ // Confirmation can fail before the registry learns the child's PID. The
522
+ // owned child handle still proves its physical exit, not a productive ACK.
523
+ if (stopped && !original.details?.cleanup_failed && !original.details?.provider_cleanup_failed && state.pid === stopped.pid && !provider && state.provider_pid == null && owner) {
524
+ original.details = { ...original.details, startup_child_cleanup: {
525
+ ...stopped, daemon_id: daemonId, process_id: daemonRecord.id,
526
+ owner_id: owner.owner_id, project_id: owner.project_id, run_id: owner.run_id,
527
+ operation_id: owner.operation_id, state_dir: owner.state_dir
528
+ } };
529
+ }
505
530
  } catch (cleanup) {
506
531
  original.details = { ...original.details, provider_cleanup_failed: errorRecord(cleanup) };
507
532
  }
@@ -587,29 +612,39 @@ async function stopOwnedChild(child, graceMs, detached, settlementMs) {
587
612
 
588
613
  /** A daemon acknowledgement is not a completed stop until its endpoint closes. */
589
614
  export async function confirmDaemonStopped(request, socket, timeoutMs = 30_000, { readState } = {}) {
590
- const deadline = performance.now() + timeoutMs;
591
615
  const expected = await readState?.();
592
616
  const identity = state => state?.daemon_id ?? state?.config?.daemonId;
593
617
  if (readState && (!identity(expected) || !Number.isSafeInteger(expected?.pid) || expected.pid < 1)) throw Object.assign(new Error('daemon stop has no retained incarnation/PID proof'), { code: 'daemon_cleanup_identity_mismatch' });
594
618
  const failure = async observer => {
595
619
  const state = await readState?.();
596
- if (expected && identity(state) !== identity(expected)) throw Object.assign(new Error("daemon incarnation changed during stop"), { code: "daemon_identity_mismatch" });
620
+ if (expected && (identity(state) !== identity(expected) || state?.pid !== expected.pid)) throw Object.assign(new Error("daemon incarnation changed during stop"), { code: "daemon_identity_mismatch" });
597
621
  const primary = state?.shutdown_state === "cleanup_failed" ? state.cleanup_error : null;
598
622
  if (primary) throw Object.assign(new Error(primary.message), primary, { details: { ...primary.details, socket, phase: "confirm_daemon_stop", observer_error: errorRecord(observer) } });
599
623
  throw observer;
600
624
  };
601
- let result;
625
+ let result, observationError;
602
626
  try { result = await request(); }
603
- catch (error) { return await failure(error); }
604
- if (result?.stopped !== true) return await failure(Object.assign(new Error("stop acknowledgement does not prove shutdown"), { code: "daemon_stop_incomplete" }));
627
+ catch (error) {
628
+ if (!expected || !["daemon_timeout", "daemon_not_running", "daemon_connection_closed", "operation_observation_lost"].includes(error.code)) return await failure(error);
629
+ observationError = error;
630
+ }
631
+ if (!observationError && result?.stopped !== true) return await failure(Object.assign(new Error("stop acknowledgement does not prove shutdown"), { code: "daemon_stop_incomplete" }));
632
+ // The RPC wait and physical retirement are separate phases. A lost ACK
633
+ // consumes the former, not the existing confirmation budget.
634
+ const deadline = performance.now() + timeoutMs;
605
635
  for (;;) {
606
636
  const state = await readState?.();
607
- if (expected && identity(state) !== identity(expected)) return await failure(Object.assign(new Error("daemon incarnation changed"), { code: "daemon_identity_mismatch" }));
637
+ if (expected && (identity(state) !== identity(expected) || state?.pid !== expected.pid)) return await failure(Object.assign(new Error("daemon incarnation changed"), { code: "daemon_identity_mismatch" }));
638
+ if (state?.shutdown_state === "cleanup_failed") return await failure(observationError ?? Object.assign(new Error("daemon cleanup failed"), { code: "daemon_stop_incomplete" }));
608
639
  let endpointPresent = true;
609
640
  try { await lstat(socket); } catch (error) { if (error.code === "ENOENT") endpointPresent = false; else throw error; }
610
641
  let daemonPresent = false;
611
642
  if (expected?.pid) { try { process.kill(expected.pid, 0); daemonPresent = true; } catch (error) { if (error.code !== "ESRCH") daemonPresent = true; } }
612
643
  if (!endpointPresent && !daemonPresent) {
644
+ if (observationError) {
645
+ if (state?.shutdown_state === "clean" && state.active_tree === false && !state.cleanup_error) return { stopped: true, clean: true };
646
+ return await failure(observationError);
647
+ }
613
648
  if (readState && result.clean === true && (state?.shutdown_state !== 'clean' || state.active_tree !== false)) return await failure(Object.assign(new Error('daemon clean ACK conflicts with its retained state'), { code: 'daemon_stop_incomplete' }));
614
649
  return result;
615
650
  }
@@ -0,0 +1,15 @@
1
+ /** Final native overlay: launch-bound routing, never inherited invocation authority. */
2
+ export function managedFlowEnv(config, daemonId = config.daemonId) {
3
+ const managed = !config.noFlow && Boolean(config.ddFlowHome && config.ddFlowBin && daemonId);
4
+ return {
5
+ DD_FLOW_DAEMON_ID: managed ? daemonId : undefined,
6
+ DD_FLOW_HOME: managed ? config.ddFlowHome : undefined,
7
+ DD_FLOW_BIN: managed ? config.ddFlowBin : undefined,
8
+ DD_FLOW_RESOURCE_HOME: config.resourceHome ?? undefined,
9
+ DD_FLOW_RUNTIME_OWNER: managed && config.runtime_owner ? JSON.stringify(config.runtime_owner) : undefined,
10
+ DD_FLOW_INVOCATION_SCOPE: undefined, DD_FLOW_CURRENT_INVOCATION: undefined,
11
+ DD_FLOW_SESSION_ID: undefined, DD_FLOW_WORK_ID: undefined, DD_FLOW_CONTROLLER_TOKEN: undefined,
12
+ DD_FLOW_CONTROL_TOKEN: undefined, DD_FLOW_SCOPE_CONTROL_TOKEN: undefined,
13
+ DD_FLOW_HOOK_DEADLINE_AT: undefined,
14
+ };
15
+ }
@@ -62,7 +62,7 @@ export async function readNativeHookFailures(stateDir, requests, current, reject
62
62
  }
63
63
 
64
64
  /** The caller supplies its qualified native limit; phases share that deadline. */
65
- export function hookBudget(timeoutMs = 25_000, deadlineAt = process.env.DD_FLOW_HOOK_DEADLINE_AT === undefined ? null : Number(process.env.DD_FLOW_HOOK_DEADLINE_AT)) {
65
+ export function hookBudget(timeoutMs = 25_000, deadlineAt = process.env.DD_FLOW_HOOK_DEADLINE_AT ? Number(process.env.DD_FLOW_HOOK_DEADLINE_AT) : null) {
66
66
  if (deadlineAt !== null && (!Number.isSafeInteger(deadlineAt) || deadlineAt < 0)) throw Object.assign(new Error("Invalid native hook deadline"), { code: "hook_deadline_invalid", details: { effect: "unknown" } });
67
67
  const started = performance.now();
68
68
  const duration = Math.max(0, Math.min(timeoutMs, Number.isFinite(deadlineAt) ? deadlineAt - Date.now() : timeoutMs));
@@ -0,0 +1,2 @@
1
+ export function nativeCommandFingerprint(input: { executable: string; args: string[]; cwd: string; env: Record<string, string | undefined>; operationId: string }): string;
2
+ export function prepareDaemonRequest<T extends { id: string; operation: string; params?: Record<string, unknown> }>(request: T): T & { invocation_sha256?: string };
@@ -0,0 +1,65 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import process from 'node:process';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { hashJson } from './runner-events.mjs';
6
+
7
+ const environmentKeys = ['HOME', 'CODEX_HOME', 'DD_FLOW_CONFIG_HOME', 'DD_FLOW_RESOURCE_HOME', 'DD_FLOW_HOME', 'DD_FLOW_BIN', 'DD_FLOW_ENGINE_HOME'];
8
+ const fail = reason => { throw Object.assign(new Error('Native request proof does not match its prepared command'), {
9
+ code: 'native_request_proof_conflict', details: { reason, effect: 'no_effect' }
10
+ }); };
11
+
12
+ /** Independent transport identity: prepared by the caller before launching
13
+ * the adapter, and recomputed by the adapter's shared request builder. */
14
+ export function nativeCommandFingerprint({ executable, args, cwd, env, operationId }) {
15
+ return hashJson({ executable, args, cwd, operation_id: operationId,
16
+ environment: Object.fromEntries(environmentKeys.map(key => [key, env[key] ?? null])) });
17
+ }
18
+
19
+ /** Called after the adapter's own argument preparation but before its RPC or
20
+ * daemon-start action. No provider response can supply this fingerprint. */
21
+ export function prepareDaemonRequest(request) {
22
+ const file = process.env.DD_FLOW_NATIVE_REQUEST_PROOF_FILE;
23
+ const expected = process.env.DD_FLOW_NATIVE_COMMAND_SHA256;
24
+ const operation = process.env.DD_FLOW_NATIVE_REQUEST_OPERATION;
25
+ if (file === undefined && expected === undefined) return request;
26
+ // Control helpers may inspect status before their owned stop RPC. Status
27
+ // cannot consume the productive/control request's independently bound proof.
28
+ if (operation && request.operation !== operation) {
29
+ if (request.operation === 'daemon.status' && operation === 'daemon.stop') return request;
30
+ fail('operation_changed');
31
+ }
32
+ const ownerText = process.env.DD_FLOW_NATIVE_REQUEST_PROOF_OWNER;
33
+ // Never forward caller proof authority into the daemon/provider process.
34
+ delete process.env.DD_FLOW_NATIVE_REQUEST_PROOF_FILE;
35
+ delete process.env.DD_FLOW_NATIVE_COMMAND_SHA256;
36
+ delete process.env.DD_FLOW_NATIVE_REQUEST_OPERATION;
37
+ delete process.env.DD_FLOW_NATIVE_REQUEST_PROOF_OWNER;
38
+ if (!file || !path.isAbsolute(file) || !/^[a-f0-9]{64}$/.test(expected ?? '')
39
+ || typeof request.id !== 'string' || !request.id || typeof request.operation !== 'string' || request.operation !== operation) fail('invalid_owner');
40
+ const command = nativeCommandFingerprint({ executable: process.argv[1], args: process.argv.slice(2),
41
+ cwd: process.cwd(), env: process.env, operationId: request.id });
42
+ if (command !== expected || fs.realpathSync(path.dirname(file)) !== path.dirname(file)) fail('command_changed');
43
+ let owner;
44
+ try { owner = JSON.parse(ownerText ?? 'null'); } catch { fail('invalid_parent_owner'); }
45
+ const parent = fs.lstatSync(path.dirname(file));
46
+ if (!parent.isDirectory() || parent.dev !== owner?.dev || parent.ino !== owner?.ino) fail('proof_parent_changed');
47
+ const proof = { schema_id: 'dd-flow/native-request-proof@1', operation_id: request.id, operation: request.operation,
48
+ command_sha256: command, params_sha256: hashJson({ operation: request.operation, params: request.params ?? {} }),
49
+ session_id: request.params?.sessionId ?? null };
50
+ const temporary = `${file}.${randomUUID()}.tmp`;
51
+ const fd = fs.openSync(temporary, 'wx', 0o600);
52
+ try {
53
+ fs.writeFileSync(fd, JSON.stringify(proof)); fs.fsyncSync(fd);
54
+ const current = fs.lstatSync(path.dirname(file));
55
+ if (parent.dev !== current.dev || parent.ino !== current.ino || !current.isDirectory()) fail('proof_parent_changed');
56
+ try { fs.linkSync(temporary, file); }
57
+ catch (error) {
58
+ if (error.code !== 'EEXIST') throw error;
59
+ const stat = fs.lstatSync(file);
60
+ if (!stat.isFile() || stat.nlink !== 1 || stat.size > 16 * 1024
61
+ || hashJson(JSON.parse(fs.readFileSync(file, 'utf8'))) !== hashJson(proof)) fail('retained_request_changed');
62
+ }
63
+ } finally { fs.closeSync(fd); fs.unlinkSync(temporary); }
64
+ return { ...request, invocation_sha256: command };
65
+ }
@@ -1,4 +1,5 @@
1
1
  import path from "node:path";
2
+ import { managedFlowEnv } from './managed-flow-env.mjs';
2
3
  import * as Plugin from "../vendor/opencode-plugin.mjs";
3
4
  import { hookBudget, invokeNativeHook } from "./native-hook-command.mjs";
4
5
 
@@ -26,6 +27,7 @@ export function makeOpenCodePlugin(binding, invoke = invokeNativeHook) {
26
27
  args: ["opencode", "event", "handle", "--project-root", binding.projectRoot, "--json"],
27
28
  payload, home: binding.ddFlowHome, timeoutMs: budget.remaining("admission", 15000) });
28
29
  await ctx.shell.hook("create.before", event => {
30
+ Object.assign(event.env, managedFlowEnv(binding));
29
31
  delete event.env.OPENCODE_PASSWORD; delete event.env.OPENCODE_SERVER_PASSWORD;
30
32
  });
31
33
  await ctx.tool.hook("execute.before", async event => {
@@ -64,7 +64,9 @@ export function publishOperationProgress(root, id, phase, metadata = {}) {
64
64
  const prior = await readOperationProgress(root, id);
65
65
  const observedAt = metadata.observed_at ?? new Date().toISOString();
66
66
  if (prior && Date.parse(observedAt)<Date.parse(prior.observed_at)) return prior;
67
- if (prior && phase === prior.phase && phase !== 'native_progress') return prior;
67
+ // A later successful ownership gate is fresh liveness evidence while admission waits.
68
+ if (prior && phase === prior.phase && phase !== 'native_progress'
69
+ && (phase !== 'ownership_confirmed' || Date.parse(observedAt) <= Date.parse(prior.observed_at))) return prior;
68
70
  if (prior && phase === 'native_progress' && Date.parse(observedAt) - Date.parse(prior.observed_at) < 1_000) return prior;
69
71
  const value = { schema_id: OPERATION_PROGRESS_CONTRACT,
70
72
  ...Object.fromEntries(fields.map(field => [field, requested[field] ?? null])),
@@ -11,7 +11,7 @@ export function createRouterContext(env, deadlineAt) {
11
11
  }
12
12
  export function createContext(env, mode = "initialize", importPermitId, options = {}) {
13
13
  const ddFlowHome = resolveDdFlowHome(env);
14
- if (options.resourceDatabaseMode === "prepared_existing" || options.resourceDatabaseMode === "initialize")
14
+ if (options.resourceDatabaseMode === "prepared_existing" || options.resourceDatabaseMode === "initialize" || options.resourceDatabaseMode === "create_new")
15
15
  assertImportAdmission(ddFlowHome, importPermitId);
16
16
  return {
17
17
  ddFlowHome,
@@ -0,0 +1,184 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "dd-flow/discovery-result@1",
4
+ "type": "object",
5
+ "additionalProperties": false,
6
+ "required": [
7
+ "schema_id",
8
+ "request_id",
9
+ "observed_at",
10
+ "probe_scope",
11
+ "live_authorization_receipt",
12
+ "entries"
13
+ ],
14
+ "properties": {
15
+ "schema_id": {
16
+ "const": "dd-flow/discovery-result@1"
17
+ },
18
+ "request_id": {
19
+ "type": "string",
20
+ "minLength": 1
21
+ },
22
+ "observed_at": {
23
+ "type": "string",
24
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\.[0-9]+)?Z$"
25
+ },
26
+ "probe_scope": {
27
+ "enum": [
28
+ "static",
29
+ "non-generative",
30
+ "live"
31
+ ]
32
+ },
33
+ "live_authorization_receipt": {
34
+ "anyOf": [
35
+ {
36
+ "type": "object",
37
+ "additionalProperties": false,
38
+ "required": [
39
+ "path",
40
+ "sha256"
41
+ ],
42
+ "properties": {
43
+ "path": {
44
+ "type": "string",
45
+ "pattern": "^[a-zA-Z0-9_-][a-zA-Z0-9_.-]*(/[a-zA-Z0-9_-][a-zA-Z0-9_.-]*)*$"
46
+ },
47
+ "sha256": {
48
+ "type": "string",
49
+ "pattern": "^[a-f0-9]{64}$"
50
+ }
51
+ }
52
+ },
53
+ {
54
+ "type": "null"
55
+ }
56
+ ]
57
+ },
58
+ "entries": {
59
+ "type": "array",
60
+ "minItems": 0,
61
+ "items": {
62
+ "type": "object",
63
+ "additionalProperties": false,
64
+ "required": [
65
+ "harness",
66
+ "candidates",
67
+ "installed",
68
+ "compatible",
69
+ "protocol",
70
+ "auth",
71
+ "qualified",
72
+ "reason",
73
+ "evidence"
74
+ ],
75
+ "properties": {
76
+ "harness": {
77
+ "enum": [
78
+ "codex",
79
+ "zcode",
80
+ "agy",
81
+ "opencode",
82
+ "grok",
83
+ "droid"
84
+ ]
85
+ },
86
+ "candidates": {
87
+ "type": "array",
88
+ "minItems": 0,
89
+ "items": {
90
+ "type": "object",
91
+ "additionalProperties": false,
92
+ "required": [
93
+ "path",
94
+ "version",
95
+ "selected"
96
+ ],
97
+ "properties": {
98
+ "path": {
99
+ "type": "string",
100
+ "minLength": 1
101
+ },
102
+ "version": {
103
+ "anyOf": [
104
+ {
105
+ "type": "string",
106
+ "minLength": 1
107
+ },
108
+ {
109
+ "type": "null"
110
+ }
111
+ ]
112
+ },
113
+ "selected": {
114
+ "type": "boolean"
115
+ }
116
+ }
117
+ }
118
+ },
119
+ "installed": {
120
+ "enum": [
121
+ "yes",
122
+ "no",
123
+ "unknown"
124
+ ]
125
+ },
126
+ "compatible": {
127
+ "enum": [
128
+ "yes",
129
+ "no",
130
+ "unknown"
131
+ ]
132
+ },
133
+ "protocol": {
134
+ "enum": [
135
+ "verified",
136
+ "incompatible",
137
+ "unknown"
138
+ ]
139
+ },
140
+ "auth": {
141
+ "enum": [
142
+ "verified",
143
+ "unavailable",
144
+ "unknown"
145
+ ]
146
+ },
147
+ "qualified": {
148
+ "enum": [
149
+ "yes",
150
+ "no",
151
+ "unknown"
152
+ ]
153
+ },
154
+ "reason": {
155
+ "type": "string",
156
+ "minLength": 1
157
+ },
158
+ "evidence": {
159
+ "type": "array",
160
+ "minItems": 0,
161
+ "items": {
162
+ "type": "object",
163
+ "additionalProperties": false,
164
+ "required": [
165
+ "path",
166
+ "sha256"
167
+ ],
168
+ "properties": {
169
+ "path": {
170
+ "type": "string",
171
+ "pattern": "^[a-zA-Z0-9_-][a-zA-Z0-9_.-]*(/[a-zA-Z0-9_-][a-zA-Z0-9_.-]*)*$"
172
+ },
173
+ "sha256": {
174
+ "type": "string",
175
+ "pattern": "^[a-f0-9]{64}$"
176
+ }
177
+ }
178
+ }
179
+ }
180
+ }
181
+ }
182
+ }
183
+ }
184
+ }