@tea-agent/loop-agent 0.42.0-next.1 → 0.42.0-next.3
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 +260 -1
- package/dist/application/dag/generate-task-dag.js +75 -19
- package/dist/application/task-lifecycle/advance.js +24 -5
- package/dist/application/task-lifecycle/observe.js +171 -17
- package/dist/application/task-lifecycle/plan-transitions.js +42 -7
- package/dist/application/task-lifecycle/recommendations.js +13 -2
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +7 -0
- package/dist/cli/program.js +5 -0
- package/dist/commands/client-recovery.js +3 -0
- package/dist/commands/dag-follow-up.js +138 -0
- package/dist/commands/init.js +27 -1
- package/dist/commands/task-advance.js +19 -0
- package/dist/executors/dag-pi-executor.js +3784 -231
- package/dist/executors/pi-executor.js +15 -4
- package/dist/executors/pi-read-budget-policy.js +239 -0
- package/dist/executors/shell-executor.js +942 -187
- package/dist/executors/shell-write-guard.js +7 -0
- package/dist/infrastructure/console/operation-store.js +226 -6
- package/dist/shared/dag-failure-category.js +12 -0
- package/dist/shared/openspec-spec.js +70 -4
- package/dist/shared/operator/capabilities.js +7 -0
- package/dist/task/config-types.js +105 -5
- package/dist/task/contract/adopt.js +4 -0
- package/dist/task/contract/import-revision.js +4 -0
- package/dist/task/contract/project.js +3 -0
- package/dist/task/contract/schema.js +2 -1
- package/dist/task/frontend-project-capability.js +203 -20
- package/dist/task/runtime.js +5 -2
- package/dist/task/source-prepare/build-draft.js +3 -3
- package/dist/task/source-prepare/fragment-inventory.js +15 -9
- package/dist/task/source-prepare/prepare.js +89 -1
- package/dist/task/source-prepare/semantic-intake.js +6 -2
- package/dist/task/source-references.js +22 -1
- package/dist/worker/console/chat/chat-ui-policy.js +4 -2
- package/dist/worker/console/chat/pi-runtime.js +173 -7
- package/dist/worker/console/chat/repo-browser.js +10 -3
- package/dist/worker/console/chat/routes.js +30 -19
- package/dist/worker/console/chat/session-store.js +117 -70
- package/dist/worker/console/chat/shortcuts.js +1 -1
- package/dist/worker/console/chat/turn-process.js +111 -56
- package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
- package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
- package/dist/worker/console/index.js +3 -0
- package/dist/worker/console/operator-actions.js +169 -7
- package/dist/worker/console/server.js +3 -0
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DZZ8m3PO.js → abnfDiagram-N423BO3Z-CeznJ4Iw.js} +1 -1
- package/dist/worker/console/static/assets/{arc-D6PvaVd-.js → arc-B4u9u7SX.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-B_OTOiI8.js → architectureDiagram-T3A2C74G-oghpT81p.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-Bv6rqHBg.js → blockDiagram-VBNYF7ZC-BPJ5Kvcj.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B8eHr0oz.js → c4Diagram-5PPSVZJV-CoIu69pd.js} +1 -1
- package/dist/worker/console/static/assets/channel-DmIXqJ6B.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DYwR0im2.js → chunk-2GRJ4B5K-DtgiAsSc.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-D2WPGqXt.js → chunk-2Q5K7J3B-YSu_EVSD.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-CQISJ_I7.js → chunk-5RXB4S5H-DaRO-NB6.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-C0o2Du1e.js → chunk-5VM5RSS4-Dh0ZKiGA.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-4f8kr-U7.js → chunk-6Q2QTUOP-DOrvbR-H.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-D_OWvXzX.js → chunk-GF5L2VYU-C3b22LN2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CasdPz5X.js → chunk-JWPE2WC7-CXlqO8l6.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Cj8lRrla.js → chunk-KBJHAD2P-BblTBrDu.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CUvD1FSd.js → chunk-RYQCIY6F-DswqPALe.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CqLw_eqB.js → chunk-XXDRQBXY-CfTwVOlc.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-MFV5Qxb5.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-MFV5Qxb5.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-ku-WqJPx.js → cose-bilkent-JH36ORCC-bLD8jVFo.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-Bq_PYDhl.js → cynefin-VYW2F7L2-BvoG1fh5.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BiGZV641.js → cynefinDiagram-MW4NZA55-DEPAdJu3.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-C4JRlglF.js → dagre-VZM6K2ZE-DmrNvBLy.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B0dNeWYG.js → diagram-7IWD3JNH-CdwzIwvs.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Cj35YvYM.js → diagram-B4RE2ZJO-CJ3hj-md.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-uzQoJ2-8.js → diagram-LBJQPF4R-C6YJT2a4.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D1a-Buoz.js → diagram-Q27KOJAE-C_LPfjhX.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-XjRrLRSs.js → diagram-UB23O5K3-xvsSb5Ie.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Da_O24cW.js → ebnfDiagram-BXEA7PRR-Rxp2tKcD.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BLJ8jrYU.js → erDiagram-JOGREHBK-DRyLYmZh.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-B9GrLjM0.js → flowDiagram-UKHOOZJN-TLSU_D2B.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CeJ0TqiK.js → ganttDiagram-PKOTCBZU-DGqeTvHC.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bm2eNNFX.js → gitGraphDiagram-DS77QQ5N-BZxyH2wh.js} +1 -1
- package/dist/worker/console/static/assets/index-C8V8K9b4.js +451 -0
- package/dist/worker/console/static/assets/index-Cnf_KxTQ.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DB26i3d8.js → infoDiagram-6WML65LV-BsYtzx01.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BOGRyeZT.js → ishikawaDiagram-WSZJBQD7-Qr-ezGv-.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CZRoBO_6.js → journeyDiagram-NVQOT4AX-DYbB3ibo.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-qlWhJyoB.js → kanban-definition-27J2QSJJ-BHJHewZO.js} +1 -1
- package/dist/worker/console/static/assets/{linear-CfUiDDB3.js → linear-CmPIPmCG.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BQe6fpqj.js → mermaid.core-F--C1uV0.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-zFzWHw64.js → mindmap-definition-FAOFIHXS-YTXfL1st.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DFGUqjZO.js → pegDiagram-VL7TDLO6-DdDMYXP9.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BB5i0l8Z.js → pieDiagram-7S7Q4E2Y-C3nH4KVd.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-obbVZ_X5.js → quadrantDiagram-CIZ2JOQS-kXyxjG7Y.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BwdaNzc1.js → railroadDiagram-AXF67PYL-KNzbl-7w.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CkTJaugh.js → requirementDiagram-LRYGKXZP-CSwI6dG0.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BwJ-hIgq.js → sankeyDiagram-W5VNT64P-Bl1AyuXy.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BzSgmm7w.js → sequenceDiagram-SI44F4Z6-2y4EmXQa.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-Bn3oj5Q1.js → sizeCapture-X5ZJPWSS-E8v8YrMg.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-dxhGtL8K.js → stateDiagram-OKZ733FA-eGGlR856.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8e4J3at.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BLpEInya.js → swimlanes-SLNWSIFB-B7g7xc07.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Do88I1Nb.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-ChJGYcXy.js → timeline-definition-Z64GVDOM-DR3VCHBG.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DK-qjRer.js → vennDiagram-T6HMQDX7-LUvapZIy.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-3qeaWAg-.js → wardleyDiagram-T6FBY63Y-C7GDcaXq.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CootlyP9.js → xychartDiagram-ELKLHX3M-CbNAqoGK.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/active-run-badge.js +17 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +19 -8
- package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +15 -7
- package/dist/worker/console/workspace-context.js +111 -1
- package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
- package/dist/worker/observe/static/dag-node-purpose.js +5 -0
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +99 -4
- package/dist/workflows/dag/backend-test-markdown-workflow.js +13 -1
- package/dist/workflows/dag/backend-test-writer-completeness.js +45 -3
- package/dist/workflows/dag/contract-validator-registrations.js +1 -2
- package/dist/workflows/dag/dag-retry-schema.js +138 -0
- package/dist/workflows/dag/frontend-closeout.js +221 -0
- package/dist/workflows/dag/frontend-design-policy.js +400 -0
- package/dist/workflows/dag/frontend-human-decision.js +182 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +907 -162
- package/dist/workflows/dag/frontend-plan-render.js +2 -1
- package/dist/workflows/dag/frontend-prewrite-gate.js +255 -349
- package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
- package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
- package/dist/workflows/dag/frontend-recovery-controller.js +202 -0
- package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
- package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
- package/dist/workflows/dag/frontend-recovery-run.js +33 -20
- package/dist/workflows/dag/frontend-repair.js +1 -432
- package/dist/workflows/dag/frontend-review-context.js +261 -15
- package/dist/workflows/dag/frontend-review-findings.js +270 -0
- package/dist/workflows/dag/frontend-shadow-dual-write.js +914 -0
- package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
- package/dist/workflows/dag/frontend-shape-facts.js +419 -0
- package/dist/workflows/dag/frontend-shape.js +435 -0
- package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
- package/dist/workflows/dag/frontend-split-application-service.js +203 -0
- package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
- package/dist/workflows/dag/frontend-typed-event-store.js +452 -0
- package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
- package/dist/workflows/dag/frontend-verification-trace.js +249 -24
- package/dist/workflows/dag/frontend-worktree-diff.js +250 -17
- package/dist/workflows/dag/frontend-writer-admission.js +285 -0
- package/dist/workflows/dag/frontend-writer-status.js +256 -0
- package/dist/workflows/dag/init-hybrid.js +777 -525
- package/dist/workflows/dag/interrupt-request.js +7 -0
- package/dist/workflows/dag/node-execution.js +752 -4
- package/dist/workflows/dag/prompt.js +34 -1
- package/dist/workflows/dag/recovery-lease.js +80 -0
- package/dist/workflows/dag/report.js +37 -1
- package/dist/workflows/dag/rerun-feedback.js +256 -1
- package/dist/workflows/dag/rerun-plan.js +17 -6
- package/dist/workflows/dag/rerun-run.js +23 -0
- package/dist/workflows/dag/rerun-task.js +29 -0
- package/dist/workflows/dag/retry-policy.js +219 -104
- package/dist/workflows/dag/runner.js +433 -126
- package/dist/workflows/dag/scheduler.js +114 -16
- package/dist/workflows/dag/types.js +239 -12
- package/dist/workflows/dag/validate.js +28 -12
- package/docs/examples/README.md +5 -0
- package/docs/init-surface.manifest.json +30 -12
- package/docs/skills/vetted-skill-registry.md +4 -2
- package/docs/templates/README.md +2 -0
- package/docs/templates/agent-dag-report.schema.json +8 -2
- package/docs/templates/agent-dag.schema.json +1 -1
- package/docs/templates/backend-test-dag.json +18 -18
- package/docs/templates/frontend-implementation-contract.schema.json +4 -1
- package/docs/templates/frontend-implementation-dag.json +89 -0
- package/docs/templates/spec-registry.schema.json +45 -0
- package/harness.json +1 -1
- package/package.json +1 -1
- package/skills/frontend-bounded-implement/SKILL.md +15 -14
- package/skills/frontend-bounded-implement/references/code-standards.md +19 -0
- package/skills/frontend-contract/SKILL.md +23 -0
- package/skills/frontend-contract/references/contract-protocol.md +34 -0
- package/skills/frontend-design-review/SKILL.md +22 -41
- package/skills/frontend-plan/SKILL.md +26 -0
- package/skills/frontend-plan/references/decision-contract.md +37 -0
- package/skills/frontend-plan/references/design-decisions.md +17 -0
- package/skills/frontend-review/SKILL.md +20 -15
- package/skills/frontend-review/references/review-findings.md +6 -7
- package/skills/frontend-scout/SKILL.md +25 -0
- package/skills/frontend-scout/references/design-evidence.md +16 -0
- package/skills/frontend-scout/references/scout-evidence.md +23 -0
- package/skills/frontend-verification/SKILL.md +1 -1
- package/skills/loop-agent/references/command-reference.md +1 -0
- package/skills/loop-agent/references/hybrid-dag.md +2 -2
- package/dist/worker/console/static/assets/channel-BU5gOilw.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DIzKJGHr.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DIzKJGHr.js +0 -1
- package/dist/worker/console/static/assets/index-H9rFJiGL.css +0 -1
- package/dist/worker/console/static/assets/index-xwu9GxEc.js +0 -451
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cld2qK9v.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-TIHpiT7w.js +0 -8
- package/skills/frontend-implementation/SKILL.md +0 -52
- package/skills/frontend-implementation/references/code-standards.md +0 -33
- package/skills/frontend-implementation/references/design-spec.md +0 -56
- package/skills/frontend-implementation/references/node-contracts.md +0 -31
|
@@ -46,9 +46,42 @@ function formatResolvedSkillInstructions(instructions) {
|
|
|
46
46
|
if (!instructions || instructions.length === 0)
|
|
47
47
|
return "(none)";
|
|
48
48
|
return instructions
|
|
49
|
-
.map((instruction) => instruction
|
|
49
|
+
.map((instruction) => formatSkillChecklist(instruction))
|
|
50
50
|
.join("\n\n---\n\n");
|
|
51
51
|
}
|
|
52
|
+
/** Keep skill provenance, but turn bounded inline manuals into a small action list. */
|
|
53
|
+
function formatSkillChecklist(instruction) {
|
|
54
|
+
const metadata = [
|
|
55
|
+
`Skill: ${instruction.name}`,
|
|
56
|
+
`Resolution: ${instruction.resolution}`,
|
|
57
|
+
...(instruction.path ? [`Path: ${instruction.path}`] : []),
|
|
58
|
+
...(instruction.sha256 ? [`Sha256: ${instruction.sha256}`] : []),
|
|
59
|
+
"Injection mode: checklist",
|
|
60
|
+
];
|
|
61
|
+
if (instruction.resolution !== "resolved")
|
|
62
|
+
return [...metadata, instruction.promptText].join("\n");
|
|
63
|
+
const source = instruction.promptText.split("Instructions:\n")[1] ?? "";
|
|
64
|
+
const candidates = source
|
|
65
|
+
.split(/\r?\n/)
|
|
66
|
+
.map((line) => line.trim())
|
|
67
|
+
.filter((line) => /^(?:#{1,3}\s+|[-*]\s+|\d+[.)]\s+)/.test(line) || line.length > 0)
|
|
68
|
+
.map((line) => line.replace(/^#{1,3}\s+/, "").replace(/^[-*]\s+/, "").replace(/^\d+[.)]\s+/, ""))
|
|
69
|
+
.filter((line) => line.length > 0);
|
|
70
|
+
const checklist = [];
|
|
71
|
+
let chars = 0;
|
|
72
|
+
for (const item of candidates) {
|
|
73
|
+
const bounded = item.slice(0, 240);
|
|
74
|
+
if (checklist.length >= 10 || chars + bounded.length > 1_600)
|
|
75
|
+
break;
|
|
76
|
+
checklist.push(`- ${bounded}`);
|
|
77
|
+
chars += bounded.length;
|
|
78
|
+
}
|
|
79
|
+
return [
|
|
80
|
+
...metadata,
|
|
81
|
+
"Checklist (apply these actions; consult the immutable resolved file only when a listed action requires detail):",
|
|
82
|
+
...(checklist.length > 0 ? checklist : ["- Follow the resolved skill's declared workflow."]),
|
|
83
|
+
].join("\n");
|
|
84
|
+
}
|
|
52
85
|
export function formatOutputLanguageBlock(language = DEFAULT_DAG_OUTPUT_LANGUAGE) {
|
|
53
86
|
if (language === "en") {
|
|
54
87
|
return [
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
|
|
5
|
+
import { DAG_RUNS_DIR } from "./lifecycle.js";
|
|
6
|
+
function recoveryLeasePath(cwd, parentRunId) {
|
|
7
|
+
// Never derive a filesystem name directly from an externally supplied legacy
|
|
8
|
+
// run id. The content remains auditable while the filename is traversal-safe.
|
|
9
|
+
const key = createHash("sha256").update(parentRunId).digest("hex");
|
|
10
|
+
return path.join(cwd, DAG_RUNS_DIR, ".recovery-leases", `${key}.json`);
|
|
11
|
+
}
|
|
12
|
+
function parseLease(raw, parentRunId) {
|
|
13
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
14
|
+
throw new Error("invalid dag recovery lease: expected object");
|
|
15
|
+
}
|
|
16
|
+
const record = raw;
|
|
17
|
+
if (record.schemaVersion !== 1 ||
|
|
18
|
+
record.parentRunId !== parentRunId ||
|
|
19
|
+
typeof record.requestId !== "string" ||
|
|
20
|
+
record.requestId.length === 0 ||
|
|
21
|
+
(record.kind !== "auto-frontend-recovery" &&
|
|
22
|
+
record.kind !== "rerun-from-node" &&
|
|
23
|
+
record.kind !== "standalone-task-rerun") ||
|
|
24
|
+
typeof record.leaseToken !== "string" ||
|
|
25
|
+
record.leaseToken.length === 0 ||
|
|
26
|
+
typeof record.acquiredAt !== "string" ||
|
|
27
|
+
typeof record.executionRef !== "undefined" &&
|
|
28
|
+
typeof record.executionRef !== "string") {
|
|
29
|
+
throw new Error("invalid dag recovery lease: schema mismatch");
|
|
30
|
+
}
|
|
31
|
+
return record;
|
|
32
|
+
}
|
|
33
|
+
export async function acquireDagRecoveryLease(input) {
|
|
34
|
+
const leasePath = recoveryLeasePath(input.cwd, input.parentRunId);
|
|
35
|
+
const lease = {
|
|
36
|
+
schemaVersion: 1,
|
|
37
|
+
parentRunId: input.parentRunId,
|
|
38
|
+
requestId: input.requestId,
|
|
39
|
+
kind: input.kind,
|
|
40
|
+
leaseToken: randomBytes(16).toString("hex"),
|
|
41
|
+
acquiredAt: (input.now ?? (() => new Date()))().toISOString(),
|
|
42
|
+
};
|
|
43
|
+
await mkdir(path.dirname(leasePath), { recursive: true });
|
|
44
|
+
try {
|
|
45
|
+
// `wx` is the cross-process compare-and-create primitive. The lease is
|
|
46
|
+
// deliberately immutable for a parent run: a later recovery must follow
|
|
47
|
+
// the committed child lineage rather than fork another DAG.
|
|
48
|
+
await writeFile(leasePath, `${JSON.stringify(lease, null, 2)}\n`, {
|
|
49
|
+
encoding: "utf8",
|
|
50
|
+
flag: "wx",
|
|
51
|
+
});
|
|
52
|
+
return { kind: "acquired", leasePath, lease };
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (!(error && typeof error === "object" && "code" in error && error.code === "EEXIST")) {
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
const existing = parseLease(JSON.parse(await readFile(leasePath, "utf8")), input.parentRunId);
|
|
59
|
+
return { kind: "held", leasePath, lease: existing };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Persist the operation/run identity before the caller is allowed to execute it. */
|
|
63
|
+
export async function bindDagRecoveryLeaseExecution(input) {
|
|
64
|
+
if (!input.executionRef.trim()) {
|
|
65
|
+
throw new Error("dag recovery lease executionRef is required");
|
|
66
|
+
}
|
|
67
|
+
const existing = parseLease(JSON.parse(await readFile(input.leasePath, "utf8")), input.lease.parentRunId);
|
|
68
|
+
if (existing.leaseToken !== input.lease.leaseToken) {
|
|
69
|
+
throw new Error("dag recovery lease ownership drift");
|
|
70
|
+
}
|
|
71
|
+
if (existing.executionRef && existing.executionRef !== input.executionRef) {
|
|
72
|
+
throw new Error("dag recovery lease execution identity drift");
|
|
73
|
+
}
|
|
74
|
+
const bound = {
|
|
75
|
+
...existing,
|
|
76
|
+
executionRef: input.executionRef,
|
|
77
|
+
};
|
|
78
|
+
await writeJsonAtomic(input.leasePath, bound);
|
|
79
|
+
return bound;
|
|
80
|
+
}
|
|
@@ -125,6 +125,8 @@ const dagNodeArtifactsReportSchema = z
|
|
|
125
125
|
humanEscalationMd: dagArtifactRefSchema,
|
|
126
126
|
humanApproval: dagArtifactRefSchema,
|
|
127
127
|
humanRejection: dagArtifactRefSchema,
|
|
128
|
+
changeManifest: dagArtifactRefSchema,
|
|
129
|
+
changeManifestChangedFiles: z.array(z.string()).optional(),
|
|
128
130
|
})
|
|
129
131
|
.strict();
|
|
130
132
|
const dagNodeReportRowSchema = z
|
|
@@ -259,6 +261,20 @@ export async function probeArtifact(filePath) {
|
|
|
259
261
|
export async function buildNodeArtifactsReport(runDir, nodeId) {
|
|
260
262
|
const nodeDir = path.join(runDir, nodeId);
|
|
261
263
|
const artifact = (name) => probeArtifact(path.join(nodeDir, name));
|
|
264
|
+
const changeManifestRef = await artifact("change-manifest.json");
|
|
265
|
+
let changeManifestChangedFiles;
|
|
266
|
+
if (changeManifestRef.exists) {
|
|
267
|
+
try {
|
|
268
|
+
const manifest = JSON.parse(await readFile(path.join(nodeDir, "change-manifest.json"), "utf-8"));
|
|
269
|
+
changeManifestChangedFiles = Array.isArray(manifest.changedFiles)
|
|
270
|
+
? manifest.changedFiles
|
|
271
|
+
.filter((item) => typeof item === "string")
|
|
272
|
+
: undefined;
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
// unreadable manifest → leave undefined
|
|
276
|
+
}
|
|
277
|
+
}
|
|
262
278
|
return {
|
|
263
279
|
resultSummary: await artifact("result.summary.md"),
|
|
264
280
|
promptRedacted: await artifact("prompt.redacted.md"),
|
|
@@ -270,6 +286,8 @@ export async function buildNodeArtifactsReport(runDir, nodeId) {
|
|
|
270
286
|
humanEscalationMd: await artifact("human-escalation.md"),
|
|
271
287
|
humanApproval: await artifact("human-approval.json"),
|
|
272
288
|
humanRejection: await artifact("human-rejection.json"),
|
|
289
|
+
changeManifest: changeManifestRef,
|
|
290
|
+
changeManifestChangedFiles,
|
|
273
291
|
};
|
|
274
292
|
}
|
|
275
293
|
function taskById(spec) {
|
|
@@ -726,6 +744,12 @@ function formatArtifactFlags(artifacts) {
|
|
|
726
744
|
flags.push("approval");
|
|
727
745
|
if (artifacts.humanRejection.exists)
|
|
728
746
|
flags.push("rejection");
|
|
747
|
+
if (artifacts.changeManifest.exists) {
|
|
748
|
+
const count = artifacts.changeManifestChangedFiles?.length;
|
|
749
|
+
flags.push(count !== undefined
|
|
750
|
+
? `changed:${count}`
|
|
751
|
+
: "change-manifest");
|
|
752
|
+
}
|
|
729
753
|
return flags.length > 0 ? flags.join(",") : "-";
|
|
730
754
|
}
|
|
731
755
|
function formatTimestamp(value) {
|
|
@@ -749,7 +773,19 @@ function summarizeNodeStatuses(nodes) {
|
|
|
749
773
|
return `${total} total / ${finished} finished / ${failed} failed / ${skipped} skipped / ${pending} pending`;
|
|
750
774
|
}
|
|
751
775
|
function hasArtifact(artifacts) {
|
|
752
|
-
return
|
|
776
|
+
return [
|
|
777
|
+
artifacts.resultSummary,
|
|
778
|
+
artifacts.promptRedacted,
|
|
779
|
+
artifacts.skills,
|
|
780
|
+
artifacts.nodeRecord,
|
|
781
|
+
artifacts.decisionEnvelope,
|
|
782
|
+
artifacts.decisionEnvelopeValidation,
|
|
783
|
+
artifacts.humanEscalationJson,
|
|
784
|
+
artifacts.humanEscalationMd,
|
|
785
|
+
artifacts.humanApproval,
|
|
786
|
+
artifacts.humanRejection,
|
|
787
|
+
artifacts.changeManifest,
|
|
788
|
+
].some((artifact) => artifact.exists);
|
|
753
789
|
}
|
|
754
790
|
function failureRows(run) {
|
|
755
791
|
return run.nodes.filter((node) => node.status === "ERROR" ||
|
|
@@ -2,6 +2,8 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { parseJsonReviewVerdict } from "./output-protocol.js";
|
|
5
|
+
import { FRONTEND_DESIGN_REVIEW_NODE_ID, FRONTEND_REVIEW_NODE_ID, readCommittedDesignRequestFact, readCommittedReviewRequestFact, } from "./frontend-review-findings.js";
|
|
6
|
+
import { readTypedEventStoreFromJsonl } from "./frontend-typed-event-store.js";
|
|
5
7
|
export const MAX_RERUN_FEEDBACK_CHARS = 12_000;
|
|
6
8
|
const RERUN_FEEDBACK_TRUNCATION_MARKER = "\n\n...[上一轮反馈已按长度上限截断]...\n\n";
|
|
7
9
|
const MAX_EVIDENCE_REFS = 8;
|
|
@@ -35,6 +37,44 @@ function requestRevision(text) {
|
|
|
35
37
|
return (emphasized?.[1] ?? trimmed).trim() === "VERDICT: request-revision";
|
|
36
38
|
});
|
|
37
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Deterministically render a typed `request_review_changes` fact into feedback
|
|
42
|
+
* text. Unlike the legacy text channel (VERDICT: request-revision), the
|
|
43
|
+
* frontend review terminal is a committed typed fact; the findings are the
|
|
44
|
+
* actionable body the next round must consume.
|
|
45
|
+
*/
|
|
46
|
+
export function formatTypedReviewRequestFeedback(fact) {
|
|
47
|
+
const lines = [];
|
|
48
|
+
lines.push(`frontend review committed request_review_changes (issueCategory=${typeof fact.issueCategory === "string" ? fact.issueCategory : "unknown"})`);
|
|
49
|
+
if (Array.isArray(fact.findings) && fact.findings.length > 0) {
|
|
50
|
+
lines.push("review findings:");
|
|
51
|
+
for (const finding of fact.findings) {
|
|
52
|
+
if (!finding || typeof finding !== "object")
|
|
53
|
+
continue;
|
|
54
|
+
const record = finding;
|
|
55
|
+
const severity = typeof record.severity === "string" ? record.severity : "";
|
|
56
|
+
const issue = typeof record.issue === "string" ? record.issue : "";
|
|
57
|
+
const location = [
|
|
58
|
+
typeof record.file === "string" ? record.file : "",
|
|
59
|
+
typeof record.line === "number" ? String(record.line) : "",
|
|
60
|
+
]
|
|
61
|
+
.filter(Boolean)
|
|
62
|
+
.join(":");
|
|
63
|
+
const prefix = location ? `[${location}] ` : "";
|
|
64
|
+
const requiredChange = typeof record.requiredChange === "string" &&
|
|
65
|
+
record.requiredChange.length > 0
|
|
66
|
+
? ` (requiredChange: ${record.requiredChange})`
|
|
67
|
+
: "";
|
|
68
|
+
lines.push(`- ${prefix}${severity}: ${issue}${requiredChange}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (Array.isArray(fact.evidenceRefs) && fact.evidenceRefs.length > 0) {
|
|
72
|
+
lines.push(`evidenceRefs: ${fact.evidenceRefs
|
|
73
|
+
.filter((ref) => typeof ref === "string")
|
|
74
|
+
.join(", ")}`);
|
|
75
|
+
}
|
|
76
|
+
return lines.join("\n");
|
|
77
|
+
}
|
|
38
78
|
function pathInside(root, candidate) {
|
|
39
79
|
const relative = path.relative(root, candidate);
|
|
40
80
|
return (relative === "" ||
|
|
@@ -127,6 +167,162 @@ function evidenceRefs(repoRoot, runDir, pass, sourceNodeId) {
|
|
|
127
167
|
}
|
|
128
168
|
return result;
|
|
129
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* Read the committed frontend review request fact (typed terminal channel).
|
|
172
|
+
* Returns { nodeId, fact } when the review node committed `request_review_changes`.
|
|
173
|
+
*/
|
|
174
|
+
async function readTypedReviewRequestFact(runDir) {
|
|
175
|
+
const fact = await readCommittedReviewRequestFact({ runDir });
|
|
176
|
+
if (!fact)
|
|
177
|
+
return undefined;
|
|
178
|
+
return {
|
|
179
|
+
nodeId: FRONTEND_REVIEW_NODE_ID,
|
|
180
|
+
fact: {
|
|
181
|
+
issueCategory: fact.issueCategory,
|
|
182
|
+
evidenceRefs: fact.evidenceRefs,
|
|
183
|
+
findings: fact.findings,
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Read the committed frontend design request fact (typed terminal channel).
|
|
189
|
+
* Returns { nodeId, fact } when the design review node committed
|
|
190
|
+
* `request_design_changes` — the design analog of request_review_changes.
|
|
191
|
+
*/
|
|
192
|
+
async function readTypedDesignRequestFact(runDir) {
|
|
193
|
+
const fact = await readCommittedDesignRequestFact({ runDir });
|
|
194
|
+
if (!fact)
|
|
195
|
+
return undefined;
|
|
196
|
+
return {
|
|
197
|
+
nodeId: FRONTEND_DESIGN_REVIEW_NODE_ID,
|
|
198
|
+
fact: {
|
|
199
|
+
issueCategory: fact.issueCategory,
|
|
200
|
+
evidenceRefs: fact.evidenceRefs,
|
|
201
|
+
findings: fact.findings,
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
const RERUN_FEEDBACK_LINEAGE_MAX_DEPTH = 5;
|
|
206
|
+
const RERUN_FEEDBACK_LINEAGE_LIFECYCLES = [
|
|
207
|
+
"active",
|
|
208
|
+
"paused",
|
|
209
|
+
"completed",
|
|
210
|
+
];
|
|
211
|
+
function safeLineageRunId(value) {
|
|
212
|
+
if (typeof value !== "string" || !value || value !== value.trim())
|
|
213
|
+
return undefined;
|
|
214
|
+
if (value === "." ||
|
|
215
|
+
value === ".." ||
|
|
216
|
+
value.includes("/") ||
|
|
217
|
+
value.includes("\\") ||
|
|
218
|
+
value.includes("\0"))
|
|
219
|
+
return undefined;
|
|
220
|
+
return value;
|
|
221
|
+
}
|
|
222
|
+
async function readCommittedTerminalKind(input) {
|
|
223
|
+
try {
|
|
224
|
+
const records = await readTypedEventStoreFromJsonl(path.join(input.runDir, input.nodeId, input.factsFile));
|
|
225
|
+
for (const record of records) {
|
|
226
|
+
if (record.phase !== "committed")
|
|
227
|
+
continue;
|
|
228
|
+
const kind = record.fact.kind;
|
|
229
|
+
if (typeof kind === "string" && input.kinds.includes(kind))
|
|
230
|
+
return kind;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
// A missing/unreadable terminal store means this run did not establish a
|
|
235
|
+
// usable resolution barrier; lineage lookup may continue.
|
|
236
|
+
}
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
async function readReviewTerminalKind(runDir) {
|
|
240
|
+
return readCommittedTerminalKind({
|
|
241
|
+
runDir,
|
|
242
|
+
nodeId: FRONTEND_REVIEW_NODE_ID,
|
|
243
|
+
factsFile: "review-typed-facts.jsonl",
|
|
244
|
+
kinds: ["request_review_changes", "approve_review"],
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
async function readDesignTerminalKind(runDir) {
|
|
248
|
+
return readCommittedTerminalKind({
|
|
249
|
+
runDir,
|
|
250
|
+
nodeId: FRONTEND_DESIGN_REVIEW_NODE_ID,
|
|
251
|
+
factsFile: "design-typed-facts.jsonl",
|
|
252
|
+
kinds: ["request_design_changes", "approve_design"],
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Walk the rerun-task lineage chain (`<runDir>/.runtime/rerun-task-lineage.json`
|
|
257
|
+
* → parentRunId) looking for the nearest ancestor run with a committed review
|
|
258
|
+
* or design request fact. A nearer final-review approval resolves all older
|
|
259
|
+
* findings; a nearer design approval resolves older design findings while
|
|
260
|
+
* still allowing an older final-review request to survive. Bounded depth;
|
|
261
|
+
* unsafe ids, mismatched state identities, cycles, and unreadable lineage
|
|
262
|
+
* files terminate the walk with undefined.
|
|
263
|
+
*/
|
|
264
|
+
async function readAncestorTypedRequestFact(runDir) {
|
|
265
|
+
let currentRunDir = runDir;
|
|
266
|
+
let currentRunId;
|
|
267
|
+
let designResolved = false;
|
|
268
|
+
const dagRunsRoot = path.resolve(runDir, "..", "..");
|
|
269
|
+
const visitedRunDirs = new Set([path.resolve(runDir)]);
|
|
270
|
+
for (let depth = 0; depth <= RERUN_FEEDBACK_LINEAGE_MAX_DEPTH; depth += 1) {
|
|
271
|
+
const reviewTerminalKind = await readReviewTerminalKind(currentRunDir);
|
|
272
|
+
if (currentRunId && reviewTerminalKind === "request_review_changes") {
|
|
273
|
+
const fact = await readTypedReviewRequestFact(currentRunDir);
|
|
274
|
+
if (fact)
|
|
275
|
+
return { ...fact, parentRunId: currentRunId };
|
|
276
|
+
}
|
|
277
|
+
if (reviewTerminalKind === "approve_review")
|
|
278
|
+
return undefined;
|
|
279
|
+
const designTerminalKind = await readDesignTerminalKind(currentRunDir);
|
|
280
|
+
if (currentRunId &&
|
|
281
|
+
!designResolved &&
|
|
282
|
+
designTerminalKind === "request_design_changes") {
|
|
283
|
+
const fact = await readTypedDesignRequestFact(currentRunDir);
|
|
284
|
+
if (fact)
|
|
285
|
+
return { ...fact, parentRunId: currentRunId };
|
|
286
|
+
}
|
|
287
|
+
if (designTerminalKind === "approve_design")
|
|
288
|
+
designResolved = true;
|
|
289
|
+
if (depth === RERUN_FEEDBACK_LINEAGE_MAX_DEPTH)
|
|
290
|
+
return undefined;
|
|
291
|
+
let parentRunId;
|
|
292
|
+
try {
|
|
293
|
+
const lineage = JSON.parse(await readFile(path.join(currentRunDir, ".runtime", "rerun-task-lineage.json"), "utf8"));
|
|
294
|
+
parentRunId = safeLineageRunId(lineage.parentRunId);
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
if (!parentRunId)
|
|
300
|
+
return undefined;
|
|
301
|
+
let ancestorRunDir;
|
|
302
|
+
for (const lifecycle of RERUN_FEEDBACK_LINEAGE_LIFECYCLES) {
|
|
303
|
+
const candidate = path.join(dagRunsRoot, lifecycle, parentRunId);
|
|
304
|
+
try {
|
|
305
|
+
const ancestorState = JSON.parse(await readFile(path.join(candidate, "state.json"), "utf8"));
|
|
306
|
+
if (ancestorState.runId === parentRunId) {
|
|
307
|
+
ancestorRunDir = candidate;
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
// try next lifecycle
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (!ancestorRunDir)
|
|
316
|
+
return undefined;
|
|
317
|
+
const resolvedAncestorRunDir = path.resolve(ancestorRunDir);
|
|
318
|
+
if (visitedRunDirs.has(resolvedAncestorRunDir))
|
|
319
|
+
return undefined;
|
|
320
|
+
visitedRunDirs.add(resolvedAncestorRunDir);
|
|
321
|
+
currentRunDir = ancestorRunDir;
|
|
322
|
+
currentRunId = parentRunId;
|
|
323
|
+
}
|
|
324
|
+
return undefined;
|
|
325
|
+
}
|
|
130
326
|
/**
|
|
131
327
|
* Derive only the latest unresolved verify/review feedback from canonical
|
|
132
328
|
* parent-run facts. Provider-only failures intentionally return undefined.
|
|
@@ -144,8 +340,67 @@ export async function deriveDagRerunFeedback(input) {
|
|
|
144
340
|
break;
|
|
145
341
|
}
|
|
146
342
|
}
|
|
147
|
-
if (!sourceNodeId || !sourceNode)
|
|
343
|
+
if (!sourceNodeId || !sourceNode) {
|
|
344
|
+
// Typed terminal channel: the frontend review/design review commit their
|
|
345
|
+
// verdicts as typed facts (request_review_changes / request_design_changes)
|
|
346
|
+
// instead of VERDICT text, and the frontend DAG has no convergence
|
|
347
|
+
// passHistory. Fall back to reading the typed event stores so findings
|
|
348
|
+
// still flow into the next round as unresolved feedback. Prefer review;
|
|
349
|
+
// if absent, fall through to the design review (a design rejection also
|
|
350
|
+
// carries actionable findings the next plan round must consume). When
|
|
351
|
+
// the direct parent has neither because it failed before review, walk the
|
|
352
|
+
// rerun-task lineage chain — chained
|
|
353
|
+
// reruns otherwise lose the grandparent's code-review findings and the
|
|
354
|
+
// reviewer re-flags them as "unresolved previous-round feedback" (r18).
|
|
355
|
+
// A committed approval is a resolution barrier and prevents older
|
|
356
|
+
// findings from being resurrected.
|
|
357
|
+
const typedFact = (await readTypedReviewRequestFact(input.runDir)) ??
|
|
358
|
+
(await readTypedDesignRequestFact(input.runDir)) ??
|
|
359
|
+
(await readAncestorTypedRequestFact(input.runDir));
|
|
360
|
+
if (typedFact) {
|
|
361
|
+
const parentSourceBindingHash = sourceBindingHash(input.spec);
|
|
362
|
+
return withDigest({
|
|
363
|
+
schemaVersion: 1,
|
|
364
|
+
kind: "unresolved-terminal-feedback",
|
|
365
|
+
parentRunId: "parentRunId" in typedFact &&
|
|
366
|
+
typeof typedFact.parentRunId === "string"
|
|
367
|
+
? typedFact.parentRunId
|
|
368
|
+
: input.parentRunId,
|
|
369
|
+
taskId: input.taskId,
|
|
370
|
+
sourceNodeId: typedFact.nodeId,
|
|
371
|
+
verdict: "request-revision",
|
|
372
|
+
failureCategory: typedFact.nodeId === FRONTEND_REVIEW_NODE_ID
|
|
373
|
+
? "review-request-changes"
|
|
374
|
+
: "design-request-changes",
|
|
375
|
+
feedbackText: scrubAndBoundFeedbackText(formatTypedReviewRequestFeedback(typedFact.fact), input.runDir, input.state.cwd),
|
|
376
|
+
evidenceRefs: [
|
|
377
|
+
{
|
|
378
|
+
nodeId: typedFact.nodeId,
|
|
379
|
+
preservedNodeRecordPath: `${typedFact.nodeId}.json`,
|
|
380
|
+
},
|
|
381
|
+
],
|
|
382
|
+
attemptSummary: {
|
|
383
|
+
completedPasses: 1,
|
|
384
|
+
maxPasses: 1,
|
|
385
|
+
...(input.state.terminalReason
|
|
386
|
+
? { terminalReason: input.state.terminalReason }
|
|
387
|
+
: {}),
|
|
388
|
+
},
|
|
389
|
+
binding: {
|
|
390
|
+
...(parentSourceBindingHash
|
|
391
|
+
? { sourceCanonicalHash: parentSourceBindingHash }
|
|
392
|
+
: {}),
|
|
393
|
+
...(input.spec.taskContractBinding?.canonicalHash
|
|
394
|
+
? {
|
|
395
|
+
taskContractCanonicalHash: input.spec.taskContractBinding.canonicalHash,
|
|
396
|
+
}
|
|
397
|
+
: {}),
|
|
398
|
+
status: "unknown",
|
|
399
|
+
},
|
|
400
|
+
});
|
|
401
|
+
}
|
|
148
402
|
return undefined;
|
|
403
|
+
}
|
|
149
404
|
const completedPasses = pass?.pass ?? Math.max(1, input.state.convergence?.currentPass ?? 1);
|
|
150
405
|
const maxPasses = Math.max(completedPasses, input.state.convergence?.maxPasses ?? 1);
|
|
151
406
|
const parentSourceBindingHash = sourceBindingHash(input.spec);
|
|
@@ -415,12 +415,16 @@ function isCanonicalGeneratedVerifyShell(task) {
|
|
|
415
415
|
return false;
|
|
416
416
|
if (shell.projectGovernanceGate)
|
|
417
417
|
return false;
|
|
418
|
-
if (shell.
|
|
418
|
+
if (shell.frontendDesignPolicy)
|
|
419
|
+
return false;
|
|
420
|
+
if (shell.frontendWriterAdmission)
|
|
419
421
|
return false;
|
|
420
422
|
if (shell.frontendVerificationBundle)
|
|
421
423
|
return false;
|
|
422
424
|
if (shell.frontendReviewContext)
|
|
423
425
|
return false;
|
|
426
|
+
if (shell.frontendCloseout)
|
|
427
|
+
return false;
|
|
424
428
|
if (shell.backendTestPipeline)
|
|
425
429
|
return false;
|
|
426
430
|
if (shell.nonZeroExitPolicy === "record")
|
|
@@ -444,15 +448,18 @@ function isCanonicalFrontendArtifactContinuationShell(task) {
|
|
|
444
448
|
switch (task.id) {
|
|
445
449
|
case "frontend-prewrite-gate-shell":
|
|
446
450
|
return Boolean(shell.frontendPrewriteGate) && (shell.commands?.length ?? 0) === 0;
|
|
447
|
-
case "frontend-
|
|
448
|
-
return Boolean(shell.
|
|
449
|
-
|
|
450
|
-
|
|
451
|
+
case "frontend-design-policy-shell":
|
|
452
|
+
return Boolean(shell.frontendDesignPolicy) && (shell.commands?.length ?? 0) === 0;
|
|
453
|
+
case "frontend-writer-admission-shell":
|
|
454
|
+
return Boolean(shell.frontendWriterAdmission) && (shell.commands?.length ?? 0) === 0;
|
|
451
455
|
case "frontend-verify-assess-shell":
|
|
452
456
|
case "frontend-reverify-shell":
|
|
457
|
+
case "frontend-verify-shell":
|
|
453
458
|
return Boolean(shell.frontendVerificationBundle) && (shell.commands?.length ?? 0) === 0;
|
|
454
459
|
case "frontend-review-context-shell":
|
|
455
460
|
return Boolean(shell.frontendReviewContext) && (shell.commands?.length ?? 0) === 0;
|
|
461
|
+
case "frontend-closeout-shell":
|
|
462
|
+
return Boolean(shell.frontendCloseout) && (shell.commands?.length ?? 0) === 0;
|
|
456
463
|
default:
|
|
457
464
|
return false;
|
|
458
465
|
}
|
|
@@ -474,12 +481,16 @@ function isSafeGateOnlyShell(task) {
|
|
|
474
481
|
return false;
|
|
475
482
|
if (shell.preset)
|
|
476
483
|
return false;
|
|
477
|
-
if (shell.
|
|
484
|
+
if (shell.frontendDesignPolicy)
|
|
485
|
+
return false;
|
|
486
|
+
if (shell.frontendWriterAdmission)
|
|
478
487
|
return false;
|
|
479
488
|
if (shell.frontendVerificationBundle)
|
|
480
489
|
return false;
|
|
481
490
|
if (shell.frontendReviewContext)
|
|
482
491
|
return false;
|
|
492
|
+
if (shell.frontendCloseout)
|
|
493
|
+
return false;
|
|
483
494
|
if (shell.backendTestPipeline)
|
|
484
495
|
return false;
|
|
485
496
|
if (shell.verifyEvidence)
|
|
@@ -13,6 +13,7 @@ import { prepareActiveRunDir, writeNodeRecord, writeRunSpec, writeRunState, } fr
|
|
|
13
13
|
import { cloneParentSkillSnapshotForContinuation, readSkillSnapshot, } from "./skill-snapshot.js";
|
|
14
14
|
import { topoSortToRanks } from "./topo.js";
|
|
15
15
|
import { captureWorkspaceCheckpoint, readWorkspaceCheckpoint, WORKSPACE_CHECKPOINT_START_REL, WORKSPACE_CHECKPOINT_TERMINAL_REL, writeWorkspaceCheckpoint, } from "./workspace-checkpoint.js";
|
|
16
|
+
import { acquireDagRecoveryLease, bindDagRecoveryLeaseExecution, } from "./recovery-lease.js";
|
|
16
17
|
import { captureWriterBaseline, readJournalFile, recordWriterAttemptFromCurrent, restoreJournal, writeJournalFile, } from "./frontend-writer-rollback.js";
|
|
17
18
|
export const RERUN_PLAN_REL_PATH = ".runtime/rerun-plan.json";
|
|
18
19
|
export const IMPORT_MANIFEST_REL_PATH = ".runtime/import-manifest.json";
|
|
@@ -970,6 +971,20 @@ export async function executeDagRerun(input) {
|
|
|
970
971
|
}
|
|
971
972
|
const parentSpec = await readDagRunSpec(located.runDir);
|
|
972
973
|
const parentState = await readDagRunState(located.runDir);
|
|
974
|
+
const recoveryLease = await acquireDagRecoveryLease({
|
|
975
|
+
cwd: input.cwd,
|
|
976
|
+
parentRunId: input.parentRunId,
|
|
977
|
+
requestId: input.requestId,
|
|
978
|
+
kind: "rerun-from-node",
|
|
979
|
+
});
|
|
980
|
+
if (recoveryLease.kind === "held") {
|
|
981
|
+
return {
|
|
982
|
+
ok: false,
|
|
983
|
+
plan,
|
|
984
|
+
errorCode: "RECOVERY_LEASE_HELD",
|
|
985
|
+
message: `parent run recovery is already claimed by ${recoveryLease.lease.kind} request ${recoveryLease.lease.requestId}`,
|
|
986
|
+
};
|
|
987
|
+
}
|
|
973
988
|
const staged = await stageContinuationRun({
|
|
974
989
|
cwd: input.cwd,
|
|
975
990
|
parentRunDir: located.runDir,
|
|
@@ -981,6 +996,14 @@ export async function executeDagRerun(input) {
|
|
|
981
996
|
requestId: input.requestId,
|
|
982
997
|
promptOverrides: input.promptOverrides,
|
|
983
998
|
});
|
|
999
|
+
// Stage is non-executing; bind the child identity immediately before handing
|
|
1000
|
+
// it to the runner so no recovery DAG can start without durable parent
|
|
1001
|
+
// lineage ownership.
|
|
1002
|
+
await bindDagRecoveryLeaseExecution({
|
|
1003
|
+
leasePath: recoveryLease.leasePath,
|
|
1004
|
+
lease: recoveryLease.lease,
|
|
1005
|
+
executionRef: staged.state.runId,
|
|
1006
|
+
});
|
|
984
1007
|
const continuationOptions = {
|
|
985
1008
|
cwd: input.cwd,
|
|
986
1009
|
spec: staged.spec,
|
|
@@ -4,6 +4,7 @@ import { generateTaskDagUseCase } from "../../application/dag/generate-task-dag.
|
|
|
4
4
|
import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
|
|
5
5
|
import { DAG_RUNS_DIR, isTerminalDagRunStatus, locateDagRun, readDagRunSpec, readDagRunState, } from "./lifecycle.js";
|
|
6
6
|
import { deriveDagRerunFeedback } from "./rerun-feedback.js";
|
|
7
|
+
import { acquireDagRecoveryLease, bindDagRecoveryLeaseExecution, } from "./recovery-lease.js";
|
|
7
8
|
import { parseDagSpec, } from "./types.js";
|
|
8
9
|
export const RERUN_TASK_LINEAGE_REL_PATH = ".runtime/rerun-task-lineage.json";
|
|
9
10
|
export function parseDagRerunTaskArgs(args) {
|
|
@@ -369,6 +370,34 @@ export async function rerunStandaloneTask(input) {
|
|
|
369
370
|
}
|
|
370
371
|
}
|
|
371
372
|
}
|
|
373
|
+
const recoveryLease = execute
|
|
374
|
+
? await acquireDagRecoveryLease({
|
|
375
|
+
cwd: input.cwd,
|
|
376
|
+
parentRunId: input.parentRunId,
|
|
377
|
+
requestId: input.requestId,
|
|
378
|
+
kind: "standalone-task-rerun",
|
|
379
|
+
})
|
|
380
|
+
: undefined;
|
|
381
|
+
if (recoveryLease?.kind === "held") {
|
|
382
|
+
return blockedResult({
|
|
383
|
+
parentRunId: input.parentRunId,
|
|
384
|
+
reason: input.reason,
|
|
385
|
+
requestId: input.requestId,
|
|
386
|
+
blockedReason: "recovery-lease-held",
|
|
387
|
+
note: `parent run recovery is already claimed by ${recoveryLease.lease.kind} request ${recoveryLease.lease.requestId}`,
|
|
388
|
+
taskId: taskResolution.taskId,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
if (recoveryLease?.kind === "acquired") {
|
|
392
|
+
// Persist a non-empty execution identity before generateTaskDagUseCase can
|
|
393
|
+
// execute its new DAG. The generated run id is not available yet, so the
|
|
394
|
+
// request id is the stable lineage handle for this entry point.
|
|
395
|
+
await bindDagRecoveryLeaseExecution({
|
|
396
|
+
leasePath: recoveryLease.leasePath,
|
|
397
|
+
lease: recoveryLease.lease,
|
|
398
|
+
executionRef: `standalone-task-rerun:${input.requestId}`,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
372
401
|
const generateResult = await generateTaskDagUseCase({
|
|
373
402
|
repoRoot: input.cwd,
|
|
374
403
|
taskId: taskResolution.taskId,
|