@deksden-com/dd-flow-cli 0.9.0-beta.152 → 0.9.0-beta.155

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 (226) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/build-info.json +8 -8
  3. package/dist/cli/command-inputs.js +33 -3
  4. package/dist/cli/command-metadata.js +73 -2
  5. package/dist/cli/delivery.js +108 -0
  6. package/dist/cli/diagnostic-invocation.js +23 -7
  7. package/dist/cli/diagnostic.js +14 -3
  8. package/dist/cli/help.js +117 -1
  9. package/dist/cli/hook-ingress.js +36 -49
  10. package/dist/cli/interactive-invocation.js +33 -0
  11. package/dist/cli/interactive.js +114 -0
  12. package/dist/cli/memory-fix-invocation.js +52 -0
  13. package/dist/cli/memory-fix.js +100 -0
  14. package/dist/cli/memory-flow-admission.js +7 -3
  15. package/dist/cli/memory-init.js +5 -4
  16. package/dist/cli/native-hook-ingress.js +5 -1
  17. package/dist/cli/native-hook-preparation.js +31 -4
  18. package/dist/cli/run-cli.js +273 -19
  19. package/dist/cli.js +17 -3
  20. package/dist/domain/delivery-flows.js +8 -0
  21. package/dist/domain/distill-flow.js +20 -0
  22. package/dist/domain/interactive-flow.js +1 -0
  23. package/dist/domain/memory-fix-flow.js +2 -0
  24. package/dist/domain/review-fix-flow.js +2 -0
  25. package/dist/domain/source-snapshot-flows.js +4 -0
  26. package/dist/harness-runtime/bin/dd-agy.mjs +18 -3
  27. package/dist/harness-runtime/bin/dd-grok.mjs +15 -5
  28. package/dist/harness-runtime/bin/dd-zcode.mjs +15 -7
  29. package/dist/harness-runtime/lib/agy-error-boundary.mjs +18 -10
  30. package/dist/harness-runtime/lib/agy-native-inspection.mjs +12 -3
  31. package/dist/harness-runtime/lib/agy-native-inventory.mjs +19 -0
  32. package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +13 -6
  33. package/dist/harness-runtime/lib/agy-transcript-stream.mjs +54 -0
  34. package/dist/harness-runtime/lib/daemon-observation.d.mts +1 -0
  35. package/dist/harness-runtime/lib/daemon-observation.mjs +12 -4
  36. package/dist/harness-runtime/lib/daemon-operations.mjs +20 -5
  37. package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +1 -0
  38. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +371 -114
  39. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +43 -3
  40. package/dist/harness-runtime/lib/dd-codex.mjs +36 -4
  41. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +25 -11
  42. package/dist/harness-runtime/lib/dd-droid.mjs +124 -11
  43. package/dist/harness-runtime/lib/dd-grok-daemon.d.mts +1 -0
  44. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +137 -25
  45. package/dist/harness-runtime/lib/dd-grok.mjs +4 -2
  46. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +114 -16
  47. package/dist/harness-runtime/lib/dd-opencode.mjs +10 -7
  48. package/dist/harness-runtime/lib/dd-zcode-daemon.d.mts +1 -0
  49. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +115 -22
  50. package/dist/harness-runtime/lib/dd-zcode.mjs +75 -36
  51. package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -0
  52. package/dist/harness-runtime/lib/delegation-instructions.mjs +10 -3
  53. package/dist/harness-runtime/lib/delivery-command.mjs +84 -0
  54. package/dist/harness-runtime/lib/driver-recovery.mjs +0 -19
  55. package/dist/harness-runtime/lib/droid-observation.mjs +9 -5
  56. package/dist/harness-runtime/lib/lifecycle-contract.d.mts +1 -0
  57. package/dist/harness-runtime/lib/lifecycle-contract.mjs +2 -1
  58. package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -2
  59. package/dist/harness-runtime/lib/native-hook-command.mjs +63 -8
  60. package/dist/harness-runtime/lib/native-streams.mjs +3 -10
  61. package/dist/harness-runtime/lib/opencode-plugin.mjs +36 -9
  62. package/dist/harness-runtime/lib/operation-errors.mjs +13 -0
  63. package/dist/harness-runtime/lib/operation-progress.mjs +6 -2
  64. package/dist/harness-runtime/lib/process-json.mjs +4 -5
  65. package/dist/harness-runtime/lib/tool-observations.mjs +20 -10
  66. package/dist/harness-runtime/lib/zcode-native-hook-ownership.mjs +109 -0
  67. package/dist/schemas/code-work-result.schema.json +2 -1
  68. package/dist/schemas/delivery-stage-result.schema.json +286 -0
  69. package/dist/schemas/distill-stage-result.schema.json +667 -0
  70. package/dist/schemas/distill-work-result.schema.json +428 -0
  71. package/dist/schemas/execution-bundle.schema.json +6 -2
  72. package/dist/schemas/interactive-stage-result.schema.json +47 -0
  73. package/dist/schemas/interactive-work-result.schema.json +63 -0
  74. package/dist/schemas/mb-fix-stage-result.schema.json +93 -0
  75. package/dist/schemas/project-flow-pack-manifest.schema.json +2 -2
  76. package/dist/schemas/review-fix-stage-result.schema.json +210 -0
  77. package/dist/schemas/run-control-receipt.schema.json +1 -0
  78. package/dist/services/assigned-lifecycle.js +382 -0
  79. package/dist/services/check-runner-evidence.js +45 -8
  80. package/dist/services/cli-operation-classifier.js +6 -1
  81. package/dist/services/code-checks.js +46 -29
  82. package/dist/services/codex-hook-delivery.js +11 -2
  83. package/dist/services/controller-execution.js +4 -1
  84. package/dist/services/controller-fanout.js +9 -4
  85. package/dist/services/controller-memory-worker.js +6 -4
  86. package/dist/services/controller-result-authority.js +122 -0
  87. package/dist/services/dashboard.js +4 -2
  88. package/dist/services/delivery-access.js +123 -0
  89. package/dist/services/delivery-approval.js +68 -0
  90. package/dist/services/delivery-artifact.js +86 -0
  91. package/dist/services/delivery-checks.js +178 -0
  92. package/dist/services/delivery-execution.js +181 -0
  93. package/dist/services/delivery-intents.js +212 -0
  94. package/dist/services/delivery-operation.js +81 -0
  95. package/dist/services/delivery-policy.js +91 -0
  96. package/dist/services/delivery-procedures.js +43 -0
  97. package/dist/services/delivery-request.js +99 -0
  98. package/dist/services/delivery-result.js +144 -0
  99. package/dist/services/delivery-stage-commands.js +26 -0
  100. package/dist/services/delivery-stage-entry.js +56 -0
  101. package/dist/services/delivery-stage-handlers.js +135 -0
  102. package/dist/services/delivery-stage-results.js +94 -0
  103. package/dist/services/descriptor-coordinator-session.js +3 -1
  104. package/dist/services/descriptor-run-start.js +24 -4
  105. package/dist/services/descriptor-stage-coordinator.js +78 -10
  106. package/dist/services/descriptor-stage-settlement.js +59 -14
  107. package/dist/services/diagnostic-repair-intake.js +123 -0
  108. package/dist/services/diagnostic-repair-source-intake.js +102 -0
  109. package/dist/services/diagnostic-report.js +30 -0
  110. package/dist/services/diagnostic-run-bundle.js +48 -9
  111. package/dist/services/diagnostic-snapshot.js +2 -2
  112. package/dist/services/diagnostic-stage-handlers.js +3 -2
  113. package/dist/services/diagnostic-stage-results.js +26 -0
  114. package/dist/services/diagnostic-start.js +56 -6
  115. package/dist/services/diagnostic-work-collection.js +3 -3
  116. package/dist/services/distill-document-publication.js +280 -0
  117. package/dist/services/distill-promotion.js +197 -0
  118. package/dist/services/distill-publication-custody.js +162 -0
  119. package/dist/services/distill-stage-handlers.js +249 -0
  120. package/dist/services/distill-stage-results.js +105 -0
  121. package/dist/services/distill-user-inputs.js +4 -0
  122. package/dist/services/distill-work-batch.js +42 -0
  123. package/dist/services/distill-work-collection.js +74 -0
  124. package/dist/services/distill-work-registration.js +93 -0
  125. package/dist/services/distill-work-results.js +98 -0
  126. package/dist/services/eval-snapshots.js +17 -7
  127. package/dist/services/external-work-launch.js +8 -2
  128. package/dist/services/flow-catalog.js +24 -0
  129. package/dist/services/frozen-engine.js +19 -2
  130. package/dist/services/generic-execution.js +4 -3
  131. package/dist/services/harness-adapter.js +2 -14
  132. package/dist/services/hook-memory-routing.js +1 -1
  133. package/dist/services/hooks.js +31 -6
  134. package/dist/services/hybrid-contracts.js +6 -1
  135. package/dist/services/installed-flow-source.js +37 -2
  136. package/dist/services/interactive-attached-session.js +215 -0
  137. package/dist/services/interactive-baseline-custody.js +54 -0
  138. package/dist/services/interactive-before-versions.js +148 -0
  139. package/dist/services/interactive-bundle-data.js +85 -0
  140. package/dist/services/interactive-checks.js +179 -0
  141. package/dist/services/interactive-execution.js +33 -0
  142. package/dist/services/interactive-finish-integration.js +236 -0
  143. package/dist/services/interactive-git-baseline.js +230 -0
  144. package/dist/services/interactive-git-dispatch.js +278 -0
  145. package/dist/services/interactive-input-delivery.js +82 -0
  146. package/dist/services/interactive-mutation-scope.js +231 -0
  147. package/dist/services/interactive-request.js +287 -0
  148. package/dist/services/interactive-run-bundle.js +109 -0
  149. package/dist/services/interactive-stage-entry.js +41 -0
  150. package/dist/services/interactive-stage-handlers.js +289 -0
  151. package/dist/services/interactive-stage-observation.js +136 -0
  152. package/dist/services/interactive-stage-results.js +111 -0
  153. package/dist/services/interactive-user-inputs.js +142 -0
  154. package/dist/services/interactive-work-batch.js +43 -0
  155. package/dist/services/interactive-work-collection.js +119 -0
  156. package/dist/services/interactive-work-completion.js +180 -0
  157. package/dist/services/interactive-work-context.js +249 -0
  158. package/dist/services/interactive-work-history.js +112 -0
  159. package/dist/services/interactive-work-registration.js +124 -0
  160. package/dist/services/interactive-work-results.js +115 -0
  161. package/dist/services/interactive-work-start.js +155 -0
  162. package/dist/services/lifecycle-invocations.js +214 -24
  163. package/dist/services/managed-daemon-binding.js +52 -1
  164. package/dist/services/memory-compose-handler.js +2 -22
  165. package/dist/services/memory-finish-integration.js +58 -17
  166. package/dist/services/memory-fix-finish-integration.js +129 -0
  167. package/dist/services/memory-fix-git-dispatch.js +139 -0
  168. package/dist/services/memory-fix-intake-custody.js +11 -0
  169. package/dist/services/memory-fix-intake-settlement.js +127 -0
  170. package/dist/services/memory-fix-publication.js +196 -0
  171. package/dist/services/memory-fix-repair-admission.js +116 -0
  172. package/dist/services/memory-fix-request.js +141 -0
  173. package/dist/services/memory-fix-run-bundle.js +83 -0
  174. package/dist/services/memory-fix-scope.js +78 -0
  175. package/dist/services/memory-fix-sources.js +85 -0
  176. package/dist/services/memory-fix-stage-entry.js +24 -0
  177. package/dist/services/memory-fix-stage-handlers.js +282 -0
  178. package/dist/services/memory-fix-stage-results.js +106 -0
  179. package/dist/services/memory-fix-workspace.js +127 -0
  180. package/dist/services/memory-flow-user-inputs.js +88 -0
  181. package/dist/services/memory-init-contexts.js +55 -5
  182. package/dist/services/memory-init-controller-start.js +20 -4
  183. package/dist/services/memory-layer.js +7 -3
  184. package/dist/services/memory-native-review-copy.js +27 -3
  185. package/dist/services/memory-repair-document-delta.js +1 -0
  186. package/dist/services/project-flow-source.js +195 -0
  187. package/dist/services/project-git-integration.js +92 -0
  188. package/dist/services/repair-intents.js +2 -7
  189. package/dist/services/retained-native-hook.js +245 -0
  190. package/dist/services/review-fix-handoff.js +82 -0
  191. package/dist/services/review-fix-intake-custody.js +17 -0
  192. package/dist/services/review-fix-stage-entry.js +43 -0
  193. package/dist/services/review-fix-stage-handlers.js +176 -0
  194. package/dist/services/review-fix-stage-results.js +86 -0
  195. package/dist/services/run-control-worker.js +42 -1
  196. package/dist/services/run-control.js +40 -5
  197. package/dist/services/run-controller-adapter.js +37 -3
  198. package/dist/services/run-controller-process.js +12 -4
  199. package/dist/services/run-controller-recovery.js +46 -6
  200. package/dist/services/run-controller-state.js +3 -2
  201. package/dist/services/run-controller.js +209 -43
  202. package/dist/services/run-recovery.js +31 -2
  203. package/dist/services/run-timeline.js +59 -0
  204. package/dist/services/runs.js +18 -31
  205. package/dist/services/sessions.js +2 -1
  206. package/dist/services/stage-handlers.js +55 -3
  207. package/dist/services/stage-lifecycle.js +4 -3
  208. package/dist/services/stage-pause.js +26 -9
  209. package/dist/services/stage-report-renderer.js +2 -6
  210. package/dist/services/technical-notes.js +31 -0
  211. package/dist/services/verification-matrix.js +1 -1
  212. package/dist/services/version-status.js +1 -1
  213. package/dist/services/vnext-code-review.js +74 -5
  214. package/dist/services/vnext-code.js +8 -32
  215. package/dist/services/vnext-fanout.js +53 -8
  216. package/dist/services/vnext-merge.js +10 -5
  217. package/dist/services/vnext-plan-review.js +1 -1
  218. package/dist/services/vnext-plan.js +1 -1
  219. package/dist/services/vnext-protocolize.js +1 -1
  220. package/dist/services/vnext-specify.js +10 -5
  221. package/dist/services/work-registry.js +252 -66
  222. package/dist/services/worktrees.js +7 -4
  223. package/dist/storage/database.js +2 -1
  224. package/dist/verification-components.json +82 -0
  225. package/package.json +3 -2
  226. package/tools/verification-jobs.mjs +35 -10
@@ -11,9 +11,9 @@ import net from "node:net";
11
11
  import { durableDaemonDispatch, completeDaemonShutdown, commitDaemonShutdown, assertDaemonDispatch, assertDaemonRestartable, trackDaemonConnections, retireDaemonEndpoint, retireFailedDaemon } from "./daemon-operations.mjs";
12
12
  import path from "node:path";
13
13
  import { assertStartupChild, prepareRuntimeOwner, cleanupFailedDaemonStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, startManagedBridge, stopProcessGroup } from "./managed-daemon.mjs";
14
- import { acceptNativeHookRequest, hookBudget, readNativeHookFailures } from "./native-hook-command.mjs";
15
- import { parseLifecycleCommand, continuationCommand } from "./lifecycle-command.mjs";
16
- import { errorRecord, isProvenShellCorrection } from "./operation-errors.mjs";
14
+ import { acceptNativeHookRequest, readNativeHookFailures, writeNativeHookFailure } from "./native-hook-command.mjs";
15
+ import { parseLifecycleCommand, continuationCommand, commandOption } from "./lifecycle-command.mjs";
16
+ import { errorRecord, isNativeHookObservationLoss, isProvenShellCorrection, reportedError } from "./operation-errors.mjs";
17
17
 
18
18
  import {
19
19
  AcpBridge,
@@ -225,7 +225,7 @@ export class DaemonRuntime {
225
225
  const adapter = params.adapterSessionId ?? known?.adapter_session_id ?? params.sessionId;
226
226
  const generation = this.activeGeneration ?? this.controlGeneration ?? 0;
227
227
  return {
228
- ...this.state.config, ...params, operationId: this.state.active_request?.request_id, knownChildSessionIds: known?.known_child_session_ids ?? [], adapterSessionId: adapter,
228
+ ...this.state.config, ...params, operationId: this.state.active_request?.request_id, nativeHookOwner: this.nativeHookOperation ?? null, knownChildSessionIds: known?.known_child_session_ids ?? [], adapterSessionId: adapter,
229
229
  neverPromptedRootProof: this.neverPromptedRootProof(params.sessionId),
230
230
  daemonId: this.state.daemon_id, allowBackground: true, liveSession: this.liveAdapters.has(adapter),
231
231
  assertDispatch: () => { if (generation !== (this.controlGeneration ?? 0)) throw new DaemonError("operation_cancelled", "ZCode prompt was cancelled before native dispatch"); },
@@ -369,7 +369,8 @@ export class DaemonRuntime {
369
369
  if (this.activeProductive) throw new DaemonError("operation_busy", `${this.activeProductive} is already running`, true);
370
370
  this.activeProductive = name;
371
371
  this.activeGeneration = this.controlGeneration ?? 0;
372
- const current = this.nativeHookOperation = { operationId: request?.operation_id, generation: this.activeGeneration, requests: new Map() };
372
+ const current = this.nativeHookOperation = { operationId: request?.operation_id, rootProviderSessionId: request?.session_id, generation: this.activeGeneration, requests: new Map() };
373
+ current.valid = () => current.generation === (this.controlGeneration ?? 0);
373
374
  let rejectHook;
374
375
  const hookFailed = new Promise((_, reject) => { rejectHook = reject; });
375
376
  void hookFailed.catch(() => {}); // A hook can fail while the dispatch receipt is still persisting.
@@ -379,6 +380,12 @@ export class DaemonRuntime {
379
380
  if (name === "session.prompt") this.state.last_native_failure = null;
380
381
  await this.persist({ active_tree: true, active_operation: name, ...(request ? { active_request: request } : {}) });
381
382
  const result = await Promise.race([task(), hookFailed]);
383
+ // The native tool cannot finish before its hook returns. Drain any
384
+ // already-written failure before discarding this operation's requests.
385
+ await this.observeHookFailures(current);
386
+ if (current.error) throw current.error;
387
+ const hookDiagnostics = (this.bridge.lifecycleDiagnostics ?? []).filter(item => item.operation_id === current.operationId && item.generation === current.generation);
388
+ if (hookDiagnostics.length) result.native_hook_diagnostics = [...new Map([...(result.native_hook_diagnostics ?? []), ...hookDiagnostics].map(item => [JSON.stringify(item), item])).values()];
382
389
  this.track(result);
383
390
  const running = this.treeRunning(result);
384
391
  await this.persist({ active_tree: running, active_operation: null, active_request: null });
@@ -393,12 +400,12 @@ export class DaemonRuntime {
393
400
  if (name === "session.prompt" && response?.method === "session/prompt" && error.details.provider_session_id)
394
401
  this.state.last_native_failure = { provider_session_id: error.details.provider_session_id, request_id: response.request_id };
395
402
  try {
396
- await hookBudget(1_000, null).run(() => this.persist({
403
+ await this.persist({
397
404
  active_operation: null,
398
405
  active_request: active?.allocation
399
406
  ? { ...active, outcome: "unknown", error: errorPayload(error) }
400
407
  : null,
401
- }), "failure_persistence");
408
+ });
402
409
  } catch (cleanupError) {
403
410
  // Preserve the operation's primary failure if durable cleanup itself
404
411
  // is unavailable. The caller will retain the outcome as unknown and
@@ -406,15 +413,50 @@ export class DaemonRuntime {
406
413
  if (error && typeof error === "object") error.cleanup_error = errorPayload(cleanupError);
407
414
  }
408
415
  throw error;
409
- } finally { clearInterval(timer); current.requests.clear(); if (this.nativeHookOperation === current) this.nativeHookOperation = null; this.activeProductive = null; this.activeGeneration = null; }
416
+ } finally { clearInterval(timer); if (!current.retainNativeHooks) current.requests.clear(); if (this.nativeHookOperation === current) this.nativeHookOperation = null; this.activeProductive = null; this.activeGeneration = null; }
410
417
  }
411
418
 
412
419
  async observeHookFailures(current = this.nativeHookOperation) {
413
- if (!current || this.nativeHookOperation !== current || current.reading) return;
414
- current.reading = true;
415
- try { await readNativeHookFailures(this.paths.dir, current.requests, current, error => {
416
- if (!isProvenShellCorrection(error)) current.reject(error);
417
- }); } finally { current.reading = false; }
420
+ if (!current || this.nativeHookOperation !== current) return;
421
+ while (current.reading) {
422
+ await current.reading;
423
+ if (this.nativeHookOperation !== current) return;
424
+ }
425
+ const reading = readNativeHookFailures(this.paths.dir, current.requests, current, error => this.acceptHookFailure(current, error));
426
+ current.reading = reading;
427
+ try { await reading; } finally { if (current.reading === reading) current.reading = null; }
428
+ }
429
+
430
+ acceptHookFailure(current, error) {
431
+ if (this.nativeHookOperation !== current) return;
432
+ const unconfirmed = error.details?.native_hook_binding?.native_creator === "unconfirmed";
433
+ if (!unconfirmed && isProvenShellCorrection(error)) return;
434
+ if (unconfirmed || isNativeHookObservationLoss(error)) {
435
+ const diagnostic = { ...errorRecord(error), session_id: this.state.active_request?.adapter_session_id ?? null,
436
+ operation_id: current.operationId, generation: current.generation, delivery_stage: "hook_observation", completeness: "incomplete" };
437
+ this.bridge.lifecycleDiagnostics ??= [];
438
+ this.bridge.lifecycleDiagnostics.push(diagnostic);
439
+ try { void Promise.resolve(this.bridge.journal?.write("native_hook_observation_failure", diagnostic)).catch(() => {}); } catch { /* diagnostic is optional */ }
440
+ return;
441
+ }
442
+ current.reject(error);
443
+ }
444
+
445
+ hookOwner(binding) {
446
+ if (!binding?.request_id) return null;
447
+ const turns = this.bridge.nativeHookOwnership?.turns?.values() ?? [];
448
+ const owners = new Set([this.nativeHookOperation, ...[...turns].filter(turn => !turn.invalid).map(turn => turn.owner)]);
449
+ for (const owner of owners) {
450
+ if (!owner || owner.operationId !== binding.operation_id || owner.generation !== binding.turn_generation || owner.generation !== (this.controlGeneration ?? 0)) continue;
451
+ const accepted = owner.requests.get(binding.request_id);
452
+ if (accepted?.current !== owner || JSON.stringify(accepted.binding) !== JSON.stringify(binding)) continue;
453
+ if (binding.source_harness === "zcode-acp") {
454
+ const proof = this.bridge.nativeHookOwnership?.fact(binding.provider_session_id, binding.tool_call_id, binding.native_command);
455
+ if (!proof || proof.owner !== owner || proof.parent !== binding.parent_provider_session_id) continue;
456
+ }
457
+ return owner;
458
+ }
459
+ return null;
418
460
  }
419
461
 
420
462
  async requireSettled(params) {
@@ -491,23 +533,67 @@ export class DaemonRuntime {
491
533
  };
492
534
  if (operation === "hook.resolve") {
493
535
  const sessionId = String(params.sessionId ?? "");
494
- const session = this.sessions.get(sessionId);
536
+ const payload = params.payload, command = payload?.tool_input?.command ?? payload?.tool_input?.cmd;
537
+ const tool = payload?.tool_use_id ?? payload?.tool_call_id;
538
+ const source = this.bridge.nativeHookOwnership;
539
+ let proof = source?.fact(sessionId, tool, command);
540
+ const tracked = this.sessions.get(sessionId);
541
+ if (!proof && (!tracked || tracked.parent_provider_session_id) && source && tool && typeof command === "string") {
542
+ await source.wait(sessionId, tool, command, params.hookDeadlineAt);
543
+ proof = source.fact(sessionId, tool, command);
544
+ }
545
+ const session = proof ? { root_provider_session_id: proof.root, parent_provider_session_id: proof.parent } : tracked;
495
546
  if (!session?.root_provider_session_id) throw new DaemonError("hook_identity_unknown", "ZCode hook Session is not a tracked controlled Session", false, { session_id: sessionId });
496
547
  const identity = { daemonId: this.state.daemon_id, rootProviderSessionId: session.root_provider_session_id, parentProviderSessionId: session.parent_provider_session_id ?? null, observedProfile: this.bridge.modelProfiles?.get(sessionId) ?? {} };
497
- const payload = params.payload, command = payload?.tool_input?.command ?? payload?.tool_input?.cmd;
498
548
  if (payload?.hook_event_name === "PreToolUse" && typeof command === "string" && (["lifecycle", "auxiliary"].includes(parseLifecycleCommand(command).participation) || continuationCommand(command))) {
499
- const current = this.nativeHookOperation, active = this.state.active_request;
500
- const tool = payload.tool_use_id ?? payload.tool_call_id;
549
+ // A retained child can only be attributed through the exact observed
550
+ // native turn and tool. Otherwise preserve the legacy handler without
551
+ // guessing that the currently active prompt created it.
552
+ if (sessionId !== session.root_provider_session_id && !proof) return identity;
553
+ const current = proof?.owner ?? this.nativeHookOperation, active = this.state.active_request;
501
554
  const pending = [...(this.bridge.pending?.values() ?? [])].some(request => request.method === "session/prompt" && request.sessionId === active?.adapter_session_id);
502
555
  const root = active?.session_id ?? active?.allocation?.provider_session_id;
503
- if (!current || !active || !pending || current.generation !== (this.controlGeneration ?? 0) || session.root_provider_session_id !== root || !tool) throw new DaemonError("native_hook_unproven", "Native hook has no exact active Session operation");
504
- const binding = await hookBudget(1_000, null).run(() => acceptNativeHookRequest(this.paths.dir, { daemon_id: this.state.daemon_id, operation_id: current.operationId, turn_generation: current.generation, provider_session_id: sessionId, root_provider_session_id: session.root_provider_session_id, tool_call_id: tool, event: "PreToolUse", command_sha256: createHash("sha256").update(command).digest("hex"), native_cwd: payload.cwd ?? null }, current), "hook_binding");
505
- if (this.nativeHookOperation !== current) throw new DaemonError("native_hook_unproven", "Native hook operation changed during acceptance");
556
+ if (!current || current.generation !== (this.controlGeneration ?? 0) || !tool || !proof && (!active || !pending || session.root_provider_session_id !== root)) throw new DaemonError("native_hook_unproven", "Native hook has no exact owned Session operation");
557
+ assertDaemonDispatch(this.paths.dir);
558
+ const nativePayload = proof ? { ...proof.message, _meta: { ...(proof.message._meta ?? {}), ddZcode: { daemonId: this.state.daemon_id, rootProviderSessionId: proof.root, parentProviderSessionId: proof.parent } } } : null;
559
+ const binding = await acceptNativeHookRequest(this.paths.dir, { daemon_id: this.state.daemon_id, operation_id: current.operationId, turn_generation: current.generation, provider_session_id: sessionId, root_provider_session_id: session.root_provider_session_id, parent_provider_session_id: session.parent_provider_session_id ?? null, tool_call_id: tool, event: "PreToolUse", command_sha256: createHash("sha256").update(command).digest("hex"), native_cwd: payload.cwd ?? null, native_creator: proof ? "confirmed" : "unconfirmed", ...(proof ? { source_harness: "zcode-acp", native_command: command, native_payload: nativePayload } : {}) }, current);
560
+ if (current.generation !== (this.controlGeneration ?? 0) || proof && source.fact(sessionId, tool, command) !== proof || !proof && this.nativeHookOperation !== current) throw new DaemonError("native_hook_unproven", "Native hook operation changed during acceptance");
506
561
  current.requests.set(binding.request_id, { current, binding }); identity.nativeHookBinding = binding;
562
+ const parsed = parseLifecycleCommand(command);
563
+ if (proof && parsed.kind === "standalone" && parsed.invocation.operation === "work_start" && commandOption(parsed.invocation, "invocation-id")) identity.fastAck = true;
507
564
  }
508
565
  return identity;
509
566
  }
510
- if (operation === "hook.failed") { await this.observeHookFailures(); return { observed: true }; }
567
+ if (operation === "hook.verify") {
568
+ if (params.daemonId !== this.state.daemon_id) throw new DaemonError("native_hook_unproven", "Native hook daemon identity differs");
569
+ const owner = this.hookOwner(params.binding);
570
+ if (!owner) throw new DaemonError("native_hook_unproven", "Native hook has no matching retained operation");
571
+ assertDaemonDispatch(this.paths.dir);
572
+ return { verified: true, nativeHookBinding: params.binding };
573
+ }
574
+ if (operation === "hook.failed") {
575
+ if (params.binding !== undefined || params.failure !== undefined) {
576
+ const binding = params.binding, failure = params.failure, current = this.hookOwner(binding);
577
+ // Join an in-flight file observation before checking the request. A
578
+ // direct fallback and its receipt must not be delivered twice.
579
+ while (current?.reading) await current.reading;
580
+ const accepted = current?.requests.get(binding?.request_id);
581
+ if (!current || this.hookOwner(binding) !== current || accepted?.current !== current
582
+ || JSON.stringify(accepted.binding) !== JSON.stringify(binding) || typeof failure?.code !== "string" || typeof failure?.message !== "string")
583
+ throw new DaemonError("native_hook_unproven", "Hook failure has no exact current owned delivery");
584
+ assertDaemonDispatch(this.paths.dir);
585
+ if (this.nativeHookOperation !== current) {
586
+ await writeNativeHookFailure(this.paths.dir, binding, reportedError(failure, "Native hook failed"));
587
+ return { observed: true, retained: true };
588
+ }
589
+ current.requests.delete(binding.request_id);
590
+ const error = reportedError(failure, "Native hook failed");
591
+ error.details = { ...error.details, native_hook_binding: binding };
592
+ this.acceptHookFailure(current, error);
593
+ return { observed: true };
594
+ }
595
+ await this.observeHookFailures(); return { observed: true };
596
+ }
511
597
  if (operation === "session.create") {
512
598
  const request = {
513
599
  operation_id: operationId,
@@ -668,6 +754,13 @@ export class DaemonRuntime {
668
754
  await this.persist({ native_stop_proof: { daemon_id: this.state.daemon_id, sessions: [...this.sessions.keys()], observed_at: new Date().toISOString() } });
669
755
  }
670
756
  commitDaemonShutdown(this.paths.dir);
757
+ if (!cancel) {
758
+ // The native tree has been observed idle. Publish that execution proof
759
+ // before attempting to close the independent bridge process. A close
760
+ // failure is cleanup debt and must not erase the already observed turn.
761
+ await this.persist({ active_tree: false, shutdown_state: "stopping", active_operation: null, active_request: null });
762
+ return { daemon_id: this.state.daemon_id, clean: true, settled: true, native_settled: true };
763
+ }
671
764
  // A cooperative close can make the native Session non-resident while its
672
765
  // original ACP prompt is still pending. For an explicitly whole-RUN
673
766
  // cancellation this daemon owns the bridge process group, so retire that
@@ -698,7 +791,7 @@ export class DaemonRuntime {
698
791
  }
699
792
 
700
793
  export function zcodeBridgeEnv(config, daemonId) {
701
- return { ...config.env, ...managedFlowEnv(config, daemonId) };
794
+ return { ...config.env, ...managedFlowEnv(config, daemonId), DD_ZCODE_STATE_DIR: !config.noFlow && daemonId ? config.runtime_owner?.state_dir : undefined };
702
795
  }
703
796
 
704
797
  export async function serveDaemon(stateDir) {
@@ -1,6 +1,7 @@
1
1
  import { ObservationClock, observedTimeout, validateObservationDuration } from "./observation-clock.mjs";
2
2
  import { ToolObservations, acpToolObservation, zcodePhysicalSession } from "./tool-observations.mjs";
3
- import { hookBudget, invokeNativeHook } from "./native-hook-command.mjs";
3
+ import { ZcodeNativeHookOwnership } from "./zcode-native-hook-ownership.mjs";
4
+ import { invokeNativeHook } from "./native-hook-command.mjs";
4
5
  import { parseLifecycleCommand, continuationCommand, commandOption } from "./lifecycle-command.mjs";
5
6
  import { spawn } from "node:child_process";
6
7
  import { createHash, randomUUID } from "node:crypto";
@@ -136,6 +137,7 @@ export class AcpBridge {
136
137
  this.options = options;
137
138
  this.nextId = 1;
138
139
  this.pending = new Map();
140
+ this.nativeHookOwnership = new ZcodeNativeHookOwnership();
139
141
  this.allocationObservers = new Map();
140
142
  this.lateResponses = new Map();
141
143
  this.expiredRequests = new Map();
@@ -146,6 +148,7 @@ export class AcpBridge {
146
148
  this.connectionId = randomUUID();
147
149
  this.receivedSequence = 0;
148
150
  this.lifecycleNotifications = new Set();
151
+ this.lifecycleDiagnostics = [];
149
152
  this.notificationError = null;
150
153
  this.bridgeStderr = "";
151
154
  this.toolObservations = new ToolObservations();
@@ -230,7 +233,6 @@ export class AcpBridge {
230
233
  }
231
234
  }
232
235
  if (!isResponse || request) this.journal.writeInbound(message, request?.method);
233
- if (typeof message.params?.sessionId === "string" && isProductiveSessionNotification(message)) this.observeActivity(message.params.sessionId, message.params.update);
234
236
  const update = message.params?.update;
235
237
  // ZCode emits this as `session/update`; Grok Build wraps the same ACP
236
238
  // payload in `_x.ai/session_notification`. Preserve either provider's
@@ -256,32 +258,18 @@ export class AcpBridge {
256
258
  void this.notifications.catch(error => { this.notificationError ??= error; });
257
259
  }
258
260
  }
261
+ if (message.method === "zcode/session/native_turn_started") {
262
+ const pending = this.pending.get(message.params?.requestId);
263
+ for (const routed of this.nativeHookOwnership.bind(message.params, pending)) { this.observeAttributedActivity(routed.message); this.dispatchLifecycleNotification(routed.message, routed.observer, routed.owner); }
264
+ }
265
+ this.observeAttributedActivity(message);
259
266
  this.observeToolCall(message);
260
- if (message.method === "session/update" && this.options.onLifecycleNotification) {
261
- const observer = this.options.onLifecycleNotification;
262
- const pending = Promise.resolve().then(() => observer(message)).catch(async error => {
263
- // A validated no-effect rejection is returned through the invocation
264
- // ledger. It is evidence, not a poisoned ACP Session.
265
- const current = this.options.onLifecycleNotification === observer;
266
- const correctable = current && correctableLifecycleErrors.has(error);
267
- if (current && !correctable) {
268
- this.lifecycleFailure ??= this.notificationError?.code === "profile_integrity_violation" ? this.notificationError : error;
269
- this.notificationError = this.lifecycleFailure;
270
- }
271
- // A missing/foreign authority is not a recoverable CLI validation
272
- // result. Stop admitting productive requests immediately; the managed
273
- // controller still owns physical stop/drain and evidence capture.
274
- if (current && !correctable) this.rejectAll(this.notificationError);
275
- try {
276
- await hookBudget(1_000, null).run(() => this.journal.write("notification_failure", { ...errorRecord(error),
277
- session_id: message.params?.sessionId ?? null, tool_call_id: update?.toolCallId ?? null, event_time: now(), delivery_stage: "lifecycle_receipt", completeness: "incomplete" }), "failure_persistence");
278
- } catch (persistenceError) {
279
- if (current) this.notificationError ??= persistenceError;
280
- process.stderr.write(`${JSON.stringify({ code: error.code, message: error.message, persistence_error: persistenceError.message })}\n`);
281
- }
282
- });
283
- this.lifecycleNotifications.add(pending);
284
- void pending.then(() => this.lifecycleNotifications.delete(pending), () => this.lifecycleNotifications.delete(pending));
267
+ const pendingPrompts = [...this.pending.values()].filter(item => item.method === "session/prompt" && item.sessionId === message.params?.sessionId);
268
+ const owned = this.nativeHookOwnership.observe(message, pendingPrompts.length > 0, this.nativeHookOwnership.isManaged(message.params?.sessionId, this.pending));
269
+ if (owned !== null) {
270
+ for (const routed of owned) this.dispatchLifecycleNotification(routed.message, routed.observer, routed.owner);
271
+ } else if (message.method === "session/update" && this.options.onLifecycleNotification) {
272
+ this.dispatchLifecycleNotification(message, this.options.onLifecycleNotification);
285
273
  }
286
274
  if (message.id !== undefined && message.method) { void this.answer(message).catch((error) => this.rejectAll(error)); return; }
287
275
  if (message.id !== undefined) {
@@ -293,6 +281,7 @@ export class AcpBridge {
293
281
  return;
294
282
  }
295
283
  clearTimeout(pending.timer); if (pending.idleTimer) clearInterval(pending.idleTimer); this.pending.delete(message.id);
284
+ if (pending.method === "session/prompt") this.discardUnboundIfIdle(pending.sessionId);
296
285
  if (pending.timedOut) this.journal.write("late_response", { ...responseSummary(message, pending.method), grace_ms: LATE_RESPONSE_GRACE_MS });
297
286
  if (message.error) {
298
287
  const data = message.error.data && typeof message.error.data === "object" && !Array.isArray(message.error.data) ? message.error.data : null;
@@ -332,6 +321,46 @@ export class AcpBridge {
332
321
  });
333
322
  }
334
323
 
324
+ observeAttributedActivity(message) {
325
+ if (typeof message.params?.sessionId === "string" && isProductiveSessionNotification(message) && this.nativeHookOwnership.isCurrent(message, this.pending)) this.observeActivity(message.params.sessionId, message.params.update);
326
+ }
327
+
328
+ discardUnboundIfIdle(sessionId) {
329
+ if (![...this.pending.values()].some(item => item.method === "session/prompt" && item.sessionId === sessionId)) this.nativeHookOwnership.discardUnbound(sessionId);
330
+ }
331
+
332
+ dispatchLifecycleNotification(message, observer, owner = null) {
333
+ const update = message.params?.update;
334
+ const pending = Promise.resolve().then(() => observer(message)).catch(async error => {
335
+ // A validated no-effect rejection is returned through the invocation
336
+ // ledger. It is evidence, not a poisoned ACP Session.
337
+ const current = this.options.onLifecycleNotification === observer && [...this.pending.values()].some(request => request.method === "session/prompt" && request.sessionId === message.params?.sessionId && (!owner || request.nativeHookOwner === owner));
338
+ const correctable = current && correctableLifecycleErrors.has(error);
339
+ // This callback observes an already-delivered native tool event. Its
340
+ // own timeout is not the result of the CLI command or the Turn.
341
+ const observationTimeout = current && error.code === "native_hook_timeout";
342
+ const diagnostic = { ...errorRecord(error), session_id: message.params?.sessionId ?? null,
343
+ tool_call_id: update?.toolCallId ?? null, event_time: now(), delivery_stage: "lifecycle_receipt", completeness: "incomplete" };
344
+ if (observationTimeout) this.lifecycleDiagnostics.push(diagnostic);
345
+ if (current && !correctable && !observationTimeout) {
346
+ this.lifecycleFailure ??= this.notificationError?.code === "profile_integrity_violation" ? this.notificationError : error;
347
+ this.notificationError = this.lifecycleFailure;
348
+ }
349
+ // A missing/foreign authority is not a recoverable CLI validation
350
+ // result. Stop admitting productive requests immediately; the managed
351
+ // controller still owns physical stop/drain and evidence capture.
352
+ if (current && !correctable && !observationTimeout) this.rejectAll(this.notificationError);
353
+ try {
354
+ await this.journal.write("notification_failure", diagnostic);
355
+ } catch (persistenceError) {
356
+ if (current && !observationTimeout) this.notificationError ??= persistenceError;
357
+ process.stderr.write(`${JSON.stringify({ code: error.code, message: error.message, persistence_error: persistenceError.message })}\n`);
358
+ }
359
+ });
360
+ this.lifecycleNotifications.add(pending);
361
+ void pending.then(() => this.lifecycleNotifications.delete(pending), () => this.lifecycleNotifications.delete(pending));
362
+ }
363
+
335
364
  observeToolCall(message) {
336
365
  const rootSessionId = this.modelSessions.get(message.params?.sessionId)?.sessionId;
337
366
  this.toolObservations.add(acpToolObservation(message, { harness: this.options.observationHarness ?? "zcode-acp", rootSessionId }));
@@ -364,7 +393,7 @@ export class AcpBridge {
364
393
  const rejectObservation = cause => {
365
394
  if (this.pending.get(pending.id) !== pending) return;
366
395
  clearTimeout(pending.timer); if (pending.idleTimer) clearInterval(pending.idleTimer);
367
- this.pending.delete(pending.id); pending.reject(nativeObservationError(cause));
396
+ this.pending.delete(pending.id); this.discardUnboundIfIdle(pending.sessionId); pending.reject(nativeObservationError(cause));
368
397
  };
369
398
  void pending.publish("native_progress", { native_request_id: String(pending.id), session_id: adapterId, observed_at: new Date(observedAt).toISOString() }).catch(rejectObservation);
370
399
  this.sessionActivity.set(adapterId, observedAt);
@@ -430,7 +459,7 @@ export class AcpBridge {
430
459
  return new Promise((resolve, reject) => {
431
460
  const finish = (error) => {
432
461
  const pending = this.pending.get(id); if (!pending) return;
433
- clearTimeout(pending.timer); if (pending.idleTimer) clearInterval(pending.idleTimer); this.pending.delete(id); reject(error);
462
+ clearTimeout(pending.timer); if (pending.idleTimer) clearInterval(pending.idleTimer); this.pending.delete(id); if (pending.method === "session/prompt") this.discardUnboundIfIdle(pending.sessionId); reject(error);
434
463
  };
435
464
  // Productive Turns use the sliding session-activity timeout below. A
436
465
  // second wall-clock timeout would kill an active, long-running Turn.
@@ -484,7 +513,7 @@ export class AcpBridge {
484
513
  void this.journal.write("inactivity_observation_failure", { method, request_id: id, session_id: idleSessionId, error: { code: error?.code ?? "inactivity_observation_failed", message: error?.message ?? String(error) } }).catch(() => {});
485
514
  });
486
515
  }, Math.max(10, Math.min(1_000, Math.floor(validIdleTimeout / 4)))) : null;
487
- this.pending.set(id, { id, resolve, reject, timer, idleTimer, method, sessionId: params.sessionId ?? null, operationId: this.dispatchOperationId ?? process.env.DD_EVAL_OPERATION_ID ?? null, generation: this.dispatchGeneration ?? null, dispatchedAt: Date.now(), publish: operationProgressReporter() });
516
+ this.pending.set(id, { id, resolve, reject, timer, idleTimer, method, sessionId: params.sessionId ?? null, operationId: this.dispatchOperationId ?? process.env.DD_EVAL_OPERATION_ID ?? null, generation: this.dispatchGeneration ?? null, nativeHookOwner: method === "session/prompt" ? this.dispatchNativeHookOwner ?? null : null, dispatchedAt: Date.now(), publish: operationProgressReporter() });
488
517
  if (method === "session/prompt") {
489
518
  void (async () => {
490
519
  const dispatch = { operation_id: this.pending.get(id)?.operationId, session_id: params.sessionId, native_request_id: id,
@@ -520,6 +549,7 @@ export class AcpBridge {
520
549
  clearTimeout(pending.timer); if (pending.idleTimer) clearInterval(pending.idleTimer); pending.reject(error);
521
550
  }
522
551
  this.pending.clear();
552
+ this.nativeHookOwnership.buffer.clear();
523
553
  }
524
554
  async flush() {
525
555
  await this.terminalOutcomes;
@@ -536,8 +566,7 @@ export class AcpBridge {
536
566
  await Promise.all([this.nativeDrain, this.diagnostics]);
537
567
  await this.terminalOutcomes;
538
568
  await Promise.allSettled([...this.lifecycleNotifications, this.notifications]);
539
- if (this.notificationError) await hookBudget(1_000, null).run(() => this.journal.flush(), "failure_drain");
540
- else await this.journal.flush();
569
+ await this.journal.flush();
541
570
  });
542
571
  }
543
572
  }
@@ -648,7 +677,6 @@ export async function readZcodeSubagents(bridge, sessionId, timeoutMs = 30_000,
648
677
  }
649
678
  if (typeof cursor !== "string" || cursors.has(cursor) || !Number.isSafeInteger(first.revision)) fail("ZCode child pagination has no advancing, versioned cursor", "native_topology_incomplete");
650
679
  cursors.add(cursor);
651
- if (cursors.size > 1_024) fail("ZCode child pagination exceeds its bounded directory limit", "native_topology_incomplete");
652
680
  clock.sample(cursor);
653
681
  }
654
682
  }
@@ -714,7 +742,7 @@ export async function observeClosedZcodeTree(bridge, root, retainedChildren = []
714
742
  const clock = new ObservationClock({ timeoutMs: 30_000 });
715
743
  try {
716
744
  for (const sessionId of pending) {
717
- if (pending.size > 1024 || clock.sample()) throw new Error("Closed tree observation limit exceeded");
745
+ if (clock.sample()) throw new Error("Closed tree observation timed out");
718
746
  const resident = await bridge.request("zcode/session/resident", { sessionId }, 5_000);
719
747
  if (resident?.sessionId !== sessionId || typeof resident.resident !== "boolean") throw new Error("Invalid residency identity or state");
720
748
  residents.push(resident);
@@ -1001,6 +1029,12 @@ export function zcodeLifecycleQualification(sha256, bridgeCommit, harnessContrac
1001
1029
  ["9c5ca6cc81c4eb5065e43f8b7ab6b7cc5ab7ae47", "dd-zcode-harness@2"],
1002
1030
  // CP253 installed artifact: exact same-child Read continuation and retained proof.
1003
1031
  ["b18dfdbedaa9330e765315bce0f1a609792d6c91", "dd-zcode-harness@2"],
1032
+ // Plan108 native 0.17.0: exact input/turn binding and a late child shell
1033
+ // event after the root reply. Clean rebuild retained identical code bytes.
1034
+ ["ac3e1063934fd516b7e69e900dc196eaafd3e7a9", "dd-zcode-harness@2"],
1035
+ // Reviewed late-progress repair: unchanged request/turn contract retains
1036
+ // ac3e106 native evidence; changed delivery is covered by scoped regressions.
1037
+ ["f9a3c5948ffbb0a31ec00bf8986610e17b8b9d77", "dd-zcode-harness@2"],
1004
1038
  ]);
1005
1039
  const qualified = qualifiedBridgeCommits.get(bridgeCommit) === harnessContract;
1006
1040
  return { status: qualified ? "qualified" : "unqualified", mechanism: "cli-invocation@1", sha256,
@@ -1087,7 +1121,9 @@ export async function promptSessionWithBridge(bridge, options) {
1087
1121
  bridge.configure({ ...options, onNotification: async (event) => { await forward?.(event); await options.onNotification?.(event); } });
1088
1122
  const identity = await controlledIdentity(bridge, { ...options, cwd }); forward = flowForwarder(options, identity.providerSessionId, bridge);
1089
1123
  await bridge.journal.flush();
1090
- bridge.configure({ onLifecycleNotification: await zcodeInvocationObserver({ ...options, adapterSessionId: identity.adapterSessionId }, identity.providerSessionId) });
1124
+ const observer = await zcodeInvocationObserver({ ...options, adapterSessionId: identity.adapterSessionId }, identity.providerSessionId);
1125
+ bridge.configure({ onLifecycleNotification: observer });
1126
+ bridge.nativeHookOwnership?.register(options.nativeHookOwner?.operationId, observer, identity.adapterSessionId);
1091
1127
  const profile_receipt = await applyProfile(bridge, identity.adapterSessionId, requested, options.assertDispatch);
1092
1128
  options.assertDispatch?.();
1093
1129
  let initial_usage_error = null;
@@ -1097,8 +1133,10 @@ export async function promptSessionWithBridge(bridge, options) {
1097
1133
  // never turn a long native child request into a fabricated provider failure.
1098
1134
  const timeoutMs = null;
1099
1135
  const textCursor = bridge.assistantTextCursor(identity.adapterSessionId);
1136
+ const diagnosticsCursor = bridge.lifecycleDiagnostics?.length ?? 0;
1100
1137
  options.assertDispatch?.();
1101
1138
  bridge.dispatchOperationId = options.operationId;
1139
+ bridge.dispatchNativeHookOwner = options.nativeHookOwner ?? null;
1102
1140
  const managed = options.operationId ? { schema_id: "dd-flow/managed-provider-recovery@1", owner: "controller", operation_id: options.operationId, root_operation_id: options.operationId.replace(/:capacity:\d+:session\.prompt$/, "") } : null;
1103
1141
  const acknowledged = value => managed && value && Object.keys(managed).every(key => value[key] === managed[key]);
1104
1142
  if (managed && (bridge.recoveryCapability?.schema_id !== managed.schema_id || bridge.recoveryCapability?.owner !== "controller")) fail("ZCode bridge does not acknowledge controller-owned provider recovery", "provider_recovery_unavailable");
@@ -1155,7 +1193,8 @@ export async function promptSessionWithBridge(bridge, options) {
1155
1193
  await cancelTree(bridge, identity.adapterSessionId, evidence.subagents, identity.providerSessionId);
1156
1194
  fail("background ZCode subagents cannot outlive the one-shot dd-zcode connection; the live child tree was cancelled");
1157
1195
  }
1158
- return receipt(options, { harness: "zcode-acp", provider_session_id: identity.providerSessionId, adapter_session_id: identity.adapterSessionId, assistant_text: bridge.assistantTextSince(identity.adapterSessionId, textCursor), turn, profile: profile_receipt, evidence: publicEvidence(evidence) });
1196
+ const native_hook_diagnostics = (bridge.lifecycleDiagnostics ?? []).slice(diagnosticsCursor).filter(item => item.session_id === identity.adapterSessionId);
1197
+ return receipt(options, { harness: "zcode-acp", provider_session_id: identity.providerSessionId, adapter_session_id: identity.adapterSessionId, assistant_text: bridge.assistantTextSince(identity.adapterSessionId, textCursor), turn, profile: profile_receipt, evidence: publicEvidence(evidence), ...(native_hook_diagnostics.length ? { native_hook_diagnostics } : {}) });
1159
1198
  }
1160
1199
 
1161
1200
  export async function inspectSession(options) {
@@ -6,6 +6,7 @@ export function delegationContract(harness: string): string;
6
6
  export const lifecycleRetryInstruction: string;
7
7
  export const lifecycleCommandCompletionInstruction: string;
8
8
  export const stageLifecycleInstruction: string;
9
+ export const acceptedResultInstruction: string;
9
10
  export function workerDelegationTask(input: { workId: string; startCommand: string; launchPolicy?: string }): string;
10
11
  export function renderDelegationInstructions(input: DelegationInput): string;
11
12
  export function renderCapacityInstructions(input: { harness: string; maximum: number }): string;
@@ -71,12 +71,16 @@ export function delegationContract(harness) {
71
71
 
72
72
  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. When the result provides prompt_delivery, read its complete plain Markdown file at prompt_delivery.path before working; it contains the current instructions, unlike the historical prompt_path. If delivery publication is pending, stop for reconciliation without repeating the accepted command.";
73
73
 
74
- 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.`;
74
+ export const nonLifecycleInputCorrectionInstruction = "For a non-lifecycle command explicitly reporting phase=prepare, effect=no_effect and recoverable=true, correct only its rejected arguments/JSON or an already-authorized missing precondition, then retry that non-lifecycle operation. Read that exact command's --help when its options are unclear. This includes check/integration input rejected before dispatch: an unsupported option or invalid JSON is not a failed check or an unknown Git effect. For example, remove an unsupported --stage from interactive integrate, or retain the current authorized interactive scope before reading git-inputs. This does not permit changing accepted data under an old request ID, expanding scope, replaying lifecycle commands without their issued successor, repeating dispatched checks/integration with failed, partial, committed or unknown effects, or bypassing ownership, recovery, storage, trust or operator-control refusals. An error lacking any of the three explicit preparation fields does not grant this correction path.";
75
+
76
+ 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. On any other lifecycle failure report the primary error and stop; never invent IDs, call hooks directly, or run status as a substitute. ${nonLifecycleInputCorrectionInstruction}`;
77
+ export const acceptedResultInstruction = "dd-flow's accepted Work/Stage result is authoritative; a native completion message or a result file alone is not acceptance. After dd-flow accepts your Work result, stop writing files, running tools and creating tasks, and end this Turn. A late native error, idle child card or failed session shutdown does not revoke an accepted result: do not repeat the Work or send another model prompt just to close it. Report technical_notes and cleanup_pending separately; the controller owns resource retirement. If another assigned Work or the Stage gate is still incomplete, continue only that remaining assignment through the issued graph. A possible conflicting writer requires controller reconciliation before dependent work, not an invented completion or a duplicate worker.";
75
78
 
76
79
  export const stageLifecycleInstruction = [
77
80
  "The project root identifies this RUN; use the Stage packet's declared execution workspace and absolute artifact paths. Do not change a native Session's working directory to a workspace provisioned for a later Stage.",
78
81
  "In a controller-managed fanout, the coordinator ends the entry Turn after the Work graph is returned; the controller dispatches children. A completed child or Work is not Stage success: settle the declared graph and Stage gate before finishing.",
79
82
  "Execute each runtime-issued lifecycle operation and its arguments exactly. One literal lifecycle call may be composed with ordinary shell commands; preserve conditions, stdin and redirects, and never replay the surrounding shell chain after failure. Invocation IDs are internal authority; never add, copy or invent one.",
83
+ acceptedResultInstruction,
80
84
  lifecycleRetryInstruction
81
85
  ].join(" ");
82
86
 
@@ -97,6 +101,7 @@ export function workerDelegationTask({ workId, startCommand, launchPolicy }) {
97
101
  "Continue only if it succeeds and returns the authoritative Work packet.",
98
102
  lifecycleRetryInstruction,
99
103
  "After a successful start, complete the assigned Work, write its required result, invoke its exact work finish command, then stop.",
104
+ acceptedResultInstruction,
100
105
  "Do not start another Work, create a child, change dependencies, ask the user, or treat a quiet sibling as failed. You cannot ask the user or pause the parent Stage. Missing facts go through this Work's declared result/failure contract; only the coordinator handles Stage HITL."
101
106
  ].join("\n");
102
107
  }
@@ -120,10 +125,11 @@ export function renderDelegationInstructions({ harness, stage, capacity, works =
120
125
  adapter.launch,
121
126
  "Retain the frozen coordinator model and reasoning for native children; do not choose a different model, reasoning level or permission mode.",
122
127
  "You are the coordinator. The JSON assignments below contain child-only messages, not instructions for you to execute. Pass each message verbatim as the native tool's task/message/prompt argument. For context=fresh, select empty conversation context before launch; if unavailable, report the limitation and stop. Use only fields exposed by your actual native tool schema; if the native tool is absent, report that error without substituting a shell command.",
123
- "Every child must be a direct child of this current Session. Wait for all launched children to settle before returning. One failed child is evidence for the coordinator, not a reason to cancel its siblings.",
128
+ "Every child must be a direct child of this current Session. Wait while assigned Work remains in progress; once dd-flow has accepted all assigned results, return without waiting merely for native session retirement. One failed child is evidence for the coordinator, not a reason to cancel its siblings.",
124
129
  adapter.partial ?? "If a spawn is refused, stop issuing new spawns and retain every successful child handle. Wait for all already-launched children, then release completed children using the adapter's exposed native close/release mechanism when supported. A failed batch does not undo successful spawns. Report completed/launched assignments, assignments not launched and the exact refusal; never claim no children launched when any spawn succeeded. Return after this partial wave; the controller will issue remaining Work when capacity is available. Do not replay an ambiguous spawn.",
125
130
  "Do not finish this Stage, repair it, or start a successor in this Turn. Do not invent provider or internal session IDs; lifecycle evidence comes from the adapter.",
126
131
  adapter.wait,
132
+ acceptedResultInstruction,
127
133
  "Child assignments (JSON data):",
128
134
  JSON.stringify(assignments, null, 2)
129
135
  ].join("\n\n");
@@ -148,13 +154,14 @@ export function renderWaitInstructions({ harness, stage }) {
148
154
  return [
149
155
  `Native child Work for ${stage} is still running.`,
150
156
  adapter.wait,
157
+ acceptedResultInstruction,
151
158
  "Do not create a Session, Work, or additional child agent. Do not cancel siblings merely because one child failed, and do not finish this Stage."
152
159
  ].join("\n");
153
160
  }
154
161
 
155
162
  export function renderAdapterPolicy({ harness }) {
156
163
  const adapter = requireAdapter(harness);
157
- return [adapter.launch, adapter.wait, "Lifecycle identity and completion must come from adapter observations; never synthesize provider or internal Session IDs."].join(" ");
164
+ return [adapter.launch, adapter.wait, "Native launch identity comes from adapter observations; never synthesize provider or internal Session IDs.", acceptedResultInstruction].join(" ");
158
165
  }
159
166
 
160
167
  export function renderRecoveryProbeInstructions({ harness, command }) {
@@ -0,0 +1,84 @@
1
+ import fs from 'node:fs';
2
+ import process from 'node:process';
3
+ import { spawn } from 'node:child_process';
4
+
5
+ // Internal transport, not operation authorization. The RUN owner registers and
6
+ // confirms this wrapper's PID/group, then releases stdin after its final fence.
7
+ // fd3 privately carries fixed argv and only permitted environment values; these
8
+ // resolved values never enter policy or persisted configuration. fd4 carries raw stdout to the
9
+ // owner in memory. Public stdout/stderr never contain provider output or env.
10
+ const outputLimit = 1024 * 1024;
11
+ const text = value => typeof value === 'string' && value.length > 0 && value.length <= 16384 && !value.includes('\0');
12
+ const id = value => typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/.test(value);
13
+ const emit = value => process.stdout.write(`${JSON.stringify(value)}\n`);
14
+ process.stdout.on('error', () => {}); // Observer loss is not command completion.
15
+ process.stderr.on('error', () => {});
16
+ async function bounded(stream, limit) {
17
+ const chunks = []; let bytes = 0;
18
+ for await (const chunk of stream) {
19
+ bytes += chunk.length;
20
+ if (bytes > limit) throw new Error('bounded_input');
21
+ chunks.push(chunk);
22
+ }
23
+ return Buffer.concat(chunks);
24
+ }
25
+ let config = null, admitted = false, started = false;
26
+ const result = (exitCode, signal, error, outputAvailable = true, truncated = false) => {
27
+ emit({ schema_id: 'dd-flow/delivery-command-result@1', operation_id: config?.operation_id ?? null,
28
+ step_id: config?.step_id ?? null, admitted, started, exit_code: exitCode, signal, error,
29
+ output_available: outputAvailable, stdout_truncated: truncated });
30
+ process.exitCode = error ? 1 : 0;
31
+ };
32
+ try {
33
+ const bytes = await bounded(fs.createReadStream('', { fd: 3, autoClose: true }), 3 * outputLimit);
34
+ const value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
35
+ if (!value || typeof value !== 'object' || Array.isArray(value)
36
+ || Object.keys(value).sort().join() !== 'argv,environment,operation_id,permitted_environment_variables,schema_id,step_id'
37
+ || value.schema_id !== 'dd-flow/delivery-command@1' || !id(value.operation_id) || !id(value.step_id)
38
+ || !Array.isArray(value.argv) || !value.argv.length || value.argv.length > 128 || !value.argv.every(text)
39
+ || !Array.isArray(value.permitted_environment_variables) || value.permitted_environment_variables.length > 128
40
+ || !value.permitted_environment_variables.every(name => typeof name === 'string' && /^[A-Z_][A-Z0-9_]*$/.test(name))
41
+ || new Set(value.permitted_environment_variables).size !== value.permitted_environment_variables.length
42
+ || !value.environment || typeof value.environment !== 'object' || Array.isArray(value.environment)
43
+ || Object.entries(value.environment).some(([name, content]) => !value.permitted_environment_variables.includes(name)
44
+ || typeof content !== 'string' || content.includes('\0') || content.length > 16384)) throw new Error('invalid_configuration');
45
+ // Fail before readiness if the private output channel was not installed.
46
+ const channel = fs.fstatSync(4);
47
+ if (!channel.isFIFO() && !channel.isSocket()) throw new Error('private_pipe_required');
48
+ fs.writeSync(4, Buffer.alloc(0));
49
+ config = value;
50
+ emit({ schema_id: 'dd-flow/delivery-command-ready@1', operation_id: config.operation_id, step_id: config.step_id });
51
+ const admission = await bounded(process.stdin, 32);
52
+ if (!admission.equals(Buffer.from('admitted\n'))) result(null, null, 'admission_missing');
53
+ else {
54
+ admitted = true;
55
+ const environment = { ...config.environment };
56
+ environment.DD_FLOW_DELIVERY_OPERATION_ID = config.operation_id;
57
+ environment.DD_FLOW_DELIVERY_STEP_ID = config.step_id;
58
+ const output = fs.createWriteStream('', { fd: 4, autoClose: true });
59
+ let available = true, truncated = false, count = 0;
60
+ const flushed = new Promise(resolve => { output.once('finish', resolve); output.on('error', () => { available = false; resolve(); }); });
61
+ const child = spawn(config.argv[0], config.argv.slice(1), { env: environment, shell: false, windowsHide: true,
62
+ detached: false, stdio: ['ignore', 'pipe', 'pipe'] });
63
+ started = Boolean(child.pid);
64
+ let spawnFailed = false;
65
+ child.once('error', () => { spawnFailed = true; });
66
+ child.stdout.on('error', () => { available = false; });
67
+ child.stderr.on('error', () => { available = false; });
68
+ child.stderr.resume(); // Never persist or echo authenticated stderr.
69
+ child.stdout.on('data', bytes => {
70
+ const remaining = Math.max(0, outputLimit - count);
71
+ if (bytes.length > remaining) truncated = true;
72
+ if (remaining && available) output.write(bytes.subarray(0, remaining));
73
+ count += bytes.length;
74
+ });
75
+ const completed = await new Promise(resolve => child.once('close', (code, signal) => resolve({ code, signal })));
76
+ output.end(); await flushed;
77
+ result(completed.code, completed.signal, spawnFailed ? 'spawn_failed' : !available ? 'private_output_unavailable'
78
+ : truncated ? 'output_limit_exceeded' : completed.code !== 0 || completed.signal ? 'command_failed' : null, available, truncated);
79
+ }
80
+ } catch {
81
+ // Input may contain secrets accidentally. Never print it or an exception
82
+ // whose message/stack could contain an authenticated argv or raw payload.
83
+ result(null, null, admitted ? 'transport_failed' : 'configuration_or_admission_invalid', false);
84
+ }