@deksden-com/dd-flow-cli 0.9.0-beta.133 → 0.9.0-beta.135

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 (118) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +31 -0
  3. package/dist/build-info.json +7 -7
  4. package/dist/harness-runtime/OPENCODE-LICENSE.txt +24 -0
  5. package/dist/harness-runtime/bin/dd-opencode.mjs +1 -1
  6. package/dist/harness-runtime/lib/codex-capacity-policy.mjs +13 -0
  7. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +193 -39
  8. package/dist/harness-runtime/lib/dd-opencode.mjs +295 -100
  9. package/dist/harness-runtime/lib/dd-zcode.mjs +21 -1
  10. package/dist/harness-runtime/lib/delegation-instructions.mjs +3 -3
  11. package/dist/harness-runtime/lib/managed-daemon.mjs +1 -1
  12. package/dist/harness-runtime/lib/native-children.d.mts +1 -0
  13. package/dist/harness-runtime/lib/native-children.mjs +16 -0
  14. package/dist/harness-runtime/lib/native-hook-command.d.mts +2 -2
  15. package/dist/harness-runtime/lib/native-hook-command.mjs +4 -4
  16. package/dist/harness-runtime/lib/opencode-plugin.mjs +60 -0
  17. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-1y4a697q.js +81 -0
  18. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-2ny4vxct.js +92 -0
  19. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-6mptkzbc.js +0 -0
  20. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-7x67cf1q.js +52 -0
  21. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-9rqn6x4v.js +16 -0
  22. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-9vdz0zwn.js +1661 -0
  23. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ecs5ftka.js +1466 -0
  24. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-frbwqjmf.js +145 -0
  25. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-gne7h7mq.js +50 -0
  26. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-gx9wnkzr.js +105 -0
  27. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-m2eywc8w.js +0 -0
  28. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-nwcpakzx.js +12 -0
  29. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-q4g3afnw.js +623 -0
  30. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-qav6mfdz.js +0 -0
  31. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-sxt88q69.js +12 -0
  32. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-t1xa31vm.js +39 -0
  33. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ve4aws36.js +103 -0
  34. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vmytr4q8.js +4 -0
  35. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vt9jbch8.js +36 -0
  36. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vyas3vgt.js +0 -0
  37. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-wjyeq25y.js +30 -0
  38. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-xgnj2w4v.js +20 -0
  39. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-y2etg5bs.js +9 -0
  40. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ydhz1zrh.js +238 -0
  41. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-z6qdwcsf.js +62 -0
  42. package/dist/harness-runtime/vendor/opencode-client/contract.d.ts +1 -0
  43. package/dist/harness-runtime/vendor/opencode-client/contract.js +12 -0
  44. package/dist/harness-runtime/vendor/opencode-client/effect/api/api.d.ts +2806 -0
  45. package/dist/harness-runtime/vendor/opencode-client/effect/api/api.js +0 -0
  46. package/dist/harness-runtime/vendor/opencode-client/effect/api.d.ts +4 -0
  47. package/dist/harness-runtime/vendor/opencode-client/effect/api.js +0 -0
  48. package/dist/harness-runtime/vendor/opencode-client/effect/client.d.ts +2530 -0
  49. package/dist/harness-runtime/vendor/opencode-client/effect/client.js +15 -0
  50. package/dist/harness-runtime/vendor/opencode-client/effect/generated/client-error.d.ts +7 -0
  51. package/dist/harness-runtime/vendor/opencode-client/effect/generated/client-error.js +6 -0
  52. package/dist/harness-runtime/vendor/opencode-client/effect/generated/client.d.ts +2529 -0
  53. package/dist/harness-runtime/vendor/opencode-client/effect/generated/client.js +8 -0
  54. package/dist/harness-runtime/vendor/opencode-client/effect/generated/index.d.ts +2 -0
  55. package/dist/harness-runtime/vendor/opencode-client/effect/generated/index.js +12 -0
  56. package/dist/harness-runtime/vendor/opencode-client/effect/index.d.ts +35 -0
  57. package/dist/harness-runtime/vendor/opencode-client/effect/index.js +75 -0
  58. package/dist/harness-runtime/vendor/opencode-client/effect/rpc.d.ts +20 -0
  59. package/dist/harness-runtime/vendor/opencode-client/effect/rpc.js +11 -0
  60. package/dist/harness-runtime/vendor/opencode-client/effect/service.d.ts +90 -0
  61. package/dist/harness-runtime/vendor/opencode-client/effect/service.js +187 -0
  62. package/dist/harness-runtime/vendor/opencode-client/package.json +84 -0
  63. package/dist/harness-runtime/vendor/opencode-client/promise/api.d.ts +21 -0
  64. package/dist/harness-runtime/vendor/opencode-client/promise/api.js +0 -0
  65. package/dist/harness-runtime/vendor/opencode-client/promise/client.d.ts +252 -0
  66. package/dist/harness-runtime/vendor/opencode-client/promise/client.js +14 -0
  67. package/dist/harness-runtime/vendor/opencode-client/promise/generated/client-error.d.ts +8 -0
  68. package/dist/harness-runtime/vendor/opencode-client/promise/generated/client-error.js +6 -0
  69. package/dist/harness-runtime/vendor/opencode-client/promise/generated/client.d.ts +259 -0
  70. package/dist/harness-runtime/vendor/opencode-client/promise/generated/client.js +7 -0
  71. package/dist/harness-runtime/vendor/opencode-client/promise/generated/index.d.ts +3 -0
  72. package/dist/harness-runtime/vendor/opencode-client/promise/generated/index.js +79 -0
  73. package/dist/harness-runtime/vendor/opencode-client/promise/generated/types.d.ts +8932 -0
  74. package/dist/harness-runtime/vendor/opencode-client/promise/generated/types.js +70 -0
  75. package/dist/harness-runtime/vendor/opencode-client/promise/index.d.ts +6 -0
  76. package/dist/harness-runtime/vendor/opencode-client/promise/index.js +83 -0
  77. package/dist/harness-runtime/vendor/opencode-client/promise/rpc.d.ts +32 -0
  78. package/dist/harness-runtime/vendor/opencode-client/promise/rpc.js +7 -0
  79. package/dist/harness-runtime/vendor/opencode-client/promise/service.d.ts +24 -0
  80. package/dist/harness-runtime/vendor/opencode-client/promise/service.js +187 -0
  81. package/dist/harness-runtime/vendor/opencode-client/pty-handoff.d.ts +12 -0
  82. package/dist/harness-runtime/vendor/opencode-client/pty-handoff.js +14 -0
  83. package/dist/harness-runtime/vendor/opencode-client/rpc-runtime.d.ts +20 -0
  84. package/dist/harness-runtime/vendor/opencode-client/rpc-runtime.js +14 -0
  85. package/dist/harness-runtime/vendor/opencode-client/service-contender.d.ts +37 -0
  86. package/dist/harness-runtime/vendor/opencode-client/service-contender.js +10 -0
  87. package/dist/harness-runtime/vendor/opencode-client/service-probe.d.ts +70 -0
  88. package/dist/harness-runtime/vendor/opencode-client/service-probe.js +16 -0
  89. package/dist/harness-runtime/vendor/opencode-client/service-timing.d.ts +12 -0
  90. package/dist/harness-runtime/vendor/opencode-client/service-timing.js +10 -0
  91. package/dist/harness-runtime/vendor/opencode-client/service-version.d.ts +2 -0
  92. package/dist/harness-runtime/vendor/opencode-client/service-version.js +6 -0
  93. package/dist/harness-runtime/vendor/opencode-client/service.d.ts +52 -0
  94. package/dist/harness-runtime/vendor/opencode-client/service.js +1 -0
  95. package/dist/harness-runtime/vendor/opencode-client/shared-events.d.ts +11 -0
  96. package/dist/harness-runtime/vendor/opencode-client/shared-events.js +8 -0
  97. package/dist/harness-runtime/vendor/opencode-client/solid/connection.d.ts +37 -0
  98. package/dist/harness-runtime/vendor/opencode-client/solid/connection.js +10 -0
  99. package/dist/harness-runtime/vendor/opencode-client/solid/data.d.ts +211 -0
  100. package/dist/harness-runtime/vendor/opencode-client/solid/data.js +18 -0
  101. package/dist/harness-runtime/vendor/opencode-client/solid/index.d.ts +3 -0
  102. package/dist/harness-runtime/vendor/opencode-client/solid/index.js +32 -0
  103. package/dist/harness-runtime/vendor/opencode-client/solid/pty.d.ts +22 -0
  104. package/dist/harness-runtime/vendor/opencode-client/solid/pty.js +8 -0
  105. package/dist/harness-runtime/vendor/opencode-plugin.mjs +3 -0
  106. package/dist/services/controller-fanout.js +20 -3
  107. package/dist/services/engines.js +22 -24
  108. package/dist/services/eval-snapshots.js +1 -1
  109. package/dist/services/external-work-launch.js +1 -1
  110. package/dist/services/hooks.js +17 -11
  111. package/dist/services/lifecycle-invocations.js +66 -12
  112. package/dist/services/merge-server.js +1 -1
  113. package/dist/services/run-controller-recovery.js +2 -2
  114. package/dist/services/run-controller.js +2 -2
  115. package/dist/services/vnext-fanout.js +6 -1
  116. package/dist/services/work-registry.js +54 -11
  117. package/dist/storage/database.js +4 -0
  118. package/package.json +4 -1
@@ -18,6 +18,11 @@ import { registerProject, requireProjectByRoot } from "./projects.js";
18
18
  import { activeFlowSessionsForProject, recordFlowSessionObservation } from "./sessions.js";
19
19
  const defaultProfileName = "default";
20
20
  const maxSanitizedSummaryLength = 1600;
21
+ function lifecycleObservationResult(receipt) {
22
+ return { observed: receipt.disposition === "admitted", duplicate: receipt.duplicate,
23
+ event_key: receipt.authoritativeEventKey, current_event_key: receipt.currentEventKey,
24
+ authoritative_event_key: receipt.authoritativeEventKey, receipt_disposition: receipt.disposition };
25
+ }
21
26
  export const codexHomeSharedEntries = [
22
27
  "auth.json",
23
28
  "auth.bk",
@@ -410,6 +415,7 @@ export function handleCodexHook(context, input) {
410
415
  throw new AppError("codex_identity_missing", "Managed Codex lifecycle hook has no complete native invocation identity", 1, { invocation_id: invocationId });
411
416
  const executionCwd = assignedLifecycleCwd(context, project.root, lifecycle, providerSessionId, command, stringValue(payload.cwd), { daemonId, rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId: parentProviderSessionId, toolCallId }, "codex-desktop");
412
417
  const receipt = observeLifecycleInvocation(context, {
418
+ currentEventKey: eventKey,
413
419
  ...(invocationId ? { id: invocationId } : {}),
414
420
  command,
415
421
  harness: "codex-desktop",
@@ -421,12 +427,11 @@ export function handleCodexHook(context, input) {
421
427
  const stored = context.db.get("SELECT protocol_id FROM hook_events WHERE project_id = ? AND event_key = ?", [project.id, receipt.eventKey]);
422
428
  const admittedInvocationId = receipt.invocationId ?? invocationId;
423
429
  if (admittedInvocationId)
424
- return { ...common, protocol_id: stored?.protocol_id ?? null, observed: !receipt.duplicate, duplicate: receipt.duplicate, event_key: receipt.eventKey, invocation_id: admittedInvocationId };
430
+ return { ...common, protocol_id: stored?.protocol_id ?? null, ...lifecycleObservationResult(receipt), invocation_id: admittedInvocationId };
425
431
  return {
426
432
  ...common,
427
433
  protocol_id: stored?.protocol_id ?? null,
428
- observed: !receipt.duplicate,
429
- duplicate: receipt.duplicate,
434
+ ...lifecycleObservationResult(receipt),
430
435
  hookSpecificOutput: {
431
436
  hookEventName: "PreToolUse",
432
437
  permissionDecision: "allow",
@@ -588,6 +593,7 @@ function recordZcodeLifecycle(context, input, invocationId, nativePreTool = fals
588
593
  let persistedResult;
589
594
  const executionCwd = assignedLifecycleCwd(context, expectedRoot, lifecycle, providerSessionId, command, observedCwd ?? undefined, { daemonId, rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId: parentProviderSessionId ?? null, toolCallId: toolUseId }, "zcode-acp");
590
595
  const receipt = observeLifecycleInvocation(context, {
596
+ currentEventKey: eventKey,
591
597
  ...(invocationId ? { id: invocationId } : {}), command,
592
598
  harness: "zcode-acp",
593
599
  projectId: project.id,
@@ -595,7 +601,7 @@ function recordZcodeLifecycle(context, input, invocationId, nativePreTool = fals
595
601
  identity: { daemonId, rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId: parentProviderSessionId ?? null, toolCallId: toolUseId },
596
602
  recordReceipt: scope => { persistedResult = persist(scope); return eventKey; }
597
603
  });
598
- if (receipt.duplicate) {
604
+ if (receipt.disposition === "replay") {
599
605
  // Keep first admission immutable. A second transport is independently
600
606
  // retained corroboration, never another claim or an invented ACP cwd.
601
607
  const retained = context.db.get("SELECT event_name, match_key, harness, provider_session_id, parent_session_id, daemon_id FROM hook_events WHERE project_id = ? AND event_key = ?", [project.id, receipt.eventKey]);
@@ -615,7 +621,7 @@ function recordZcodeLifecycle(context, input, invocationId, nativePreTool = fals
615
621
  }
616
622
  const admittedInvocationId = receipt.invocationId ?? invocationId;
617
623
  if (admittedInvocationId)
618
- return { ok: true, observed: !receipt.duplicate, duplicate: receipt.duplicate, event_key: receipt.eventKey, invocation_id: admittedInvocationId };
624
+ return { ok: true, ...lifecycleObservationResult(receipt), invocation_id: admittedInvocationId };
619
625
  // Without an issued managed attempt this is still ordinary hook evidence.
620
626
  // Preserve persist()'s replay result instead of overwriting it with the
621
627
  // admission layer's non-authoritative duplicate flag.
@@ -685,14 +691,13 @@ export function handleGrokEvent(context, input) {
685
691
  let inserted = false;
686
692
  const executionCwd = assignedLifecycleCwd(context, expectedRoot, lifecycle, providerSessionId, command, observedCwd ?? undefined, { daemonId: daemonId ?? "", rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId, toolCallId: eventKey }, "grok-acp");
687
693
  const receipt = observeLifecycleInvocation(context, {
694
+ currentEventKey: eventKey,
688
695
  command, harness: "grok-acp", projectId: project.id,
689
696
  ...(executionCwd ? { executionCwd } : {}),
690
697
  identity: { daemonId: daemonId ?? "", rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId, toolCallId: eventKey },
691
698
  recordReceipt: scope => { inserted = persist(scope); return eventKey; }
692
699
  });
693
- if (receipt.invocationId)
694
- return { ...result(inserted), invocation_id: receipt.invocationId, hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: commandWithHookEvent(command, eventKey) } } };
695
- return { ...result(inserted), hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: commandWithHookEvent(command, eventKey) } } };
700
+ return { ...result(inserted), ...lifecycleObservationResult(receipt), ...(receipt.invocationId ? { invocation_id: receipt.invocationId } : {}), hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: receipt.eventKey ? commandWithHookEvent(command, receipt.eventKey) : command } } };
696
701
  }
697
702
  /** Convert a controlled OpenCode tool hook into the shared lifecycle receipt. */
698
703
  export function handleOpenCodeEvent(context, input) {
@@ -838,7 +843,7 @@ function handleControlledToolEvent(context, input, event, harness) {
838
843
  });
839
844
  return { ok: true, observed: inserted, duplicate: !inserted, event_key: baseKey, phase, harness, session: nativeSessionIdentity(harness, providerSessionId), ...(nativeParentId ? { parent_session: nativeSessionIdentity(harness, nativeParentId) } : {}) };
840
845
  }
841
- if (!command || !(droid ? ["Execute"] : ["bash", "Bash", "run_command", "run_terminal_command", "RunTerminalCommand"]).includes(toolName)) {
846
+ if (!command || !(droid ? ["Execute"] : ["bash", "Bash", "shell", "run_command", "run_terminal_command", "RunTerminalCommand"]).includes(toolName)) {
842
847
  return { ok: true, observed: false, reason: command ? "non_bash_tool" : "event_not_participating" };
843
848
  }
844
849
  const lifecycle = lifecycleFacts(command);
@@ -900,11 +905,12 @@ function handleControlledToolEvent(context, input, event, harness) {
900
905
  if (lifecycle) {
901
906
  let inserted = false;
902
907
  const receipt = observeLifecycleInvocation(context, {
908
+ currentEventKey: baseKey,
903
909
  command, harness, projectId: project.id, ...(!shellRefusal ? { executionCwd: expectedCwd } : {}), identity: { daemonId, rootSessionId: nativeParentId ?? providerSessionId, sessionId: providerSessionId, parentSessionId: nativeParentId ?? null, toolCallId: toolCallId },
904
910
  recordReceipt: scope => { inserted = persist(scope); return baseKey; }
905
911
  });
906
- const admitted = { ...result(inserted), ...(receipt.invocationId ? { invocation_id: receipt.invocationId } : {}) };
907
- return agy || shellRefusal ? admitted : { ...admitted, hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: commandWithHookEvent(command, baseKey) } } };
912
+ const admitted = { ...result(inserted), ...lifecycleObservationResult(receipt), ...(receipt.invocationId ? { invocation_id: receipt.invocationId } : {}) };
913
+ return agy || shellRefusal ? admitted : { ...admitted, hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: receipt.eventKey ? commandWithHookEvent(command, receipt.eventKey) : command } } };
908
914
  }
909
915
  const inserted = context.db.writeTransaction(() => persist());
910
916
  return agy ? result(inserted) : { ...result(inserted), hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: commandWithHookEvent(command, baseKey) } } };
@@ -60,6 +60,11 @@ export function assertAuxiliaryCommandScope(context, command, projectRoot) {
60
60
  }
61
61
  throw new AppError("invocation_scope_unavailable", "Auxiliary command has no live managed daemon binding", 1, { phase: "prepare", effect: "no_effect" });
62
62
  }
63
+ function sameNativeCall(left, right) {
64
+ const a = JSON.parse(left), b = JSON.parse(right);
65
+ // Parent and execution route are immutable facts, not a different call ID.
66
+ return [0, 1, 2, 4].every(index => a[index] === b[index]);
67
+ }
63
68
  // Conclusive result/check rejections, never transport, ownership, registry or
64
69
  // unknown-effect failures. These validators reject before accepting the result.
65
70
  export function isRetryableLifecycleRejection(code, details = {}) {
@@ -567,6 +572,8 @@ export function managedLifecycleCommand(context, command, options = {}) {
567
572
  const scope = JSON.parse(configured);
568
573
  command = canonicalManagedCommand(context, command, scope);
569
574
  const parsed = invocation(command);
575
+ if (options.freshWorkAttempt && !["work_finish", "work_fail"].includes(parsed.operation))
576
+ throw new AppError("invocation_command_mismatch", "Fresh Work packet may only issue Work settlement commands", 1);
570
577
  const suppliedId = commandOption(parsed, "invocation-id");
571
578
  // An invocation id is authority, never decorative text. In particular,
572
579
  // callers that add arguments after rendering must not be able to reuse an
@@ -590,7 +597,7 @@ export function managedLifecycleCommand(context, command, options = {}) {
590
597
  }
591
598
  storage(context);
592
599
  return context.db.writeTransaction(() => {
593
- const prior = context.db.get("SELECT * FROM lifecycle_invocations WHERE scope_json = ? AND fingerprint = ? ORDER BY rowid DESC LIMIT 1", [JSON.stringify(scope), fingerprint(parsed)]);
600
+ const prior = options.freshWorkAttempt ? undefined : context.db.get("SELECT * FROM lifecycle_invocations WHERE scope_json = ? AND fingerprint = ? ORDER BY rowid DESC LIMIT 1", [JSON.stringify(scope), fingerprint(parsed)]);
594
601
  if (prior && prior.id !== context.env.DD_FLOW_CURRENT_INVOCATION) {
595
602
  const replacesAnswer = options.replaceUnobservedAnswer && parsed.operation === "stage_resume"
596
603
  && (prior.status === "settled" || commandOption(invocation(prior.command), "answer-file") !== commandOption(parsed, "answer-file"));
@@ -817,6 +824,39 @@ export function issueLifecycleInvocation(context, command, scope) {
817
824
  context.db.run("INSERT INTO lifecycle_invocations (id, scope_json, command, fingerprint, status, created_at, updated_at) VALUES (?, ?, ?, ?, 'issued', ?, ?)", [id, row.scope_json, prepared, row.fingerprint, context.now(), context.now()]);
818
825
  return { id, command: prepared };
819
826
  }
827
+ /** Prepare outside a write transaction; the Work retry CAS owns issuance. */
828
+ export function prepareWorkStartAuthority(context, command, previousId) {
829
+ const prior = previousId ? load(context, previousId) : null;
830
+ const configured = context.env.DD_FLOW_INVOCATION_SCOPE;
831
+ if (!configured && !prior)
832
+ return null;
833
+ const scope = JSON.parse(configured ?? prior.scope_json);
834
+ if (prior && prior.scope_json !== JSON.stringify(scope))
835
+ throw new AppError("invocation_scope_mismatch", "Work start belongs to another controller scope", 1);
836
+ command = canonicalManagedCommand(context, command, scope);
837
+ const { parsed } = prepareLifecycleIssuance(command, scope);
838
+ if (parsed.operation !== "work_start")
839
+ throw new AppError("invocation_command_mismatch", "Work authority must start its Work", 1);
840
+ if (prior && prior.fingerprint !== fingerprint(parsed))
841
+ throw new AppError("invocation_command_mismatch", "Retained Work start authority belongs to a different Work", 1, { invocation_id: prior.id });
842
+ assertLifecycleInvocationCurrent(context, scope, command);
843
+ return { command, scope };
844
+ }
845
+ /** Caller owns the retry CAS. Preserve settled outcomes; never steal an
846
+ * observed/executing terminal operation from the previous Work attempt. */
847
+ export function retireWorkPacketAuthorities(context, workId) {
848
+ if (!context.db.get("SELECT 1 FROM sqlite_master WHERE name = 'lifecycle_packet_assignments'"))
849
+ return;
850
+ const attempts = context.db.all("SELECT DISTINCT li.* FROM lifecycle_invocations li JOIN lifecycle_packet_assignments pa ON pa.invocation_id=li.id JOIN work_sessions ws ON pa.packet_sha256=json_extract(ws.start_receipt_json,'$.prompt_sha256') WHERE ws.work_id=?", [workId]);
851
+ for (const attempt of attempts) {
852
+ if (!["work_finish", "work_fail"].includes(invocation(attempt.command).operation))
853
+ continue;
854
+ if (["observed", "executing"].includes(attempt.status))
855
+ throw new AppError("work_retry_incomplete", "Previous Work settlement still has native authority in flight", 1, { work_id: workId, invocation_id: attempt.id });
856
+ if (attempt.status === "issued")
857
+ context.db.run("UPDATE lifecycle_invocations SET status='expired',updated_at=? WHERE id=? AND status='issued'", [context.now(), attempt.id]);
858
+ }
859
+ }
820
860
  function commandWithInvocationId(command, id) {
821
861
  const at = shellSuffixOffset(command);
822
862
  const suffix = command.slice(at);
@@ -836,8 +876,10 @@ function assertRenderableInvocation(row) {
836
876
  function renderInvocationCommand(row, command) {
837
877
  assertRenderableInvocation(row);
838
878
  const parsed = invocation(command);
839
- if (commandOption(parsed, "invocation-id"))
840
- throw new AppError("invocation_command_invalid", "Rendered lifecycle command already has an invocation id", 1);
879
+ if (commandOption(parsed, "invocation-id")) {
880
+ checkCommand(row, command);
881
+ return command;
882
+ }
841
883
  const rendered = commandWithInvocationId(command, row.id);
842
884
  checkCommand(row, rendered);
843
885
  return rendered;
@@ -1175,6 +1217,18 @@ export function isControllerManagedInvocation(context, projectRoot, runId) {
1175
1217
  /** recordReceipt is a synchronous SQL-only hook ingress callback. Native
1176
1218
  * adapters must prepare any filesystem or process evidence before calling. */
1177
1219
  export function observeLifecycleInvocation(context, input) {
1220
+ const priorReceipt = input.currentEventKey && input.projectId
1221
+ && context.db.get("SELECT 1 FROM hook_events WHERE project_id = ? AND event_key = ?", [input.projectId, input.currentEventKey]);
1222
+ const result = observeLifecycleInvocationReceipt(context, input);
1223
+ // Hook-only calls have no invocation row, but their immutable receipt still
1224
+ // distinguishes a repeated native delivery from a new observation.
1225
+ if (priorReceipt && result.disposition === "admitted" && result.eventKey === input.currentEventKey) {
1226
+ result.disposition = "replay";
1227
+ result.duplicate = true;
1228
+ }
1229
+ return { ...result, currentEventKey: input.currentEventKey ?? null, authoritativeEventKey: result.eventKey ?? null };
1230
+ }
1231
+ function observeLifecycleInvocationReceipt(context, input) {
1178
1232
  storage(context);
1179
1233
  const projectRoot = input.projectId ? context.db.get("SELECT root FROM projects WHERE id = ?", [input.projectId])?.root : undefined;
1180
1234
  const provenScope = projectRoot && input.harness ? managedNativeInvocationScope(context, projectRoot, input.identity, input.harness) : null;
@@ -1246,7 +1300,7 @@ export function observeLifecycleInvocation(context, input) {
1246
1300
  }
1247
1301
  if (analysis.kind !== "none")
1248
1302
  throw new AppError("lifecycle_shell_syntax_invalid", "This shell form cannot bind exactly one lifecycle execution; use the exact issued lifecycle command without the unsupported shell context", 1, { phase: "prepare", effect: "no_effect", recoverable: true, disposition: "pre_cli_shell_refusal", reason: analysis.kind === "compound" ? analysis.reason : "unsupported_lifecycle_command", event_key: eventKey, ...issuedBarrier });
1249
- return { eventKey, duplicate: false, invocationId: null };
1303
+ return { eventKey, duplicate: false, disposition: "admitted", invocationId: null };
1250
1304
  }
1251
1305
  // Admit the one parsed leaf, never execute or fingerprint the prefix/suffix.
1252
1306
  const leafCommand = analysis.invocation.command;
@@ -1269,7 +1323,7 @@ export function observeLifecycleInvocation(context, input) {
1269
1323
  return false;
1270
1324
  }
1271
1325
  if (row.identity_json && row.identity_json !== deliveredIdentity
1272
- && JSON.stringify(JSON.parse(row.identity_json).slice(0, 5)) === JSON.stringify(JSON.parse(deliveredIdentity).slice(0, 5))) {
1326
+ && sameNativeCall(row.identity_json, deliveredIdentity)) {
1273
1327
  throw new AppError("invocation_identity_mismatch", "The same native tool call changed its bound execution route", 1, { phase: "prepare", effect: "no_effect", recoverable: false });
1274
1328
  }
1275
1329
  if (row.status === "executing" && row.identity_json !== deliveredIdentity)
@@ -1302,7 +1356,7 @@ export function observeLifecycleInvocation(context, input) {
1302
1356
  if (retainedOwner !== ownerSnapshot())
1303
1357
  throw new AppError("invocation_scope_unproven", "Native owner changed before hook-only recovery receipt", 1, { effect: "no_effect" });
1304
1358
  const eventKey = input.recordReceipt();
1305
- return { eventKey, duplicate: false, invocationId: null };
1359
+ return { eventKey, duplicate: false, disposition: "admitted", invocationId: null };
1306
1360
  });
1307
1361
  }
1308
1362
  const resolved = input.id === initial.id ? {} : { invocationId: initial.id };
@@ -1313,7 +1367,7 @@ export function observeLifecycleInvocation(context, input) {
1313
1367
  // reattach. Do not manufacture a new receipt or reject that read as a second
1314
1368
  // executor; the CLI returns the immutable stored outcome without dispatch.
1315
1369
  if (initial.status === "settled")
1316
- return { eventKey: initial.event_key, duplicate: true, ...resolved };
1370
+ return { eventKey: initial.event_key, duplicate: true, disposition: initial.identity_json === deliveredIdentity ? "replay" : "settled_read", ...resolved };
1317
1371
  const project = requireProjectByRoot(context, scope.projectRoot);
1318
1372
  const identity = input.identity;
1319
1373
  const identityJson = deliveredIdentity;
@@ -1324,16 +1378,16 @@ export function observeLifecycleInvocation(context, input) {
1324
1378
  if (row.scope_json !== initial.scope_json)
1325
1379
  throw new AppError("invocation_scope_mismatch", "Lifecycle scope changed before native receipt", 1, { effect: "no_effect" });
1326
1380
  if (row.status === "settled")
1327
- return { eventKey: row.event_key, duplicate: true, ...resolved };
1381
+ return { eventKey: row.event_key, duplicate: true, disposition: row.identity_json === identityJson ? "replay" : "settled_read", ...resolved };
1328
1382
  // Observation records native facts only. CLI owns deadline, argument and
1329
1383
  // RUN-state validation, including decisions about retrying an invocation.
1330
1384
  if (row.status === "expired" || (row.deadline !== null && row.deadline <= Date.now() && !row.event_key)) {
1331
- return { eventKey: row.event_key, duplicate: true, ...resolved };
1385
+ return { eventKey: row.event_key, duplicate: true, disposition: "expired", ...resolved };
1332
1386
  }
1333
1387
  if (row.identity_json === identityJson) {
1334
- return { eventKey: row.event_key, duplicate: true, ...resolved };
1388
+ return { eventKey: row.event_key, duplicate: true, disposition: "replay", ...resolved };
1335
1389
  }
1336
- if (row.identity_json && JSON.stringify(JSON.parse(row.identity_json).slice(0, 5)) === JSON.stringify(JSON.parse(identityJson).slice(0, 5))) {
1390
+ if (row.identity_json && sameNativeCall(row.identity_json, identityJson)) {
1337
1391
  throw new AppError("invocation_identity_mismatch", "The same native tool call changed its bound execution route", 1, { phase: "prepare", effect: "no_effect", recoverable: false });
1338
1392
  }
1339
1393
  const eventKey = input.recordReceipt({ projectRoot: scope.projectRoot, runId: scope.runId, command: initial.command });
@@ -1353,7 +1407,7 @@ export function observeLifecycleInvocation(context, input) {
1353
1407
  const updated = context.db.run("UPDATE lifecycle_invocations SET identity_json = ?, event_key = ?, status = 'observed', updated_at = ? WHERE id = ? AND status IN ('issued', 'observed')", [identityJson, eventKey, context.now(), initial.id]);
1354
1408
  // Once execution has been claimed, later PreToolUse observations are
1355
1409
  // retained as hook evidence but cannot replace the executor's receipt.
1356
- return { eventKey: updated.changes === 1 ? eventKey : row.event_key, duplicate: updated.changes !== 1, ...resolved };
1410
+ return { eventKey: updated.changes === 1 ? eventKey : row.event_key, duplicate: updated.changes !== 1, disposition: updated.changes === 1 ? "admitted" : "in_flight", ...resolved };
1357
1411
  });
1358
1412
  }
1359
1413
  /** Correlate exact native argv for diagnostics, independently of assignment
@@ -164,7 +164,7 @@ async function dispatch(context, input) {
164
164
  prompted = await adapterCall(["session", "prompt", "--session-id", sessionId, "--prompt-file", promptFile, ...common], null, `MERGE ${input.request.merge_request_id} Session ${sessionId} is still running`);
165
165
  }
166
166
  catch (error) {
167
- if (runtimeHarness(input.profile) !== "codex-desktop")
167
+ if (!["codex-desktop", "opencode-server"].includes(runtimeHarness(input.profile)))
168
168
  throw error;
169
169
  const capacityIntents = (retained.capacity_intents ?? []);
170
170
  const capacityFailures = (retained.capacity_failures ?? []);
@@ -211,10 +211,10 @@ export async function resumeControllerSession(context, row, manifest, state, ass
211
211
  receipt = await sendAck(dispatch.operation_id, dispatch.prompt);
212
212
  }
213
213
  catch (error) {
214
- if (runtimeHarness(session.profile) !== "codex-desktop")
214
+ if (!["codex-desktop", "opencode-server"].includes(runtimeHarness(session.profile)))
215
215
  throw error;
216
216
  const continuationText = `Continue the same retained Session and recovery generation. First inspect the current RUN recovery state; only if the exact ACK still awaits acceptance, execute the exact standalone acceptance command below once. Never deliver it again after acceptance. Then stop at the original Stage or Work-graph boundary.\n\n${dispatch.prompt}`;
217
- const inspect = (args, operationId) => runHarnessAdapter({ executable: manifest.adapters["codex-desktop"].executable,
217
+ const inspect = (args, operationId) => runHarnessAdapter({ executable: manifest.adapters[runtimeHarness(session.profile)].executable,
218
218
  args, env: controllerEnvironment(executionContext, manifest, session), runtimeOwner: session.owner, operationId, timeoutMs: 30_000 });
219
219
  const resumed = await continueCodexCapacity({ error, sessionId: session.id, operationId: dispatch.operation_id, allowSettledDescendants: true, continuationText,
220
220
  retainedIntents: () => {
@@ -578,12 +578,12 @@ async function executeController(context, row, manifest, state, assertOwnership,
578
578
  receipt = await send(text, firstOperationId);
579
579
  }
580
580
  catch (error) {
581
- if (runtimeHarness(session.profile) !== "codex-desktop")
581
+ if (!["codex-desktop", "opencode-server"].includes(runtimeHarness(session.profile)))
582
582
  throw error;
583
583
  const resumeCommand = hitl ? text.split("\n\n")[1] : undefined;
584
584
  if (hitl && !resumeCommand?.includes(" stage resume "))
585
585
  throw error;
586
- const inspect = (args, id) => runHarnessAdapter({ executable: manifest.adapters["codex-desktop"].executable,
586
+ const inspect = (args, id) => runHarnessAdapter({ executable: manifest.adapters[runtimeHarness(session.profile)].executable,
587
587
  args, env: controllerEnvironment(context, manifest, session), runtimeOwner: session.owner, operationId: id, timeoutMs: 30_000, signal: cancellation.signal });
588
588
  const resumed = await continueCodexCapacity({ error, sessionId: session.id, operationId: firstOperationId, signal: cancellation.signal, allowSettledDescendants: true, atomicIntent: true,
589
589
  retainedIntents: () => context.db.all("SELECT receipt_json FROM run_controller_operations WHERE controller_id = ? AND generation = ?", [row.controller_id, row.generation])
@@ -11,6 +11,7 @@ import { reconcileUnfinishedChecks } from "./code-checks.js";
11
11
  import { vnextStageDirectory } from "../domain/stage-catalog.js";
12
12
  import { stageWorkIds, stageGraphScope } from "./stage-work-graph.js";
13
13
  import { eligibleCodexChildOverload } from "./codex-capacity.js";
14
+ import { safeNativeFailure } from "../harness-runtime/lib/native-children.mjs";
14
15
  export const subagentCapacityKey = "runtime.subagent.available_slots";
15
16
  /** External concurrency is policy, never evidence of native child qualification. */
16
17
  export function getStageFanoutCapacity(context, input) {
@@ -175,7 +176,11 @@ export async function reconcileVnextFanout(context, input) {
175
176
  issues.push({ code: "execution_ended_without_work_result", work_id: link.work_id, session_id: child.session_id });
176
177
  continue;
177
178
  }
178
- const reason = JSON.stringify({ source: "native_child_observation", harness_id: value.harness_id, session_id: child.session_id, work_session_id: link.id, outcome: child.status });
179
+ const providerError = safeNativeFailure(child.provider_error);
180
+ const reason = JSON.stringify({ source: "native_child_observation", harness_id: value.harness_id, session_id: child.session_id, work_session_id: link.id, outcome: child.status,
181
+ ...(typeof child.terminal_turn_id === "string" ? { terminal_turn_id: child.terminal_turn_id } : {}),
182
+ ...(typeof child.terminal_observed_at === "string" ? { terminal_observed_at: child.terminal_observed_at } : {}),
183
+ ...(providerError ? { provider_error: providerError } : {}) });
179
184
  const result = await (child.status === "failed" ? failWork : cancelWork)(context, link.work_id, reason);
180
185
  reconciled.push({ work_id: link.work_id, work_session_id: link.id, outcome: child.status, result });
181
186
  }
@@ -1,4 +1,4 @@
1
- import { bindLifecyclePacketAssignments, managedLifecycleCommand } from "./lifecycle-invocations.js";
1
+ import { bindLifecyclePacketAssignments, managedLifecycleCommand, prepareWorkStartAuthority, issueLifecycleInvocation, retireWorkPacketAuthorities } from "./lifecycle-invocations.js";
2
2
  import { bindStageWork, currentStageWorkIds, stageWorkIds } from "./stage-work-graph.js";
3
3
  import { runtimeCommand } from "./runtime-command.js";
4
4
  import crypto from "node:crypto";
@@ -449,7 +449,38 @@ export function shortWorkId(id) { return shortEntityReference(id); }
449
449
  * PATH is not inherited; using the shared lifecycle command keeps them on the
450
450
  * same captured runtime as their parent stage.
451
451
  */
452
- export function workStartCommand(context, work) { const run = requireRun(context, work.project_id, work.run_id); const recovery = recoveryGuard(context, work.project_id, work.run_id); return managedLifecycleCommand(context, runtimeCommand(context, ["work", "start", shortWorkId(work.work_id), "--project-root", run.project_root, ...(recovery ? ["--recovery-id", recovery.recovery_id] : []), "--json"])); }
452
+ function rawWorkStartCommand(context, work) {
453
+ const run = requireRun(context, work.project_id, work.run_id), recovery = recoveryGuard(context, work.project_id, work.run_id);
454
+ return runtimeCommand(context, ["work", "start", shortWorkId(work.work_id), "--project-root", run.project_root, ...(recovery ? ["--recovery-id", recovery.recovery_id] : []), "--json"]);
455
+ }
456
+ export function workStartCommand(context, work) {
457
+ const command = rawWorkStartCommand(context, work);
458
+ const retained = context.db.get("SELECT status,current_start_invocation_id FROM works WHERE work_id = ?", [work.work_id]);
459
+ if (retained?.current_start_invocation_id) {
460
+ const prepared = prepareWorkStartAuthority(context, command, retained.current_start_invocation_id);
461
+ return managedLifecycleCommand({ ...context, env: { ...context.env, DD_FLOW_INVOCATION_SCOPE: JSON.stringify(prepared.scope) } }, `${command} --invocation-id ${retained.current_start_invocation_id}`);
462
+ }
463
+ // Legacy terminal/running Work is historical, never a fresh dispatch.
464
+ if (retained?.status !== "created")
465
+ return managedLifecycleCommand(context, command);
466
+ const prepared = prepareWorkStartAuthority(context, command);
467
+ if (!prepared)
468
+ return command;
469
+ if (!context.db.writable || context.env.DD_FLOW_INVOCATION_READONLY === "1")
470
+ throw new AppError("invocation_command_unprepared", "Work start authority has not been prepared", 1);
471
+ const id = context.db.writeTransaction(() => {
472
+ assertRunMutationAllowed(context, work.project_id, work.run_id, prepared.scope.generation);
473
+ const current = context.db.get("SELECT status,current_start_invocation_id FROM works WHERE work_id = ?", [work.work_id]);
474
+ if (current?.current_start_invocation_id)
475
+ return current.current_start_invocation_id;
476
+ if (current?.status !== "created")
477
+ throw new AppError("work_retry_conflict", "Work changed before start issuance", 1);
478
+ const issued = issueLifecycleInvocation(context, prepared.command, prepared.scope);
479
+ context.db.run("UPDATE works SET current_start_invocation_id = ? WHERE work_id = ?", [issued.id, work.work_id]);
480
+ return issued.id;
481
+ });
482
+ return managedLifecycleCommand(context, `${command} --invocation-id ${id}`);
483
+ }
453
484
  export function assertWorkLaunchReady(context, id) {
454
485
  ensureWorkRegistry(context);
455
486
  const work = requireWork(context, id);
@@ -639,7 +670,10 @@ export function startBoundWork(context, work, run, claimIdentity, hookEventId, s
639
670
  if (JSON.stringify(dependencyResults) !== JSON.stringify(preparedDependencies) || JSON.stringify(recovery) !== JSON.stringify(preparedRecovery))
640
671
  throw new AppError("work_not_ready", "Work dependencies or recovery segment changed while preparing its packet", 2, { work_id: current.work_id });
641
672
  const assignments = new Set();
642
- const commands = workerLifecycleCommands({ ...context, captureLifecycleAssignment: (id) => assignments.add(id) }, current, workerRun);
673
+ const currentStart = context.db.get("SELECT current_start_invocation_id FROM works WHERE work_id = ?", [work.work_id])?.current_start_invocation_id;
674
+ if (currentStart && context.env.DD_FLOW_CURRENT_INVOCATION && currentStart !== context.env.DD_FLOW_CURRENT_INVOCATION)
675
+ throw new AppError("work_generation_stale", "Work start authority belongs to an earlier attempt", 1, { work_id: work.work_id });
676
+ const commands = workerLifecycleCommands({ ...context, captureLifecycleAssignment: (id) => assignments.add(id) }, current, workerRun, true);
643
677
  const prompt = promptTemplate.replace(finishMarker, commands.finish).replace(failMarker, commands.fail);
644
678
  bindLifecyclePacketAssignments(context, prompt, assignments);
645
679
  const workContext = { schema_id: "dd-flow/work-context@1", run_id: current.run_id, work_id: current.work_id, parent_work_id: current.parent_work_id, project_root: run.project_root, workspace_root: workerRun.workspace_root, run_root: requireRunHome(run), depends_on: parseDependencies(current), launch_policy: current.launch_policy, result_schema: current.result_schema, read_only: readOnly, ...(recovery ? { recovery } : {}), ...(readOnly ? { workspace_fingerprint: fingerprint, ...(reviewInput ? { review_input: reviewInput } : {}), ...(reviewCopy ? { review_copy: reviewCopy } : {}) } : {}) };
@@ -771,7 +805,7 @@ export function prepareWorkRetry(context, id, reason) {
771
805
  throw new AppError("work_retry_invalid", "Retry journal is not valid JSON", 1);
772
806
  }
773
807
  const item = value;
774
- if (!item || typeof item !== "object" || item.work_id !== work.work_id || typeof item.archive !== "string" || !/^ATT-\d{3,}$/.test(item.archive) || !Array.isArray(item.entries) || item.entries.some(file => !["prompt.md", "result.json", "context.json"].includes(file)) || new Set(item.entries).size !== item.entries.length || !(item.result_sha256 === null || typeof item.result_sha256 === "string" && /^[a-f0-9]{64}$/.test(item.result_sha256)))
808
+ if (!item || typeof item !== "object" || item.work_id !== work.work_id || typeof item.archive !== "string" || !/^ATT-\d{3,}$/.test(item.archive) || !Array.isArray(item.entries) || item.entries.some(file => !["prompt.md", "result.json", "context.json", "result-input.json"].includes(file)) || new Set(item.entries).size !== item.entries.length || !(item.result_sha256 === null || typeof item.result_sha256 === "string" && /^[a-f0-9]{64}$/.test(item.result_sha256)))
775
809
  throw new AppError("work_retry_invalid", "Retry journal does not identify this Work", 1);
776
810
  pending = item;
777
811
  }
@@ -781,7 +815,7 @@ export function prepareWorkRetry(context, id, reason) {
781
815
  let number = 1;
782
816
  while (fs.existsSync(path.join(attempts, `ATT-${String(number).padStart(3, "0")}`)))
783
817
  number += 1;
784
- const prepared = pending ?? { work_id: work.work_id, archive: `ATT-${String(number).padStart(3, "0")}`, entries: ["prompt.md", "result.json", "context.json"].filter(file => fs.existsSync(path.join(directory, file))), result_sha256: work.result === null ? null : crypto.createHash("sha256").update(work.result).digest("hex") };
818
+ const prepared = pending ?? { work_id: work.work_id, archive: `ATT-${String(number).padStart(3, "0")}`, entries: ["prompt.md", "result.json", "context.json", "result-input.json"].filter(file => fs.existsSync(path.join(directory, file))), result_sha256: work.result === null ? null : crypto.createHash("sha256").update(work.result).digest("hex") };
785
819
  const archive = path.join(attempts, prepared.archive);
786
820
  if (work.status === "failed") {
787
821
  if (prepared.result_sha256 !== (work.result === null ? null : crypto.createHash("sha256").update(work.result).digest("hex")))
@@ -805,6 +839,8 @@ export function retryWork(context, id, reason) {
805
839
  // live path can be archived for the next attempt.
806
840
  materializePendingWorkResults(context, pending.project_id, pending.run_id, pending.work_id);
807
841
  const { work, run, directory, journal, pending: retainedJournal, prepared, archive } = prepareWorkRetry(context, id, reason);
842
+ const previousStart = context.db.get("SELECT current_start_invocation_id FROM works WHERE work_id = ?", [id])?.current_start_invocation_id;
843
+ const startAuthority = work.status === "failed" ? prepareWorkStartAuthority(context, rawWorkStartCommand(context, work), previousStart) : null;
808
844
  if (work.status === "failed") {
809
845
  // The durable journal and archive are projections of the failed SQL
810
846
  // receipt. They must finish before the short transaction opens a new
@@ -836,15 +872,18 @@ export function retryWork(context, id, reason) {
836
872
  }
837
873
  context.db.writeTransaction(() => {
838
874
  const current = requireWork(context, work.work_id);
839
- assertRunMutationAllowed(context, current.project_id, current.run_id);
875
+ assertRunMutationAllowed(context, current.project_id, current.run_id, startAuthority?.scope.generation);
840
876
  if (current.status !== "failed" || current.result !== work.result || current.updated_at !== work.updated_at)
841
877
  throw new AppError("work_retry_conflict", "Failed Work receipt changed during retry", 1);
878
+ retireWorkPacketAuthorities(context, work.work_id);
842
879
  for (const column of ["prompt_path", "result_path"]) {
843
880
  const file = column === "prompt_path" ? "prompt.md" : "result.json";
844
881
  context.db.run(`UPDATE work_sessions SET ${column} = ? WHERE work_id = ? AND ${column} = ?`, [path.join(archive, file), work.work_id, path.join(directory, file)]);
845
882
  }
846
883
  if (context.db.run("UPDATE works SET status = 'created', result = NULL, started_at = NULL, completed_at = NULL, updated_at = ? WHERE work_id = ? AND status = 'failed'", [context.now(), work.work_id]).changes !== 1)
847
884
  throw new AppError("work_retry_conflict", "Failed Work changed during retry", 1);
885
+ const startId = startAuthority ? issueLifecycleInvocation(context, startAuthority.command, startAuthority.scope).id : null;
886
+ context.db.run("UPDATE works SET current_start_invocation_id = ? WHERE work_id = ?", [startId, work.work_id]);
848
887
  });
849
888
  }
850
889
  fs.rmSync(journal, { force: true });
@@ -1138,8 +1177,12 @@ async function settle(context, id, status, result, progress, retry) {
1138
1177
  const control = runControlDirective(context, work.project_id, work.run_id);
1139
1178
  const newlyReady = control ? [] : context.db.all(`SELECT ${workColumns} FROM works WHERE project_id = ? AND run_id = ? AND status = 'created' ORDER BY created_at, work_id`, [work.project_id, work.run_id]).filter((candidate) => isReady(context, candidate)).map((candidate) => ({ work_id: candidate.work_id, launch_policy: candidate.launch_policy, start_command: workStartCommand(context, candidate) }));
1140
1179
  appendFlowRunTimelineEvent(context, work.project_id, work.run_id, { type: `work_${status}`, work_id: work.work_id, session_id: link?.session_id ?? null, ...(coordinationDrift.length ? { coordination_drift: coordinationDrift } : {}) });
1141
- for (const ready of newlyReady)
1142
- appendFlowRunTimelineEvent(context, work.project_id, work.run_id, { type: "work_dependency_unblocked", work_id: ready.work_id, completed_dependency: work.work_id });
1180
+ if (status === "completed")
1181
+ for (const ready of newlyReady) {
1182
+ const candidate = requireWork(context, ready.work_id);
1183
+ if (parseDependencies(candidate).includes(work.work_id))
1184
+ appendFlowRunTimelineEvent(context, work.project_id, work.run_id, { type: "work_dependency_unblocked", work_id: ready.work_id, completed_dependency: work.work_id });
1185
+ }
1143
1186
  return { ok: true, ...(control ? { control } : {}), work_id: id, status, checks: receipts, coordination: { planned_write_areas: codePacket(work)?.planned_write_areas ?? [], drift: coordinationDrift }, newly_ready: newlyReady, graph: codeWorkGraph(context, work.project_id, work.run_id), usage: { status: "provisional", reason: "final usage is controller-owned" } };
1144
1187
  }
1145
1188
  function recoveryPrompt(recovery) { return ["<recovery_identity>", `- recovery_id: ${recovery.recoveryId}`, `- execution_generation: ${recovery.generation}`, "This Work was interrupted and is now an authorized recovery segment of the same assignment.", "</recovery_identity>", "", "<recovery_instructions>", `Read the retained pre-interruption evidence at ${recovery.evidencePath}.`, "Before changing files or rerunning a command, inspect the workspace, receipts, logs, and partial artifacts. Do not assume the last command succeeded or failed merely because its reply was lost.", "Preserve accepted work. Do not repeat completed Work, create undeclared Work, alter the original requirements, or edit dd-flow runtime state/evidence.", "Observe an existing check or finish instead of duplicating it. Retry only through an exact runtime retry command after establishing that its prior outcome permits it.", "Record the concrete unresolved operation rather than guessing if an external effect cannot be reconciled.", "</recovery_instructions>"].join("\n"); }
@@ -1291,11 +1334,11 @@ function renderWorkerPrompt(context, work, run, dependencies, commands, workspac
1291
1334
  const sources = matrix?.matrix.sources.filter(source => ["specify", "protocolize", "protocolize_report"].includes(source.role)) ?? [];
1292
1335
  return ["<accepted_decision_precedence>", `Read ${path.join(requireRunHome(run), "01-specify", "specify.json")} and later accepted RUN decisions relevant to this Work.`, ...sources.map(source => `Accepted ${source.role}: ${path.join(source.root === "run" ? requireRunHome(run) : run.workspace_root, source.path)} (sha256 ${source.sha256}). PROTOCOLIZE includes the effective amendments; preserve SPECIFY non-goals.`), acceptedDecisionPrecedence, "</accepted_decision_precedence>", "", body, verificationMatrixInstruction(matrix?.metadata ?? null, requireRunHome(run), `assigned Work ${work.work_id}`)].join("\n");
1293
1336
  }
1294
- function workerLifecycleCommands(context, work, run) {
1337
+ function workerLifecycleCommands(context, work, run, freshWorkAttempt = false) {
1295
1338
  const resultInputPath = path.join(requireRunHome(run), "works", work.work_id, "result-input.json");
1296
1339
  return {
1297
- finish: managedLifecycleCommand(context, runtimeCommand(context, ["work", "finish", work.work_id, "--result-file", resultInputPath, "--project-root", run.project_root, "--json", "--progress-jsonl"])),
1298
- fail: managedLifecycleCommand(context, runtimeCommand(context, ["work", "fail", shortWorkId(work.work_id), "--reason", "receipt path + exact external or semantic blocker", "--project-root", run.project_root, "--json"]))
1340
+ finish: managedLifecycleCommand(context, runtimeCommand(context, ["work", "finish", work.work_id, "--result-file", resultInputPath, "--project-root", run.project_root, "--json", "--progress-jsonl"]), { freshWorkAttempt }),
1341
+ fail: managedLifecycleCommand(context, runtimeCommand(context, ["work", "fail", shortWorkId(work.work_id), "--reason", "receipt path + exact external or semantic blocker", "--project-root", run.project_root, "--json"]), { freshWorkAttempt })
1299
1342
  };
1300
1343
  }
1301
1344
  function renderWorkerPromptBody(context, work, run, dependencies, commands) {
@@ -301,6 +301,9 @@ export function ensureVnextWorkStorage(db) {
301
301
  // A committed Work start is authoritative even if publishing its prompt
302
302
  // files is interrupted after commit. Keep the exact bytes for repair.
303
303
  db.writeTransaction(() => {
304
+ const workColumns = db.all("PRAGMA table_info(works)");
305
+ if (!workColumns.some(column => column.name === "current_start_invocation_id"))
306
+ db.execSchema("ALTER TABLE works ADD COLUMN current_start_invocation_id TEXT");
304
307
  const columns = db.all("PRAGMA table_info(work_sessions)");
305
308
  if (!columns.some((column) => column.name === "start_receipt_json"))
306
309
  db.execSchema("ALTER TABLE work_sessions ADD COLUMN start_receipt_json TEXT");
@@ -1558,6 +1561,7 @@ function migrate(db) {
1558
1561
  ensureColumn(db, "runs", "layout_version", "ALTER TABLE runs ADD COLUMN layout_version TEXT");
1559
1562
  ensureColumn(db, "runs", "artifact_root_kind", "ALTER TABLE runs ADD COLUMN artifact_root_kind TEXT");
1560
1563
  ensureColumn(db, "works", "payload_json", "ALTER TABLE works ADD COLUMN payload_json TEXT");
1564
+ ensureColumn(db, "works", "current_start_invocation_id", "ALTER TABLE works ADD COLUMN current_start_invocation_id TEXT");
1561
1565
  ensureColumn(db, "work_sessions", "start_receipt_json", "ALTER TABLE work_sessions ADD COLUMN start_receipt_json TEXT");
1562
1566
  ensureColumn(db, "work_sessions", "start_publication_status", "ALTER TABLE work_sessions ADD COLUMN start_publication_status TEXT");
1563
1567
  ensureColumn(db, "work_sessions", "result_receipt_json", "ALTER TABLE work_sessions ADD COLUMN result_receipt_json TEXT");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deksden-com/dd-flow-cli",
3
- "version": "0.9.0-beta.133",
3
+ "version": "0.9.0-beta.135",
4
4
  "description": "Mechanical runtime CLI for dd-flow workflows.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.23.0",
@@ -41,6 +41,8 @@
41
41
  "release:publish": "node scripts/publish-release.mjs"
42
42
  },
43
43
  "devDependencies": {
44
+ "@opencode/client": "2.0.28",
45
+ "@opencode/plugin": "2.0.28",
44
46
  "@changesets/cli": "^2.31.0",
45
47
  "@eslint/js": "^9.39.1",
46
48
  "@types/node": "^24.10.1",
@@ -51,6 +53,7 @@
51
53
  },
52
54
  "dependencies": {
53
55
  "ajv": "^8.20.0",
56
+ "jsonc-parser": "3.3.1",
54
57
  "shell-quote": "^1.10.0",
55
58
  "smol-toml": "^1.6.1",
56
59
  "tree-sitter": "0.25.1",