@deksden-com/dd-flow-cli 0.9.0-beta.155 → 0.9.0-beta.158
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -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 +26 -8
- 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 +3 -3
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +1 -1
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +2 -1
- package/dist/harness-runtime/lib/delegation-instructions.mjs +8 -2
- 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 +117 -0
- package/dist/services/controller-fanout.js +33 -20
- package/dist/services/controller-result-authority.js +13 -8
- package/dist/services/controller-work-failure.js +198 -0
- package/dist/services/lifecycle-invocations.js +117 -25
- package/dist/services/managed-daemon-binding.js +40 -0
- package/dist/services/retained-native-hook.js +34 -5
- package/dist/services/run-control.js +18 -36
- package/dist/services/run-controller-process.js +19 -6
- package/dist/services/run-controller.js +154 -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 +62 -15
- package/dist/services/work-retry-context.js +55 -0
- package/dist/verification-components.json +3 -0
- package/package.json +1 -1
|
@@ -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);
|
|
@@ -1943,8 +1990,8 @@ export function validateCodeWorkResult(work, value, projectRoot, runHome, runId)
|
|
|
1943
1990
|
const ignored = ignoredWorkspacePaths(projectRoot, changedPaths);
|
|
1944
1991
|
if (ignored.length)
|
|
1945
1992
|
throw new AppError("environment_repair_not_product_change", "CODE Work must record ignored environment repair separately, not as a product changed_path", 2, { work_id: work.work_id, paths: ignored });
|
|
1946
|
-
if ((result.
|
|
1947
|
-
throw new AppError("work_contract_incomplete", "CODE Work cannot complete with unresolved
|
|
1993
|
+
if ((result.blockers?.length ?? 0) > 0)
|
|
1994
|
+
throw new AppError("work_contract_incomplete", "CODE Work cannot complete with unresolved blockers; correct the result or fail this Work with the evidenced blocker", 2, { work_id: work.work_id, blockers: result.blockers ?? [], phase: "prepare", effect: "no_effect", recoverable: true });
|
|
1948
1995
|
const packet = codePacket(work);
|
|
1949
1996
|
if (!packet)
|
|
1950
1997
|
return;
|
|
@@ -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
|
|
@@ -2124,7 +2171,7 @@ function renderWorkerPromptBody(context, work, run, dependencies, commands) {
|
|
|
2124
2171
|
: ["Read the failed receipt and its stdout/stderr. Fix project-owned source, migration, test, formatting, or configuration errors in this same Work, then use the returned retry_command with your result. Internal invocation IDs are runtime-owned; do not copy or invent them. Do not invent a cause that does not appear in the retained output."];
|
|
2125
2172
|
const codeContext = packet ? ["<semantic_spine>", JSON.stringify(packet.semantic_spine, null, 2), "</semantic_spine>", "", ...(packet.repair ? ["<repair_context>", JSON.stringify(packet.repair, null, 2), "Read the failed receipt and its linked stdout/stderr before editing. Preserve the accepted origin context and fix only the evidenced failure.", "</repair_context>", ""] : []), "<accepted_requirements>", JSON.stringify(packet.requirements, null, 2), "</accepted_requirements>", "", "<write_boundary_invariant>", "For a mutation guarded by membership, ownership, authorization or parent lifecycle state, preserve that predicate in the write statement or make guard and write one explicit transaction with the needed lock. A prior read may diagnose an error but never proves a later write remains allowed. Apply this to create, update, delete and parent-state mutations.", "</write_boundary_invariant>", "", "<acceptance_context>", "The criteria below are end-to-end context. Complete this Work's semantic contribution and declared checks; another ordered Work may own a different acceptance surface.", JSON.stringify(packet.acceptance, null, 2), "</acceptance_context>", "", "<required_read>", "These are mandatory starting sources, not a read allowlist. Read any additional project files needed to implement the Work correctly.", ...packet.required_read.map((item) => `- ${resolveRunReferences(item, work.run_id, requireRunHome(run))}`), "</required_read>", "", "<discovery_boundary>", "These are likely discovery areas, not a hard boundary. Expand project-local investigation when required and report material additions.", ...packet.discovery_boundary.map((item) => `- ${item}`), "</discovery_boundary>", "", "<planned_write_areas>", "SOFT COORDINATION HINT ONLY. These paths help the coordinator avoid concurrent collisions. They do not grant or deny write permission and do not limit the files needed for this Work. You may create or change any project file under workspace_root that is necessary and in semantic scope; report every actual changed path.", ...(packet.planned_write_areas.length ? packet.planned_write_areas.map((item) => `- ${item}`) : ["- none predicted; derive the necessary files from the task"]), "</planned_write_areas>", "", ...(packet.provides_checks.length ? ["<provided_checks>", ...packet.provides_checks.map((item) => `- ${item.id}: materialize ${item.command}${item.definition ? ` as ${item.definition}` : ""}; it is not usable until this Work finishes.`), "Update the declared project command or alias before Work finish. The CLI verifies materialization at Work finish. Execution occurs at its declared run_at gate, not necessarily in this Work.", "</provided_checks>", ""] : []), "<verification>", ...packet.checks.map((item) => `- ${item.id} at ${item.run_at}: ${item.command} — ${item.purpose}`), "The CLI executes work-scoped checks and retains their receipts. Report semantic evidence only; do not rerun declared checks manually.", "</verification>", "", "<stop_conditions>", ...packet.stop_conditions.map((item) => `- ${item}`), "</stop_conditions>", ""] : [];
|
|
2126
2173
|
if (packet)
|
|
2127
|
-
codeContext.push("<document_updates>", JSON.stringify(packet.document_updates, null, 2), "Materialize every listed update. dd-flow verifies the resulting file against its PLAN-time baseline.", "</document_updates>", "", "<completion_contract>", "Successful completion requires
|
|
2174
|
+
codeContext.push("<document_updates>", JSON.stringify(packet.document_updates, null, 2), "Materialize every listed update. dd-flow verifies the resulting file against its PLAN-time baseline.", "</document_updates>", "", "<completion_contract>", "Successful completion requires no unresolved blockers and closure of every assigned obligation/document update. Record descriptive deviations as notes for review; their presence alone does not reject the Work. A necessary path outside planned_write_areas is normal coordination drift, not a blocker; include it in changed_paths and continue.", "</completion_contract>", "");
|
|
2128
2175
|
if (packet)
|
|
2129
2176
|
codeContext.push("<temporary_services>", "Prefer the declared check launcher: it already owns check resources. If the planned scenario genuinely requires an interactive HTTP service, use the managed supervisor below. This is a template: replace the project service command, port names and readiness path from the plan/project instructions; do not invent a fixed port.", `${command} runtime process start --run ${run.id} --project-root ${JSON.stringify(run.project_root)} --command '<project-service-command>' --ports api --ready-port api --ready-path /health --json --progress-jsonl`, "The service receives DD_FLOW_PORT_API (and equivalent variables for all declared names). The command stays running as its supervisor. Retain its tool handle; wait for the service ready event and read its service.json receipt. Pass those exact ports and the same project environment to reset/seed, API and browser operations.", "A ready receipt proves service readiness only. Record the scenario outcome and real evidence separately. After the scenario, execute the exact stop_command from that receipt, then wait for the supervisor to exit. Never use pkill/killall or stop a sibling's process. If cleanup fails, retain the process id and report the failure; do not claim the resource is free.", "</temporary_services>", "");
|
|
2130
2177
|
return ["<work>", `- work_id: ${work.work_id}`, `- run_id: ${work.run_id}`, `- project_root: ${run.project_root}`, `- workspace_root: ${run.workspace_root}`, `- run_home: ${requireRunHome(run)}`, "</work>", "", "<cli_context>", `- Work commands use the short ID ${shortWorkId(work.work_id)}; dd-flow binds it to this RUN.`, `- @project = ${run.project_root}`, `- @workspace = ${run.workspace_root}`, `- @run = ${requireRunHome(run)}`, "- @ aliases are accepted only by declared dd-flow path parameters. Copy the quoted alias literally; do not add backslashes. Shell tools such as cat and rg require ordinary paths relative to cwd or absolute paths.", "- Lifecycle invocation IDs are internal runtime authority and are intentionally omitted from commands.", "</cli_context>", "", "<hard_write_boundary>", ...writeBoundary, "</hard_write_boundary>", "", ...codeContext, "<dependency_results>", JSON.stringify(dependencies.filter(Boolean), null, 2), "</dependency_results>", "", "<task>", resolveRunReferences(work.task, work.run_id, requireRunHome(run)), "</task>", "", ...(work.result_schema ? ["<result_contract>", `Return JSON matching \`${work.result_schema}\`.`, ...resultSchemaGuidance(work, run.id), `Write the completed JSON to ${resultInputPath}. This is an attempt input, not the canonical result.json receipt. dd-flow validates the file and atomically stores the canonical receipt.`, "</result_contract>", ""] : []), "<completion>", repairPacket ? "Repair Finish validates and publishes the exact private delta before Work settlement. Independent re-review, correction checks and Stage/RUN acceptance remain separate obligations." : packet?.repair?.verification_check_refs?.length ? "Work finish runs its normal work-scoped checks plus the listed causal repair checks. Their original run_at remains an aggregate obligation; this is the additional proof required before accepting this repair." : "Work finish runs only declared run_at=work checks. Stage finish owns readiness/code/merge gates; successful Work completion does not mean those gates have passed.", "A failed receipt means only that the check failed; it is not proof of an engine, harness, dependency, or environment blocker.", ...completionRepair, ...(repairPacket ? [] : [lifecycleRetryInstruction]), repairPacket ? "Use Fail for an evidenced external blocker or assignment conflict, including any required edit outside the exact write_paths allowlist. Never expand that allowlist or edit the shared checkout." : "Use Fail only for a concrete external blocker after deterministic bootstrap or a contradiction with an accepted requirement/non-goal. Never fail merely because a necessary project path was absent from planned_write_areas.", "Finish may run for several minutes. Preserve the shell tool's process/session handle and poll that same invocation until it exits; progress arrives as JSONL on stderr. Never reissue Finish merely because final stdout has not arrived.", `First write only the result JSON to ${resultInputPath}. Then run this standalone command:\n${finishWorkCommand}`, `Fail only for an evidenced external or semantic-contract blocker: ${failWorkCommand}`, "</completion>", ""].join("\n");
|
|
@@ -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))
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import { AppError } from "../shared/errors.js";
|
|
5
|
+
import { managedDaemonExecutionAbsent } from "./managed-daemon-binding.js";
|
|
6
|
+
export const retryCorrectionFile = "retry-correction.json";
|
|
7
|
+
export const retryReceiptFile = "retry-receipt.json";
|
|
8
|
+
const hash = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex");
|
|
9
|
+
export function prepareRetryCorrection(input, work, runHome) {
|
|
10
|
+
if (!input || typeof input.episode_id !== "string" || !input.episode_id.trim() || typeof input.correction !== "string" || !input.correction.trim()
|
|
11
|
+
|| typeof input.decision_ref !== "string" || !path.isAbsolute(input.decision_ref) || input.failed_result_sha256 !== hash(work.result ?? ""))
|
|
12
|
+
throw new AppError("work_retry_correction_invalid", "Retry correction does not identify the retained failed Work", 1);
|
|
13
|
+
const home = fs.realpathSync(runHome), decision = fs.realpathSync(input.decision_ref);
|
|
14
|
+
if (!decision.startsWith(`${home}${path.sep}`) || !fs.statSync(decision).isFile())
|
|
15
|
+
throw new AppError("work_retry_correction_invalid", "Retry decision must be a retained file inside this RUN", 1);
|
|
16
|
+
const bytes = fs.readFileSync(decision), accepted = JSON.parse(bytes.toString("utf8"));
|
|
17
|
+
if (accepted.schema_id !== "work-failure-decision@1" || accepted.action !== "retry" || accepted.episode_id !== input.episode_id || accepted.correction !== input.correction)
|
|
18
|
+
throw new AppError("work_retry_correction_invalid", "Retry correction differs from the retained coordinator decision", 1);
|
|
19
|
+
return { ...input, decision_ref: decision, decision_sha256: hash(bytes), work_id: work.work_id, project_id: work.project_id, run_id: work.run_id };
|
|
20
|
+
}
|
|
21
|
+
/** Snapshot the old launch before its current-row slot is reused; no process I/O in SQL. */
|
|
22
|
+
export function prepareExternalRetry(context, work) {
|
|
23
|
+
if (!context.db.get("SELECT 1 FROM sqlite_master WHERE type='table' AND name='work_launches'"))
|
|
24
|
+
return null;
|
|
25
|
+
const launch = context.db.get("SELECT * FROM work_launches WHERE work_id=?", [work.work_id]);
|
|
26
|
+
if (!launch)
|
|
27
|
+
return null;
|
|
28
|
+
if (!["start_requested", "completed", "failed", "cancelled"].includes(launch.status)
|
|
29
|
+
|| !managedDaemonExecutionAbsent(context, { stateDir: launch.state_dir, projectId: work.project_id, runId: work.run_id }))
|
|
30
|
+
throw new AppError("work_retry_external_unsettled", "Previous external Work launch still has unknown or live execution; reconcile it before retry", 1, { work_id: work.work_id, operation_id: launch.operation_id, state_dir: launch.state_dir });
|
|
31
|
+
return launch;
|
|
32
|
+
}
|
|
33
|
+
export function renderRetryCorrection(context, work, runHome) {
|
|
34
|
+
const directory = path.join(runHome, "works", work.work_id), file = path.join(directory, retryCorrectionFile);
|
|
35
|
+
if (!fs.existsSync(file))
|
|
36
|
+
return "";
|
|
37
|
+
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
38
|
+
const receipt = JSON.parse(fs.readFileSync(path.join(directory, retryReceiptFile), "utf8"));
|
|
39
|
+
const current = context.db.get("SELECT current_start_invocation_id FROM works WHERE work_id=?", [work.work_id]);
|
|
40
|
+
const realHome = fs.realpathSync(runHome), realDecision = fs.realpathSync(value.decision_ref);
|
|
41
|
+
if (value.work_id !== work.work_id || value.project_id !== work.project_id || value.run_id !== work.run_id
|
|
42
|
+
|| receipt.work_id !== work.work_id || receipt.episode_id !== value.episode_id || receipt.current_start_invocation_id !== current?.current_start_invocation_id
|
|
43
|
+
|| !realDecision.startsWith(`${realHome}${path.sep}`))
|
|
44
|
+
throw new AppError("work_retry_correction_invalid", "Retained retry correction no longer matches this Work attempt", 1);
|
|
45
|
+
const decisionBytes = fs.readFileSync(realDecision);
|
|
46
|
+
if (value.decision_sha256 !== hash(decisionBytes))
|
|
47
|
+
throw new AppError("work_retry_correction_invalid", "Retained retry decision changed", 1);
|
|
48
|
+
const accepted = JSON.parse(decisionBytes.toString("utf8"));
|
|
49
|
+
if (accepted.schema_id !== "work-failure-decision@1" || accepted.action !== "retry"
|
|
50
|
+
|| accepted.episode_id !== value.episode_id || accepted.correction !== value.correction)
|
|
51
|
+
throw new AppError("work_retry_correction_invalid", "Retained retry correction no longer matches this Work attempt", 1);
|
|
52
|
+
return ["<accepted_work_retry_correction>", `- failed-result sha256: ${value.failed_result_sha256}`, `- retained decision: ${value.decision_ref}`,
|
|
53
|
+
`- episode: ${value.episode_id}`, value.correction,
|
|
54
|
+
"This is the coordinator's correction for this fresh attempt, not a new user requirement or permission to widen accepted scope. Read the retained decision. Apply its interpretation to the historical stop conditions; preserve accepted requirements, non-goals and previous receipts. Do not repeat an accepted predecessor Work.", "</accepted_work_retry_correction>", ""].join("\n");
|
|
55
|
+
}
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
"services/command-context.js",
|
|
66
66
|
"services/config.js",
|
|
67
67
|
"services/controller-result-authority.js",
|
|
68
|
+
"services/controller-work-failure.js",
|
|
68
69
|
"services/delivery-access.js",
|
|
69
70
|
"services/delivery-approval.js",
|
|
70
71
|
"services/delivery-artifact.js",
|
|
@@ -329,6 +330,7 @@
|
|
|
329
330
|
"services/session-identity.js",
|
|
330
331
|
"services/sessions.js",
|
|
331
332
|
"services/setup.js",
|
|
333
|
+
"services/stage-blocker.js",
|
|
332
334
|
"services/stage-context.js",
|
|
333
335
|
"services/stage-handlers.js",
|
|
334
336
|
"services/stage-lifecycle.js",
|
|
@@ -352,6 +354,7 @@
|
|
|
352
354
|
"services/vnext-specify.js",
|
|
353
355
|
"services/vnext-workspace-policy.js",
|
|
354
356
|
"services/work-registry.js",
|
|
357
|
+
"services/work-retry-context.js",
|
|
355
358
|
"services/workspace-bootstrap.js",
|
|
356
359
|
"services/workspace-files.js",
|
|
357
360
|
"services/worktrees.js",
|