@deksden-com/dd-flow-cli 0.9.0-beta.156 → 0.9.0-beta.159

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 +24 -7
  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 +1 -1
  13. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +1 -1
  14. package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -0
  15. package/dist/harness-runtime/lib/delegation-instructions.mjs +3 -0
  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/continuation-outcome.js +11 -6
  20. package/dist/services/controller-child-repair.js +46 -24
  21. package/dist/services/controller-fanout.js +62 -21
  22. package/dist/services/controller-result-authority.js +13 -8
  23. package/dist/services/controller-work-failure.js +198 -0
  24. package/dist/services/lifecycle-invocations.js +280 -96
  25. package/dist/services/managed-daemon-binding.js +40 -0
  26. package/dist/services/run-control.js +18 -36
  27. package/dist/services/run-controller-state.js +8 -0
  28. package/dist/services/run-controller.js +136 -27
  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 +59 -12
  33. package/dist/services/work-retry-context.js +55 -0
  34. package/dist/verification-components.json +3 -0
  35. package/package.json +1 -1
@@ -2,6 +2,21 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { AppError } from "../shared/errors.js";
4
4
  import { managedProcessRuntimeHome, managedProcessStatus, processTreeIsAlive } from "./managed-processes.js";
5
+ function retainedResourceStopped(resource, daemonId) {
6
+ if (resource.state !== "stopped" || resource.observation_conflict)
7
+ return false;
8
+ if (resource.native_retirement?.method)
9
+ return true;
10
+ // Completed native tasks retain an ordered terminal envelope, not a later
11
+ // stop-RPC receipt. Requiring that RPC after provider exit loses valid proof.
12
+ const source = resource.native_source, terminal = resource.terminal_source;
13
+ return Boolean(daemonId && resource.owner_daemon_id === daemonId
14
+ && resource.provider_session_id && resource.resource_id?.startsWith(`${resource.provider_session_id}/task-`)
15
+ && source?.locator && source.locator === terminal?.locator
16
+ && /^[a-f0-9]{64}$/.test(source.sha256 ?? "") && /^[a-f0-9]{64}$/.test(terminal?.sha256 ?? "")
17
+ && Number.isSafeInteger(source.step_index) && source.step_index >= 0
18
+ && Number.isSafeInteger(terminal?.step_index) && terminal.step_index > source.step_index);
19
+ }
5
20
  /** Native execution and AGY's potentially scheduled writers are separate facts. */
6
21
  export function managedExecutionReusable(state, harness) {
7
22
  const shutdown = state.shutdown, result = shutdown?.result;
@@ -85,5 +100,30 @@ export function managedDaemonExecutionAbsent(context, input) {
85
100
  if (!provider || !managedProviderBelongsToDaemon(provider, binding.process, binding.state))
86
101
  return false;
87
102
  }
103
+ // The registry leaders are not the entire native inventory: retain detached
104
+ // tools and schedules even after their provider endpoint disappears.
105
+ if (binding.state.native_topology_error || binding.state.native_retirement_notifications?.some(notice => notice.disposition !== "after_owned_native_stop" || notice.owner_daemon_id !== (binding.state.daemon_id ?? binding.state.config?.daemonId)
106
+ || !Number.isSafeInteger(notice.generation) || notice.revision !== binding.state.native_execution_revision))
107
+ return false;
108
+ const nativeResources = [...(binding.state.native_resources_entries ?? []), ...(binding.state.shutdown?.result?.native_resources?.entries ?? [])];
109
+ const retained = [...(binding.state.owned_processes ?? []), ...(binding.state.shutdown?.owned_processes ?? []),
110
+ ...nativeResources.filter(resource => resource.pid !== undefined || !retainedResourceStopped(resource, binding.state.daemon_id ?? binding.state.config?.daemonId))];
111
+ if (nativeResources.some(resource => resource.observation_conflict || resource.owner_daemon_id !== undefined
112
+ && resource.owner_daemon_id !== (binding.state.daemon_id ?? binding.state.config?.daemonId)))
113
+ return false;
114
+ for (const process of retained) {
115
+ if (!Number.isSafeInteger(process.pid) || process.pid <= 0 || typeof process.started !== "string" || !process.started.trim())
116
+ return false;
117
+ if (records.some(record => record.pid === process.pid && record.pid_started_at === process.started))
118
+ continue;
119
+ try {
120
+ globalThis.process.kill(process.pid, 0);
121
+ return false;
122
+ }
123
+ catch (error) {
124
+ if (error.code !== "ESRCH")
125
+ return false;
126
+ }
127
+ }
88
128
  return true;
89
129
  }
@@ -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));
@@ -85,6 +85,14 @@ export function retainControllerContinuation(state, fingerprint, error) {
85
85
  throw new AppError(error.code, error.message, 1, error.details);
86
86
  state.last_continuation = fingerprint;
87
87
  }
88
+ /** Keep a bounded diagnostic projection, not the whole failing prompt/receipt. */
89
+ export function controllerDependencyDiagnostic(recorded, protectedAction) {
90
+ const details = recorded.details && typeof recorded.details === "object" ? recorded.details : {};
91
+ return { primary: { code: String(recorded.code), message: String(recorded.message) }, protected_action: protectedAction,
92
+ ...Object.fromEntries(["reason", "work_id", "operation_id", "refusal_invocation_id", "successor_invocation_id", "stage", "attempt", "action", "invocation_id"].filter(key => typeof details[key] === "string").map(key => [key, details[key]])),
93
+ ...(details.checkpoint && typeof details.checkpoint === "object" ? { checkpoint: details.checkpoint } : {}),
94
+ ...(details.authority && typeof details.authority === "object" ? { authority: details.authority } : {}) };
95
+ }
88
96
  export function digest(value) { return digestBytes(JSON.stringify(value)); }
89
97
  export function digestBytes(value) { return crypto.createHash("sha256").update(value).digest("hex"); }
90
98
  export function errorRecord(error) {
@@ -1,4 +1,5 @@
1
- import { assertLifecycleOutcomes, currentLifecycleFailure, managedInvocationContext, managedLifecycleCommand, awaitLifecycleInvocation, assertLifecycleInvocationCurrent, bindControllerStageAssignment } from "./lifecycle-invocations.js";
1
+ import { assertLifecycleOutcomes, currentLifecycleFailure, currentStageFinishAuthority, managedInvocationContext, managedLifecycleCommand, awaitLifecycleInvocation, assertLifecycleInvocationCurrent, bindControllerStageAssignment } from "./lifecycle-invocations.js";
2
+ import { stageAttempt } from "./stage-work-graph.js";
2
3
  import { isSourceSnapshotFlow } from "../domain/source-snapshot-flows.js";
3
4
  import { isDeliveryFlow } from "../domain/delivery-flows.js";
4
5
  export function recoveryPacketMatchesStage(packet, scope) {
@@ -36,12 +37,12 @@ import { readNativeDaemonState, requireManagedDaemonBinding, managedDaemonExecut
36
37
  import { initCodexHome } from "./hooks.js";
37
38
  import { launchExternalWork } from "./external-work-launch.js";
38
39
  import { runControllerMemoryTask } from "./controller-memory-task.js";
39
- import { controllerNativeChildren, controllerStageEntryPrompt, controllerStageResumePrompt, nextControllerFanout } from "./controller-fanout.js";
40
+ import { controllerNativeChildren, controllerSemanticCheckpoint, controllerStageEntryPrompt, controllerStageResumePrompt, nextControllerFanout } from "./controller-fanout.js";
40
41
  import { publishPromptDelivery } from "./prompt-delivery.js";
41
42
  import { publishStagePacket } from "./stage-report-renderer.js";
42
43
  import { claimQueuedMergeRequest } from "./merge-server.js";
43
44
  import { prepareVnextCodeWorkspace } from "./vnext-code.js";
44
- import { requireRun, requireController, controllerReceipt, appendEvent, saveState, retainControllerContinuation, retainControllerCapacityIntent, digest, digestBytes, errorRecord } from "./run-controller-state.js";
45
+ import { requireRun, requireController, controllerReceipt, controllerDependencyDiagnostic, appendEvent, saveState, retainControllerContinuation, retainControllerCapacityIntent, digest, digestBytes, errorRecord } from "./run-controller-state.js";
45
46
  import { assertNoCorrelatedRejection } from "./continuation-outcome.js";
46
47
  import { spawnController, recordControllerLauncher, claimControllerProcess, markControllerDispatchReady, assertControllerLeaseAllowed } from "./run-controller-process.js";
47
48
  import { controllerAdapter, controllerEnvironment, sessionArguments } from "./run-controller-adapter.js";
@@ -56,6 +57,7 @@ import { RECOVERY_POLICY as CAPACITY_POLICY, nativeItemsSettled, recoveryExhaust
56
57
  import { renderNativeChildFollowupInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
57
58
  import { childCapacityDispatchAllowed, childCapacityRecoveryDecision, closeSuccessfulChildCapacity, reconcileChildCapacity } from "./controller-child-capacity.js";
58
59
  import { completedChildRepair, reserveChildRepair } from "./controller-child-repair.js";
60
+ import { prepareWorkFailureResolution, readFailureDecision, failureDecisionReady, acceptFailureDecision, applyWorkFailureDecision, reconcilePendingWorkFailure, workFailureResolutionPrompt, workFailureDiagnostic, workFailureBlockActive } from "./controller-work-failure.js";
59
61
  import { getVnextFanoutStatus } from "./vnext-fanout.js";
60
62
  import { isObservationLoss } from "../harness-runtime/lib/operation-errors.mjs";
61
63
  import { recordTechnicalNote } from "./technical-notes.js";
@@ -610,14 +612,18 @@ async function executeController(context, row, manifest, state, assertOwnership,
610
612
  };
611
613
  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));
612
614
  const stopAfterFailure = (error) => {
615
+ // The published pause already fences this owner. A later observer failure
616
+ // must not supersede that same control with a second pause or fatal stop.
617
+ if (technicalPauseRequested)
618
+ return Promise.resolve();
613
619
  // A retained successor may already be running. Fencing/recovery records
614
620
  // uncertainty; cancellation is not evidence of its missing outcome.
615
621
  if (capacityUncertain(error))
616
622
  return Promise.resolve();
617
623
  if (retainControllerLeaseUncertainty(context, row, error))
618
624
  return Promise.resolve();
619
- 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)
620
- return pauseDependency(state.sessions[state.current_session]).catch(cause => {
625
+ 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)
626
+ return pauseDependency(state.sessions[state.current_session], undefined, error, "productive_continuation").catch(cause => {
621
627
  if (!(cause instanceof AppError && cause.code === "controller_technical_dependency_paused"))
622
628
  return stopAfterFailure(new AppError("controller_dependency_control_unavailable", "The dependency pause could not be published safely", 1, { primary: errorRecord(error), control_error: errorRecord(cause) }));
623
629
  });
@@ -674,11 +680,22 @@ async function executeController(context, row, manifest, state, assertOwnership,
674
680
  note(session, "accepted_work_native_child_failed", "native_completion", "cleanup_pending", child.terminal_turn_id);
675
681
  return false;
676
682
  });
677
- const pauseDependency = async (session, sourceOperationId) => {
683
+ const pauseDependency = async (session, sourceOperationId, cause, protectedAction = "stage_transition") => {
678
684
  // The control intent contains the retained productive failure; it must not
679
685
  // require that same failure to disappear before the owner can pause it.
680
686
  assertPhysicalOwner();
681
- note(session, "native_writer_dependency_blocked", "stage_transition", "dependency_blocked");
687
+ const recorded = cause ? errorRecord(cause) : { code: "native_writer_dependency_blocked", message: "Conflicting native execution has not settled" };
688
+ const dependency = controllerDependencyDiagnostic(recorded, protectedAction);
689
+ const primary = dependency.primary;
690
+ const details = cause instanceof AppError ? cause.details : {};
691
+ recordTechnicalNote(context, row.project_id, row.run_id, { code: primary.code, message: primary.message,
692
+ phase: protectedAction, generation: row.generation, ...(state.stage ? { stage: state.stage } : {}),
693
+ ...(typeof details.work_id === "string" ? { work_id: details.work_id } : {}),
694
+ harness: runtimeHarness(session.profile), resource_identity: session.state_dir,
695
+ consequence: "dependency_blocked", ...(sourceOperationId ? { source_operation_id: sourceOperationId } : {}),
696
+ diagnostic_reference: JSON.stringify(dependency) });
697
+ // Diagnostic observations can race (adapter read vs lifecycle monitor), but
698
+ // they own one pause of this execution, not one control per error message.
682
699
  const requestId = `technical:${row.controller_id}:${row.generation}:${digest([state.stage, session.state_dir]).slice(0, 20)}`;
683
700
  const input = { projectRoot: manifest.request.project_root, runId: row.run_id, requestId, action: "pause" };
684
701
  // Publish source binding in the same SQL transaction as the control
@@ -692,13 +709,13 @@ async function executeController(context, row, manifest, state, assertOwnership,
692
709
  return;
693
710
  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 ?? ""]);
694
711
  if (!prior)
695
- 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 } : {}) });
712
+ 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 } : {}) });
696
713
  });
697
714
  if (typeof intent.control_id !== "string")
698
715
  throw new AppError("controller_dependency_control_unavailable", "Technical control was not created", 1, { intent });
699
716
  technicalPauseRequested = true;
700
717
  const control = await requestRunControl(executionContext, { ...input, waitMs: 0 });
701
- throw new AppError("controller_technical_dependency_paused", "Conflicting native execution requires control reconciliation before continuation", 1, { control, effect: "no_effect" });
718
+ throw new AppError("controller_technical_dependency_paused", "A retained dependency requires control reconciliation before continuation", 1, { control, ...dependency, effect: "no_effect" });
702
719
  };
703
720
  const confirmStopped = async (stateDir, receipt) => {
704
721
  const fail = () => new AppError("controller_native_shutdown_unproven", "The managed daemon is not proven clean and closed", 1, { state_dir: stateDir, receipt });
@@ -765,7 +782,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
765
782
  // A missing report is not a failed Stage. A real unstable source still
766
783
  // fences its dependent successor until the existing control can seal it.
767
784
  if (nextStage && ["recovery_observation_budget_exhausted", "controller_boundary_changed", "snapshot_source_changed", "owned_inventory_changed"].includes(code) && state.current_session !== null)
768
- await pauseDependency(state.sessions[state.current_session]);
785
+ await pauseDependency(state.sessions[state.current_session], undefined, error, "boundary_capture");
769
786
  }
770
787
  };
771
788
  const reconcilePromptPredecessor = async (session) => {
@@ -775,7 +792,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
775
792
  return;
776
793
  const receipt = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false).catch(async (error) => {
777
794
  if (accepted && controllerObligationAccepted(context, row, accepted) && lateNativeFailure(error, session.id))
778
- await pauseDependency(session);
795
+ await pauseDependency(session, undefined, error, "predecessor_settlement");
779
796
  throw error;
780
797
  });
781
798
  state.last_receipt = receipt;
@@ -786,12 +803,12 @@ async function executeController(context, row, manifest, state, assertOwnership,
786
803
  note(session, "accepted_boundary_settlement_pending", "native_observation", "cleanup_pending");
787
804
  if (receipt.native_settled === true && receipt.settled === true)
788
805
  return;
789
- await pauseDependency(session);
806
+ await pauseDependency(session, undefined, new AppError("accepted_boundary_settlement_pending", "Accepted boundary still has an unsettled native predecessor", 1), "predecessor_settlement");
790
807
  }
791
808
  throw new AppError("settlement_reconciliation_required", "The managed Session still has an unsettled productive predecessor", 1, { session_id: session.id, settlement: current });
792
809
  }
793
810
  };
794
- const prompt = async (session, text, operationId, kind, boundary = "finish", assignedWorkIds) => {
811
+ const prompt = async (session, text, operationId, kind, boundary = "finish", assignedWorkIds, failureResolution) => {
795
812
  const hitl = kind === "hitl";
796
813
  const pending = state.pending_capacity;
797
814
  if (pending && (pending.session_id !== session.id || pending.generation !== row.generation || pending.kind !== (kind ?? "stage") || operationId && operationId !== pending.root_operation_id))
@@ -800,6 +817,8 @@ async function executeController(context, row, manifest, state, assertOwnership,
800
817
  await reconcilePromptPredecessor(session);
801
818
  const firstOperationId = pending?.root_operation_id ?? operationId ?? `${row.controller_id}:prompt:${crypto.randomUUID()}`;
802
819
  const expectedBoundary = state.stage ? controllerObligation(context, row, state.stage, hitl ? "resume" : boundary, assignedWorkIds) : undefined;
820
+ if (expectedBoundary && failureResolution)
821
+ expectedBoundary.failure_resolution = failureResolution;
803
822
  const hitlText = async () => {
804
823
  const answerId = firstOperationId.slice(0, -`:prompt:${row.generation}`.length);
805
824
  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]);
@@ -959,8 +978,11 @@ async function executeController(context, row, manifest, state, assertOwnership,
959
978
  catch (error) {
960
979
  if (!fatal && error instanceof AppError && ["run_recovery_guarded", "run_recovery_generation_stale", "controller_owner_mismatch"].includes(error.code))
961
980
  throw error;
962
- if (!fatal && lateNativeFailure(error, session.id))
963
- await pauseDependency(session, firstOperationId);
981
+ if (!fatal && lateNativeFailure(error, session.id)) {
982
+ if (failureResolution && failureDecisionReady(failureResolution))
983
+ acceptFailureDecision(executionContext, row, failureResolution, assertOwner);
984
+ await pauseDependency(session, firstOperationId, error, "productive_continuation");
985
+ }
964
986
  await stopAfterFailure(error);
965
987
  if (!capacityUncertain(error) && !(error instanceof AppError && error.code === "controller_lease_unconfirmed"))
966
988
  cancellation.abort();
@@ -972,7 +994,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
972
994
  return receipt;
973
995
  };
974
996
  const monitor = setInterval(() => {
975
- if (fatal)
997
+ if (fatal || technicalPauseRequested)
976
998
  return;
977
999
  try {
978
1000
  checkOutcomes();
@@ -984,6 +1006,12 @@ async function executeController(context, row, manifest, state, assertOwnership,
984
1006
  }
985
1007
  }, 1000);
986
1008
  monitor.unref();
1009
+ const resolutionWriterSettled = async (session) => {
1010
+ const inspected = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
1011
+ state.last_receipt = inspected;
1012
+ saveState(context, row, state);
1013
+ return inspected.settled === true && !controllerNativeChildren(inspected, session.id).some(child => ["running", "unknown"].includes(child.status));
1014
+ };
987
1015
  try {
988
1016
  for (;;) {
989
1017
  if (technicalPauseRequested)
@@ -1007,7 +1035,17 @@ async function executeController(context, row, manifest, state, assertOwnership,
1007
1035
  const file = args?.includes("--prompt-file") ? args[args.indexOf("--prompt-file") + 1] : undefined;
1008
1036
  if (!session || pending.generation !== row.generation || !file || digestBytes(fs.readFileSync(file)) !== dispatch?.prompt_sha256)
1009
1037
  throw new AppError("capacity_continuation_conflict", "Retained capacity resume reference has no exact owned prompt", 1);
1010
- await prompt(session, fs.readFileSync(file, "utf8"), pending.root_operation_id, pending.kind === "stage" ? undefined : pending.kind);
1038
+ 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);
1039
+ continue;
1040
+ }
1041
+ const failureRecovery = await reconcilePendingWorkFailure(executionContext, row, state, next.kind, run.project_root, assertOwner, resolutionWriterSettled);
1042
+ if (failureRecovery === "waiting") {
1043
+ setStatus("waiting_for_children");
1044
+ await delay(1000);
1045
+ continue;
1046
+ }
1047
+ if (failureRecovery === "applied") {
1048
+ saveState(context, row, state);
1011
1049
  continue;
1012
1050
  }
1013
1051
  if (next.kind !== "paused" && state.current_session !== null) {
@@ -1055,6 +1093,11 @@ async function executeController(context, row, manifest, state, assertOwnership,
1055
1093
  appendEvent(context, row.controller_id, "terminal", { reason: next.reason });
1056
1094
  return;
1057
1095
  }
1096
+ if ((next.kind === "paused" || next.kind === "blocked") && workFailureBlockActive(executionContext, row, view.index)) {
1097
+ setStatus("waiting_for_dependency");
1098
+ await delay(1000, undefined, { signal: cancellation.signal });
1099
+ continue;
1100
+ }
1058
1101
  if (next.kind === "paused") {
1059
1102
  setStatus("waiting_for_user");
1060
1103
  const pause = view.index.stage_runs.find(stage => stage.stage === next.stage)?.pause;
@@ -1156,7 +1199,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
1156
1199
  }
1157
1200
  if (!session || transition.session_mode === "new_session" || !sameSessionProfile(session.profile, transition.coordinator) || path.resolve(session.cwd) !== stageCwd) {
1158
1201
  if (session && !await retireSession(session))
1159
- await pauseDependency(session);
1202
+ await pauseDependency(session, undefined, new AppError("native_session_cleanup_pending", "Retired Session still prevents conflicting workspace reuse", 1), "stage_transition");
1160
1203
  await renewLease();
1161
1204
  assertOwner();
1162
1205
  const directory = path.join(row.state_dir, `session-${state.sessions.length + 1}`);
@@ -1437,12 +1480,16 @@ async function executeController(context, row, manifest, state, assertOwnership,
1437
1480
  repairBlockedWorks.push(workId);
1438
1481
  recordTechnicalNote(context, row.project_id, row.run_id, { code: "child_work_repair_blocked", message: blocker.message,
1439
1482
  phase: "work_repair", generation: row.generation, stage: next.stage, work_id: workId, harness: childHarness,
1440
- resource_identity: workId, consequence: "dependency_blocked", diagnostic_reference: String(blocker.details.reason) });
1483
+ resource_identity: workId, consequence: "dependency_blocked", diagnostic_reference: JSON.stringify({
1484
+ protected_action: "work_finish", reason: blocker.details.reason, primary: blocker.details.primary_error,
1485
+ refusal_invocation_id: blocker.details.refusal_invocation_id, successor_invocation_id: blocker.details.successor_invocation_id,
1486
+ operation_id: blocker.details.operation_id
1487
+ }) });
1441
1488
  }
1442
1489
  }
1443
1490
  }
1444
1491
  const memoryFlow = ["mb-init", "mb-upgrade", "mb-fix", "interactive"].includes(run.flow_kind) || isSourceSnapshotFlow(run.flow_kind) || routing.kind === "descriptor" && isDeliveryFlow(run.flow_kind);
1445
- 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 };
1492
+ 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 } : {}) };
1446
1493
  const fanout = memoryFlow ? await runControllerMemoryTask(executionContext, { operation: "fanout", input: fanoutInput }, cancellation.signal)
1447
1494
  : await nextControllerFanout(executionContext, fanoutInput);
1448
1495
  if (memoryFlow)
@@ -1474,6 +1521,43 @@ async function executeController(context, row, manifest, state, assertOwnership,
1474
1521
  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 });
1475
1522
  throw new AppError("incomplete_subject_turn", "Managed Turn ended without a completed Stage or declared continuation", 1, { stage: next.stage });
1476
1523
  }
1524
+ if (fanout.kind === "failure") {
1525
+ const resolution = state.work_failure ?? prepareWorkFailureResolution(executionContext, row, next.stage, fanout.failure);
1526
+ if (resolution.work_id !== fanout.failure.work_id || resolution.work_session_id !== fanout.failure.work_session_id)
1527
+ throw new AppError("work_failure_scope_changed", "Pending resolution belongs to another failed attempt", 1);
1528
+ state.work_failure = resolution;
1529
+ saveState(context, row, state);
1530
+ while (!failureDecisionReady(resolution)) {
1531
+ const attempt = state.work_failure.submission_attempt ?? 0;
1532
+ if (attempt >= 2)
1533
+ 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");
1534
+ 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);
1535
+ try {
1536
+ readFailureDecision(resolution);
1537
+ }
1538
+ catch (error) {
1539
+ state.work_failure.submission_diagnostic = error instanceof Error ? error.message : String(error);
1540
+ }
1541
+ state.work_failure.submission_attempt = attempt + 1;
1542
+ saveState(context, row, state);
1543
+ }
1544
+ const decision = acceptFailureDecision(executionContext, row, resolution, assertOwner);
1545
+ if (decision.action === "block" && fanout.ready) {
1546
+ state.work_failure.deferred = true;
1547
+ saveState(context, row, state);
1548
+ continue;
1549
+ }
1550
+ if (!await resolutionWriterSettled(session)) {
1551
+ setStatus("waiting_for_children");
1552
+ await delay(1000);
1553
+ continue;
1554
+ }
1555
+ applyWorkFailureDecision(executionContext, row, resolution, decision, run.project_root, assertOwner);
1556
+ delete state.work_failure;
1557
+ state.last_continuation = null;
1558
+ saveState(context, row, state);
1559
+ continue;
1560
+ }
1477
1561
  if (fanout.kind === "wait") {
1478
1562
  if (fanout.reason === "accepted_work_native_cleanup") {
1479
1563
  const pending = children.filter(child => ["running", "unknown"].includes(child.status) && acceptedChildWork(context, row, childHarness, session.id, child.session_id));
@@ -1482,20 +1566,20 @@ async function executeController(context, row, manifest, state, assertOwnership,
1482
1566
  try {
1483
1567
  await call(session, ["session", "cancel", "--session-id", child.session_id, ...sessionArguments(context, manifest, session)], false);
1484
1568
  }
1485
- catch {
1486
- await pauseDependency(session);
1569
+ catch (error) {
1570
+ await pauseDependency(session, undefined, error, "accepted_child_retirement");
1487
1571
  }
1488
1572
  }
1489
1573
  const operationId = `${row.controller_id}:child-retirement:${crypto.randomUUID()}`;
1490
1574
  const observed = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false, operationId).catch(async (error) => {
1491
1575
  if (lateNativeFailure(error, session.id))
1492
- await pauseDependency(session);
1576
+ await pauseDependency(session, operationId, error, "accepted_child_retirement");
1493
1577
  throw error;
1494
1578
  });
1495
1579
  state.last_receipt = observed;
1496
1580
  saveState(context, row, state);
1497
1581
  if (!acceptedChildrenRetired(pending.map(child => child.session_id), observed, session.id, operationId))
1498
- await pauseDependency(session);
1582
+ await pauseDependency(session, operationId, new AppError("accepted_child_retirement_unconfirmed", "Accepted child results are retained but native retirement remains unconfirmed", 1), "accepted_child_retirement");
1499
1583
  continue;
1500
1584
  }
1501
1585
  setStatus(fanout.reason === "qualified_capacity_required" ? "waiting_for_capacity" : "waiting_for_children");
@@ -1551,13 +1635,38 @@ async function executeController(context, row, manifest, state, assertOwnership,
1551
1635
  if (rejected)
1552
1636
  throw fatal ?? rejected.reason;
1553
1637
  if (cleanupConflict)
1554
- await pauseDependency(session);
1638
+ await pauseDependency(session, undefined, new AppError("external_work_cleanup_pending", "Accepted external Work still has an unretired workspace writer", 1), "workspace_reuse");
1555
1639
  state.last_continuation = null;
1556
1640
  saveState(context, row, state);
1557
1641
  continue;
1558
1642
  }
1559
- if (state.last_continuation === fanout.fingerprint)
1560
- assertNoCorrelatedRejection(context, { projectId: row.project_id, runId: row.run_id, generation: row.generation, stage: next.stage, turn: state.turns });
1643
+ if (state.last_continuation === fanout.fingerprint) {
1644
+ // Re-read committed evidence before pausing; a settlement can race the
1645
+ // graph observation. No new prompt or invocation is issued to reconcile.
1646
+ const latest = executionContext.db.get("SELECT status,index_json FROM runs WHERE project_id=? AND id=?", [row.project_id, row.run_id]);
1647
+ const latestStage = latest && [...(JSON.parse(latest.index_json).stage_runs ?? [])].reverse().find((stage) => stage.stage === next.stage);
1648
+ if (latest?.status === "done" || latestStage?.status === "done")
1649
+ continue;
1650
+ if (fanout.authority) {
1651
+ const current = currentStageFinishAuthority(executionContext, next.stage);
1652
+ if (current?.status === "issued" && current.command) {
1653
+ const checkpoint = controllerSemanticCheckpoint(executionContext, { stage: next.stage, phase: "stage_finish", command: current.command, invocationId: current.id });
1654
+ if (JSON.stringify(checkpoint) !== JSON.stringify(fanout.checkpoint))
1655
+ continue;
1656
+ }
1657
+ }
1658
+ let rejected;
1659
+ try {
1660
+ assertNoCorrelatedRejection(context, { projectId: row.project_id, runId: row.run_id, generation: row.generation, stage: next.stage, turn: state.turns });
1661
+ }
1662
+ catch (error) {
1663
+ if (!(error instanceof AppError))
1664
+ throw error;
1665
+ rejected = error;
1666
+ }
1667
+ await pauseDependency(session, undefined, new AppError(rejected?.code ?? "fanout_stage_nonprogressing", rejected?.message ?? "The coordinator repeated an unchanged continuation", 1, { ...rejected?.details, stage: next.stage, attempt: stageAttempt(executionContext, row.project_id, row.run_id, next.stage), action: fanout.action,
1668
+ checkpoint: JSON.parse(fanout.fingerprint), ...(fanout.authority ? { authority: fanout.authority } : {}), reconciliation_required: true }), "productive_continuation");
1669
+ }
1561
1670
  retainControllerContinuation(state, fanout.fingerprint, { code: "fanout_stage_nonprogressing", message: "The coordinator repeated an unchanged Work-graph continuation", details: { stage: next.stage, action: fanout.action, before: state.last_continuation, after: fanout.fingerprint, controller_turn: state.turns } });
1562
1671
  saveState(context, row, state);
1563
1672
  await prompt(session, fanout.prompt, undefined, undefined, fanout.action === "materialize_graph" ? "graph" : fanout.action === "dispatch_work" ? "work" : "finish", fanout.work_ids);
@@ -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"