@tea-agent/loop-agent 0.42.0-next.14 → 0.42.0-next.16
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 +19 -3
- package/dist/application/task-lifecycle/advance.js +9 -4
- package/dist/build-stamp.json +3 -3
- package/dist/commands/task-source-prepare.js +3 -1
- package/dist/executors/dag-pi-executor.js +1437 -154
- package/dist/executors/shell-executor.js +107 -35
- package/dist/shared/dag-failure-category.js +6 -0
- package/dist/task/contract/apply.js +36 -2
- package/dist/task/source-prepare/parse-intent.js +7 -0
- package/dist/worker/console/chat/pi-runtime.js +45 -2
- package/dist/worker/console/chat/resource-loader.js +4 -1
- package/dist/worker/console/chat/routes.js +8 -0
- package/dist/worker/console/chat/subagents/agent-tool.js +62 -0
- package/dist/worker/console/chat/subagents/explore-agent.js +169 -0
- package/dist/worker/console/chat/subagents/index.js +5 -0
- package/dist/worker/console/chat/subagents/orchestrator.js +273 -0
- package/dist/worker/console/chat/subagents/tool-policy.js +53 -0
- package/dist/worker/console/chat/subagents/types.js +13 -0
- package/dist/worker/console/chat/tool-preview.js +10 -0
- package/dist/worker/console/chat/tools.js +11 -1
- package/dist/worker/console/interview/tools.js +1 -0
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BGMbAdAG.js → abnfDiagram-N423BO3Z-CgXb0EVO.js} +1 -1
- package/dist/worker/console/static/assets/{arc-CJu1LND8.js → arc-DN59MZqN.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BmJh9gMQ.js → architectureDiagram-T3A2C74G-BUk3sWpn.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-BWjaX3ZF.js → blockDiagram-VBNYF7ZC-IH-cPBFE.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-CLEOAxLB.js → c4Diagram-5PPSVZJV-BJWf1nGo.js} +1 -1
- package/dist/worker/console/static/assets/channel-i1DjpDIw.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-CydnyQUX.js → chunk-2GRJ4B5K-BCPb5H0y.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-tjJ1IoZ1.js → chunk-2Q5K7J3B-Cfs3jeW2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DR358Phd.js → chunk-5RXB4S5H-BhQI1B_T.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-B-LuWFZD.js → chunk-5VM5RSS4-Cc_m4gyV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-FG53fA6v.js → chunk-6Q2QTUOP-JPqDS3IV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Bkt-w8SZ.js → chunk-GF5L2VYU-AeGX6EAg.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-WxHo0z2I.js → chunk-JWPE2WC7-pEyTjekV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-CqHrBJhp.js → chunk-KBJHAD2P-Bi5UopKd.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-D4MVae_f.js → chunk-RYQCIY6F-FkGFJxgQ.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CamBh0Ll.js → chunk-XXDRQBXY-D-7LGrV2.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DgDRdE1V.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DgDRdE1V.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-C3qheCuW.js → cose-bilkent-JH36ORCC-CCEDNDaX.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DQlWfkS7.js → cynefin-VYW2F7L2-DDd-t7fm.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BAxa9rFb.js → cynefinDiagram-MW4NZA55-LWe4Ikzr.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-YM09nLJw.js → dagre-VZM6K2ZE-BBCZScc-.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-I4W9EJj2.js → diagram-7IWD3JNH-B6liRVig.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-DH8eJ8Ff.js → diagram-B4RE2ZJO-DO_hIrpW.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-D0D0ntDW.js → diagram-LBJQPF4R-dmCy93uR.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BIISKq1s.js → diagram-Q27KOJAE-D6mFTIxP.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3--xyAWj0w.js → diagram-UB23O5K3-DCteVUfY.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-B3sYXLJ4.js → ebnfDiagram-BXEA7PRR-DTv530VK.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-zRSYWhJP.js → erDiagram-JOGREHBK-DCXDMxCr.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BUYlie03.js → flowDiagram-UKHOOZJN-BwEBLOJh.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DP4kzzrM.js → ganttDiagram-PKOTCBZU-Ck-Vymjm.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CAPoUxTF.js → gitGraphDiagram-DS77QQ5N-Hqs6X_3L.js} +1 -1
- package/dist/worker/console/static/assets/{index-CWhTyxvc.js → index-BmMi-Bve.js} +71 -71
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BXPfjekV.js → infoDiagram-6WML65LV-Cjc6M9eg.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-aOI3pV6t.js → ishikawaDiagram-WSZJBQD7-CPchYZMl.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DAPZuvi4.js → journeyDiagram-NVQOT4AX-BABdJNNC.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-atwDzL3C.js → kanban-definition-27J2QSJJ-1oXhbM4j.js} +1 -1
- package/dist/worker/console/static/assets/{linear-D5E09qji.js → linear-PTmQ9LkV.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-CE4idY-s.js → mermaid.core-B6Lxil0V.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CbZxtPca.js → mindmap-definition-FAOFIHXS-BUqjNGEw.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-De-FYEte.js → pegDiagram-VL7TDLO6-8D1N-orJ.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-Bs0USobz.js → pieDiagram-7S7Q4E2Y-BlPMN9d9.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Bq3ZrGCJ.js → quadrantDiagram-CIZ2JOQS-BIL5YDes.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bk8ojyIj.js → railroadDiagram-AXF67PYL-Dqmil3ie.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CGhoyws1.js → requirementDiagram-LRYGKXZP-C_-Dp4mH.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-DmVSQs3b.js → sankeyDiagram-W5VNT64P-C0s5-bQS.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-C80h1u0Q.js → sequenceDiagram-SI44F4Z6-DiVCHPfi.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CcbksQVZ.js → sizeCapture-X5ZJPWSS-CxQ9WVvx.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-CFTTTQKj.js → stateDiagram-OKZ733FA-D9mFHA-Y.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BqXQxfFC.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-Cw7Jr7Tg.js → swimlanes-SLNWSIFB-DDnC5l-M.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CXwWNRJl.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CVcdTVa5.js → timeline-definition-Z64GVDOM-C8QySPC-.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BpLbmWp9.js → vennDiagram-T6HMQDX7-BeYdDiz6.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-6PXfbwj-.js → wardleyDiagram-T6FBY63Y-D8hjFWLH.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-Bjkcz4ih.js → xychartDiagram-ELKLHX3M-ClqG_JGK.js} +1 -1
- package/dist/worker/console/static/index.html +1 -1
- package/dist/worker/console/static-src/operator-chat/tools-catalog.js +21 -2
- package/dist/workflows/dag/dag-retry-schema.js +3 -0
- package/dist/workflows/dag/frontend-contract-facts.js +130 -0
- package/dist/workflows/dag/frontend-design-policy.js +100 -15
- package/dist/workflows/dag/frontend-implementation-contract.js +56 -8
- package/dist/workflows/dag/frontend-plan-render.js +13 -2
- package/dist/workflows/dag/frontend-risk.js +2 -0
- package/dist/workflows/dag/frontend-shadow-dual-write.js +17 -1
- package/dist/workflows/dag/frontend-shape.js +16 -6
- package/dist/workflows/dag/frontend-test-execution-evidence.js +48 -0
- package/dist/workflows/dag/frontend-typed-event-store.js +3 -0
- package/dist/workflows/dag/frontend-verification-trace.js +32 -0
- package/dist/workflows/dag/init-hybrid.js +16 -13
- package/dist/workflows/dag/node-execution.js +178 -88
- package/dist/workflows/dag/rerun-feedback.js +1 -0
- package/dist/workflows/dag/rerun-plan.js +90 -4
- package/dist/workflows/dag/rerun-run.js +7 -0
- package/dist/workflows/dag/retry-policy.js +16 -10
- package/dist/workflows/dag/runner-exit-diagnostics.js +125 -0
- package/dist/workflows/dag/runner.js +67 -7
- package/dist/workflows/dag/structured-output-repair.js +4 -1
- package/dist/workflows/dag/validate.js +10 -8
- package/dist/workflows/dag/workspace-checkpoint.js +66 -0
- package/docs/operations/README.md +1 -0
- package/docs/templates/agent-dag.schema.json +2 -2
- package/docs/templates/frontend-implementation-contract.schema.json +34 -2
- package/package.json +2 -3
- package/skills/frontend-plan/SKILL.md +14 -1
- package/skills/frontend-plan/references/decision-contract.md +84 -10
- package/skills/frontend-plan/references/design-decisions.md +32 -0
- package/dist/worker/console/static/assets/channel-DOsaUk1k.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D4-D49vY.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D4-D49vY.js +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-C4YCesmi.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-lN2U_DLl.js +0 -8
|
@@ -2559,8 +2559,8 @@ function resolveFrontendMockContextBlock(sources) {
|
|
|
2559
2559
|
if (mode === "not-required") {
|
|
2560
2560
|
parts.push("Generation-time evidence does not require Mock. The assessment must still use contract/scout evidence: select not-needed when Mock is intentionally skipped, or select a safe Mock strategy if project evidence supports one.");
|
|
2561
2561
|
if (frontendMockStrategyMustBeNotNeeded(sources)) {
|
|
2562
|
-
parts.push('Auto mode has no confirmed project Mock capability or no deterministic Mock verification command. The structured contract must set mockApi.strategy to "not-needed".
|
|
2563
|
-
parts.push('HARD CONSTRAINT (frozen at generation time): this DAG allows only mockApi.strategy "not-needed"; the prewrite gate rejects any other strategy. If project governance (openspec / ai_workspace / decision records, e.g. a DEC rule requiring native) demands Mock-backed verification, that is a generation-time contract gap, not a plan-revision defect: declare frontendMock.verifyCommands (or policy: "required") in task.json and regenerate the DAG.
|
|
2562
|
+
parts.push('Auto mode has no confirmed project Mock capability or no deterministic Mock verification command. The structured contract must set mockApi.strategy to "not-needed". Keep the real request path as the default, record any unproved backend behavior as Real Integration Gap, and do not add Mock files or dependencies within this run.');
|
|
2563
|
+
parts.push('HARD CONSTRAINT (frozen at generation time): this DAG allows only mockApi.strategy "not-needed"; the prewrite gate rejects any other strategy. If project governance (openspec / ai_workspace / decision records, e.g. a DEC rule requiring native) demands Mock-backed verification, that is a generation-time contract gap, not a plan-revision defect: declare frontendMock.verifyCommands (or policy: "required") in task.json and regenerate the DAG.');
|
|
2564
2564
|
}
|
|
2565
2565
|
}
|
|
2566
2566
|
if (mode === "blocked") {
|
|
@@ -3226,9 +3226,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
3226
3226
|
"Reference frozen commands ONLY by commandId (record_plan_verification_target entry.commandId). The runtime resolves mode and label; never invent a mode or type.",
|
|
3227
3227
|
...frontendVerifyDirectory.map((entry) => ` - ${entry.commandId} [${entry.mode}]: ${JSON.stringify(entry.label)}`),
|
|
3228
3228
|
`- Static command source: ${staticVerifyEvidence.commandSource}`,
|
|
3229
|
-
...staticVerifyEvidence.commandLabels.map((command) => ` - ${JSON.stringify(command)}`),
|
|
3230
3229
|
`- Behavior command source: ${behaviorVerifyEvidence.commandSource}`,
|
|
3231
|
-
...behaviorVerifyEvidence.commandLabels.map((command) => ` - ${JSON.stringify(command)}`),
|
|
3232
3230
|
].join("\n");
|
|
3233
3231
|
const advisories = [];
|
|
3234
3232
|
if (!hasDeclaredFrontendVerification &&
|
|
@@ -3344,13 +3342,16 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
3344
3342
|
allowedPaths: readOnlyPaths,
|
|
3345
3343
|
forbiddenPaths,
|
|
3346
3344
|
skills: FRONTEND_CONTRACT_SKILLS,
|
|
3347
|
-
outputContract: "Typed requirement facts plus a concise Markdown contract. Submit through the incremental typed tools record_requirement / record_constraint / record_evidence_expectation / record_handoff_intent / record_open_question / record_split_proposal / record_openspec_selection, then call finalize_contract exactly once.
|
|
3345
|
+
outputContract: "Typed requirement facts plus a concise Markdown contract. Submit through the incremental typed tools record_requirement / record_constraint / record_evidence_expectation / record_handoff_intent / record_open_question / record_split_proposal / record_ui_state / record_required_deliverables / record_openspec_selection, then call finalize_contract exactly once. record_requirement takes only the canonical ledger requirement id — the runtime owns the authoritative text, source spans, fragment bindings, and disposition. UI-visible or interactive requirements register a non-blocking frontend-test handoff intent, and any source-declared UI-state table is extracted verbatim through record_ui_state. End finalize_contract with a single contract disposition of ready | ready-with-assumptions | blocked. Cover scope, non-goals, acceptance criteria, UI states, target runtime environment, risks, and verification expectations; do not fix target files, components, or implementation methods as requirements. When OpenSpec candidates exist, classify only the ones you actually use: call record_openspec_selection once per required/relevant path; never enumerate irrelevant candidates (unmentioned defaults to irrelevant) and never emit a fenced selection JSON. No file writes.",
|
|
3348
3346
|
subtask_prompt: [
|
|
3349
3347
|
"OUTPUT BUDGET DISCIPLINE (hard requirement, extreme-environment safe): the provider output window is small — NEVER attempt to emit the whole contract in one response; a single large JSON dump will be truncated and rejected. Incremental submission through the typed tools is the ONLY supported output mode. Start submitting with the FIRST tool call: after each read, call record_requirement for the requirements you have already confirmed, one or a few per call. Every tool-call round MUST make progress by submitting at least one record_* fact. Do not re-read the same source file that is already materialized in this session; read each file at most once.",
|
|
3350
3348
|
"Read task source and produce a concise frontend implementation contract as typed requirement facts plus narrative Markdown.",
|
|
3351
|
-
"
|
|
3352
|
-
"
|
|
3349
|
+
"Confirm each requirement by the SAME id as the ledger canonical requirement it covers (sourceBinding.requirementIds, e.g. AC-001) — do NOT invent new REQ/BR prefixed ids for canonical requirements: the compiled contract must match the ledger canonical requirement ids exactly or schema validation rejects it (unknown requirement id). record_requirement takes ONLY the canonical id; the runtime commits the authoritative text and sourceFragmentIds from the frozen ledger. Never pass text/statement/sourceFragmentIds yourself — model rewrites and JSON-stringified fragment arrays are rejected.",
|
|
3350
|
+
"Requirement semantics, source spans, dispositions, and fragment bindings are ledger/runtime-owned. If a canonical requirement is genuinely blocked, say so in the Markdown contract narrative and finalize with the matching disposition instead of trying to encode it in the requirement fact.",
|
|
3353
3351
|
"Register evidence expectations for each requirement across static, behavior, Mock, and real integration as required | optional | not-applicable; required must follow from user requirements, task risk, or project governance, never from model convenience. For UI-visible or interactive requirements, register a non-blocking frontend-test handoff intent.",
|
|
3352
|
+
'Use record_evidence_expectation with {requirementId,evidence:{static,behavior,mock,"real-integration"}}; every lane is required | optional | not-applicable. Requirement text and provenance remain runtime-owned.',
|
|
3353
|
+
'Before finalize_contract ready, call record_required_deliverables once with the complete {items:[{path,requirementId,sourceFragmentId}]} inventory, or {items:[]} when no file delivery is mandatory. Interpret the original source, including lists and tables: allowedPaths/only-allowed-to-modify is permission, not an obligation; do not promote prohibited files, examples or references into deliverables. Paths must occur exactly in a frozen source fragment bound to that canonical requirement. Correct the whole inventory before finalizing if needed.',
|
|
3354
|
+
"Authoritative UI states: when the task source declares a UI-state table (state id / trigger / observable outcome), extract it VERBATIM through record_ui_state, one call per state, using the source's own state ids. The planner must bind these ids later — do not rename, merge, or invent states.",
|
|
3354
3355
|
"Cover scope, non-goals, acceptance criteria, UI states, target runtime environment, risks, and verification expectations. Do not fix target files, components, styling, or implementation methods as requirements; leave those to Scout and Plan.",
|
|
3355
3356
|
"If the task is too large for one bounded writer, record a task split proposal instead of silently widening scope.",
|
|
3356
3357
|
"End the contract with a single disposition: ready, ready-with-assumptions (bounded assumptions that do not change product behavior), or blocked.",
|
|
@@ -3391,7 +3392,10 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
3391
3392
|
depends_on: ["frontend-contract-pi", "frontend-scout-pi"],
|
|
3392
3393
|
role: "planner",
|
|
3393
3394
|
executor: "pi",
|
|
3394
|
-
|
|
3395
|
+
// Small topology has already proven a concentrated, no-remote scope;
|
|
3396
|
+
// keep its bounded plan on the LOW model tier. Standard/High retain
|
|
3397
|
+
// MED for broader contract-to-surface decisions.
|
|
3398
|
+
complexity: frontendTaskShape.shape === "small" ? "LOW" : "MED",
|
|
3395
3399
|
writePolicy: "read-only",
|
|
3396
3400
|
retryPolicy: FRONTEND_PLAN_LADDER_RETRY_POLICY,
|
|
3397
3401
|
allowedPaths: readOnlyPaths,
|
|
@@ -3408,6 +3412,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
3408
3412
|
"Record only: requirement-to-file/verification coverage; component/styling choices; applicable UI state and interaction behavior; data/Mock strategy; and a dependency policy or genuine evidence gap. Reuse Scout paths. If scope is missing, record a blocking gap instead of inventing a path.",
|
|
3409
3413
|
"Use the typed tool schemas as the field contract. Runtime owns schemaVersion, sourceBinding, riskLevel, targets.files, mockApi.productionDefaultOff, aliases, command allowlisting, path containment, and final validation; do not restate those rules or emit a full JSON contract.",
|
|
3410
3414
|
`Cover each frozen requirement ID exactly once: ${requirementIds.join(", ") || "(none)"}. Bind every verification target to a frozen commandId from the directory above plus a Scout-confirmed file. Behavior commands prove observable behavior: one target may cover multiple related requirementIds when one test behavior proves them together; do not mechanically create one target per requirement. A behavior target id is the stable machine trace token and its file must be a test file. Static commands are project-wide checks traced by file and command only.`,
|
|
3415
|
+
"UX vocabulary protocol: record_state_registry FIRST with the full global vocabulary — one stable kebab-case behavior-domain name per UI state/interaction (e.g. planner-task-edit, focus-queue-move), never one name per AC number and never a rename of an already-recorded concept. Coverage slices by requirement; UX does not. Then record_state_flow entries whose names all come from that registry; uiState names must use the contract's declared authoritative ids (declaredUiStates in the plan input) when present. Retry attempts see committedUx in this input — reuse those exact names. Components: one choice may cover many state/interaction ids via covers; reuse-existing requires evidencePath naming an existing repo file (greenfield must be decision=new).",
|
|
3411
3416
|
...(requiresOpenspecClassification ? ["When a component choice uses an OpenSpec selection, cite that selection; otherwise do not classify unrelated candidates."] : []),
|
|
3412
3417
|
"Call finalize_plan exactly once after the necessary typed facts. Return no Markdown narrative.",
|
|
3413
3418
|
"TOOL-ONLY PLAN: Do not read Contract/Scout stdout, task sources, or Scout-confirmed target files. Contract and Scout already own evidence discovery; use the injected upstream facts, record a genuine evidence gap when those facts are insufficient, and start committing record_* facts immediately. For decision=new, pass sourceRequirementIds to record_component_choice; runtime derives the exact PRD citation from the frozen ledger.",
|
|
@@ -3499,18 +3504,16 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
3499
3504
|
"Your authoritative terminal verdict is exactly one committed typed tool call: approve_design or request_design_changes. Call exactly one of them; after calling one, do not call the other.",
|
|
3500
3505
|
"request_design_changes must carry a typed issueCategory, at least one evidenceRef, and non-empty findings.",
|
|
3501
3506
|
"Your verdict is consumed as deterministic data input by frontend-writer-admission-shell. approve_design permits admission; request_design_changes blocks writer admission until a recovery plan incorporates every Critical/Important finding.",
|
|
3502
|
-
"Request design changes when the Mock strategy is
|
|
3507
|
+
"Request design changes when the Mock strategy is blocked, missing, unsupported by repository evidence, inconsistent with the API contract, outside authorized paths/dependencies, unable to prove production-default-off behavior with the fixed production/default-real-path static check, or missing deterministic behavior verification for a declared behavior target or selected Mock strategy. Mock strategies require Mock-backed evidence; a static-only contract is allowed only when every verification target is static and maps to a declared static entrypoint; not-needed requires applicable real/no-remote behavior evidence unless auto mode explicitly skipped Mock because no project Mock capability exists, in which case the plan must preserve the real request path and record the Real Integration Gap.",
|
|
3503
3508
|
"Also request design changes for missing applicable UI states, unsupported dependency additions, design-system drift without reason, weak interaction coverage, broad scope, inline fake data, schema drift, or missing deterministic verification commands.",
|
|
3504
|
-
"Component selection conformance is a hard blocking condition: request_design_changes when the frontend spec (component/theme/rule.components bucket) already defines a component for a purpose but the plan selects another or self-invents one without a declared deviation; when uiComponentChoices is missing/empty for UI-visible work while the frozen component/theme bucket is non-empty; when a decision=specified specReference.path is missing a ledger OpenSpec reference or successful read event; or when a decision=new component lacks a traceable task-source/PRD specReference. A PRD reference for decision=new is not an OpenSpec citation and must not be rejected merely for lacking an OpenSpec read event.",
|
|
3505
|
-
"
|
|
3506
|
-
"You must NOT make authoritative assertions about the execution result of frozen verification commands (typecheck/test/build/lint/etc.). Predicting that a command will necessarily pass or fail, or declaring an acceptance criterion unreachable on that basis, is out of your authority: command results are deterministically established by frontend-verify-shell. Any concern about verification feasibility must be recorded only as a non-blocking verification concern in findings (severity must not be Critical, and it must never be the sole fatal basis for request_design_changes). Only semantic design defects (component selection, state flow, interaction contract, or conflicts with the specification) may be Critical. A pure command-will-fail prediction must not be classified as contract-requirement-gap.",
|
|
3509
|
+
"Component selection conformance is a hard blocking condition: request_design_changes when the frontend spec (component/theme/rule.components bucket) already defines a component for a purpose but the plan selects another or self-invents one without a declared deviation; when uiComponentChoices is missing/empty for UI-visible work while the frozen component/theme bucket is non-empty; when a decision=specified specReference.path is missing a ledger OpenSpec reference or successful read event; or when a decision=new component lacks a traceable task-source/PRD specReference. A PRD reference for decision=new is not an OpenSpec citation and must not be rejected merely for lacking an OpenSpec read event. For uiComponentChoices, purpose is the stable coverage key and must match interaction.name or uiState.name; responsibility is expressed by the matched expectedBehavior plus rationale, and you must not reject it merely for matching an interaction or component identifier.",
|
|
3510
|
+
"You must NOT make authoritative assertions about the execution result of frozen verification commands: command results are deterministically established by frontend-verify-shell. Record a verification-feasibility concern only as a non-blocking finding (severity must not be Critical, and it must never be the sole fatal basis for request_design_changes). Only semantic design defects (component selection, state flow, interaction contract, or conflicts with the specification) may be Critical; a pure command-will-fail prediction must not be classified as contract-requirement-gap.",
|
|
3507
3511
|
"Read-only: do not modify repository files.",
|
|
3508
3512
|
"LARGE-FILE AUDIT (avoid full reads): style/theme audit files can be large (e.g. styles.css is often hundreds of KB). Prefer grep to locate the exact rules/variables you must verify (e.g. grep the oc- class, is-* modifier, or --oc- theme variables with their line numbers), then read only the narrow line range when surrounding context is needed. Do not read a large style/test file in full — a single full read can exhaust the read budget and fail the attempt.",
|
|
3509
3513
|
"Canonical contract reading: frontend-design-policy-shell prints absolute paths for Contract, Contract index, and the non-blocking Capacity diagnostic. Read the capacity diagnostic first. When it recommends full-contract, read the exact Contract path. When it recommends indexed-sections, read the Contract index and its hash-bound section files instead of opening the full contract. Never resolve a bare contracts/... path against the repository root or hunt for substitutes. Implementation target files inside the writeSet are created later by the implement node: do not read them and do not treat their absence as a design defect.",
|
|
3510
3514
|
fixedVerificationContext,
|
|
3511
3515
|
sourceContexts.designReview,
|
|
3512
3516
|
scopedOpenspecContext,
|
|
3513
|
-
frontendContractFieldSummary,
|
|
3514
3517
|
mockContextBlock,
|
|
3515
3518
|
].join("\n\n"),
|
|
3516
3519
|
},
|
|
@@ -13,7 +13,7 @@ import { buildDagNodePromptEnvelope, formatConvergenceFeedbackBlock, } from "./p
|
|
|
13
13
|
import { persistLongNodeOutputArtifacts } from "./upstream-artifacts.js";
|
|
14
14
|
import { materializeDeclaredArtifactFacts } from "./artifact-bindings.js";
|
|
15
15
|
import { buildOutputLimitRecoverySection, loadBackendTestWriterProgressForRetry, } from "./backend-test-writer-completeness.js";
|
|
16
|
-
import { computeBackoffDelayMs, isCanonicalFinalVerifyShellRetryCandidate, isRetryablePiFailureCategory, isSafeReadOnlyPiRetryCandidate, isTargetTemplateTransientRetryNode, isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, projectFrontendNodeProtocolFailureReason, resolveFrontendPlanBackupRoute, resolveFrontendPlanRetryStep, VERIFY_SHELL_RETRY_POLICY, } from "./retry-policy.js";
|
|
16
|
+
import { computeBackoffDelayMs, isCanonicalFinalVerifyShellRetryCandidate, isRetryablePiFailureCategory, isSafeReadOnlyPiRetryCandidate, isTargetTemplateTransientRetryNode, isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, OUTPUT_LIMIT_RETRY_CATEGORY, projectFrontendNodeProtocolFailureReason, resolveFrontendPlanBackupRoute, resolveFrontendPlanRetryStep, VERIFY_SHELL_RETRY_POLICY, } from "./retry-policy.js";
|
|
17
17
|
import { applyNodeActivity, evaluateNodeLiveness, resolveLivenessPolicy, } from "./liveness-policy.js";
|
|
18
18
|
import { buildProtocolRetryInstruction, normalizeReviewVerdictAfterRetries, parseJsonReviewVerdict, validateOutputProtocol, } from "./output-protocol.js";
|
|
19
19
|
import { getStructuredContractValidator } from "./contract-output-registry.js";
|
|
@@ -21,6 +21,7 @@ import "./contract-validator-registrations.js";
|
|
|
21
21
|
import { computeNormalizedFailureFingerprint } from "./frontend-recovery-lineage.js";
|
|
22
22
|
import { parseLedgerJson } from "../../task/source-prepare/ledger.js";
|
|
23
23
|
import { readTypedEventStoreFromJsonl } from "./frontend-typed-event-store.js";
|
|
24
|
+
import { collectCanonicalStateFlowNames, extractRequiredDeliverablePaths, resolveFrontendContractRequirements, } from "./frontend-contract-facts.js";
|
|
24
25
|
import { allowedRepairReadPaths, auditRepairAttemptToolUse, buildStructuredOutputRepairPrompt, freezeStructuredOutputRepairContext, hasNonEmptyStructuredCandidate, isFrontendStructuredRepairSchemaId, isStructuredRepairableFailureCategory, persistStructuredAttemptRaw, sessionEventsByteLength, GOVERNANCE_BLOCKED_CATEGORY, STRUCTURED_REPAIR_EXHAUSTED_CATEGORY, } from "./structured-output-repair.js";
|
|
25
26
|
import { readFrontendCanonicalCandidate } from "./frontend-implementation-contract.js";
|
|
26
27
|
import { writeDagNodeJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
|
|
@@ -256,6 +257,9 @@ function frontendPlanValidationRetryGuidance(reason) {
|
|
|
256
257
|
if (/verificationTargets.*uiStates|uiStates.*verificationTargets/i.test(reason)) {
|
|
257
258
|
guidance.push("For this retry, provide verificationTargets[].uiStates as an array; use [] when the target has no named UI state.");
|
|
258
259
|
}
|
|
260
|
+
if (/duplicate|already recorded/i.test(reason)) {
|
|
261
|
+
guidance.push("For this retry, re-commit the corrected entry with the same id and replace=true; the ledger compiles the latest submission as the full replacement. A duplicate receipt is a correction invitation, not a prohibition, and backfilling a cited fact's reverse reference in-node does not re-derive committed facts.");
|
|
262
|
+
}
|
|
259
263
|
return guidance;
|
|
260
264
|
}
|
|
261
265
|
function compactRetryText(text, maxChars) {
|
|
@@ -325,6 +329,8 @@ const FRONTEND_CONTRACT_RECORD_TOOL_NAMES_LOCAL = new Set([
|
|
|
325
329
|
"record_handoff_intent",
|
|
326
330
|
"record_open_question",
|
|
327
331
|
"record_split_proposal",
|
|
332
|
+
"record_ui_state",
|
|
333
|
+
"record_required_deliverables",
|
|
328
334
|
]);
|
|
329
335
|
async function countContractRecordSubmissions(runDir, nodeId) {
|
|
330
336
|
const eventsPath = path.join(runDir, nodeId, "session-events.jsonl");
|
|
@@ -553,8 +559,9 @@ export function renderFrontendPlanInputContext(input) {
|
|
|
553
559
|
: []);
|
|
554
560
|
const contractFacts = committedFacts(input.contractRecords);
|
|
555
561
|
const scoutFacts = committedFacts(input.scoutRecords);
|
|
556
|
-
const
|
|
557
|
-
|
|
562
|
+
const requirementFacts = resolveFrontendContractRequirements(contractFacts);
|
|
563
|
+
const requiredDeliverables = extractRequiredDeliverablePaths(contractFacts);
|
|
564
|
+
const requirements = requirementFacts
|
|
558
565
|
.map((fact) => ({
|
|
559
566
|
id: planInputText(fact.id, 80),
|
|
560
567
|
text: planInputText(fact.text),
|
|
@@ -580,19 +587,28 @@ export function renderFrontendPlanInputContext(input) {
|
|
|
580
587
|
paths: fact.paths,
|
|
581
588
|
conflicts: fact.conflicts,
|
|
582
589
|
}));
|
|
590
|
+
// Authoritative UI states (contract-declared): the source's UI-state table
|
|
591
|
+
// extracted by the contract node. The planner binds these ids instead of
|
|
592
|
+
// inventing list-visibility variants.
|
|
593
|
+
const declaredUiStates = contractFacts
|
|
594
|
+
.filter((fact) => fact.kind === "ui-state-declaration" && fact.origin === "contract")
|
|
595
|
+
.map((fact) => ({
|
|
596
|
+
id: planInputText(fact.id, 80),
|
|
597
|
+
trigger: planInputText(fact.trigger),
|
|
598
|
+
observableOutcome: planInputText(fact.observableOutcome),
|
|
599
|
+
}))
|
|
600
|
+
.filter((state) => state.id !== undefined);
|
|
601
|
+
// Replay registry edits and state-flow removals/additions in commit order.
|
|
602
|
+
const committedUxNames = collectCanonicalStateFlowNames(input.planRecords ?? [], true);
|
|
603
|
+
const committedUiStateNames = [...committedUxNames.uiStateNames];
|
|
604
|
+
const committedInteractionNames = [...committedUxNames.interactionNames];
|
|
583
605
|
// Reviewer-rubric scaffold: the design reviewer re-runs the design-policy
|
|
584
606
|
// checks on the committed facts, so publish the checklist to the producer.
|
|
585
607
|
// Requirements whose contract evidence expects behavioural verification are
|
|
586
608
|
// enumerated explicitly — those are the slots the reviewer finds missing
|
|
587
609
|
// when the plan models interactions ad hoc (r8/r9 findings).
|
|
588
|
-
const behaviorRequiredIds =
|
|
589
|
-
.filter((requirement) =>
|
|
590
|
-
const fact = contractFacts.find((candidate) => candidate.kind === "requirement" &&
|
|
591
|
-
candidate.origin === "contract" &&
|
|
592
|
-
candidate.id === requirement.id);
|
|
593
|
-
const evidence = fact?.evidence;
|
|
594
|
-
return evidence?.behavior === "required";
|
|
595
|
-
})
|
|
610
|
+
const behaviorRequiredIds = requirementFacts
|
|
611
|
+
.filter((requirement) => requirement.evidence.behavior === "required")
|
|
596
612
|
.map((requirement) => requirement.id);
|
|
597
613
|
const serializeAtCap = (cap) => JSON.stringify({
|
|
598
614
|
requirements: requirements.map((requirement) => ({
|
|
@@ -600,6 +616,7 @@ export function renderFrontendPlanInputContext(input) {
|
|
|
600
616
|
text: planInputText(requirement.text, cap.text),
|
|
601
617
|
sourceFragmentIds: planInputStrings(requirement.sourceFragmentIds).slice(0, cap.array),
|
|
602
618
|
})),
|
|
619
|
+
requiredDeliverables,
|
|
603
620
|
targetSurface: targetSurface.map((surface) => ({
|
|
604
621
|
completeness: planInputText(surface.completeness, 32),
|
|
605
622
|
entrypoint: planInputText(surface.entrypoint, cap.text),
|
|
@@ -615,6 +632,18 @@ export function renderFrontendPlanInputContext(input) {
|
|
|
615
632
|
paths: planInputStrings(evidence.paths).slice(0, cap.array),
|
|
616
633
|
conflicts: planInputStrings(evidence.conflicts).slice(0, cap.array),
|
|
617
634
|
})),
|
|
635
|
+
declaredUiStates: declaredUiStates.map((state) => ({
|
|
636
|
+
id: state.id,
|
|
637
|
+
trigger: planInputText(state.trigger, cap.text),
|
|
638
|
+
observableOutcome: planInputText(state.observableOutcome, cap.text),
|
|
639
|
+
})),
|
|
640
|
+
committedUx: committedUiStateNames.length > 0 ||
|
|
641
|
+
committedInteractionNames.length > 0
|
|
642
|
+
? {
|
|
643
|
+
uiStateNames: committedUiStateNames,
|
|
644
|
+
interactionNames: committedInteractionNames,
|
|
645
|
+
}
|
|
646
|
+
: undefined,
|
|
618
647
|
});
|
|
619
648
|
let serialized = serializeAtCap(FRONTEND_PLAN_INPUT_CAP_LADDER[0]);
|
|
620
649
|
for (const cap of FRONTEND_PLAN_INPUT_CAP_LADDER.slice(1)) {
|
|
@@ -628,6 +657,7 @@ export function renderFrontendPlanInputContext(input) {
|
|
|
628
657
|
// to the minimum, and declare the degradation instead of corrupting JSON.
|
|
629
658
|
let fallback = {
|
|
630
659
|
degraded: "requirement-texts-truncated",
|
|
660
|
+
requiredDeliverables,
|
|
631
661
|
requirements: requirements.map((requirement) => ({
|
|
632
662
|
id: requirement.id,
|
|
633
663
|
text: "(truncated)",
|
|
@@ -652,20 +682,20 @@ export function renderFrontendPlanInputContext(input) {
|
|
|
652
682
|
serialized = bounded;
|
|
653
683
|
}
|
|
654
684
|
const checklistLines = [
|
|
655
|
-
"1.
|
|
656
|
-
"2. Every
|
|
657
|
-
"3. Every
|
|
658
|
-
"4.
|
|
685
|
+
"1. Record the GLOBAL UX vocabulary with record_state_registry BEFORE any record_state_flow: one stable behavior-domain name per state/interaction (e.g. planner-task-edit), never one name per AC number, and never a rename of an already-recorded concept. Coverage slices by AC; UX does not.",
|
|
686
|
+
"2. Every recorded interaction/uiState name must be in that registry, and uiState names must use the contract's declared authoritative ids (declaredUiStates below) when present.",
|
|
687
|
+
"3. Every interaction and every applicable UI state must be covered by a uiComponentChoices entry: its purpose equals the name, or the choice lists the name in covers (one choice may cover many ids). A reuse-existing decision without evidencePath is rejected; stylingStrategy alone covers nothing.",
|
|
688
|
+
"4. Every requirement marked (behavior) below needs modelled interactions plus at least one verification target that references it.",
|
|
659
689
|
"5. Verification targets may only reference UI states and requirements you actually recorded (the record_* tools reject unknown references).",
|
|
660
690
|
`Requirements requiring behavioural coverage: ${behaviorRequiredIds.length > 0 ? behaviorRequiredIds.join(", ") : "(none)"}`,
|
|
661
|
-
"6. A decision=new component must declare sourceRequirementIds and pass sourceFragmentId for the frozen PRD fragment whose section matches the component's purpose — the runtime validates that binding and derives specReference (path/section/line); the reviewer checks purpose↔citation consistency.",
|
|
691
|
+
"6. A decision=new component must declare sourceRequirementIds and pass sourceFragmentId for the frozen PRD fragment whose section matches the component's purpose — the runtime validates that binding and derives specReference (path/section/line); the reviewer checks purpose↔citation consistency. A decision=reuse-existing component must pass evidencePath pointing at the existing repo file that proves the reuse.",
|
|
662
692
|
...[...input.componentSourceCitations ?? []]
|
|
663
693
|
.filter(([id]) => behaviorRequiredIds.includes(id))
|
|
664
694
|
.flatMap(([id, citations]) => citations.map((citation) => ` ${id} + ${citation.fragmentId} → ${citation.section}${citation.line ? ` (line ${citation.line})` : ""}`)),
|
|
665
695
|
];
|
|
666
696
|
return [
|
|
667
697
|
"<frontend_plan_input>",
|
|
668
|
-
"Committed Contract/Scout facts, compiled by the runner. Treat them as the complete planning evidence.",
|
|
698
|
+
"Committed Contract/Scout facts, compiled by the runner. Treat them as the complete planning evidence. declaredUiStates are the authoritative UI states from the task source; committedUx (retry attempts) is the UX vocabulary already recorded — reuse those names, never re-invent them.",
|
|
669
699
|
serialized,
|
|
670
700
|
"Do not read upstream artifacts, task sources, or repository files. If this input cannot support a decision, record a genuine evidence gap.",
|
|
671
701
|
"</frontend_plan_input>",
|
|
@@ -692,6 +722,16 @@ export async function buildFrontendPlanInputContext(runDir, componentSourceCitat
|
|
|
692
722
|
// forbids reading anything.
|
|
693
723
|
throw new Error(`frontend-plan-input-unavailable: cannot read committed typed facts (${error instanceof Error ? error.message : String(error)})`);
|
|
694
724
|
}
|
|
725
|
+
// Retry-attempt continuity (UX slice visibility): the plan node's own
|
|
726
|
+
// committed facts are absent on the first attempt and present on retries;
|
|
727
|
+
// a missing file is normal there, not a broken pipeline.
|
|
728
|
+
let planRecords = [];
|
|
729
|
+
try {
|
|
730
|
+
planRecords = await readTypedEventStoreFromJsonl(path.join(runDir, "frontend-plan-pi", "plan-typed-facts.jsonl"));
|
|
731
|
+
}
|
|
732
|
+
catch {
|
|
733
|
+
planRecords = [];
|
|
734
|
+
}
|
|
695
735
|
const committedCount = [...contractRecords, ...scoutRecords].filter((record) => record.phase === "committed").length;
|
|
696
736
|
if (committedCount === 0) {
|
|
697
737
|
throw new Error(`frontend-plan-input-unavailable: no committed Contract/Scout facts in ${contractFactsPath} / ${scoutFactsPath}`);
|
|
@@ -699,6 +739,7 @@ export async function buildFrontendPlanInputContext(runDir, componentSourceCitat
|
|
|
699
739
|
return renderFrontendPlanInputContext({
|
|
700
740
|
contractRecords,
|
|
701
741
|
scoutRecords,
|
|
742
|
+
planRecords,
|
|
702
743
|
componentSourceCitations,
|
|
703
744
|
});
|
|
704
745
|
}
|
|
@@ -736,6 +777,87 @@ export function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFail
|
|
|
736
777
|
"</retry_instruction>",
|
|
737
778
|
].join("\n");
|
|
738
779
|
}
|
|
780
|
+
// Repair-category guidance must outrank the retry ladder position: the
|
|
781
|
+
// ladder advances monotonically on transport failures (e.g. length →
|
|
782
|
+
// compact-terminal-first), and its "keep the committed ledger intact"
|
|
783
|
+
// instruction directly contradicts the repair action for invalid-output /
|
|
784
|
+
// truncated ledger facts (re-commit corrected record_* facts). When both
|
|
785
|
+
// apply, the model receives the repair instruction, not the rung script.
|
|
786
|
+
if (previousFailureCategory === "invalid-output" &&
|
|
787
|
+
task.structuredContractOutput &&
|
|
788
|
+
previousProtocolReason) {
|
|
789
|
+
// The frontend plan node's compile authority is the committed typed
|
|
790
|
+
// ledger, not a fenced JSON text artifact: its retry guidance must
|
|
791
|
+
// direct the model to re-commit corrected record_* facts and
|
|
792
|
+
// finalize_plan. The legacy full-contract JSON guidance below applies
|
|
793
|
+
// only to nodes whose authority is still a text contract artifact.
|
|
794
|
+
if (task.structuredContractOutput.schemaId ===
|
|
795
|
+
"frontend-implementation-contract-plan-patch-v1") {
|
|
796
|
+
const splitGuidance = /write-set-too-large|split the task/.test(previousProtocolReason ?? "")
|
|
797
|
+
? [
|
|
798
|
+
"",
|
|
799
|
+
"The writeSet is too large for one implement node. Preserve the complete requirement and file coverage. This needs a Contract-level task split, not a Plan formatting repair. Report the write-set-too-large finding and the affected paths for the controller to resume Contract/split orchestration. Plan cannot change protected targets.files or call Contract-only tools; do not invent a split tool or discard required files.",
|
|
800
|
+
]
|
|
801
|
+
: [];
|
|
802
|
+
return [
|
|
803
|
+
basePrompt,
|
|
804
|
+
"",
|
|
805
|
+
"<retry_instruction>",
|
|
806
|
+
"Previous plan ledger facts failed canonical contract validation:",
|
|
807
|
+
previousProtocolReason,
|
|
808
|
+
"Fix the reported violations by re-committing corrected record_* facts and calling finalize_plan exactly once. The committed typed ledger is the only compile authority.",
|
|
809
|
+
"Do not emit a fenced JSON contract, protected skeleton fields, or an openspec-citations block; narrative JSON is not validated and wastes the output budget.",
|
|
810
|
+
...frontendPlanValidationRetryGuidance(previousProtocolReason),
|
|
811
|
+
...splitGuidance,
|
|
812
|
+
"</retry_instruction>",
|
|
813
|
+
].join("\n");
|
|
814
|
+
}
|
|
815
|
+
return [
|
|
816
|
+
basePrompt,
|
|
817
|
+
"",
|
|
818
|
+
"<retry_instruction>",
|
|
819
|
+
"Previous attempt produced an invalid frontend implementation contract:",
|
|
820
|
+
previousProtocolReason,
|
|
821
|
+
...frontendStructuredArtifactRetryGuidance(task.structuredContractOutput.schemaId),
|
|
822
|
+
"Fix every reported field violation: do not emit null for optional fields, do not misspell field names, and match the required types exactly.",
|
|
823
|
+
"</retry_instruction>",
|
|
824
|
+
].join("\n");
|
|
825
|
+
}
|
|
826
|
+
if (previousFailureCategory === "invalid-output" &&
|
|
827
|
+
task.id === "frontend-scout-pi") {
|
|
828
|
+
return [
|
|
829
|
+
basePrompt,
|
|
830
|
+
"",
|
|
831
|
+
"<retry_instruction>",
|
|
832
|
+
"The previous Scout attempt did not commit a complete, runtime-evidenced target surface.",
|
|
833
|
+
"Search the repository only as needed to establish the real entrypoint, implementation ownership, and applicable test path. Commit record_target_surface with completeness=complete and unresolvedPaths=[] only after at least one named target has fresh runtime evidence, unless the task source explicitly declares a greenfield target: in that case every future path must be source-declared by the runtime-enriched fact. If ownership truly cannot be established, record completeness=blocked with each unresolved path; do not make Plan discover it.",
|
|
834
|
+
"</retry_instruction>",
|
|
835
|
+
].join("\n");
|
|
836
|
+
}
|
|
837
|
+
if (previousFailureCategory === "structured-output-truncated" &&
|
|
838
|
+
task.structuredContractOutput) {
|
|
839
|
+
if (task.structuredContractOutput.schemaId ===
|
|
840
|
+
"frontend-implementation-contract-plan-patch-v1") {
|
|
841
|
+
return [
|
|
842
|
+
basePrompt,
|
|
843
|
+
"",
|
|
844
|
+
"<retry_instruction>",
|
|
845
|
+
"Previous attempt was truncated by the provider (stopReason=length) before the plan facts were fully committed.",
|
|
846
|
+
"Re-commit the missing record_* facts and call finalize_plan exactly once; the committed typed ledger is the only compile authority. Do NOT re-read contract/scout outputs or source files — use the facts already in context. Commit record_* facts one tool call per message, then finalize_plan immediately.",
|
|
847
|
+
"Do not emit a fenced JSON contract, protected skeleton fields, or an openspec-citations block; narrative JSON is not validated and wastes the output budget.",
|
|
848
|
+
"</retry_instruction>",
|
|
849
|
+
].join("\n");
|
|
850
|
+
}
|
|
851
|
+
return [
|
|
852
|
+
basePrompt,
|
|
853
|
+
"",
|
|
854
|
+
"<retry_instruction>",
|
|
855
|
+
"Previous attempt was truncated by the provider (stopReason=length) before the JSON contract was completed.",
|
|
856
|
+
...frontendStructuredArtifactRetryGuidance(task.structuredContractOutput.schemaId),
|
|
857
|
+
"The JSON artifact must be complete; omit evidence excerpts and duplicated upstream context.",
|
|
858
|
+
"</retry_instruction>",
|
|
859
|
+
].join("\n");
|
|
860
|
+
}
|
|
739
861
|
if (frontendPlanRetryStep === "compact-terminal-first") {
|
|
740
862
|
return [
|
|
741
863
|
basePrompt,
|
|
@@ -807,83 +929,25 @@ export function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFail
|
|
|
807
929
|
"</retry_instruction>",
|
|
808
930
|
].join("\n");
|
|
809
931
|
}
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
// finalize_plan. The legacy full-contract JSON guidance below applies
|
|
817
|
-
// only to nodes whose authority is still a text contract artifact.
|
|
818
|
-
if (task.structuredContractOutput.schemaId ===
|
|
819
|
-
"frontend-implementation-contract-plan-patch-v1") {
|
|
820
|
-
const splitGuidance = /write-set-too-large|split the task/.test(previousProtocolReason ?? "")
|
|
821
|
-
? [
|
|
822
|
-
"",
|
|
823
|
-
"The writeSet is too large for one implement node. Do NOT delete implementation files to squeeze under the limit — that drops required work. Split the task via record_split_proposal (or narrow targets.files to a genuine subset) so each implement node stays bounded; the full file set must remain covered across the split.",
|
|
824
|
-
]
|
|
825
|
-
: [];
|
|
826
|
-
return [
|
|
827
|
-
basePrompt,
|
|
828
|
-
"",
|
|
829
|
-
"<retry_instruction>",
|
|
830
|
-
"Previous plan ledger facts failed canonical contract validation:",
|
|
831
|
-
previousProtocolReason,
|
|
832
|
-
"Fix the reported violations by re-committing corrected record_* facts and calling finalize_plan exactly once. The committed typed ledger is the only compile authority.",
|
|
833
|
-
"Do not emit a fenced JSON contract, protected skeleton fields, or an openspec-citations block; narrative JSON is not validated and wastes the output budget.",
|
|
834
|
-
...frontendPlanValidationRetryGuidance(previousProtocolReason),
|
|
835
|
-
...splitGuidance,
|
|
836
|
-
"</retry_instruction>",
|
|
837
|
-
].join("\n");
|
|
838
|
-
}
|
|
839
|
-
return [
|
|
840
|
-
basePrompt,
|
|
841
|
-
"",
|
|
842
|
-
"<retry_instruction>",
|
|
843
|
-
"Previous attempt produced an invalid frontend implementation contract:",
|
|
844
|
-
previousProtocolReason,
|
|
845
|
-
...frontendStructuredArtifactRetryGuidance(task.structuredContractOutput.schemaId),
|
|
846
|
-
"Fix every reported field violation: do not emit null for optional fields, do not misspell field names, and match the required types exactly.",
|
|
847
|
-
"</retry_instruction>",
|
|
848
|
-
].join("\n");
|
|
849
|
-
}
|
|
850
|
-
if (previousFailureCategory === "invalid-output" &&
|
|
851
|
-
task.id === "frontend-scout-pi") {
|
|
852
|
-
return [
|
|
853
|
-
basePrompt,
|
|
854
|
-
"",
|
|
855
|
-
"<retry_instruction>",
|
|
856
|
-
"The previous Scout attempt did not commit a complete, runtime-evidenced target surface.",
|
|
857
|
-
"Search the repository only as needed to establish the real entrypoint, implementation ownership, and applicable test path. Commit record_target_surface with completeness=complete and unresolvedPaths=[] only after at least one named target has fresh runtime evidence, unless the task source explicitly declares a greenfield target: in that case every future path must be source-declared by the runtime-enriched fact. If ownership truly cannot be established, record completeness=blocked with each unresolved path; do not make Plan discover it.",
|
|
858
|
-
"</retry_instruction>",
|
|
859
|
-
].join("\n");
|
|
860
|
-
}
|
|
861
|
-
if (previousFailureCategory === "structured-output-truncated" &&
|
|
862
|
-
task.structuredContractOutput) {
|
|
863
|
-
if (task.structuredContractOutput.schemaId ===
|
|
864
|
-
"frontend-implementation-contract-plan-patch-v1") {
|
|
865
|
-
return [
|
|
866
|
-
basePrompt,
|
|
867
|
-
"",
|
|
868
|
-
"<retry_instruction>",
|
|
869
|
-
"Previous attempt was truncated by the provider (stopReason=length) before the plan facts were fully committed.",
|
|
870
|
-
"Re-commit the missing record_* facts and call finalize_plan exactly once; the committed typed ledger is the only compile authority. Do NOT re-read contract/scout outputs or source files — use the facts already in context. Commit record_* facts one tool call per message, then finalize_plan immediately.",
|
|
871
|
-
"Do not emit a fenced JSON contract, protected skeleton fields, or an openspec-citations block; narrative JSON is not validated and wastes the output budget.",
|
|
872
|
-
"</retry_instruction>",
|
|
873
|
-
].join("\n");
|
|
874
|
-
}
|
|
932
|
+
// Generic output-limit fallback. Every branch above this one carries a
|
|
933
|
+
// more precise instruction for the same capacity signal (contract repair
|
|
934
|
+
// reasons, ladder rungs — compact-terminal-first is the reason-mandated
|
|
935
|
+
// rung for length-before-terminal —, protocol/review/read-burst repair),
|
|
936
|
+
// so output-limit must not shadow them.
|
|
937
|
+
if (previousFailureCategory === OUTPUT_LIMIT_RETRY_CATEGORY) {
|
|
875
938
|
return [
|
|
876
939
|
basePrompt,
|
|
877
940
|
"",
|
|
878
941
|
"<retry_instruction>",
|
|
879
|
-
"
|
|
880
|
-
|
|
881
|
-
"
|
|
942
|
+
"The previous turn ended with stopReason=length before the required output was complete. This is output-capacity truncation, not empty output.",
|
|
943
|
+
"Continue incrementally from already committed typed facts and current in-scope workspace files. Do not repeat completed discovery, decisions, facts, or writes.",
|
|
944
|
+
"Generate the smallest unfinished unit next, validate its required format immediately, repair any format error in this node, and only then continue to the next unfinished unit or terminal tool.",
|
|
945
|
+
"Keep prose minimal and finish the required terminal/output protocol as soon as the remaining work is valid.",
|
|
882
946
|
"</retry_instruction>",
|
|
883
947
|
].join("\n");
|
|
884
948
|
}
|
|
885
949
|
if (previousFailureCategory === "writer-empty-diff") {
|
|
886
|
-
const maxAttempts = task.retryPolicy?.maxAttempts ??
|
|
950
|
+
const maxAttempts = task.retryPolicy?.maxAttempts ?? 5;
|
|
887
951
|
// When a completeness progress exists for this writer, fold the concrete
|
|
888
952
|
// target paths into the empty-diff retry so the model does not guess and
|
|
889
953
|
// does not need to read a forbidden `.harness/**` evidence file.
|
|
@@ -911,7 +975,7 @@ export function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFail
|
|
|
911
975
|
].join("\n");
|
|
912
976
|
}
|
|
913
977
|
if (previousFailureCategory === "incomplete-write-set") {
|
|
914
|
-
const maxAttempts = task.retryPolicy?.maxAttempts ??
|
|
978
|
+
const maxAttempts = task.retryPolicy?.maxAttempts ?? 5;
|
|
915
979
|
const bindingOnly = task.id?.startsWith("generate-backend-md-case-") === true &&
|
|
916
980
|
(recoveryTargetPaths?.length ?? 0) === 1 &&
|
|
917
981
|
Object.values(recoveryDiagnostics ?? {}).flat().some((detail) => /(?:unclassified Test Points|duplicate Test Point bindings)/i.test(detail));
|
|
@@ -1727,6 +1791,32 @@ export async function executeDagNode(input) {
|
|
|
1727
1791
|
durationMs: 0,
|
|
1728
1792
|
};
|
|
1729
1793
|
}
|
|
1794
|
+
// stopReason=length on top of a bare empty-output verdict is a provider
|
|
1795
|
+
// capacity signal, not a true empty response: reroute it through the
|
|
1796
|
+
// dedicated output-limit retry path while keeping the raw category for
|
|
1797
|
+
// diagnostics. Bare empty-output and transport aliases (network,
|
|
1798
|
+
// nonzero-exit, unknown) are rerouted — categories that already carry a
|
|
1799
|
+
// precise repair instruction (output-too-large, invalid-output,
|
|
1800
|
+
// protocol-invalid, structured-output-truncated via the validators below,
|
|
1801
|
+
// writer categories) keep their classification so their exact retry
|
|
1802
|
+
// guidance still reaches the model.
|
|
1803
|
+
if (task.executor === "pi" &&
|
|
1804
|
+
!result.ok &&
|
|
1805
|
+
result.stopReason === "length" &&
|
|
1806
|
+
(result.failureCategory === undefined ||
|
|
1807
|
+
["empty-output", "network", "nonzero-exit", "unknown"].includes(result.failureCategory))) {
|
|
1808
|
+
result = {
|
|
1809
|
+
...result,
|
|
1810
|
+
rawFailureCategory: result.rawFailureCategory ?? result.failureCategory,
|
|
1811
|
+
failureCategory: OUTPUT_LIMIT_RETRY_CATEGORY,
|
|
1812
|
+
stderr: [
|
|
1813
|
+
result.stderr,
|
|
1814
|
+
"output-limit: stopReason=length; preserve completed work and retry only the unfinished output",
|
|
1815
|
+
]
|
|
1816
|
+
.filter(Boolean)
|
|
1817
|
+
.join("\n"),
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1730
1820
|
if (task.id === "generate-backend-md-plan-pi" &&
|
|
1731
1821
|
!result.ok &&
|
|
1732
1822
|
(result.failureCategory === "output-too-large" ||
|
|
@@ -1860,7 +1950,7 @@ export async function executeDagNode(input) {
|
|
|
1860
1950
|
!result.ok &&
|
|
1861
1951
|
retryPolicy !== undefined) {
|
|
1862
1952
|
const submissions = await countContractRecordSubmissions(runDir, nodeId);
|
|
1863
|
-
if (submissions === 0) {
|
|
1953
|
+
if (submissions === 0 && result.stopReason !== "length") {
|
|
1864
1954
|
result = {
|
|
1865
1955
|
...result,
|
|
1866
1956
|
failureCategory: "empty-output",
|