@deksden-com/dd-flow-cli 0.9.0-beta.156 → 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.
- package/CHANGELOG.md +15 -0
- package/dist/build-info.json +8 -8
- package/dist/cli/error-output.js +11 -1
- package/dist/cli/input-preparation.js +32 -4
- package/dist/cli/run-cli.js +24 -7
- package/dist/harness-runtime/lib/daemon-operations.mjs +46 -11
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +19 -6
- package/dist/harness-runtime/lib/dd-agy.mjs +19 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +1 -1
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +1 -1
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +2 -2
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +1 -1
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +1 -1
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +3 -0
- package/dist/schemas/code-work-batch.schema.json +1 -1
- package/dist/schemas/vnext-protocol-plan.schema.json +1 -1
- package/dist/services/codex-capacity.js +367 -0
- package/dist/services/controller-child-repair.js +46 -24
- package/dist/services/controller-fanout.js +23 -15
- package/dist/services/controller-result-authority.js +13 -8
- package/dist/services/controller-work-failure.js +198 -0
- package/dist/services/lifecycle-invocations.js +81 -16
- package/dist/services/managed-daemon-binding.js +40 -0
- package/dist/services/run-control.js +18 -36
- package/dist/services/run-controller.js +107 -22
- package/dist/services/vnext-code.js +3 -1
- package/dist/services/vnext-plan-review.js +2 -2
- package/dist/services/vnext-plan.js +3 -3
- package/dist/services/work-registry.js +59 -12
- package/dist/services/work-retry-context.js +55 -0
- package/dist/verification-components.json +3 -0
- package/package.json +1 -1
|
@@ -56,6 +56,7 @@ import { RECOVERY_POLICY as CAPACITY_POLICY, nativeItemsSettled, recoveryExhaust
|
|
|
56
56
|
import { renderNativeChildFollowupInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
|
|
57
57
|
import { childCapacityDispatchAllowed, childCapacityRecoveryDecision, closeSuccessfulChildCapacity, reconcileChildCapacity } from "./controller-child-capacity.js";
|
|
58
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";
|
|
59
60
|
import { getVnextFanoutStatus } from "./vnext-fanout.js";
|
|
60
61
|
import { isObservationLoss } from "../harness-runtime/lib/operation-errors.mjs";
|
|
61
62
|
import { recordTechnicalNote } from "./technical-notes.js";
|
|
@@ -610,14 +611,18 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
610
611
|
};
|
|
611
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));
|
|
612
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();
|
|
613
618
|
// A retained successor may already be running. Fencing/recovery records
|
|
614
619
|
// uncertainty; cancellation is not evidence of its missing outcome.
|
|
615
620
|
if (capacityUncertain(error))
|
|
616
621
|
return Promise.resolve();
|
|
617
622
|
if (retainControllerLeaseUncertainty(context, row, error))
|
|
618
623
|
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 => {
|
|
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 => {
|
|
621
626
|
if (!(cause instanceof AppError && cause.code === "controller_technical_dependency_paused"))
|
|
622
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) }));
|
|
623
628
|
});
|
|
@@ -674,11 +679,24 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
674
679
|
note(session, "accepted_work_native_child_failed", "native_completion", "cleanup_pending", child.terminal_turn_id);
|
|
675
680
|
return false;
|
|
676
681
|
});
|
|
677
|
-
const pauseDependency = async (session, sourceOperationId) => {
|
|
682
|
+
const pauseDependency = async (session, sourceOperationId, cause, protectedAction = "stage_transition") => {
|
|
678
683
|
// The control intent contains the retained productive failure; it must not
|
|
679
684
|
// require that same failure to disappear before the owner can pause it.
|
|
680
685
|
assertPhysicalOwner();
|
|
681
|
-
|
|
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.
|
|
682
700
|
const requestId = `technical:${row.controller_id}:${row.generation}:${digest([state.stage, session.state_dir]).slice(0, 20)}`;
|
|
683
701
|
const input = { projectRoot: manifest.request.project_root, runId: row.run_id, requestId, action: "pause" };
|
|
684
702
|
// Publish source binding in the same SQL transaction as the control
|
|
@@ -692,13 +710,13 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
692
710
|
return;
|
|
693
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 ?? ""]);
|
|
694
712
|
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 } : {}) });
|
|
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 } : {}) });
|
|
696
714
|
});
|
|
697
715
|
if (typeof intent.control_id !== "string")
|
|
698
716
|
throw new AppError("controller_dependency_control_unavailable", "Technical control was not created", 1, { intent });
|
|
699
717
|
technicalPauseRequested = true;
|
|
700
718
|
const control = await requestRunControl(executionContext, { ...input, waitMs: 0 });
|
|
701
|
-
throw new AppError("controller_technical_dependency_paused", "
|
|
719
|
+
throw new AppError("controller_technical_dependency_paused", "A retained dependency requires control reconciliation before continuation", 1, { control, ...dependency, effect: "no_effect" });
|
|
702
720
|
};
|
|
703
721
|
const confirmStopped = async (stateDir, receipt) => {
|
|
704
722
|
const fail = () => new AppError("controller_native_shutdown_unproven", "The managed daemon is not proven clean and closed", 1, { state_dir: stateDir, receipt });
|
|
@@ -765,7 +783,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
765
783
|
// A missing report is not a failed Stage. A real unstable source still
|
|
766
784
|
// fences its dependent successor until the existing control can seal it.
|
|
767
785
|
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]);
|
|
786
|
+
await pauseDependency(state.sessions[state.current_session], undefined, error, "boundary_capture");
|
|
769
787
|
}
|
|
770
788
|
};
|
|
771
789
|
const reconcilePromptPredecessor = async (session) => {
|
|
@@ -775,7 +793,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
775
793
|
return;
|
|
776
794
|
const receipt = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false).catch(async (error) => {
|
|
777
795
|
if (accepted && controllerObligationAccepted(context, row, accepted) && lateNativeFailure(error, session.id))
|
|
778
|
-
await pauseDependency(session);
|
|
796
|
+
await pauseDependency(session, undefined, error, "predecessor_settlement");
|
|
779
797
|
throw error;
|
|
780
798
|
});
|
|
781
799
|
state.last_receipt = receipt;
|
|
@@ -786,12 +804,12 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
786
804
|
note(session, "accepted_boundary_settlement_pending", "native_observation", "cleanup_pending");
|
|
787
805
|
if (receipt.native_settled === true && receipt.settled === true)
|
|
788
806
|
return;
|
|
789
|
-
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");
|
|
790
808
|
}
|
|
791
809
|
throw new AppError("settlement_reconciliation_required", "The managed Session still has an unsettled productive predecessor", 1, { session_id: session.id, settlement: current });
|
|
792
810
|
}
|
|
793
811
|
};
|
|
794
|
-
const prompt = async (session, text, operationId, kind, boundary = "finish", assignedWorkIds) => {
|
|
812
|
+
const prompt = async (session, text, operationId, kind, boundary = "finish", assignedWorkIds, failureResolution) => {
|
|
795
813
|
const hitl = kind === "hitl";
|
|
796
814
|
const pending = state.pending_capacity;
|
|
797
815
|
if (pending && (pending.session_id !== session.id || pending.generation !== row.generation || pending.kind !== (kind ?? "stage") || operationId && operationId !== pending.root_operation_id))
|
|
@@ -800,6 +818,8 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
800
818
|
await reconcilePromptPredecessor(session);
|
|
801
819
|
const firstOperationId = pending?.root_operation_id ?? operationId ?? `${row.controller_id}:prompt:${crypto.randomUUID()}`;
|
|
802
820
|
const expectedBoundary = state.stage ? controllerObligation(context, row, state.stage, hitl ? "resume" : boundary, assignedWorkIds) : undefined;
|
|
821
|
+
if (expectedBoundary && failureResolution)
|
|
822
|
+
expectedBoundary.failure_resolution = failureResolution;
|
|
803
823
|
const hitlText = async () => {
|
|
804
824
|
const answerId = firstOperationId.slice(0, -`:prompt:${row.generation}`.length);
|
|
805
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]);
|
|
@@ -959,8 +979,11 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
959
979
|
catch (error) {
|
|
960
980
|
if (!fatal && error instanceof AppError && ["run_recovery_guarded", "run_recovery_generation_stale", "controller_owner_mismatch"].includes(error.code))
|
|
961
981
|
throw error;
|
|
962
|
-
if (!fatal && lateNativeFailure(error, session.id))
|
|
963
|
-
|
|
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
|
+
}
|
|
964
987
|
await stopAfterFailure(error);
|
|
965
988
|
if (!capacityUncertain(error) && !(error instanceof AppError && error.code === "controller_lease_unconfirmed"))
|
|
966
989
|
cancellation.abort();
|
|
@@ -972,7 +995,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
972
995
|
return receipt;
|
|
973
996
|
};
|
|
974
997
|
const monitor = setInterval(() => {
|
|
975
|
-
if (fatal)
|
|
998
|
+
if (fatal || technicalPauseRequested)
|
|
976
999
|
return;
|
|
977
1000
|
try {
|
|
978
1001
|
checkOutcomes();
|
|
@@ -984,6 +1007,12 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
984
1007
|
}
|
|
985
1008
|
}, 1000);
|
|
986
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
|
+
};
|
|
987
1016
|
try {
|
|
988
1017
|
for (;;) {
|
|
989
1018
|
if (technicalPauseRequested)
|
|
@@ -1007,7 +1036,17 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1007
1036
|
const file = args?.includes("--prompt-file") ? args[args.indexOf("--prompt-file") + 1] : undefined;
|
|
1008
1037
|
if (!session || pending.generation !== row.generation || !file || digestBytes(fs.readFileSync(file)) !== dispatch?.prompt_sha256)
|
|
1009
1038
|
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);
|
|
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);
|
|
1011
1050
|
continue;
|
|
1012
1051
|
}
|
|
1013
1052
|
if (next.kind !== "paused" && state.current_session !== null) {
|
|
@@ -1055,6 +1094,11 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1055
1094
|
appendEvent(context, row.controller_id, "terminal", { reason: next.reason });
|
|
1056
1095
|
return;
|
|
1057
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
|
+
}
|
|
1058
1102
|
if (next.kind === "paused") {
|
|
1059
1103
|
setStatus("waiting_for_user");
|
|
1060
1104
|
const pause = view.index.stage_runs.find(stage => stage.stage === next.stage)?.pause;
|
|
@@ -1156,7 +1200,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1156
1200
|
}
|
|
1157
1201
|
if (!session || transition.session_mode === "new_session" || !sameSessionProfile(session.profile, transition.coordinator) || path.resolve(session.cwd) !== stageCwd) {
|
|
1158
1202
|
if (session && !await retireSession(session))
|
|
1159
|
-
await pauseDependency(session);
|
|
1203
|
+
await pauseDependency(session, undefined, new AppError("native_session_cleanup_pending", "Retired Session still prevents conflicting workspace reuse", 1), "stage_transition");
|
|
1160
1204
|
await renewLease();
|
|
1161
1205
|
assertOwner();
|
|
1162
1206
|
const directory = path.join(row.state_dir, `session-${state.sessions.length + 1}`);
|
|
@@ -1437,12 +1481,16 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1437
1481
|
repairBlockedWorks.push(workId);
|
|
1438
1482
|
recordTechnicalNote(context, row.project_id, row.run_id, { code: "child_work_repair_blocked", message: blocker.message,
|
|
1439
1483
|
phase: "work_repair", generation: row.generation, stage: next.stage, work_id: workId, harness: childHarness,
|
|
1440
|
-
resource_identity: workId, consequence: "dependency_blocked", diagnostic_reference:
|
|
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
|
+
}) });
|
|
1441
1489
|
}
|
|
1442
1490
|
}
|
|
1443
1491
|
}
|
|
1444
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);
|
|
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 };
|
|
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 } : {}) };
|
|
1446
1494
|
const fanout = memoryFlow ? await runControllerMemoryTask(executionContext, { operation: "fanout", input: fanoutInput }, cancellation.signal)
|
|
1447
1495
|
: await nextControllerFanout(executionContext, fanoutInput);
|
|
1448
1496
|
if (memoryFlow)
|
|
@@ -1474,6 +1522,43 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1474
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 });
|
|
1475
1523
|
throw new AppError("incomplete_subject_turn", "Managed Turn ended without a completed Stage or declared continuation", 1, { stage: next.stage });
|
|
1476
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
|
+
}
|
|
1477
1562
|
if (fanout.kind === "wait") {
|
|
1478
1563
|
if (fanout.reason === "accepted_work_native_cleanup") {
|
|
1479
1564
|
const pending = children.filter(child => ["running", "unknown"].includes(child.status) && acceptedChildWork(context, row, childHarness, session.id, child.session_id));
|
|
@@ -1482,20 +1567,20 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1482
1567
|
try {
|
|
1483
1568
|
await call(session, ["session", "cancel", "--session-id", child.session_id, ...sessionArguments(context, manifest, session)], false);
|
|
1484
1569
|
}
|
|
1485
|
-
catch {
|
|
1486
|
-
await pauseDependency(session);
|
|
1570
|
+
catch (error) {
|
|
1571
|
+
await pauseDependency(session, undefined, error, "accepted_child_retirement");
|
|
1487
1572
|
}
|
|
1488
1573
|
}
|
|
1489
1574
|
const operationId = `${row.controller_id}:child-retirement:${crypto.randomUUID()}`;
|
|
1490
1575
|
const observed = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false, operationId).catch(async (error) => {
|
|
1491
1576
|
if (lateNativeFailure(error, session.id))
|
|
1492
|
-
await pauseDependency(session);
|
|
1577
|
+
await pauseDependency(session, operationId, error, "accepted_child_retirement");
|
|
1493
1578
|
throw error;
|
|
1494
1579
|
});
|
|
1495
1580
|
state.last_receipt = observed;
|
|
1496
1581
|
saveState(context, row, state);
|
|
1497
1582
|
if (!acceptedChildrenRetired(pending.map(child => child.session_id), observed, session.id, operationId))
|
|
1498
|
-
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");
|
|
1499
1584
|
continue;
|
|
1500
1585
|
}
|
|
1501
1586
|
setStatus(fanout.reason === "qualified_capacity_required" ? "waiting_for_capacity" : "waiting_for_children");
|
|
@@ -1551,7 +1636,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1551
1636
|
if (rejected)
|
|
1552
1637
|
throw fatal ?? rejected.reason;
|
|
1553
1638
|
if (cleanupConflict)
|
|
1554
|
-
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");
|
|
1555
1640
|
state.last_continuation = null;
|
|
1556
1641
|
saveState(context, row, state);
|
|
1557
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"
|
|
@@ -21,7 +21,7 @@ import { vnextStageDirectory } from "../domain/stage-catalog.js";
|
|
|
21
21
|
import { acceptStageReport, prepareStageReport } from "./stage-report-renderer.js";
|
|
22
22
|
import { currentStagePrompt, applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
|
|
23
23
|
import { subagentCapacityKey } from "./vnext-fanout.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, verificationMatrixInstruction } from "./verification-matrix.js";
|
|
26
26
|
import { stageSuccessor } from "./vnext-contracts.js";
|
|
27
27
|
export function isVnextPlanRun(context, input) {
|
|
@@ -109,7 +109,7 @@ export function startVnextPlan(context, input, prepared = prepareVnextPlanStart(
|
|
|
109
109
|
? [`This RUN must reach MERGE. Project policy already supplies the mandatory merge gate${policyMergeAliases.length === 1 ? "" : "s"}: ${policyMergeAliases.join(", ")}. Do not duplicate them in semantic checks[]. Add another merge check only when the task genuinely needs additional evidence.`]
|
|
110
110
|
: ["This RUN must reach MERGE and project policy supplies no merge gate. Select at least one real top-level checks[] entry with run_at: merge. It may use an existing project alias or a planned alias materialised by a named P* provider Work. This is a planning obligation: do not defer it to CODE-REVIEW or MERGE."]), "The CLI validates the effective merge gate but never invents one or migrates an incompatible project policy.", "</merge_gate_contract>", ""]
|
|
111
111
|
: [];
|
|
112
|
-
const prompt = ["<stage_identity>", `- RUN: ${run.id}`, `- Work: ${planWorkId}`, "- stage: plan", "</stage_identity>", "", "<trusted_runtime_context>", "These facts were collected by dd-flow. Trust them; do not repeat CLI, Git, compatibility or permission discovery.", `- Project root: ${projectRoot}`, `- Workspace: ${run.workspace_root}`, `- Stage workspace: ${root}`, `- Git: ${JSON.stringify(git)}`, capacityContext, "</trusted_runtime_context>", "", "<workspace_contract>", `- route: ${workspaceRoute.route}`, `- feature branch: ${workspaceRoute.feature_branch ?? "not applicable"}`, `- base commit: ${workspaceRoute.base_ref ?? "not applicable"}`, `- write workspace: ${run.workspace_root}`, "The CLI has verified this frozen route. All project reads and writes for PLAN and later CODE happen in the write workspace; project root is only the stable runtime identity for lifecycle commands. Do not create, switch, merge or delete branches/worktrees.", "Keep the task runner's current cwd. Use the absolute paths in this packet instead of trying to set the provisioned workspace as a tool workdir.", "</workspace_contract>", "", "<accepted_inputs>", `- ${path.join(home, "01-specify", "specify.json")}`, `- ${path.join(home, "02-protocolize", "protocolize-result.json")}`, ...protocols.map((id) => `- ${path.join(run.workspace_root, ".memory-bank", "protocol", id, "summary.md")}`), "</accepted_inputs>", "", ...(codeCheckProfile ? ["<code_check_policy>", "You, not the CLI, select evidence for every accepted requirement and acceptance criterion. The profile only lists reusable aliases, mandatory project policy gates and guarded raw command prefixes. Inspect relevant package/test manifests before choosing a check. Do not classify checks by weight and do not omit a needed check because it looks expensive.", JSON.stringify(codeCheckProfile, null, 2), "</code_check_policy>", ""] : []), ...mergeContract, "<artifacts>", "The CLI has already materialized every artifact below as a partially filled draft. Edit these files in place; do not create replacements elsewhere.", "Prefilled and CLI-owned plan fields: schema_id, plan_id, protocol_id, initial revision and source_refs.", "Prefilled and CLI-owned aspect-map fields: schema_id, protocol_id, plan_id, plan revision, catalog_ref and every catalog aspect_id.", "You own the remaining semantic fields. Empty or missing semantic values are intentional draft markers and must be completed before validation.", ...planPaths.map((value) => `- partially filled plan: ${value}`), ...mapPaths.map((value) => `- partially filled aspect map: ${value}`), "</artifacts>", "", "<output_contract>", "Complete every named plan and aspect map in place. Do not create or edit code-work-batch.json: dd-flow derives it after validation.", "The CLI owns schema_id, plan_id, protocol_id, revision and source_refs. Preserve them exactly.", planCheckInstruction, "For each R-* and AC-*, choose an actually relevant proof: an existing focused test, a new planned alias plus its provider Work, a project policy gate, or an autonomous executable check. Every plan item needs at least one check_ref. The CLI validates ids, provider ordering, materialization and guarded command policy; it never chooses a check for you. A provider Work may verify itself with the alias it has just created. A consumer must depend on that provider.", "Each check must terminate and return an exit status. Never declare a bare dev/start/serve/watch service command as a check; use a finite test that manages its own service lifecycle (for example, a browser test with a webServer fixture).", "Each plan item must name concrete existing source/test paths in required_read. planned_write_areas is optional: use stable component directories or files only when they help coordinate parallel Work; it is never a write allowlist. Reference every owned R-* and AC-* in one or more items; every AC-* needs an observable acceptance proof.", "For every selected check, inspect its command's launch path and the runtime entrypoints it starts. The fixture/reset process, service process and client process must observe one intended environment and data world. If a required runtime entrypoint needs a code change, make that change explicit in the Work task and its verification. Use planned_write_areas only to advertise likely concurrent overlap; do not treat it as ownership or assume another Work will repair an omitted change. If an independent infrastructure Work is clearer, plan that Work explicitly and order consumers after it.", reviewGroupingRule, "Complete compact contract and schema paths:", `- protocol plan schema: ${path.join(run.workspace_root, ".memory-bank", "dd-flow", "schemas", "vnext-protocol-plan.schema.json")}`, `- aspect map schema: ${path.join(run.workspace_root, ".memory-bank", "dd-flow", "schemas", "plan-aspect-map.schema.json")}`, "Minimal valid protocol-plan shape:", "```json", JSON.stringify(planExample(protocols[0]), null, 2), "```", "Minimal valid aspect-map shape:", "```json", JSON.stringify(aspectMapExample(protocols[0]), null, 2), "```", "</output_contract>", "", "<execution_commands>", "PLAN never launches independent reviewers or registers CODE Work.", "If PLAN needs a material user decision with no reasonable default, write the question packet to the named text file, then run the separate standalone lifecycle command below. Do not combine file creation and dd-flow in one shell command:", "```sh", pauseCommandTemplate, "```", "Ask the returned user_message, stop, and resume this same PLAN Work with the exact returned command.", "Validate both partially filled drafts after completing their semantic fields:", ...validationCommands.map((command) => `- ${command}`), "Finish PLAN only after all questions are resolved and both validation commands pass:", finishCommand, "The response returns the only PLAN-REVIEW start command. Follow it; do not start CODE directly.", "</execution_commands>", "", "<stage_instructions>", template, "</stage_instructions>", ""].join("\n");
|
|
112
|
+
const prompt = ["<stage_identity>", `- RUN: ${run.id}`, `- Work: ${planWorkId}`, "- stage: plan", "</stage_identity>", "", "<trusted_runtime_context>", "These facts were collected by dd-flow. Trust them; do not repeat CLI, Git, compatibility or permission discovery.", `- Project root: ${projectRoot}`, `- Workspace: ${run.workspace_root}`, `- Stage workspace: ${root}`, `- Git: ${JSON.stringify(git)}`, capacityContext, "</trusted_runtime_context>", "", "<workspace_contract>", `- route: ${workspaceRoute.route}`, `- feature branch: ${workspaceRoute.feature_branch ?? "not applicable"}`, `- base commit: ${workspaceRoute.base_ref ?? "not applicable"}`, `- write workspace: ${run.workspace_root}`, "The CLI has verified this frozen route. All project reads and writes for PLAN and later CODE happen in the write workspace; project root is only the stable runtime identity for lifecycle commands. Do not create, switch, merge or delete branches/worktrees.", "Keep the task runner's current cwd. Use the absolute paths in this packet instead of trying to set the provisioned workspace as a tool workdir.", "</workspace_contract>", "", "<accepted_inputs>", `- ${path.join(home, "01-specify", "specify.json")}`, `- ${path.join(home, "02-protocolize", "protocolize-result.json")}`, ...protocols.map((id) => `- ${path.join(run.workspace_root, ".memory-bank", "protocol", id, "summary.md")}`), "</accepted_inputs>", "", ...(codeCheckProfile ? ["<code_check_policy>", "You, not the CLI, select evidence for every accepted requirement and acceptance criterion. The profile only lists reusable aliases, mandatory project policy gates and guarded raw command prefixes. Inspect relevant package/test manifests before choosing a check. Do not classify checks by weight and do not omit a needed check because it looks expensive.", JSON.stringify(codeCheckProfile, null, 2), "</code_check_policy>", ""] : []), ...mergeContract, "<artifacts>", "The CLI has already materialized every artifact below as a partially filled draft. Edit these files in place; do not create replacements elsewhere.", "Prefilled and CLI-owned plan fields: schema_id, plan_id, protocol_id, initial revision and source_refs.", "Prefilled and CLI-owned aspect-map fields: schema_id, protocol_id, plan_id, plan revision, catalog_ref and every catalog aspect_id.", "You own the remaining semantic fields. Empty or missing semantic values are intentional draft markers and must be completed before validation.", ...planPaths.map((value) => `- partially filled plan: ${value}`), ...mapPaths.map((value) => `- partially filled aspect map: ${value}`), "</artifacts>", "", "<output_contract>", "Complete every named plan and aspect map in place. Do not create or edit code-work-batch.json: dd-flow derives it after validation.", "The CLI owns schema_id, plan_id, protocol_id, revision and source_refs. Preserve them exactly.", planCheckInstruction, workStopConditionInstruction, "For each R-* and AC-*, choose an actually relevant proof: an existing focused test, a new planned alias plus its provider Work, a project policy gate, or an autonomous executable check. Every plan item needs at least one check_ref. The CLI validates ids, provider ordering, materialization and guarded command policy; it never chooses a check for you. A provider Work may verify itself with the alias it has just created. A consumer must depend on that provider.", "Each check must terminate and return an exit status. Never declare a bare dev/start/serve/watch service command as a check; use a finite test that manages its own service lifecycle (for example, a browser test with a webServer fixture).", "Each plan item must name concrete existing source/test paths in required_read. planned_write_areas is optional: use stable component directories or files only when they help coordinate parallel Work; it is never a write allowlist. Reference every owned R-* and AC-* in one or more items; every AC-* needs an observable acceptance proof.", "For every selected check, inspect its command's launch path and the runtime entrypoints it starts. The fixture/reset process, service process and client process must observe one intended environment and data world. If a required runtime entrypoint needs a code change, make that change explicit in the Work task and its verification. Use planned_write_areas only to advertise likely concurrent overlap; do not treat it as ownership or assume another Work will repair an omitted change. If an independent infrastructure Work is clearer, plan that Work explicitly and order consumers after it.", reviewGroupingRule, "Complete compact contract and schema paths:", `- protocol plan schema: ${path.join(run.workspace_root, ".memory-bank", "dd-flow", "schemas", "vnext-protocol-plan.schema.json")}`, `- aspect map schema: ${path.join(run.workspace_root, ".memory-bank", "dd-flow", "schemas", "plan-aspect-map.schema.json")}`, "Minimal valid protocol-plan shape:", "```json", JSON.stringify(planExample(protocols[0]), null, 2), "```", "Minimal valid aspect-map shape:", "```json", JSON.stringify(aspectMapExample(protocols[0]), null, 2), "```", "</output_contract>", "", "<execution_commands>", "PLAN never launches independent reviewers or registers CODE Work.", "If PLAN needs a material user decision with no reasonable default, write the question packet to the named text file, then run the separate standalone lifecycle command below. Do not combine file creation and dd-flow in one shell command:", "```sh", pauseCommandTemplate, "```", "Ask the returned user_message, stop, and resume this same PLAN Work with the exact returned command.", "Validate both partially filled drafts after completing their semantic fields:", ...validationCommands.map((command) => `- ${command}`), "Finish PLAN only after all questions are resolved and both validation commands pass:", finishCommand, "The response returns the only PLAN-REVIEW start command. Follow it; do not start CODE directly.", "</execution_commands>", "", "<stage_instructions>", template, "</stage_instructions>", ""].join("\n");
|
|
113
113
|
const artifactMaterialization = { status: "materialized", completeness: "partially_filled", plan_paths: planPaths, aspect_map_paths: mapPaths, cli_owned_plan_fields: ["schema_id", "plan_id", "protocol_id", "revision", "source_refs"], cli_owned_aspect_map_fields: ["schema_id", "protocol_id", "plan_id", "plan_revision", "catalog_ref", "aspects[].aspect_id"], validation_commands: validationCommands };
|
|
114
114
|
const promptPath = path.join(root, "stage-prompt.md");
|
|
115
115
|
// This explicit final rule supersedes historical pack wording: a flow gate
|
|
@@ -326,7 +326,7 @@ export function validateVnextCodeHandoff(context, input) {
|
|
|
326
326
|
});
|
|
327
327
|
}
|
|
328
328
|
const planTask = "Produce accepted plan.json and aspect-map.json artifacts.";
|
|
329
|
-
function planExample(protocolId) { return { schema_id: "dd-flow/protocol-plan@6", plan_id: "PLAN-001", protocol_id: protocolId, revision: 1, title: "Example", summary: "A compact executable plan.", source_refs: [{ kind: "specify", id: "SPECIFY", path: "run://RUN-000/01-specify/specify.json", requirement_ids: ["R-001", "AC-001"] }], goal: { outcome: "Deliver the accepted behavior.", constraints: ["Keep the accepted scope."], non_goals: [] }, assessment: { scope_breadth: { level: "narrow", surfaces: ["one surface"], reason: "One vertical slice." }, solution_novelty: { level: "established", surfaces: ["existing pattern"], reason: "Reuse project practice." }, solution_uncertainty: { level: "low", surfaces: ["known behavior"], reason: "No open technical question." }, failure_impact: { level: "low", surfaces: ["local feature"], reason: "Reversible local change." }, selected_depth: "compact_plan", depth_trigger: "none" }, decisions: [], document_updates: [], checks: [{ id: "CHK-P1-TEST", command: "pnpm test", purpose: "Proves the changed behavior.", run_at: "work", availability: "available" }], items: [{ id: "P1", title: "Implement behavior", summary: "Change the owning surface.", details: "Follow the accepted requirement and project conventions.", depends_on: [], requirement_refs: ["R-001", "AC-001"], semantic_spine: { user_outcome: "The requested behavior is available.", component_responsibility: "Own the behavior.", must_preserve: ["Existing behavior."], non_goals: [], acceptance_contribution: "Makes AC-001 observable." }, execution_context: { required_read: ["apps/api/src/example.ts"], discovery_boundary: ["Related tests only."], planned_write_areas: ["apps/api/src/"], stop_conditions: [
|
|
329
|
+
function planExample(protocolId) { return { schema_id: "dd-flow/protocol-plan@6", plan_id: "PLAN-001", protocol_id: protocolId, revision: 1, title: "Example", summary: "A compact executable plan.", source_refs: [{ kind: "specify", id: "SPECIFY", path: "run://RUN-000/01-specify/specify.json", requirement_ids: ["R-001", "AC-001"] }], goal: { outcome: "Deliver the accepted behavior.", constraints: ["Keep the accepted scope."], non_goals: [] }, assessment: { scope_breadth: { level: "narrow", surfaces: ["one surface"], reason: "One vertical slice." }, solution_novelty: { level: "established", surfaces: ["existing pattern"], reason: "Reuse project practice." }, solution_uncertainty: { level: "low", surfaces: ["known behavior"], reason: "No open technical question." }, failure_impact: { level: "low", surfaces: ["local feature"], reason: "Reversible local change." }, selected_depth: "compact_plan", depth_trigger: "none" }, decisions: [], document_updates: [], checks: [{ id: "CHK-P1-TEST", command: "pnpm test", purpose: "Proves the changed behavior.", run_at: "work", availability: "available" }], items: [{ id: "P1", title: "Implement behavior", summary: "Change the owning surface.", details: "Follow the accepted requirement and project conventions.", depends_on: [], requirement_refs: ["R-001", "AC-001"], semantic_spine: { user_outcome: "The requested behavior is available.", component_responsibility: "Own the behavior.", must_preserve: ["Existing behavior."], non_goals: [], acceptance_contribution: "Makes AC-001 observable." }, execution_context: { required_read: ["apps/api/src/example.ts"], discovery_boundary: ["Related tests only."], planned_write_areas: ["apps/api/src/"], stop_conditions: [] }, verification: { check_refs: ["CHK-P1-TEST"] } }], acceptance: [{ criterion_id: "AC-001", plan_item_ids: ["P1"], changed_surfaces: ["apps/api/src/example.ts"], path: "Exercise the accepted user path.", environment: "Local test environment.", fixtures: [], cleanup: "No persistent fixture.", check_refs: ["CHK-P1-TEST"], expected_evidence: ["Focused check passes."], proof_limits: ["Manual production evidence is not claimed."], gate: "work" }] }; }
|
|
330
330
|
function aspectMapExample(protocolId) { return { $schema: "plan-aspect-map.schema.json", schema_id: "dd-flow/plan-aspect-map@3", protocol_id: protocolId, plan_id: "PLAN-001", plan_revision: 1, catalog_ref: { path: ".memory-bank/dd-flow/mb-sdlc/plan-aspects/aspects" }, routing: { initial_state: "orchestrator_local", selected_route: "local_compact", reason: "One genuinely small semantic unit.", groups: [] }, review_groups: [], aspects: [{ aspect_id: "example_aspect", applicability: "not_applicable", reason: "Only an example; use the supplied real catalog.", planned_artifact_refs: [] }] }; }
|
|
331
331
|
function requireRun(context, root, id) { const project = requireProjectByRoot(context, root); const run = context.db.get("SELECT id, project_id, workspace_root, run_root FROM runs WHERE project_id = ? AND id = ?", [project.id, id]); if (!run)
|
|
332
332
|
throw new AppError("not_found", "RUN is not registered", 1); return run; }
|
|
@@ -37,7 +37,8 @@ import { publicSessionIdentity } from "./session-identity.js";
|
|
|
37
37
|
import { activateRecoveryWork, assertRecoveryHookCurrent, assertRunMutationAllowed, assertWorkSettlementAllowed, recoveryGuard, recoveryPromptForWork, settleRecoveryWork } from "./run-recovery.js";
|
|
38
38
|
import { runControlDirective } from "./run-control.js";
|
|
39
39
|
import { prepareError, prepareJsonFile } from "../cli/input-preparation.js";
|
|
40
|
-
import { lifecycleRetryInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
|
|
40
|
+
import { lifecycleRetryInstruction, workStopConditionInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
|
|
41
|
+
import { prepareRetryCorrection, prepareExternalRetry, renderRetryCorrection, retryCorrectionFile, retryReceiptFile } from "./work-retry-context.js";
|
|
41
42
|
import { acceptedDecisionPrecedence, loadExternalStageContext, renderRetainedStageContext, renderRunOperationalContext, runContextRelocations } from "./stage-context.js";
|
|
42
43
|
import { reviewEnvironmentAllowed, reviewExecutionCopyHash, reviewSourceHash, reviewSourcePaths } from "./review-copy.js";
|
|
43
44
|
import { latestRunVerificationMatrix, verificationMatrixInstruction, verificationMatrixContract } from "./verification-matrix.js";
|
|
@@ -948,7 +949,7 @@ export function startBoundWork(context, work, run, claimIdentity, hookEventId, s
|
|
|
948
949
|
const finishMarker = `DD_FLOW_FINISH_${crypto.randomUUID()}`;
|
|
949
950
|
const failMarker = `DD_FLOW_FAIL_${crypto.randomUUID()}`;
|
|
950
951
|
const workspaceView = preparedCopy ? { work_id: work.work_id, from: run.workspace_root, to: reviewInputPath(requireRunHome(run), preparedCopy.copy.root), source_sha256: preparedCopy.copy.source_sha256, copy_sha256: preparedCopy.copy.sha256 } : undefined;
|
|
951
|
-
let promptTemplate = renderWorkerPrompt(context, work, workerRun, preparedDependencies, { finish: finishMarker, fail: failMarker }, workspaceView, preparedReviewInput ?? undefined)
|
|
952
|
+
let promptTemplate = `${renderRetryCorrection(context, work, requireRunHome(run))}${renderWorkerPrompt(context, work, workerRun, preparedDependencies, { finish: finishMarker, fail: failMarker }, workspaceView, preparedReviewInput ?? undefined)}`;
|
|
952
953
|
if (preparedRecovery)
|
|
953
954
|
promptTemplate = `${recoveryPrompt(preparedRecovery)}\n\n${promptTemplate}`;
|
|
954
955
|
let binding;
|
|
@@ -1249,7 +1250,7 @@ export function prepareWorkSettlement(context, id, status, result) {
|
|
|
1249
1250
|
throw new AppError("work_start_publication_pending", "Finish publishing this Work's committed start packet before settlement", 1, { work_id: work.work_id, effect: "no_effect" });
|
|
1250
1251
|
return { work, run, link };
|
|
1251
1252
|
}
|
|
1252
|
-
export function prepareWorkRetry(context, id, reason) {
|
|
1253
|
+
export function prepareWorkRetry(context, id, reason, correction) {
|
|
1253
1254
|
if (typeof reason !== "string" || !reason.trim())
|
|
1254
1255
|
throw new AppError("usage", "Work retry requires a nonempty --reason", 2);
|
|
1255
1256
|
const work = requireWork(context, id);
|
|
@@ -1269,17 +1270,24 @@ export function prepareWorkRetry(context, id, reason) {
|
|
|
1269
1270
|
throw new AppError("work_retry_invalid", "Retry journal is not valid JSON", 1);
|
|
1270
1271
|
}
|
|
1271
1272
|
const item = value;
|
|
1272
|
-
if (!item || typeof item !== "object" || item.work_id !== work.work_id || typeof item.archive !== "string" || !/^ATT-\d{3,}$/.test(item.archive) || !Array.isArray(item.entries) || item.entries.some(file => !["prompt.md", "result.json", "context.json", "result-input.json"].includes(file)) || new Set(item.entries).size !== item.entries.length || !(item.result_sha256 === null || typeof item.result_sha256 === "string" && /^[a-f0-9]{64}$/.test(item.result_sha256))
|
|
1273
|
+
if (!item || typeof item !== "object" || item.work_id !== work.work_id || typeof item.archive !== "string" || !/^ATT-\d{3,}$/.test(item.archive) || !Array.isArray(item.entries) || item.entries.some(file => !["prompt.md", "result.json", "context.json", "result-input.json", retryCorrectionFile, retryReceiptFile].includes(file)) || new Set(item.entries).size !== item.entries.length || !(item.result_sha256 === null || typeof item.result_sha256 === "string" && /^[a-f0-9]{64}$/.test(item.result_sha256))
|
|
1274
|
+
|| item.correction && (item.correction.work_id !== work.work_id || item.correction.project_id !== work.project_id || item.correction.run_id !== work.run_id || item.correction.failed_result_sha256 !== item.result_sha256)
|
|
1275
|
+
|| item.external_launch && (item.external_launch.work_id !== work.work_id || typeof item.external_launch.operation_id !== "string" || typeof item.external_launch.state_dir !== "string" || !path.isAbsolute(item.external_launch.state_dir)))
|
|
1273
1276
|
throw new AppError("work_retry_invalid", "Retry journal does not identify this Work", 1);
|
|
1274
1277
|
pending = item;
|
|
1275
1278
|
}
|
|
1279
|
+
if (pending?.correction && correction && (pending.correction.episode_id !== correction.episode_id || pending.correction.correction !== correction.correction || pending.correction.failed_result_sha256 !== correction.failed_result_sha256 || pending.correction.decision_ref !== fs.realpathSync(correction.decision_ref)))
|
|
1280
|
+
throw new AppError("work_retry_conflict", "Pending retry belongs to another retained correction", 1);
|
|
1281
|
+
if (pending && !pending.correction && correction)
|
|
1282
|
+
throw new AppError("work_retry_conflict", "Cannot add a correction to an already prepared retry", 1);
|
|
1276
1283
|
if (work.status !== "failed" && !(pending && work.status === "created"))
|
|
1277
1284
|
throw new AppError("invalid_work_state", "Only failed Work may be retried", 2);
|
|
1278
1285
|
const attempts = path.join(directory, "attempts");
|
|
1279
1286
|
let number = 1;
|
|
1280
1287
|
while (fs.existsSync(path.join(attempts, `ATT-${String(number).padStart(3, "0")}`)))
|
|
1281
1288
|
number += 1;
|
|
1282
|
-
const prepared = pending ?? { work_id: work.work_id, archive: `ATT-${String(number).padStart(3, "0")}`, entries: ["prompt.md", "result.json", "context.json", "result-input.json"].filter(file => fs.existsSync(path.join(directory, file))), result_sha256: work.result === null ? null : crypto.createHash("sha256").update(work.result).digest("hex")
|
|
1289
|
+
const prepared = pending ?? { work_id: work.work_id, archive: `ATT-${String(number).padStart(3, "0")}`, entries: ["prompt.md", "result.json", "context.json", "result-input.json", retryCorrectionFile, retryReceiptFile].filter(file => fs.existsSync(path.join(directory, file))), result_sha256: work.result === null ? null : crypto.createHash("sha256").update(work.result).digest("hex"),
|
|
1290
|
+
...(correction ? { correction: prepareRetryCorrection(correction, work, requireRunHome(run)) } : {}), external_launch: prepareExternalRetry(context, work) };
|
|
1283
1291
|
const archive = path.join(attempts, prepared.archive);
|
|
1284
1292
|
if (work.status === "failed") {
|
|
1285
1293
|
if (prepared.result_sha256 !== (work.result === null ? null : crypto.createHash("sha256").update(work.result).digest("hex")))
|
|
@@ -1294,16 +1302,36 @@ export function prepareWorkRetry(context, id, reason) {
|
|
|
1294
1302
|
if (!fs.existsSync(source) && !fs.existsSync(target))
|
|
1295
1303
|
throw new AppError("archive_artifact_missing", "Work artifact is absent from both retry locations", 1, { source, target });
|
|
1296
1304
|
}
|
|
1305
|
+
if (prepared.external_launch) {
|
|
1306
|
+
const external = prepareExternalRetry(context, work);
|
|
1307
|
+
if (JSON.stringify(external) !== JSON.stringify(prepared.external_launch))
|
|
1308
|
+
throw new AppError("work_retry_conflict", "Previous external launch changed while retry was prepared", 1);
|
|
1309
|
+
const file = path.join(archive, "external-launch.json");
|
|
1310
|
+
if (fs.existsSync(file) && fs.readFileSync(file, "utf8") !== JSON.stringify(prepared.external_launch))
|
|
1311
|
+
throw new AppError("work_retry_conflict", "Archived external launch differs from the failed attempt", 1);
|
|
1312
|
+
}
|
|
1297
1313
|
}
|
|
1298
1314
|
return { work, run, directory, journal, pending, prepared, archive };
|
|
1299
1315
|
}
|
|
1300
|
-
export function retryWork(context, id, reason) {
|
|
1316
|
+
export function retryWork(context, id, reason, correction) {
|
|
1301
1317
|
const pending = requireWork(context, id);
|
|
1318
|
+
const priorReceipt = path.join(requireRunHome(requireRun(context, pending.project_id, pending.run_id)), "works", pending.work_id, retryReceiptFile);
|
|
1319
|
+
if (correction && pending.status !== "failed" && fs.existsSync(priorReceipt) && !fs.existsSync(path.join(path.dirname(priorReceipt), ".retry.json"))) {
|
|
1320
|
+
const retained = JSON.parse(fs.readFileSync(priorReceipt, "utf8"));
|
|
1321
|
+
const current = context.db.get("SELECT current_start_invocation_id FROM works WHERE work_id=?", [pending.work_id]);
|
|
1322
|
+
if (retained.episode_id === correction.episode_id && retained.failed_result_sha256 === correction.failed_result_sha256 && retained.current_start_invocation_id === current?.current_start_invocation_id) {
|
|
1323
|
+
const source = JSON.parse(fs.readFileSync(path.join(path.dirname(priorReceipt), retryCorrectionFile), "utf8"));
|
|
1324
|
+
if (source.correction !== correction.correction || source.decision_ref !== fs.realpathSync(correction.decision_ref))
|
|
1325
|
+
throw new AppError("work_retry_conflict", "Applied retry belongs to a different retained correction", 1);
|
|
1326
|
+
renderRetryCorrection(context, pending, requireRunHome(requireRun(context, pending.project_id, pending.run_id)));
|
|
1327
|
+
return { ok: true, ...retained, reason, idempotent: true };
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1302
1330
|
// A committed failed result must be published (or diagnosed) before its
|
|
1303
1331
|
// live path can be archived for the next attempt.
|
|
1304
1332
|
materializePendingWorkResults(context, pending.project_id, pending.run_id, pending.work_id);
|
|
1305
|
-
const { work, run, directory, journal, pending: retainedJournal, prepared, archive } = prepareWorkRetry(context, id, reason);
|
|
1306
|
-
const previousStart = context.db.get("SELECT current_start_invocation_id FROM works WHERE work_id = ?", [
|
|
1333
|
+
const { work, run, directory, journal, pending: retainedJournal, prepared, archive } = prepareWorkRetry(context, id, reason, correction);
|
|
1334
|
+
const previousStart = context.db.get("SELECT current_start_invocation_id FROM works WHERE work_id = ?", [work.work_id])?.current_start_invocation_id;
|
|
1307
1335
|
const startAuthority = work.status === "failed" ? prepareWorkStartAuthority(context, rawWorkStartCommand(context, work), previousStart) : null;
|
|
1308
1336
|
if (work.status === "failed") {
|
|
1309
1337
|
// The durable journal and archive are projections of the failed SQL
|
|
@@ -1315,6 +1343,10 @@ export function retryWork(context, id, reason) {
|
|
|
1315
1343
|
const receipt = path.join(archive, "accepted-receipt.json");
|
|
1316
1344
|
const receiptBytes = JSON.stringify({ work_id: work.work_id, status: work.status, result: work.result, result_sha256: prepared.result_sha256 });
|
|
1317
1345
|
publishStartReceiptFile(receipt, receiptBytes, sha256(receiptBytes));
|
|
1346
|
+
if (prepared.external_launch) {
|
|
1347
|
+
const bytes = JSON.stringify(prepared.external_launch);
|
|
1348
|
+
publishStartReceiptFile(path.join(archive, "external-launch.json"), bytes, sha256(bytes));
|
|
1349
|
+
}
|
|
1318
1350
|
for (const file of prepared.entries) {
|
|
1319
1351
|
const source = path.join(directory, file), target = path.join(archive, file);
|
|
1320
1352
|
if (fs.existsSync(source)) {
|
|
@@ -1339,6 +1371,12 @@ export function retryWork(context, id, reason) {
|
|
|
1339
1371
|
assertRunMutationAllowed(context, current.project_id, current.run_id, startAuthority?.scope.generation);
|
|
1340
1372
|
if (current.status !== "failed" || current.result !== work.result || current.updated_at !== work.updated_at)
|
|
1341
1373
|
throw new AppError("work_retry_conflict", "Failed Work receipt changed during retry", 1);
|
|
1374
|
+
if (prepared.external_launch) {
|
|
1375
|
+
const launch = context.db.get("SELECT * FROM work_launches WHERE work_id=?", [work.work_id]);
|
|
1376
|
+
if (JSON.stringify(launch) !== JSON.stringify(prepared.external_launch))
|
|
1377
|
+
throw new AppError("work_retry_conflict", "External launch changed before retry commit", 1);
|
|
1378
|
+
context.db.run("DELETE FROM work_launches WHERE work_id=? AND operation_id=?", [work.work_id, prepared.external_launch.operation_id]);
|
|
1379
|
+
}
|
|
1342
1380
|
retireWorkPacketAuthorities(context, work.work_id);
|
|
1343
1381
|
for (const column of ["prompt_path", "result_path"]) {
|
|
1344
1382
|
const file = column === "prompt_path" ? "prompt.md" : "result.json";
|
|
@@ -1350,9 +1388,18 @@ export function retryWork(context, id, reason) {
|
|
|
1350
1388
|
context.db.run("UPDATE works SET current_start_invocation_id = ? WHERE work_id = ?", [startId, work.work_id]);
|
|
1351
1389
|
});
|
|
1352
1390
|
}
|
|
1391
|
+
if (prepared.correction) {
|
|
1392
|
+
const bytes = JSON.stringify(prepared.correction);
|
|
1393
|
+
publishStartReceiptFile(path.join(directory, retryCorrectionFile), bytes, sha256(bytes));
|
|
1394
|
+
}
|
|
1395
|
+
const currentStart = context.db.get("SELECT current_start_invocation_id FROM works WHERE work_id=?", [work.work_id])?.current_start_invocation_id ?? null;
|
|
1396
|
+
const retryReceipt = { work_id: work.work_id, archived_attempt: path.relative(requireRunHome(run), archive).split(path.sep).join("/"), failed_result_sha256: prepared.result_sha256, current_start_invocation_id: currentStart,
|
|
1397
|
+
...(prepared.correction ? { episode_id: prepared.correction.episode_id } : {}), ...(prepared.external_launch ? { archived_launch_ref: path.join(archive, "external-launch.json") } : {}) };
|
|
1398
|
+
const retryBytes = JSON.stringify(retryReceipt);
|
|
1399
|
+
publishStartReceiptFile(path.join(directory, retryReceiptFile), retryBytes, sha256(retryBytes));
|
|
1353
1400
|
fs.rmSync(journal, { force: true });
|
|
1354
1401
|
refreshRunWorkProjection(context, work.project_id, work.run_id);
|
|
1355
|
-
return { ok: true, work_id: work.work_id, archived_attempt:
|
|
1402
|
+
return { ok: true, ...(prepared.correction ? retryReceipt : { work_id: work.work_id, archived_attempt: retryReceipt.archived_attempt }), reason };
|
|
1356
1403
|
}
|
|
1357
1404
|
export function reviewSourceInputs(context, workId) {
|
|
1358
1405
|
const work = requireWork(context, workId);
|
|
@@ -2114,7 +2161,7 @@ function renderWorkerPromptBody(context, work, run, dependencies, commands) {
|
|
|
2114
2161
|
"Workspace evidence resolves inside this private copy. RUN evidence is read-only. Write only this Work's result-input.json and assigned Work-owned evidence under run_home; neither a result JSON nor a private edit proves accepted publication or finding closure."]
|
|
2115
2162
|
: mergeWork
|
|
2116
2163
|
? ["MERGE does not own product-code repair. Use only the stage packet's merge apply command; resolve only paths that Git reports as unmerged. Do not edit integration code, tests, documentation or configuration to make a gate pass.", "RUN artifacts are read-only evidence: refer to them with run:// URIs and let dd-flow persist your submitted result."]
|
|
2117
|
-
: [`HARD RULE: project source reads and writes must remain under ${run.workspace_root}.`, "Do not write through project_root, outside workspace_root, into another RUN, or into Git/worktree control data. Do not create, switch, merge or delete branches/worktrees.", "RUN artifacts are read-only evidence: refer to them with run:// URIs and let dd-flow persist your submitted result. Accepted requirements
|
|
2164
|
+
: [`HARD RULE: project source reads and writes must remain under ${run.workspace_root}.`, "Do not write through project_root, outside workspace_root, into another RUN, or into Git/worktree control data. Do not create, switch, merge or delete branches/worktrees.", "RUN artifacts are read-only evidence: refer to them with run:// URIs and let dd-flow persist your submitted result. Accepted requirements and non-goals are semantic boundaries. planned_write_areas is not.", workStopConditionInstruction];
|
|
2118
2165
|
const completionRepair = readOnly
|
|
2119
2166
|
? ["If Finish reports input drift or an infrastructure failure, report the exact receipt and stop. Do not change the reviewed files to make the check pass."]
|
|
2120
2167
|
: repairPacket
|
|
@@ -2328,8 +2375,8 @@ function validateItem(value, requireExecutionContext = false) { if (!value || ty
|
|
|
2328
2375
|
throw new AppError("validation", "work item must be an object", 2); const item = value; if (typeof item.key !== "string" || !item.key || typeof item.task !== "string" || !item.task.trim())
|
|
2329
2376
|
throw new AppError("validation", "work item requires key and task", 2); const code = item.schema_id === "dd-flow/code-work-packet@5"; if (requireExecutionContext && !code)
|
|
2330
2377
|
throw new AppError("validation", "CODE batch requires code-work-packet@5 items", 2, { key: item.key }); for (const key of code ? ["required_read", "discovery_boundary", "planned_write_areas", "checks", "provides_checks", "stop_conditions"] : [])
|
|
2331
|
-
if (!Array.isArray(item[key]) || (!['provides_checks', 'planned_write_areas', 'checks'].includes(key) && item[key].length === 0) || (key === "checks" && item[key].length === 0 && !allowsEmptyRepairChecks(item)))
|
|
2332
|
-
throw new AppError("validation", `CODE work item requires ${['provides_checks', 'planned_write_areas'].includes(key) ? "an array" : `non-empty ${key}`}`, 2, { key: item.key }); if (item.depends_on !== undefined && (!Array.isArray(item.depends_on) || !item.depends_on.every((entry) => typeof entry === "string")))
|
|
2378
|
+
if (!Array.isArray(item[key]) || (!['provides_checks', 'planned_write_areas', 'checks', 'stop_conditions'].includes(key) && item[key].length === 0) || (key === "checks" && item[key].length === 0 && !allowsEmptyRepairChecks(item)))
|
|
2379
|
+
throw new AppError("validation", `CODE work item requires ${['provides_checks', 'planned_write_areas', 'stop_conditions'].includes(key) ? "an array" : `non-empty ${key}`}`, 2, { key: item.key }); if (item.depends_on !== undefined && (!Array.isArray(item.depends_on) || !item.depends_on.every((entry) => typeof entry === "string")))
|
|
2333
2380
|
throw new AppError("validation", "depends_on must be a string array", 2); if (item.parent !== undefined && typeof item.parent !== "string")
|
|
2334
2381
|
throw new AppError("validation", "parent must be a string", 2); if (item.launch_policy !== undefined && item.launch_policy !== "reuse_allowed" && item.launch_policy !== "fresh_agent_required")
|
|
2335
2382
|
throw new AppError("validation", "launch_policy must be reuse_allowed or fresh_agent_required", 2); if (item.result_schema !== undefined && (typeof item.result_schema !== "string" || !item.result_schema))
|