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

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 (35) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/build-info.json +8 -8
  3. package/dist/cli/error-output.js +11 -1
  4. package/dist/cli/input-preparation.js +32 -4
  5. package/dist/cli/run-cli.js +26 -8
  6. package/dist/harness-runtime/lib/daemon-operations.mjs +46 -11
  7. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +19 -6
  8. package/dist/harness-runtime/lib/dd-agy.mjs +19 -0
  9. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +1 -1
  10. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +1 -1
  11. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +2 -2
  12. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +3 -3
  13. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +1 -1
  14. package/dist/harness-runtime/lib/delegation-instructions.d.mts +2 -1
  15. package/dist/harness-runtime/lib/delegation-instructions.mjs +8 -2
  16. package/dist/schemas/code-work-batch.schema.json +1 -1
  17. package/dist/schemas/vnext-protocol-plan.schema.json +1 -1
  18. package/dist/services/codex-capacity.js +367 -0
  19. package/dist/services/controller-child-repair.js +117 -0
  20. package/dist/services/controller-fanout.js +33 -20
  21. package/dist/services/controller-result-authority.js +13 -8
  22. package/dist/services/controller-work-failure.js +198 -0
  23. package/dist/services/lifecycle-invocations.js +117 -25
  24. package/dist/services/managed-daemon-binding.js +40 -0
  25. package/dist/services/retained-native-hook.js +34 -5
  26. package/dist/services/run-control.js +18 -36
  27. package/dist/services/run-controller-process.js +19 -6
  28. package/dist/services/run-controller.js +154 -22
  29. package/dist/services/vnext-code.js +3 -1
  30. package/dist/services/vnext-plan-review.js +2 -2
  31. package/dist/services/vnext-plan.js +3 -3
  32. package/dist/services/work-registry.js +62 -15
  33. package/dist/services/work-retry-context.js +55 -0
  34. package/dist/verification-components.json +3 -0
  35. package/package.json +1 -1
@@ -12,6 +12,7 @@ import { handleAgyEvent, handleCodexHook, handleDroidEvent, handleOpenCodeEvent,
12
12
  import { withDaemonProgressObserver } from "../harness-runtime/lib/daemon-observation.mjs";
13
13
  const object = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
14
14
  const sha256 = (value) => crypto.createHash("sha256").update(value).digest("hex");
15
+ const diagnostics = new WeakMap();
15
16
  async function regularJson(file) {
16
17
  try {
17
18
  const handle = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
@@ -65,6 +66,23 @@ function payloadCommand(binding) {
65
66
  * binding from its verified native-delivery map before it becomes a receipt. */
66
67
  export async function reconcileRetainedNativeHook(context, input) {
67
68
  const { scope, invocationId } = input;
69
+ const diagnostic = (phase, error, binding) => {
70
+ if (!input.onDiagnostic)
71
+ return;
72
+ const supplied = error instanceof Error || object(error) ? error.code : undefined;
73
+ const code = typeof supplied === "string" && /^[A-Za-z][A-Za-z0-9_]{0,95}$/.test(supplied) ? supplied : "native_hook_verification_unavailable";
74
+ // Diagnostics never become execution authority or activity. Keep identifiers
75
+ // and a typed cause, not native messages/configs that can contain credentials.
76
+ const value = { phase, code, invocation_id: invocationId,
77
+ ...(typeof binding?.request_id === "string" ? { request_id: binding.request_id } : {}),
78
+ ...(typeof binding?.operation_id === "string" ? { operation_id: binding.operation_id } : {}) };
79
+ const seen = diagnostics.get(context) ?? new Set(), key = JSON.stringify(value);
80
+ if (seen.has(key))
81
+ return;
82
+ seen.add(key);
83
+ diagnostics.set(context, seen);
84
+ input.onDiagnostic(value);
85
+ };
68
86
  if (!scope.runId || context.env.DD_FLOW_DAEMON_ID !== scope.daemonId)
69
87
  return false;
70
88
  let owner = null;
@@ -84,17 +102,22 @@ export async function reconcileRetainedNativeHook(context, input) {
84
102
  let metadata;
85
103
  try {
86
104
  const state = readNativeDaemonState(stateDir);
87
- if ((state.daemon_id ?? state.config?.daemonId) !== scope.daemonId)
105
+ if ((state.daemon_id ?? state.config?.daemonId) !== scope.daemonId) {
106
+ diagnostic("owner", { code: "daemon_owner_mismatch" });
88
107
  return false;
108
+ }
89
109
  const project = requireProjectByRoot(context, scope.projectRoot);
90
110
  owned = requireManagedDaemonBinding(context, { stateDir, projectId: project.id, runId: scope.runId });
91
111
  metadata = JSON.parse(owned.process.metadata_json);
92
112
  }
93
- catch {
113
+ catch (error) {
114
+ diagnostic("owner", error);
94
115
  return false;
95
116
  }
96
- if (metadata.run_generation !== scope.generation)
117
+ if (metadata.run_generation !== scope.generation) {
118
+ diagnostic("owner", { code: "run_generation_mismatch" });
97
119
  return false;
120
+ }
98
121
  const operations = path.join(stateDir, "operations");
99
122
  if (!await realDirectory(operations))
100
123
  return false;
@@ -151,14 +174,20 @@ export async function reconcileRetainedNativeHook(context, input) {
151
174
  || !(resolved.parent_provider_session_id === null || typeof resolved.parent_provider_session_id === "string")
152
175
  || ["request_id", "operation_id", "daemon_id", "turn_generation", "source_harness", "root_provider_session_id", "provider_session_id",
153
176
  "tool_call_id", "event", "native_command", "command_sha256", "native_creator", "native_admission"].some(key => resolved[key] !== binding[key])
154
- || payloadCommand(resolved) !== binding.native_command)
177
+ || payloadCommand(resolved) !== binding.native_command) {
178
+ diagnostic("verify", { code: "native_hook_unproven" }, binding);
155
179
  continue;
180
+ }
156
181
  verified.push(resolved);
157
182
  }
158
183
  else if (proof.verified === true && JSON.stringify(proof.nativeHookBinding) === JSON.stringify(binding))
159
184
  verified.push(binding);
185
+ else
186
+ diagnostic("verify", { code: "native_hook_unproven" }, binding);
187
+ }
188
+ catch (error) {
189
+ diagnostic("verify", error, binding);
160
190
  }
161
- catch { /* Legacy/retired daemons cannot attest a retained fact. */ }
162
191
  }
163
192
  // Exact old tool delivery belongs to its immutable prior attempt. A public
164
193
  // command may be identical in a fresh no-effect successor; do not repeatedly
@@ -302,16 +302,19 @@ async function reconcileOwnedSessions(context, input, interrupt, boundary) {
302
302
  // third-party session/card never acknowledged completion.
303
303
  const physicalContext = { ...context, env: { ...context.env, DD_FLOW_RESOURCE_HOME: registeredProcesses.find(item => records.includes(item.record))?.home ?? resourceHome(context) } };
304
304
  let executionAbsent = false;
305
- if ((boundary || technicalPause) && !interrupt && !conflicts.has(directory) && !unsafeDaemons.has(canonicalPath(directory, false))) {
305
+ let absenceVersion;
306
+ if ((boundary || technicalPause || control?.action === "stop") && !interrupt && !conflicts.has(directory) && !unsafeDaemons.has(canonicalPath(directory, false))) {
306
307
  try {
307
- executionAbsent = managedDaemonExecutionAbsent(physicalContext, { stateDir: directory, projectId: project.id, runId: input.runId });
308
+ absenceVersion = nativeInventoryVersion(directory);
309
+ executionAbsent = managedDaemonExecutionAbsent(physicalContext, { stateDir: directory, projectId: project.id, runId: input.runId })
310
+ && absenceVersion === nativeInventoryVersion(directory);
308
311
  }
309
312
  catch { /* Unknown physical ownership remains on strict reconciliation. */ }
310
313
  }
311
314
  if (executionAbsent) {
312
315
  const leader = records.find(record => record.kind.endsWith("-daemon") && record.id === state.resource_process?.id && record.pid === state.pid && record.lease_token === state.resource_process.lease_token);
313
316
  if (leader && (state.daemon_id ?? state.config?.daemonId)) {
314
- nativeVersions.set(directory, nativeInventoryVersion(directory));
317
+ nativeVersions.set(directory, absenceVersion);
315
318
  closedDaemons.set(directory, { source: "owned_execution_absent", daemon_id: state.daemon_id ?? state.config?.daemonId,
316
319
  session_ids: state.sessions.map(item => typeof item === "string" ? item : adapterSessionId(item)), execution_settled: true,
317
320
  native_cleanup_confirmed: state.shutdown_state === "clean", processes: records.map(record => managedProcessBinding(record, registeredProcesses.find(item => item.record === record).home)) });
@@ -320,34 +323,6 @@ async function reconcileOwnedSessions(context, input, interrupt, boundary) {
320
323
  continue;
321
324
  }
322
325
  }
323
- // Explicit stop can reconcile a retired owner whose last bookkeeping ACK
324
- // failed. This is physical retirement, not a fabricated clean shutdown.
325
- if (control?.action === "stop" && state.shutdown_state === "cleanup_failed")
326
- try {
327
- const daemonId = state.daemon_id ?? state.config?.daemonId;
328
- const daemon = records.filter(record => record.kind.endsWith("-daemon"));
329
- const provider = records.filter(record => record.kind.endsWith("-provider"));
330
- const operations = retainedDaemonOperations(directory);
331
- const owner = state.config?.runtime_owner;
332
- if (daemonId && !conflicts.has(directory) && !unsafeDaemons.has(canonicalPath(directory, false))
333
- && state.active_tree === false && !state.active_operation && state.shutdown?.daemon_id === daemonId
334
- && state.shutdown.phases?.tree === true && state.shutdown.phases.provider === true
335
- && daemon.length === 1 && provider.length === 1 && records.length === 2
336
- && daemon[0].id === state.resource_process?.id && daemon[0].lease_token === state.resource_process.lease_token && daemon[0].pid === state.pid
337
- && provider[0].id === state.provider_process?.id && provider[0].lease_token === state.provider_process.lease_token && provider[0].pid === state.provider_pid
338
- && JSON.parse(daemon[0].metadata_json).dispatch_barrier === "run-generation@1"
339
- && records.every(record => record.pid && record.pid_started_at && ["stopped", "failed"].includes(record.state) && record.finished_at && !processTreeIsAlive(record))
340
- && operations.length > 0 && operations.every(operation => ["completed", "failed", "interrupted"].includes(String(operation.state))
341
- && (operation.operation === "daemon.start" ? operation.daemon_id === null && operation.launch?.runtime_owner === JSON.stringify(owner)
342
- : operation.daemon_id === daemonId && ["daemon.stop", "daemon.status", "session.create", "session.prompt", "session.start", "session.fork", "session.resume", "session.cancel", "session.inspect"].includes(String(operation.operation))
343
- && (operation.session_id == null || state.sessions.some(item => (typeof item === "string" ? item : adapterSessionId(item)) === operation.session_id))))) {
344
- nativeVersions.set(directory, nativeInventoryVersion(directory, operations));
345
- closedDaemons.set(directory, { source: "observed_owned_retirement", daemon_id: daemonId, stopped: true, settled: true,
346
- session_ids: state.sessions.map(item => typeof item === "string" ? item : adapterSessionId(item)),
347
- processes: records.map(record => ({ process_id: record.id, pid: record.pid, pid_started_at: record.pid_started_at })) });
348
- }
349
- }
350
- catch { /* Invalid or unfinished evidence remains on the existing recovery path. */ }
351
326
  if (!interrupt) {
352
327
  try {
353
328
  const nativeOperations = retainedDaemonOperations(directory);
@@ -477,10 +452,17 @@ async function reconcileOwnedSessions(context, input, interrupt, boundary) {
477
452
  if (!Array.isArray(args) || args.some(value => typeof value !== "string") || !saved.state_dir || !saved.executable || saved.owner !== operation.controller_id || conflicts.has(saved.state_dir) || directoryAdapters.get(saved.state_dir) !== saved.executable)
478
453
  throw new AppError("controller_operation_binding_unknown", "Unfinished controller operation has no matching frozen runtime binding", 1);
479
454
  const method = `${args[0]}.${args[1] === "status" ? "inspect" : args[1]}`;
480
- if ((boundary || technicalPause) && closedDaemons.get(saved.state_dir)?.source === "owned_execution_absent"
481
- && (["daemon.stop", "session.cancel", "session.inspect"].includes(method)
455
+ if (control?.action === "stop" && closedDaemons.get(saved.state_dir)?.source === "owned_execution_absent"
456
+ && (!["session.create", "session.prompt", "session.start", "session.fork", "session.cancel", "session.resume", "session.inspect", "daemon.start", "daemon.stop"].includes(method)
457
+ || !args.includes("--state-dir") || args[args.indexOf("--state-dir") + 1] !== saved.state_dir
458
+ || args.includes("--session-id") && !targets.has(JSON.stringify([saved.state_dir, args[args.indexOf("--session-id") + 1]]))))
459
+ throw new AppError("controller_operation_binding_unknown", "Fenced operation differs from its retained runtime scope", 1);
460
+ if ((boundary || technicalPause || control?.action === "stop") && closedDaemons.get(saved.state_dir)?.source === "owned_execution_absent"
461
+ && (control?.action === "stop" || ["daemon.stop", "session.cancel", "session.inspect"].includes(method)
482
462
  || saved.obligation && controllerObligationAccepted(context, requireController(context, operation.controller_id), saved.obligation))) {
483
- return { operation_id: operation.operation_id, status: "completed", receipt: { source: "accepted_boundary_execution_absent", native_outcome: "unavailable", cleanup_pending: true } };
463
+ // This completes the settlement observation, not the original unknown
464
+ // operation. Its retained row/effect remains unknown and is not replayed.
465
+ return { operation_id: operation.operation_id, status: "completed", receipt: { source: control?.action === "stop" ? "fenced_stop_execution_absent" : "accepted_boundary_execution_absent", native_outcome: "unavailable", original_operation_status: operation.status, semantic_outcome_accepted: false, cleanup_pending: true } };
484
466
  }
485
467
  if (!["session.create", "session.prompt", "session.start", "session.fork", "session.cancel", "session.resume", "session.inspect", "daemon.start", "daemon.stop"].includes(method))
486
468
  throw new AppError("controller_operation_receipt_required", "Unfinished controller operation needs its original receipt", 1);
@@ -733,7 +715,7 @@ async function reconcileOwnedSessions(context, input, interrupt, boundary) {
733
715
  // remaining idempotent resource finish. Unknown outcomes keep their ID.
734
716
  if (sessionsSettled) {
735
717
  const remainingFinish = state.shutdown_state === "cleanup_failed" && state.active_tree === false
736
- && state.shutdown?.daemon_id === daemonId && state.shutdown.phases?.tree === true
718
+ && state.shutdown?.daemon_id === daemonId && state.shutdown.phases?.execution === true
737
719
  && state.shutdown.required_phases?.includes("daemon_resource")
738
720
  && state.shutdown.phases.daemon_resource !== true
739
721
  && state.shutdown.required_phases.every(phase => phase === "daemon_resource" || state.shutdown.phases[phase] === true);
@@ -922,7 +904,7 @@ async function reconcileOwnedSessions(context, input, interrupt, boundary) {
922
904
  }
923
905
  export function nativeInventoryVersion(directory, operations = retainedDaemonOperations(directory)) {
924
906
  const state = JSON.parse(fs.readFileSync(path.join(directory, "daemon.json"), "utf8"));
925
- const keys = ["schema_id", "daemon_id", "config", "pid", "provider_pid", "shutdown_state", "shutdown", "active_operation", "active_tree", "sessions", "descendants", "root_session_id", "resource_process", "provider_process", "owned_processes"];
907
+ const keys = ["schema_id", "daemon_id", "config", "pid", "provider_pid", "shutdown_state", "shutdown", "active_operation", "active_tree", "sessions", "descendants", "root_session_id", "resource_process", "provider_process", "owned_processes", "native_execution_revision", "unclaimed_activity", "native_resources_entries", "native_topology_error", "native_retirement_notifications", "terminal_observation_pending", "tool_observation_gaps"];
926
908
  // Only these native verbs are read-only. Unknown verbs remain guarded.
927
909
  const productive = operations.filter(operation => !isObservationOperation(operation.operation));
928
910
  const knownSchema = ["dd-opencode/daemon-state@2", "dd-codex/daemon-state@1", "dd-grok/daemon-state@1", "dd-zcode/daemon-state@1", "dd-agy/daemon-state@1", "dd-droid/daemon-state@1"].includes(String(state.schema_id));
@@ -79,12 +79,25 @@ async function prepareSpawn(context, row) {
79
79
  const launch = launchRecord(context, row);
80
80
  if (!launch || launch.value.dispatch_ready || !["starting", "running", "recovery_required"].includes(row.status))
81
81
  return null;
82
- const launcher = await physicalStartup(context.databaseDeadlineAt).birth(() => {
83
- const current = requireController(context, row.controller_id);
84
- if (current.owner_token !== row.owner_token || current.generation !== row.generation)
85
- throw new AppError("controller_owner_mismatch", "Controller launch owner changed during physical observation", 1);
86
- assertLaunchAllowed(context, current);
87
- });
82
+ const superseded = new AppError("controller_owner_mismatch", "Controller launch owner changed during physical observation", 1);
83
+ let launcher;
84
+ try {
85
+ launcher = await physicalStartup(context.databaseDeadlineAt).birth(() => {
86
+ const current = requireController(context, row.controller_id);
87
+ if (current.generation !== row.generation)
88
+ throw new AppError("controller_owner_mismatch", "Controller launch generation changed during physical observation", 1);
89
+ if (current.owner_token !== row.owner_token)
90
+ throw superseded;
91
+ assertLaunchAllowed(context, current);
92
+ });
93
+ }
94
+ catch (error) {
95
+ // Like a lost launch CAS below, a concurrent winner is observed, not
96
+ // retried or replaced. Other ownership/admission errors remain refusals.
97
+ if (error === superseded)
98
+ return null;
99
+ throw error;
100
+ }
88
101
  const resourceContext = { ...context, env: { ...context.env, DD_FLOW_RESOURCE_HOME: JSON.parse(row.manifest_json).resource_home } };
89
102
  const ours = launch.value.pid === launcher.pid && launch.value.pid_started_at === launcher.pid_started_at;
90
103
  let token = row.owner_token;
@@ -55,6 +55,9 @@ import { storageSessionId } from "./session-identity.js";
55
55
  import { RECOVERY_POLICY as CAPACITY_POLICY, nativeItemsSettled, recoveryExhausted as overloadExhausted, recoveryRefusal as refusalObservation, terminalProviderRecovery as terminalProviderOverload, normalizeRecoveryFailure } from "../harness-runtime/lib/provider-overload-policy.mjs";
56
56
  import { renderNativeChildFollowupInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
57
57
  import { childCapacityDispatchAllowed, childCapacityRecoveryDecision, closeSuccessfulChildCapacity, reconcileChildCapacity } from "./controller-child-capacity.js";
58
+ import { completedChildRepair, reserveChildRepair } from "./controller-child-repair.js";
59
+ import { prepareWorkFailureResolution, readFailureDecision, failureDecisionReady, acceptFailureDecision, applyWorkFailureDecision, reconcilePendingWorkFailure, workFailureResolutionPrompt, workFailureDiagnostic, workFailureBlockActive } from "./controller-work-failure.js";
60
+ import { getVnextFanoutStatus } from "./vnext-fanout.js";
58
61
  import { isObservationLoss } from "../harness-runtime/lib/operation-errors.mjs";
59
62
  import { recordTechnicalNote } from "./technical-notes.js";
60
63
  import { acceptedChildWork, acceptedChildrenRetired, controllerObligation, controllerObligationAccepted, lateNativeFailure } from "./controller-result-authority.js";
@@ -608,14 +611,18 @@ async function executeController(context, row, manifest, state, assertOwnership,
608
611
  };
609
612
  const capacityUncertain = (error) => error instanceof AppError && ["capacity_continuation_blocked", "operation_observation_lost"].includes(error.code) && error.details.capacity_uncertainty === true || Boolean(state.pending_capacity) && (String(error?.code) === "capacity_continuation_blocked" || isObservationLoss(error));
610
613
  const stopAfterFailure = (error) => {
614
+ // The published pause already fences this owner. A later observer failure
615
+ // must not supersede that same control with a second pause or fatal stop.
616
+ if (technicalPauseRequested)
617
+ return Promise.resolve();
611
618
  // A retained successor may already be running. Fencing/recovery records
612
619
  // uncertainty; cancellation is not evidence of its missing outcome.
613
620
  if (capacityUncertain(error))
614
621
  return Promise.resolve();
615
622
  if (retainControllerLeaseUncertainty(context, row, error))
616
623
  return Promise.resolve();
617
- if (error instanceof AppError && (error.details.reconciliation_required === true || ["invocation_assignment_unproven", "invocation_outcome_pending", "invocation_receipt_timeout"].includes(error.code)) && state.current_session !== null)
618
- return pauseDependency(state.sessions[state.current_session]).catch(cause => {
624
+ if (error instanceof AppError && error.code !== "child_work_repair_blocked" && (error.details.reconciliation_required === true || ["invocation_assignment_unproven", "invocation_outcome_pending", "invocation_receipt_timeout"].includes(error.code)) && state.current_session !== null)
625
+ return pauseDependency(state.sessions[state.current_session], undefined, error, "productive_continuation").catch(cause => {
619
626
  if (!(cause instanceof AppError && cause.code === "controller_technical_dependency_paused"))
620
627
  return stopAfterFailure(new AppError("controller_dependency_control_unavailable", "The dependency pause could not be published safely", 1, { primary: errorRecord(error), control_error: errorRecord(cause) }));
621
628
  });
@@ -672,9 +679,24 @@ async function executeController(context, row, manifest, state, assertOwnership,
672
679
  note(session, "accepted_work_native_child_failed", "native_completion", "cleanup_pending", child.terminal_turn_id);
673
680
  return false;
674
681
  });
675
- const pauseDependency = async (session, sourceOperationId) => {
676
- assertOwner();
677
- note(session, "native_writer_dependency_blocked", "stage_transition", "dependency_blocked");
682
+ const pauseDependency = async (session, sourceOperationId, cause, protectedAction = "stage_transition") => {
683
+ // The control intent contains the retained productive failure; it must not
684
+ // require that same failure to disappear before the owner can pause it.
685
+ assertPhysicalOwner();
686
+ const recorded = cause ? errorRecord(cause) : { code: "native_writer_dependency_blocked", message: "Conflicting native execution has not settled" };
687
+ const primary = { code: String(recorded.code), message: String(recorded.message) };
688
+ const details = cause instanceof AppError ? cause.details : {};
689
+ const dependency = { primary, protected_action: protectedAction,
690
+ ...(typeof details.reason === "string" ? { reason: details.reason } : {}),
691
+ ...Object.fromEntries(["work_id", "operation_id", "refusal_invocation_id", "successor_invocation_id"].filter(key => typeof details[key] === "string").map(key => [key, details[key]])) };
692
+ recordTechnicalNote(context, row.project_id, row.run_id, { code: primary.code, message: primary.message,
693
+ phase: protectedAction, generation: row.generation, ...(state.stage ? { stage: state.stage } : {}),
694
+ ...(typeof details.work_id === "string" ? { work_id: details.work_id } : {}),
695
+ harness: runtimeHarness(session.profile), resource_identity: session.state_dir,
696
+ consequence: "dependency_blocked", ...(sourceOperationId ? { source_operation_id: sourceOperationId } : {}),
697
+ diagnostic_reference: JSON.stringify(dependency) });
698
+ // Diagnostic observations can race (adapter read vs lifecycle monitor), but
699
+ // they own one pause of this execution, not one control per error message.
678
700
  const requestId = `technical:${row.controller_id}:${row.generation}:${digest([state.stage, session.state_dir]).slice(0, 20)}`;
679
701
  const input = { projectRoot: manifest.request.project_root, runId: row.run_id, requestId, action: "pause" };
680
702
  // Publish source binding in the same SQL transaction as the control
@@ -688,13 +710,13 @@ async function executeController(context, row, manifest, state, assertOwnership,
688
710
  return;
689
711
  const prior = context.db.get("SELECT 1 FROM run_controller_events WHERE controller_id=? AND event_type='technical_dependency_paused' AND json_extract(data_json,'$.control_id')=? AND COALESCE(json_extract(data_json,'$.source_operation_id'),'')=?", [row.controller_id, controlId, sourceOperationId ?? ""]);
690
712
  if (!prior)
691
- appendEvent(context, row.controller_id, "technical_dependency_paused", { schema_id: "dd-flow/controller-dependency@1", request_id: requestId, control_id: controlId, generation: row.generation, control: receipt, state_dir: session.state_dir, ...(sourceOperationId ? { source_operation_id: sourceOperationId } : {}) });
713
+ appendEvent(context, row.controller_id, "technical_dependency_paused", { schema_id: "dd-flow/controller-dependency@1", request_id: requestId, control_id: controlId, generation: row.generation, control: receipt, state_dir: session.state_dir, ...dependency, ...(sourceOperationId ? { source_operation_id: sourceOperationId } : {}) });
692
714
  });
693
715
  if (typeof intent.control_id !== "string")
694
716
  throw new AppError("controller_dependency_control_unavailable", "Technical control was not created", 1, { intent });
695
717
  technicalPauseRequested = true;
696
718
  const control = await requestRunControl(executionContext, { ...input, waitMs: 0 });
697
- throw new AppError("controller_technical_dependency_paused", "Conflicting native execution requires control reconciliation before continuation", 1, { control, effect: "no_effect" });
719
+ throw new AppError("controller_technical_dependency_paused", "A retained dependency requires control reconciliation before continuation", 1, { control, ...dependency, effect: "no_effect" });
698
720
  };
699
721
  const confirmStopped = async (stateDir, receipt) => {
700
722
  const fail = () => new AppError("controller_native_shutdown_unproven", "The managed daemon is not proven clean and closed", 1, { state_dir: stateDir, receipt });
@@ -761,7 +783,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
761
783
  // A missing report is not a failed Stage. A real unstable source still
762
784
  // fences its dependent successor until the existing control can seal it.
763
785
  if (nextStage && ["recovery_observation_budget_exhausted", "controller_boundary_changed", "snapshot_source_changed", "owned_inventory_changed"].includes(code) && state.current_session !== null)
764
- await pauseDependency(state.sessions[state.current_session]);
786
+ await pauseDependency(state.sessions[state.current_session], undefined, error, "boundary_capture");
765
787
  }
766
788
  };
767
789
  const reconcilePromptPredecessor = async (session) => {
@@ -771,7 +793,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
771
793
  return;
772
794
  const receipt = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false).catch(async (error) => {
773
795
  if (accepted && controllerObligationAccepted(context, row, accepted) && lateNativeFailure(error, session.id))
774
- await pauseDependency(session);
796
+ await pauseDependency(session, undefined, error, "predecessor_settlement");
775
797
  throw error;
776
798
  });
777
799
  state.last_receipt = receipt;
@@ -782,12 +804,12 @@ async function executeController(context, row, manifest, state, assertOwnership,
782
804
  note(session, "accepted_boundary_settlement_pending", "native_observation", "cleanup_pending");
783
805
  if (receipt.native_settled === true && receipt.settled === true)
784
806
  return;
785
- await pauseDependency(session);
807
+ await pauseDependency(session, undefined, new AppError("accepted_boundary_settlement_pending", "Accepted boundary still has an unsettled native predecessor", 1), "predecessor_settlement");
786
808
  }
787
809
  throw new AppError("settlement_reconciliation_required", "The managed Session still has an unsettled productive predecessor", 1, { session_id: session.id, settlement: current });
788
810
  }
789
811
  };
790
- const prompt = async (session, text, operationId, kind, boundary = "finish", assignedWorkIds) => {
812
+ const prompt = async (session, text, operationId, kind, boundary = "finish", assignedWorkIds, failureResolution) => {
791
813
  const hitl = kind === "hitl";
792
814
  const pending = state.pending_capacity;
793
815
  if (pending && (pending.session_id !== session.id || pending.generation !== row.generation || pending.kind !== (kind ?? "stage") || operationId && operationId !== pending.root_operation_id))
@@ -796,6 +818,8 @@ async function executeController(context, row, manifest, state, assertOwnership,
796
818
  await reconcilePromptPredecessor(session);
797
819
  const firstOperationId = pending?.root_operation_id ?? operationId ?? `${row.controller_id}:prompt:${crypto.randomUUID()}`;
798
820
  const expectedBoundary = state.stage ? controllerObligation(context, row, state.stage, hitl ? "resume" : boundary, assignedWorkIds) : undefined;
821
+ if (expectedBoundary && failureResolution)
822
+ expectedBoundary.failure_resolution = failureResolution;
799
823
  const hitlText = async () => {
800
824
  const answerId = firstOperationId.slice(0, -`:prompt:${row.generation}`.length);
801
825
  const answer = context.db.get("SELECT input_json,receipt_json FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND status IN ('accepted','completed')", [answerId, row.controller_id]);
@@ -955,8 +979,11 @@ async function executeController(context, row, manifest, state, assertOwnership,
955
979
  catch (error) {
956
980
  if (!fatal && error instanceof AppError && ["run_recovery_guarded", "run_recovery_generation_stale", "controller_owner_mismatch"].includes(error.code))
957
981
  throw error;
958
- if (!fatal && lateNativeFailure(error, session.id))
959
- await pauseDependency(session, firstOperationId);
982
+ if (!fatal && lateNativeFailure(error, session.id)) {
983
+ if (failureResolution && failureDecisionReady(failureResolution))
984
+ acceptFailureDecision(executionContext, row, failureResolution, assertOwner);
985
+ await pauseDependency(session, firstOperationId, error, "productive_continuation");
986
+ }
960
987
  await stopAfterFailure(error);
961
988
  if (!capacityUncertain(error) && !(error instanceof AppError && error.code === "controller_lease_unconfirmed"))
962
989
  cancellation.abort();
@@ -968,7 +995,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
968
995
  return receipt;
969
996
  };
970
997
  const monitor = setInterval(() => {
971
- if (fatal)
998
+ if (fatal || technicalPauseRequested)
972
999
  return;
973
1000
  try {
974
1001
  checkOutcomes();
@@ -980,6 +1007,12 @@ async function executeController(context, row, manifest, state, assertOwnership,
980
1007
  }
981
1008
  }, 1000);
982
1009
  monitor.unref();
1010
+ const resolutionWriterSettled = async (session) => {
1011
+ const inspected = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
1012
+ state.last_receipt = inspected;
1013
+ saveState(context, row, state);
1014
+ return inspected.settled === true && !controllerNativeChildren(inspected, session.id).some(child => ["running", "unknown"].includes(child.status));
1015
+ };
983
1016
  try {
984
1017
  for (;;) {
985
1018
  if (technicalPauseRequested)
@@ -1003,7 +1036,17 @@ async function executeController(context, row, manifest, state, assertOwnership,
1003
1036
  const file = args?.includes("--prompt-file") ? args[args.indexOf("--prompt-file") + 1] : undefined;
1004
1037
  if (!session || pending.generation !== row.generation || !file || digestBytes(fs.readFileSync(file)) !== dispatch?.prompt_sha256)
1005
1038
  throw new AppError("capacity_continuation_conflict", "Retained capacity resume reference has no exact owned prompt", 1);
1006
- await prompt(session, fs.readFileSync(file, "utf8"), pending.root_operation_id, pending.kind === "stage" ? undefined : pending.kind);
1039
+ await prompt(session, fs.readFileSync(file, "utf8"), pending.root_operation_id, pending.kind === "stage" ? undefined : pending.kind, dispatch?.obligation?.action ?? "finish", dispatch?.obligation?.work_ids, dispatch?.obligation?.failure_resolution);
1040
+ continue;
1041
+ }
1042
+ const failureRecovery = await reconcilePendingWorkFailure(executionContext, row, state, next.kind, run.project_root, assertOwner, resolutionWriterSettled);
1043
+ if (failureRecovery === "waiting") {
1044
+ setStatus("waiting_for_children");
1045
+ await delay(1000);
1046
+ continue;
1047
+ }
1048
+ if (failureRecovery === "applied") {
1049
+ saveState(context, row, state);
1007
1050
  continue;
1008
1051
  }
1009
1052
  if (next.kind !== "paused" && state.current_session !== null) {
@@ -1051,6 +1094,11 @@ async function executeController(context, row, manifest, state, assertOwnership,
1051
1094
  appendEvent(context, row.controller_id, "terminal", { reason: next.reason });
1052
1095
  return;
1053
1096
  }
1097
+ if ((next.kind === "paused" || next.kind === "blocked") && workFailureBlockActive(executionContext, row, view.index)) {
1098
+ setStatus("waiting_for_dependency");
1099
+ await delay(1000, undefined, { signal: cancellation.signal });
1100
+ continue;
1101
+ }
1054
1102
  if (next.kind === "paused") {
1055
1103
  setStatus("waiting_for_user");
1056
1104
  const pause = view.index.stage_runs.find(stage => stage.stage === next.stage)?.pause;
@@ -1152,7 +1200,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
1152
1200
  }
1153
1201
  if (!session || transition.session_mode === "new_session" || !sameSessionProfile(session.profile, transition.coordinator) || path.resolve(session.cwd) !== stageCwd) {
1154
1202
  if (session && !await retireSession(session))
1155
- await pauseDependency(session);
1203
+ await pauseDependency(session, undefined, new AppError("native_session_cleanup_pending", "Retired Session still prevents conflicting workspace reuse", 1), "stage_transition");
1156
1204
  await renewLease();
1157
1205
  assertOwner();
1158
1206
  const directory = path.join(row.state_dir, `session-${state.sessions.length + 1}`);
@@ -1394,8 +1442,55 @@ async function executeController(context, row, manifest, state, assertOwnership,
1394
1442
  continue;
1395
1443
  }
1396
1444
  }
1445
+ const repairBlockedWorks = [];
1446
+ if (!retainedFailures.length && children.some(child => child.status === "completed" && !acceptedChildWork(context, row, childHarness, session.id, child.session_id))) {
1447
+ const inspected = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
1448
+ state.last_receipt = inspected;
1449
+ saveState(context, row, state);
1450
+ children = controllerNativeChildren(inspected, session.id).filter(child => !baseline.has(child.session_id));
1451
+ // Do not compete with an active parent/healthy sibling or turn a known
1452
+ // repair into a replacement fanout wave.
1453
+ if (inspected.settled !== true || children.some(child => ["running", "unknown"].includes(child.status))) {
1454
+ setStatus("waiting_for_children");
1455
+ await delay(1000, undefined, { signal: cancellation.signal });
1456
+ continue;
1457
+ }
1458
+ let repair = null;
1459
+ const blockers = [];
1460
+ for (const child of children) {
1461
+ try {
1462
+ repair ??= completedChildRepair(executionContext, row, { stage: next.stage, harness: childHarness, parentId: session.id, children: [child] });
1463
+ }
1464
+ catch (error) {
1465
+ if (!(error instanceof AppError) || error.code !== "child_work_repair_blocked")
1466
+ throw error;
1467
+ blockers.push(error);
1468
+ }
1469
+ }
1470
+ if (repair) {
1471
+ reserveChildRepair(executionContext, row, repair, assertOwner);
1472
+ await prompt(session, repair.prompt, repair.operation_id, undefined, "work", [repair.work_id]);
1473
+ continue;
1474
+ }
1475
+ if (blockers.length) {
1476
+ const graph = getVnextFanoutStatus(executionContext, { projectRoot: run.project_root, runId: run.id, stage: next.stage }).orchestration;
1477
+ if (!graph?.works.ready.length)
1478
+ throw blockers[0];
1479
+ for (const blocker of blockers) {
1480
+ const workId = String(blocker.details.work_id);
1481
+ repairBlockedWorks.push(workId);
1482
+ recordTechnicalNote(context, row.project_id, row.run_id, { code: "child_work_repair_blocked", message: blocker.message,
1483
+ phase: "work_repair", generation: row.generation, stage: next.stage, work_id: workId, harness: childHarness,
1484
+ resource_identity: workId, consequence: "dependency_blocked", diagnostic_reference: JSON.stringify({
1485
+ protected_action: "work_finish", reason: blocker.details.reason, primary: blocker.details.primary_error,
1486
+ refusal_invocation_id: blocker.details.refusal_invocation_id, successor_invocation_id: blocker.details.successor_invocation_id,
1487
+ operation_id: blocker.details.operation_id
1488
+ }) });
1489
+ }
1490
+ }
1491
+ }
1397
1492
  const memoryFlow = ["mb-init", "mb-upgrade", "mb-fix", "interactive"].includes(run.flow_kind) || isSourceSnapshotFlow(run.flow_kind) || routing.kind === "descriptor" && isDeliveryFlow(run.flow_kind);
1398
- const fanoutInput = { projectRoot: run.project_root, runId: run.id, stage: next.stage, harness: runtimeHarness(session.profile), sessionId: session.id, children, policy, profiles, dispatchFence: retainedFailures.length > 0 };
1493
+ const fanoutInput = { projectRoot: run.project_root, runId: run.id, stage: next.stage, harness: runtimeHarness(session.profile), sessionId: session.id, children, policy, profiles, dispatchFence: retainedFailures.length > 0, repairBlockedWorks, ...(state.work_failure?.deferred ? { deferredFailure: state.work_failure.work_id } : {}) };
1399
1494
  const fanout = memoryFlow ? await runControllerMemoryTask(executionContext, { operation: "fanout", input: fanoutInput }, cancellation.signal)
1400
1495
  : await nextControllerFanout(executionContext, fanoutInput);
1401
1496
  if (memoryFlow)
@@ -1427,6 +1522,43 @@ async function executeController(context, row, manifest, state, assertOwnership,
1427
1522
  throw new AppError(cause.code, cause.message, 1, { ...cause.details, ...(cause.invocation_id ? { invocation_id: cause.invocation_id } : {}), ...(cause.hook_event_id ? { hook_event_id: cause.hook_event_id } : {}), controller_wrapper: "incomplete_subject_turn", stage: next.stage });
1428
1523
  throw new AppError("incomplete_subject_turn", "Managed Turn ended without a completed Stage or declared continuation", 1, { stage: next.stage });
1429
1524
  }
1525
+ if (fanout.kind === "failure") {
1526
+ const resolution = state.work_failure ?? prepareWorkFailureResolution(executionContext, row, next.stage, fanout.failure);
1527
+ if (resolution.work_id !== fanout.failure.work_id || resolution.work_session_id !== fanout.failure.work_session_id)
1528
+ throw new AppError("work_failure_scope_changed", "Pending resolution belongs to another failed attempt", 1);
1529
+ state.work_failure = resolution;
1530
+ saveState(context, row, state);
1531
+ while (!failureDecisionReady(resolution)) {
1532
+ const attempt = state.work_failure.submission_attempt ?? 0;
1533
+ if (attempt >= 2)
1534
+ await pauseDependency(session, undefined, new AppError("work_failure_decision_invalid", "Coordinator did not submit a valid bound failure decision", 1, workFailureDiagnostic(resolution)), "work_failure_resolution");
1535
+ await prompt(session, workFailureResolutionPrompt(resolution, state.work_failure.submission_diagnostic), `${row.controller_id}:failure-resolution:${digest([resolution.accepted_file, attempt, row.generation])}`, undefined, "failure_resolution", undefined, resolution);
1536
+ try {
1537
+ readFailureDecision(resolution);
1538
+ }
1539
+ catch (error) {
1540
+ state.work_failure.submission_diagnostic = error instanceof Error ? error.message : String(error);
1541
+ }
1542
+ state.work_failure.submission_attempt = attempt + 1;
1543
+ saveState(context, row, state);
1544
+ }
1545
+ const decision = acceptFailureDecision(executionContext, row, resolution, assertOwner);
1546
+ if (decision.action === "block" && fanout.ready) {
1547
+ state.work_failure.deferred = true;
1548
+ saveState(context, row, state);
1549
+ continue;
1550
+ }
1551
+ if (!await resolutionWriterSettled(session)) {
1552
+ setStatus("waiting_for_children");
1553
+ await delay(1000);
1554
+ continue;
1555
+ }
1556
+ applyWorkFailureDecision(executionContext, row, resolution, decision, run.project_root, assertOwner);
1557
+ delete state.work_failure;
1558
+ state.last_continuation = null;
1559
+ saveState(context, row, state);
1560
+ continue;
1561
+ }
1430
1562
  if (fanout.kind === "wait") {
1431
1563
  if (fanout.reason === "accepted_work_native_cleanup") {
1432
1564
  const pending = children.filter(child => ["running", "unknown"].includes(child.status) && acceptedChildWork(context, row, childHarness, session.id, child.session_id));
@@ -1435,20 +1567,20 @@ async function executeController(context, row, manifest, state, assertOwnership,
1435
1567
  try {
1436
1568
  await call(session, ["session", "cancel", "--session-id", child.session_id, ...sessionArguments(context, manifest, session)], false);
1437
1569
  }
1438
- catch {
1439
- await pauseDependency(session);
1570
+ catch (error) {
1571
+ await pauseDependency(session, undefined, error, "accepted_child_retirement");
1440
1572
  }
1441
1573
  }
1442
1574
  const operationId = `${row.controller_id}:child-retirement:${crypto.randomUUID()}`;
1443
1575
  const observed = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false, operationId).catch(async (error) => {
1444
1576
  if (lateNativeFailure(error, session.id))
1445
- await pauseDependency(session);
1577
+ await pauseDependency(session, operationId, error, "accepted_child_retirement");
1446
1578
  throw error;
1447
1579
  });
1448
1580
  state.last_receipt = observed;
1449
1581
  saveState(context, row, state);
1450
1582
  if (!acceptedChildrenRetired(pending.map(child => child.session_id), observed, session.id, operationId))
1451
- await pauseDependency(session);
1583
+ await pauseDependency(session, operationId, new AppError("accepted_child_retirement_unconfirmed", "Accepted child results are retained but native retirement remains unconfirmed", 1), "accepted_child_retirement");
1452
1584
  continue;
1453
1585
  }
1454
1586
  setStatus(fanout.reason === "qualified_capacity_required" ? "waiting_for_capacity" : "waiting_for_children");
@@ -1504,7 +1636,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
1504
1636
  if (rejected)
1505
1637
  throw fatal ?? rejected.reason;
1506
1638
  if (cleanupConflict)
1507
- await pauseDependency(session);
1639
+ await pauseDependency(session, undefined, new AppError("external_work_cleanup_pending", "Accepted external Work still has an unretired workspace writer", 1), "workspace_reuse");
1508
1640
  state.last_continuation = null;
1509
1641
  saveState(context, row, state);
1510
1642
  continue;
@@ -1,5 +1,5 @@
1
1
  import { managedLifecycleCommand } from "./lifecycle-invocations.js";
2
- import { stageLifecycleInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
2
+ import { stageLifecycleInstruction, workStopConditionInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
3
3
  import { ensureAggregateRepair, ensureRepairIntent } from "./repair-intents.js";
4
4
  import { runWorkspaceBootstrap } from "./workspace-bootstrap.js";
5
5
  import crypto from "node:crypto";
@@ -546,6 +546,8 @@ export function coordinatorPrompt(context, input) {
546
546
  "</code_graph>",
547
547
  "",
548
548
  "<execution_commands>",
549
+ workStopConditionInstruction,
550
+ "An explicit child work fail ends that attempt, not automatically the Stage. The controller supplies a bound failure-resolution packet; submit its requested decision without inventing a retry, mutating runtime state or replaying accepted predecessors.",
549
551
  "Do not choose a provider delegation tool from this stage prompt. At each work_fanout boundary, stop at the Work-graph boundary; the shared controller supplies the selected adapter's native tool, exact parameters, child task, and wait contract. It launches only entries listed in graph.ready and uses at most the qualified capacity. Every registered CODE Work runs in a fresh child Session, including a serial dependency chain. The coordinator launches those children only through the controller-supplied native delegation instruction; it must never invoke a Work start_command itself. Each child receives its complete packet from dd-flow and runs its own exact start_command. After a Work finishes, the controller uses the graph returned by work finish to launch newly ready Work. To refresh the parent graph yourself use the exact command: " + runtimeCommand(context, ["work", "ls", "--run", input.run.id, "--ready", "--project-root", input.projectRoot, "--json"]),
550
552
  "A quiet child is still running until the harness reports its turn completed, failed, cancelled or explicitly needs attention. An elapsed nominal wait, silence, or no new artifact is not an unresponsive-worker failure. Never interrupt, replace, relaunch, or stage-block a still-running child for that reason, even if an external controller asks. Long work finish and stage finish commands emit check progress on stderr. After you issue the exact CODE stage finish command, wait for that same command to return once: a completed command with a non-zero exit and structured `code_gate_failed` output is its terminal result, not a reason to keep waiting. A repair_required continuation already registered the repair: end this Turn. Do not invoke that Work's start_command from the coordinator Session, inspect its PID, start a second finish command, or infer failure from quiet output. Close a disposable child only after its Work is accepted or explicitly failed/cancelled and the harness reports the turn settled.",
551
553
  `A repairable engine, harness, or environment failure is not a user question. Write the evidence summary to ${path.join(requireHome(input.run), "works", input.rootWork.work_id, "block-summary.md")}, then record it with a separate command: ${runtimeCommand(context, ["stage", "block", input.run.id, "--stage", "code", "--work", input.rootWork.work_id, "--kind", "<engine|harness|environment>", "--code", "<stable-code>", "--summary-file", `@run/works/${input.rootWork.work_id}/block-summary.md`, "--retryable", "--project-root", input.projectRoot, "--json"])}. Choose kind and stable-code from evidence; this example is not an executable continuation. Repair it externally, then run the exact unblock_command returned by dd-flow and continue this same stage.`,
@@ -21,7 +21,7 @@ import { currentStagePrompt, applyExternalStageContext, prepareExternalStageCont
21
21
  import { prepareJsonSchema, prepareOutputFile } from "../cli/input-preparation.js";
22
22
  import { getStageFanoutCapacity, readFanoutDescriptor, subagentCapacityKey, writeFanoutDescriptor } from "./vnext-fanout.js";
23
23
  import { stageGraphScope } from "./stage-work-graph.js";
24
- import { stageLifecycleInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
24
+ import { stageLifecycleInstruction, workStopConditionInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
25
25
  import { prepareRunVerificationMatrix, acceptVerificationMatrix, commitVerificationMatrixStart, resumeVerificationMatrix, verificationMatrixInstruction, latestRunVerificationMatrix } from "./verification-matrix.js";
26
26
  import { reviewerSourceInputs, reviewerSourceInstruction } from "./review-copy.js";
27
27
  import { reviewDefEvidence } from "./review-deferral.js";
@@ -366,7 +366,7 @@ function orchestratorPrompt(context, input) {
366
366
  const decision = path.join(input.root, "decision.json");
367
367
  const workspaceContract = ["<workspace_contract>", `- route: ${input.workspaceRoute.route}`, `- feature branch: ${input.workspaceRoute.feature_branch ?? "not applicable"}`, `- base commit: ${input.workspaceRoute.base_ref ?? "not applicable"}`, `- read/write workspace: ${input.run.workspace_root}`, "The CLI verified this frozen route. All plan and correction writes belong in the named workspace; project root remains only the stable lifecycle identity. Do not create, switch, merge or delete branches/worktrees.", "</workspace_contract>"].join("\n");
368
368
  const acceptanceOwnership = "For every deferred acceptance or document update, identify the named CODE Work owner, its causal checks, and when it can truthfully close the obligation. Aggregate checks run only after implementation Work completes: assign a CODE owner to pending documents and use CODE needs_checks → verification_required → repair after those receipts. A promise that MERGE or an unnamed readiness owner will finish acceptance is a material PLAN finding; bootstrap workspace readiness is not proof of the new feature.";
369
- const template = [acceptanceOwnership, planCheckInstruction,
369
+ const template = [acceptanceOwnership, planCheckInstruction, workStopConditionInstruction, "Review stop_conditions semantically: reject conditions that veto the requested change merely because the baseline differs, turn planned paths into ownership, or expand one unsupported evidence operation into a prohibition on all implementation. Preserve real safety, accepted-decision and required-evidence boundaries.",
370
370
  `The accepted check profile is ${path.join(input.run.workspace_root, ".memory-bank", "spec", "engineering", "code-check-profile.json")}. A new check required by a correction is planned, not available: name its provider Work and order consumers after that Work.`,
371
371
  input.template].join("\n\n");
372
372
  const reviewerLaunch = capacity.source === "external_policy"