@tea-agent/loop-agent 0.39.0-beta.1 → 0.39.0-beta.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/AGENTS.md +4 -1
- package/CHANGELOG.md +187 -92
- package/README.md +9 -5
- package/bin/loop-agent.js +7 -3
- package/dist/application/task-lifecycle/recommendations.js +13 -2
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +16 -5
- package/dist/cli/program.js +16 -2
- package/dist/cli/update/notifier.js +2 -2
- package/dist/cli.js +2 -1
- package/dist/commands/dag-rerun.js +55 -1
- package/dist/commands/init-model-catalog.js +464 -0
- package/dist/commands/init-upgrade.js +265 -97
- package/dist/commands/init.js +429 -28
- package/dist/commands/inspect-next.js +8 -0
- package/dist/executors/dag-pi-executor.js +1645 -29
- package/dist/executors/pi-extension-resolver.js +233 -0
- package/dist/executors/pi-playwright-cli-tool.js +51 -12
- package/dist/executors/pi-sdk-executor.js +180 -33
- package/dist/executors/shell-executor.js +1150 -171
- package/dist/executors/shell-write-guard.js +14 -0
- package/dist/shared/dag-prompt-override.js +28 -0
- package/dist/shared/openspec-spec.js +70 -4
- package/dist/shared/operator/capabilities.js +32 -0
- package/dist/shared/playwright-cli-command-policy.js +15 -0
- package/dist/shared/update/console-notifier.js +100 -0
- package/dist/{cli → shared}/update/npm-client.js +41 -8
- package/dist/{cli → shared}/update/state.js +41 -13
- package/dist/task/config-types.js +19 -5
- package/dist/task/contract/project.js +3 -0
- package/dist/task/contract/schema.js +2 -1
- package/dist/task/frontend-project-capability.js +42 -16
- package/dist/task/source-prepare/parse-intent.js +24 -4
- package/dist/task/source-prepare/prepare.js +36 -0
- package/dist/worker/cli.js +31 -1
- package/dist/worker/console/chat/pi-console-config.js +81 -10
- package/dist/worker/console/chat/routes.js +1 -0
- package/dist/worker/console/dag-execution-receipt.js +33 -0
- 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/operation-store.js +226 -6
- package/dist/worker/console/operator-actions.js +263 -0
- package/dist/worker/console/operator-user-error.js +2 -2
- package/dist/worker/console/routes.js +43 -2
- package/dist/worker/console/security.js +63 -9
- package/dist/worker/console/server.js +151 -6
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4kVYweA.js → abnfDiagram-N423BO3Z-zsNyvGaH.js} +1 -1
- package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-BDjZ5kE1.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-BkgaujnN.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-CqxOJi0j.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-C2JHLdRi.js} +1 -1
- package/dist/worker/console/static/assets/channel-BuqaGKAT.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-DF_YSvOS.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-D3Ort5Vo.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-XfAfZeUC.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-SLBibhf5.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-Dz29Ye_f.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-DB2G4H8V.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-CIoGkxdu.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-CG78FGkn.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-ZDHAvXPd.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-BKkkC4VA.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DNdSHjH9.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DNdSHjH9.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-SposUZsO.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-BSCtQpD7.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-DgFHrw9l.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-BVBasYbj.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-6r_4rN_c.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-JmuaB41P.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-xXvAY3sk.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-D2B3lY2o.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-tK54tMEX.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-CNtELzV-.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-C1xKMKcW.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-CZ7dlPMz.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-BqGb42Xe.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-D_BlXEtt.js} +1 -1
- package/dist/worker/console/static/assets/{index-DTZOKgAn.js → index-D5zGX6fG.js} +68 -68
- package/dist/worker/console/static/assets/index-DBbhESQ_.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-Vce2vmIe.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-DDwVBAvI.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-y_Ew-wv4.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-CjXJFp1K.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-1itMst1Q.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-C-D3ZRUa.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-BiYywmZc.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-l2fydNWA.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-Dz0hNbYW.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-BujI_mLZ.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-pWe84beb.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-ByYu2frg.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-CoPnilFP.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-1Md2LnS0.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-DqqqAoSH.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-C4sGkf--.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8rmop_9.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-ByxJrUzW.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B0ky-kBM.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-D8MLgM0O.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-DnBis-wZ.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-uuyYT7UO.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-BjgabhwK.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/mutation-gate.js +1 -1
- package/dist/worker/loop-agent/loop-agent-client.js +7 -3
- package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
- package/dist/worker/observe/node-input.js +74 -3
- package/dist/worker/observe/node-process.js +377 -0
- package/dist/worker/observe/routes.js +36 -3
- package/dist/worker/observe/shell-handler-keys.js +34 -0
- package/dist/worker/observe/static/api.js +66 -0
- package/dist/worker/observe/static/constants.js +8 -1
- package/dist/worker/observe/static/dag-history-labels.js +4 -0
- package/dist/worker/observe/static/format.js +23 -0
- package/dist/worker/observe/static/markdown-render.js +20 -1
- package/dist/worker/observe/static/operator-chrome.js +4 -0
- package/dist/worker/observe/static/state.js +13 -2
- package/dist/worker/observe/static/styles.css +73 -0
- package/dist/worker/observe/static/views/dag-inspector.js +612 -8
- package/dist/worker/observe/static/views/dag.js +8 -1
- package/dist/worker/observe/static/views/failures.js +5 -2
- package/dist/worker/observe/static/views/session-timeline.js +78 -9
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +20 -2
- package/dist/workflows/dag/contract-output-registry.js +15 -0
- package/dist/workflows/dag/contract-validator-registrations.js +1 -2
- package/dist/workflows/dag/failure-category.js +12 -0
- package/dist/workflows/dag/frontend-closeout.js +221 -0
- package/dist/workflows/dag/frontend-design-policy.js +347 -0
- package/dist/workflows/dag/frontend-human-decision.js +182 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +378 -150
- package/dist/workflows/dag/frontend-prewrite-gate.js +258 -393
- 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 +201 -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 +7 -424
- package/dist/workflows/dag/frontend-review-context.js +195 -8
- package/dist/workflows/dag/frontend-risk.js +15 -2
- package/dist/workflows/dag/frontend-shadow-dual-write.js +665 -0
- package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
- package/dist/workflows/dag/frontend-shape-facts.js +409 -0
- package/dist/workflows/dag/frontend-shape.js +425 -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-test-case-checklist.js +26 -2
- package/dist/workflows/dag/frontend-test-case-manifest.js +5 -0
- package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
- package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
- package/dist/workflows/dag/frontend-test-markdown.js +61 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +160 -28
- package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
- package/dist/workflows/dag/frontend-typed-event-store.js +438 -0
- package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
- package/dist/workflows/dag/frontend-verification-trace.js +52 -17
- package/dist/workflows/dag/frontend-worktree-diff.js +97 -18
- package/dist/workflows/dag/frontend-writer-admission.js +260 -0
- package/dist/workflows/dag/frontend-writer-status.js +244 -0
- package/dist/workflows/dag/init-hybrid.js +570 -457
- package/dist/workflows/dag/node-execution.js +306 -59
- package/dist/workflows/dag/prompt.js +10 -18
- package/dist/workflows/dag/rerun-plan.js +144 -5
- package/dist/workflows/dag/rerun-run.js +58 -6
- package/dist/workflows/dag/retry-policy.js +228 -4
- package/dist/workflows/dag/runner.js +314 -119
- package/dist/workflows/dag/scheduler.js +114 -16
- package/dist/workflows/dag/structured-output-repair.js +712 -0
- package/dist/workflows/dag/types.js +235 -19
- package/dist/workflows/dag/upstream-artifacts.js +0 -4
- package/dist/workflows/dag/validate.js +60 -10
- package/docs/init-surface.manifest.json +5 -0
- package/docs/operations/README.md +1 -1
- package/docs/templates/agent-dag.schema.json +44 -0
- package/docs/templates/frontend-test-dag.json +8 -10
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
- package/docs/templates/init-managed-agents.md +13 -3
- package/harness.json +3 -3
- package/package.json +5 -1
- package/scripts/next-info.mjs +356 -0
- package/scripts/next-publish-gate.mjs +238 -0
- package/scripts/release-source-binding.mjs +251 -0
- package/skills/codebase-scout/SKILL.md +1 -1
- package/skills/fe-test-ui-scout/SKILL.md +65 -0
- package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
- package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
- package/skills/frontend-implementation/SKILL.md +4 -4
- package/skills/frontend-implementation/references/design-spec.md +11 -7
- package/skills/frontend-implementation/references/node-contracts.md +20 -11
- package/skills/loop-agent/references/command-reference.md +11 -4
- package/skills/loop-agent/references/hybrid-dag.md +1 -1
- package/skills/playwright-cli/SKILL.md +22 -0
- package/skills/playwright-cli-case-generator/SKILL.md +20 -1
- package/dist/worker/console/static/assets/channel-CPF4N7pf.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +0 -1
- package/dist/worker/console/static/assets/index-Ya5FE7cD.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +0 -8
|
@@ -10,11 +10,15 @@ import { resolveContextPolicy } from "./context-policy.js";
|
|
|
10
10
|
import { buildDagNodePromptEnvelope, formatConvergenceFeedbackBlock, } from "./prompt.js";
|
|
11
11
|
import { persistLongNodeOutputArtifacts } from "./upstream-artifacts.js";
|
|
12
12
|
import { buildOutputLimitRecoverySection, loadBackendTestWriterProgressForRetry, } from "./backend-test-writer-completeness.js";
|
|
13
|
-
import { computeBackoffDelayMs, isRetryablePiFailureCategory, isSafeReadOnlyPiRetryCandidate, isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, } from "./retry-policy.js";
|
|
13
|
+
import { computeBackoffDelayMs, isCanonicalFinalVerifyShellRetryCandidate, isRetryablePiFailureCategory, isSafeReadOnlyPiRetryCandidate, isTargetTemplateTransientRetryNode, isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, projectFrontendNodeProtocolFailureReason, resolveFrontendPlanBackupRoute, resolveFrontendPlanRetryStep, VERIFY_SHELL_RETRY_POLICY, } from "./retry-policy.js";
|
|
14
14
|
import { applyNodeActivity, evaluateNodeLiveness, resolveLivenessPolicy, } from "./liveness-policy.js";
|
|
15
15
|
import { buildProtocolRetryInstruction, normalizeReviewVerdictAfterRetries, parseJsonReviewVerdict, validateOutputProtocol, } from "./output-protocol.js";
|
|
16
16
|
import { getStructuredContractValidator } from "./contract-output-registry.js";
|
|
17
17
|
import "./contract-validator-registrations.js";
|
|
18
|
+
import { computeNormalizedFailureFingerprint } from "./frontend-recovery-lineage.js";
|
|
19
|
+
import { readTypedEventStoreFromJsonl } from "./frontend-typed-event-store.js";
|
|
20
|
+
import { allowedRepairReadPaths, auditRepairAttemptToolUse, buildStructuredOutputRepairPrompt, freezeStructuredOutputRepairContext, hasNonEmptyStructuredCandidate, isFrontendStructuredRepairSchemaId, isStructuredRepairableFailureCategory, persistStructuredAttemptRaw, sessionEventsByteLength, GOVERNANCE_BLOCKED_CATEGORY, STRUCTURED_REPAIR_EXHAUSTED_CATEGORY, } from "./structured-output-repair.js";
|
|
21
|
+
import { readFrontendCanonicalCandidate } from "./frontend-implementation-contract.js";
|
|
18
22
|
import { writeDagNodeJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
|
|
19
23
|
import { buildProjectGovernanceContext, readCompletedWriterChangeManifests, writeProjectGovernanceContext, } from "./project-governance-context.js";
|
|
20
24
|
import { assertSkillSnapshotCoversSpec, buildNodePromptFromSnapshot, isDagSkillSnapshotIntegrityError, readSkillSnapshot, } from "./skill-snapshot.js";
|
|
@@ -132,16 +136,26 @@ export function buildNodePrompt(spec, task, upstream, options) {
|
|
|
132
136
|
function frontendStructuredArtifactRetryGuidance(schemaId) {
|
|
133
137
|
if (schemaId === "frontend-implementation-contract-plan-patch-v1") {
|
|
134
138
|
return [
|
|
135
|
-
"Return exactly
|
|
139
|
+
"Return exactly this artifact and nothing else: one short lead-in line, then exactly one fenced json block containing only the editable RFC 7386 plan patch, immediately followed by exactly one fenced openspec-citations block.",
|
|
136
140
|
"The runtime applies your patch to its protected contract skeleton. Do not emit schemaVersion, sourceBinding, riskLevel, targets.files, or mockApi.productionDefaultOff; do not emit the full contract.",
|
|
137
141
|
"Do not emit Markdown headings, bullets, plan prose, explanations, raw JSON, or any other fenced block.",
|
|
142
|
+
"The openspec-citations fenced block contains one JSON object per line, each with path/section/line — for example:",
|
|
143
|
+
"```openspec-citations",
|
|
144
|
+
'{"path":".harness/tasks/<task>/source/需求.md","section":"Objective","line":5}',
|
|
145
|
+
'{"path":".harness/tasks/<task>/source/references/product-requirement.md","section":"FR-1","line":16}',
|
|
146
|
+
"```",
|
|
138
147
|
];
|
|
139
148
|
}
|
|
140
149
|
if (schemaId === "frontend-implementation-contract-revision-patch-v1") {
|
|
141
150
|
return [
|
|
142
|
-
"Return exactly
|
|
151
|
+
"Return exactly this artifact and nothing else: one short lead-in line, then exactly one fenced json block containing the RFC 7386 merge-patch delta, immediately followed by exactly one fenced openspec-citations block.",
|
|
143
152
|
"The delta contains only the fields you change; null deletes a key, arrays and scalars replace, and plain objects merge recursively. Do not emit a full contract or a schemaVersion/targets section.",
|
|
144
153
|
"Do not emit Markdown headings, bullets, plan prose, explanations, raw JSON, or any other fenced block.",
|
|
154
|
+
"The openspec-citations fenced block contains one JSON object per line, each with path/section/line — for example:",
|
|
155
|
+
"```openspec-citations",
|
|
156
|
+
'{"path":".harness/tasks/<task>/source/需求.md","section":"Objective","line":5}',
|
|
157
|
+
'{"path":".harness/tasks/<task>/source/references/product-requirement.md","section":"FR-1","line":16}',
|
|
158
|
+
"```",
|
|
145
159
|
];
|
|
146
160
|
}
|
|
147
161
|
return [
|
|
@@ -149,30 +163,62 @@ function frontendStructuredArtifactRetryGuidance(schemaId) {
|
|
|
149
163
|
"The contract JSON fields are the complete implementation plan. Do not emit a separate plan document, Markdown headings, bullets, plan prose, explanations, raw JSON, or any other fenced block.",
|
|
150
164
|
];
|
|
151
165
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
166
|
+
const FRONTEND_PLAN_NODE_ID = "frontend-plan-pi";
|
|
167
|
+
function isFrontendPlanLadderTask(task) {
|
|
168
|
+
return task.id === FRONTEND_PLAN_NODE_ID;
|
|
155
169
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
"
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
170
|
+
/**
|
|
171
|
+
* Read the committed plan ledger snapshot for the ladder's "no new committed
|
|
172
|
+
* fact" check. The digest is over the sorted committed payload hashes, and
|
|
173
|
+
* `hasTerminalFact` reflects a committed `finalize_plan` terminal fact.
|
|
174
|
+
*/
|
|
175
|
+
async function readFrontendPlanCommittedSnapshot(runDir, nodeId) {
|
|
176
|
+
const records = await readTypedEventStoreFromJsonl(path.join(runDir, nodeId, "plan-typed-facts.jsonl"));
|
|
177
|
+
const committed = records.filter((record) => record.phase === "committed");
|
|
178
|
+
const digest = createHash("sha256")
|
|
179
|
+
.update(committed
|
|
180
|
+
.map((record) => record.payloadSha256)
|
|
181
|
+
.sort()
|
|
182
|
+
.join("\n"))
|
|
183
|
+
.digest("hex");
|
|
184
|
+
const hasTerminalFact = committed.some((record) => record.fact.kind === "finalize_plan");
|
|
185
|
+
return { digest, hasTerminalFact };
|
|
172
186
|
}
|
|
173
|
-
function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCategory, previousProtocolReason, recoveryTargetPaths, recoveryDiagnostics) {
|
|
187
|
+
function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCategory, previousProtocolReason, recoveryTargetPaths, recoveryDiagnostics, frontendPlanRetryStep) {
|
|
174
188
|
if (attemptNumber <= 1)
|
|
175
189
|
return basePrompt;
|
|
190
|
+
if (frontendPlanRetryStep === "compact-terminal-first") {
|
|
191
|
+
return [
|
|
192
|
+
basePrompt,
|
|
193
|
+
"",
|
|
194
|
+
"<retry_instruction>",
|
|
195
|
+
"Frontend plan retry ladder step: compact-terminal-first.",
|
|
196
|
+
"Do NOT rewrite the narrative. Only adopt already staged/quarantined fresh facts, fill the missing required facts, then call finalize_plan exactly once.",
|
|
197
|
+
"Keep the existing committed ledger intact; do not re-derive already committed facts.",
|
|
198
|
+
"</retry_instruction>",
|
|
199
|
+
].join("\n");
|
|
200
|
+
}
|
|
201
|
+
if (frontendPlanRetryStep === "bounded-tool-only") {
|
|
202
|
+
return [
|
|
203
|
+
basePrompt,
|
|
204
|
+
"",
|
|
205
|
+
"<retry_instruction>",
|
|
206
|
+
"Frontend plan retry ladder step: bounded-tool-only.",
|
|
207
|
+
"Reduce reasoning and wall-clock time. Use only read / fact / terminal tools and only for the necessary missing facts, then call finalize_plan exactly once.",
|
|
208
|
+
"Do not expand scope or re-derive already committed facts.",
|
|
209
|
+
"</retry_instruction>",
|
|
210
|
+
].join("\n");
|
|
211
|
+
}
|
|
212
|
+
if (frontendPlanRetryStep === "backup-model") {
|
|
213
|
+
return [
|
|
214
|
+
basePrompt,
|
|
215
|
+
"",
|
|
216
|
+
"<retry_instruction>",
|
|
217
|
+
"Frontend plan retry ladder step: backup-model.",
|
|
218
|
+
"A backup provider/model route was selected after repeated non-converging failures. Re-derive only the missing committed facts, then call finalize_plan exactly once; do not repeat the failed strategy.",
|
|
219
|
+
"</retry_instruction>",
|
|
220
|
+
].join("\n");
|
|
221
|
+
}
|
|
176
222
|
if (previousFailureCategory === "protocol-invalid" &&
|
|
177
223
|
task.outputProtocol &&
|
|
178
224
|
previousProtocolReason) {
|
|
@@ -185,12 +231,23 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
|
|
|
185
231
|
if (previousFailureCategory === "invalid-output" &&
|
|
186
232
|
task.structuredContractOutput &&
|
|
187
233
|
previousProtocolReason) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
234
|
+
// The frontend plan node's compile authority is the committed typed
|
|
235
|
+
// ledger, not a fenced JSON text artifact: its retry guidance must
|
|
236
|
+
// direct the model to re-commit corrected record_* facts and
|
|
237
|
+
// finalize_plan. The legacy full-contract JSON guidance below applies
|
|
238
|
+
// only to nodes whose authority is still a text contract artifact.
|
|
239
|
+
if (task.structuredContractOutput.schemaId ===
|
|
240
|
+
"frontend-implementation-contract-plan-patch-v1") {
|
|
241
|
+
return [
|
|
242
|
+
basePrompt,
|
|
243
|
+
"",
|
|
244
|
+
"<retry_instruction>",
|
|
245
|
+
"Previous plan ledger facts failed canonical contract validation:",
|
|
246
|
+
previousProtocolReason,
|
|
247
|
+
"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.",
|
|
248
|
+
"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.",
|
|
249
|
+
"</retry_instruction>",
|
|
250
|
+
].join("\n");
|
|
194
251
|
}
|
|
195
252
|
return [
|
|
196
253
|
basePrompt,
|
|
@@ -205,13 +262,17 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
|
|
|
205
262
|
}
|
|
206
263
|
if (previousFailureCategory === "structured-output-truncated" &&
|
|
207
264
|
task.structuredContractOutput) {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
return
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
265
|
+
if (task.structuredContractOutput.schemaId ===
|
|
266
|
+
"frontend-implementation-contract-plan-patch-v1") {
|
|
267
|
+
return [
|
|
268
|
+
basePrompt,
|
|
269
|
+
"",
|
|
270
|
+
"<retry_instruction>",
|
|
271
|
+
"Previous attempt was truncated by the provider (stopReason=length) before the plan facts were fully committed.",
|
|
272
|
+
"Re-commit the missing record_* facts and call finalize_plan exactly once; the committed typed ledger is the only compile authority.",
|
|
273
|
+
"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.",
|
|
274
|
+
"</retry_instruction>",
|
|
275
|
+
].join("\n");
|
|
215
276
|
}
|
|
216
277
|
return [
|
|
217
278
|
basePrompt,
|
|
@@ -284,16 +345,32 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
|
|
|
284
345
|
"</retry_instruction>",
|
|
285
346
|
].join("\n");
|
|
286
347
|
}
|
|
348
|
+
if (previousFailureCategory === "writer-clean-timeout") {
|
|
349
|
+
return [
|
|
350
|
+
basePrompt,
|
|
351
|
+
"",
|
|
352
|
+
"<retry_instruction>",
|
|
353
|
+
"Previous attempt ended in a clean provider/transport failure with zero attributed workspace writes and zero write/edit tool calls.",
|
|
354
|
+
"Do not invent existing implementation progress. Continue from the original task only if the workspace is still unchanged.",
|
|
355
|
+
"</retry_instruction>",
|
|
356
|
+
].join("\n");
|
|
357
|
+
}
|
|
287
358
|
if (previousFailureCategory !== "output-too-large") {
|
|
288
359
|
return basePrompt;
|
|
289
360
|
}
|
|
290
361
|
if (task.outputMode === "structured-required") {
|
|
291
362
|
if (task.structuredContractOutput) {
|
|
292
|
-
if (
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
363
|
+
if (task.structuredContractOutput.schemaId ===
|
|
364
|
+
"frontend-implementation-contract-plan-patch-v1") {
|
|
365
|
+
return [
|
|
366
|
+
basePrompt,
|
|
367
|
+
"",
|
|
368
|
+
"<retry_instruction>",
|
|
369
|
+
"Previous attempt exceeded the structured output size limit.",
|
|
370
|
+
"Re-commit corrected record_* facts and call finalize_plan exactly once; the committed typed ledger is the only compile authority.",
|
|
371
|
+
"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.",
|
|
372
|
+
"</retry_instruction>",
|
|
373
|
+
].join("\n");
|
|
297
374
|
}
|
|
298
375
|
return [
|
|
299
376
|
basePrompt,
|
|
@@ -715,7 +792,7 @@ export async function executeDagNode(input) {
|
|
|
715
792
|
}
|
|
716
793
|
node.resolvedSkills = resolvedSkills;
|
|
717
794
|
await writeNodeSkillArtifacts(runDir, nodeId, resolvedSkills);
|
|
718
|
-
|
|
795
|
+
let model = resolveModelForTask(task, spec.executorModels);
|
|
719
796
|
let thinking;
|
|
720
797
|
if (task.executor === "pi") {
|
|
721
798
|
try {
|
|
@@ -726,16 +803,25 @@ export async function executeDagNode(input) {
|
|
|
726
803
|
// Invalid or missing harness preserves the previous no-override behavior.
|
|
727
804
|
}
|
|
728
805
|
}
|
|
729
|
-
const
|
|
806
|
+
const specRetryPolicy = task.retryPolicy &&
|
|
730
807
|
(isSafeReadOnlyPiRetryCandidate(task) ||
|
|
731
808
|
isWriterEmptyDiffRetryCandidate(task) ||
|
|
732
809
|
isWriterTransportRetryCandidate(task))
|
|
733
810
|
? task.retryPolicy
|
|
734
811
|
: undefined;
|
|
812
|
+
const implicitVerifyRetryPolicy = specRetryPolicy === undefined &&
|
|
813
|
+
isCanonicalFinalVerifyShellRetryCandidate(task) &&
|
|
814
|
+
isTargetTemplateTransientRetryNode(task)
|
|
815
|
+
? VERIFY_SHELL_RETRY_POLICY
|
|
816
|
+
: undefined;
|
|
817
|
+
const retryPolicy = specRetryPolicy ?? implicitVerifyRetryPolicy;
|
|
735
818
|
const maxAttempts = retryPolicy?.maxAttempts ?? 1;
|
|
736
819
|
const attempts = [];
|
|
737
820
|
let totalAttemptWallDurationMs = 0;
|
|
738
821
|
let totalBackoffMs = 0;
|
|
822
|
+
const frontendPlanLadderEnabled = isFrontendPlanLadderTask(task);
|
|
823
|
+
let frontendPlanRetryStep = "normal";
|
|
824
|
+
const frontendPriorFingerprints = [];
|
|
739
825
|
const livenessPolicy = resolveLivenessPolicy(spec.defaults?.livenessPolicy, task.livenessPolicy);
|
|
740
826
|
/**
|
|
741
827
|
* Attempt-fenced, throttled activity sink. Late events from a previous
|
|
@@ -773,10 +859,24 @@ export async function executeDagNode(input) {
|
|
|
773
859
|
let terminalResult;
|
|
774
860
|
let previousFailureCategory;
|
|
775
861
|
let previousProtocolReason;
|
|
862
|
+
let pendingStructuredArtifact;
|
|
863
|
+
let lastRepairAllowlist = [];
|
|
776
864
|
for (let attemptNumber = 1; attemptNumber <= maxAttempts; attemptNumber += 1) {
|
|
777
865
|
const attemptStartedAt = new Date().toISOString();
|
|
778
866
|
node.currentAttempt = attemptNumber;
|
|
779
867
|
node.livenessStatus = "active";
|
|
868
|
+
const beforeCommittedDigest = frontendPlanLadderEnabled
|
|
869
|
+
? (await readFrontendPlanCommittedSnapshot(runDir, nodeId)).digest
|
|
870
|
+
: undefined;
|
|
871
|
+
if (frontendPlanLadderEnabled &&
|
|
872
|
+
frontendPlanRetryStep === "backup-model") {
|
|
873
|
+
model = resolveModelForTask(task, spec.executorModels);
|
|
874
|
+
}
|
|
875
|
+
// Capture the session-events.jsonl length BEFORE the executor appends this
|
|
876
|
+
// attempt's events, so the post-attempt repair tool audit can be scoped to
|
|
877
|
+
// exactly this attempt's segment (B1: earlier attempts legitimately use
|
|
878
|
+
// ls/find and must not be attributed to the repair attempt).
|
|
879
|
+
const attemptSessionEventsOffset = await sessionEventsByteLength(runDir, nodeId);
|
|
780
880
|
let result;
|
|
781
881
|
try {
|
|
782
882
|
validateRepairArtifactGateBeforeShell({
|
|
@@ -799,12 +899,60 @@ export async function executeDagNode(input) {
|
|
|
799
899
|
(acc[i.path] ??= []).push(i.detail);
|
|
800
900
|
return acc;
|
|
801
901
|
}, {});
|
|
902
|
+
let attemptPrompt = buildAttemptPrompt(task, prompt, attemptNumber, previousFailureCategory, previousProtocolReason, recoveryTargetPaths, recoveryDiagnostics, frontendPlanRetryStep);
|
|
903
|
+
if (attemptNumber > 1 &&
|
|
904
|
+
// The plan node (frontend-plan-pi) is a typed-facts ladder task:
|
|
905
|
+
// its retry is driven by the §5.1 ladder (compact-terminal-first
|
|
906
|
+
// / backup-model) and its compile authority is the committed
|
|
907
|
+
// ledger — never the legacy frozen fenced-JSON repair. The
|
|
908
|
+
// frozen repair path below applies only to non-ladder
|
|
909
|
+
// structured nodes whose output authority is a text artifact.
|
|
910
|
+
!frontendPlanLadderEnabled &&
|
|
911
|
+
isFrontendStructuredRepairSchemaId(task.structuredContractOutput?.schemaId) &&
|
|
912
|
+
isStructuredRepairableFailureCategory(previousFailureCategory) &&
|
|
913
|
+
(await hasNonEmptyStructuredCandidate({
|
|
914
|
+
runDir,
|
|
915
|
+
nodeId,
|
|
916
|
+
beforeAttempt: attemptNumber,
|
|
917
|
+
}))) {
|
|
918
|
+
const originalCanonical = task.structuredContractOutput?.schemaId ===
|
|
919
|
+
"frontend-implementation-contract-revision-patch-v1"
|
|
920
|
+
? (await readFrontendCanonicalCandidate(runDir, "frontend-plan-pi"))
|
|
921
|
+
: undefined;
|
|
922
|
+
const frozen = await freezeStructuredOutputRepairContext({
|
|
923
|
+
runDir,
|
|
924
|
+
runId: state.runId,
|
|
925
|
+
taskId: spec.sourceBinding?.taskId ?? task.id,
|
|
926
|
+
nodeId,
|
|
927
|
+
nodeAttempt: attemptNumber,
|
|
928
|
+
targetSchemaId: task.structuredContractOutput.schemaId,
|
|
929
|
+
skeleton: task.structuredContractOutput?.skeleton,
|
|
930
|
+
originalCanonical,
|
|
931
|
+
latestStopReason: previousFailureCategory === "structured-output-truncated"
|
|
932
|
+
? "length"
|
|
933
|
+
: previousFailureCategory,
|
|
934
|
+
});
|
|
935
|
+
lastRepairAllowlist = allowedRepairReadPaths({
|
|
936
|
+
runDir,
|
|
937
|
+
context: frozen.context,
|
|
938
|
+
contextPath: frozen.relativePath,
|
|
939
|
+
});
|
|
940
|
+
attemptPrompt = await buildStructuredOutputRepairPrompt({
|
|
941
|
+
runDir,
|
|
942
|
+
context: frozen.context,
|
|
943
|
+
contextPath: frozen.relativePath,
|
|
944
|
+
contextSha256: frozen.sha256,
|
|
945
|
+
maxAttempts,
|
|
946
|
+
schemaGuidance: frontendStructuredArtifactRetryGuidance(task.structuredContractOutput.schemaId),
|
|
947
|
+
previousReason: previousProtocolReason,
|
|
948
|
+
});
|
|
949
|
+
}
|
|
802
950
|
result = await executeNode({
|
|
803
951
|
task,
|
|
804
952
|
cwd,
|
|
805
953
|
model,
|
|
806
954
|
...(thinking ? { thinking } : {}),
|
|
807
|
-
prompt:
|
|
955
|
+
prompt: attemptPrompt,
|
|
808
956
|
attempt: attemptNumber,
|
|
809
957
|
reportActivity,
|
|
810
958
|
timeoutMs: livenessPolicy.absoluteMaxWallClockMs,
|
|
@@ -820,6 +968,35 @@ export async function executeDagNode(input) {
|
|
|
820
968
|
durationMs: 0,
|
|
821
969
|
};
|
|
822
970
|
}
|
|
971
|
+
if (isFrontendStructuredRepairSchemaId(task.structuredContractOutput?.schemaId)) {
|
|
972
|
+
const rawText = canonicalNodeOutput(result);
|
|
973
|
+
if (rawText.trim().length > 0) {
|
|
974
|
+
await persistStructuredAttemptRaw({
|
|
975
|
+
runDir,
|
|
976
|
+
nodeId,
|
|
977
|
+
attempt: attemptNumber,
|
|
978
|
+
text: rawText,
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
if (attemptNumber > 1 && lastRepairAllowlist.length > 0) {
|
|
982
|
+
const audit = await auditRepairAttemptToolUse({
|
|
983
|
+
runDir,
|
|
984
|
+
nodeId,
|
|
985
|
+
allowedPaths: lastRepairAllowlist,
|
|
986
|
+
fromByteOffset: attemptSessionEventsOffset,
|
|
987
|
+
});
|
|
988
|
+
if (!audit.ok) {
|
|
989
|
+
result = {
|
|
990
|
+
...result,
|
|
991
|
+
ok: false,
|
|
992
|
+
failureCategory: GOVERNANCE_BLOCKED_CATEGORY,
|
|
993
|
+
stderr: [result.stderr, audit.reason]
|
|
994
|
+
.filter(Boolean)
|
|
995
|
+
.join("\n"),
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
823
1000
|
// R0: executor ok=true still fails closed when outputProtocol is violated.
|
|
824
1001
|
// Valid semantic results (e.g. VERDICT: request-revision) pass validation.
|
|
825
1002
|
if (result.ok && task.outputProtocol) {
|
|
@@ -856,16 +1033,17 @@ export async function executeDagNode(input) {
|
|
|
856
1033
|
structuredContractOutput: task.structuredContractOutput,
|
|
857
1034
|
});
|
|
858
1035
|
if (!contractCheck.ok) {
|
|
859
|
-
// A provider stopReason=length means the response was cut before the
|
|
860
|
-
// JSON contract could complete; separate it from an ordinary bad
|
|
861
|
-
// contract so the retry switches to a JSON-only output strategy.
|
|
862
1036
|
const attemptStopReason = result.stopReason;
|
|
1037
|
+
const failureCategory = contractCheck.classification === "governance-blocked"
|
|
1038
|
+
? GOVERNANCE_BLOCKED_CATEGORY
|
|
1039
|
+
: attemptStopReason === "length" ||
|
|
1040
|
+
contractCheck.classification === "truncated"
|
|
1041
|
+
? "structured-output-truncated"
|
|
1042
|
+
: "invalid-output";
|
|
863
1043
|
result = {
|
|
864
1044
|
...result,
|
|
865
1045
|
ok: false,
|
|
866
|
-
failureCategory
|
|
867
|
-
? "structured-output-truncated"
|
|
868
|
-
: "invalid-output",
|
|
1046
|
+
failureCategory,
|
|
869
1047
|
stderr: [result.stderr, contractCheck.reason]
|
|
870
1048
|
.filter(Boolean)
|
|
871
1049
|
.join("\n"),
|
|
@@ -873,14 +1051,6 @@ export async function executeDagNode(input) {
|
|
|
873
1051
|
previousProtocolReason = contractCheck.reason;
|
|
874
1052
|
}
|
|
875
1053
|
else {
|
|
876
|
-
if (contractCheck.structuredArtifact) {
|
|
877
|
-
node.structuredArtifactPath = contractCheck.structuredArtifact.path;
|
|
878
|
-
node.structuredArtifactSha256 = contractCheck.structuredArtifact.sha256;
|
|
879
|
-
node.structuredArtifactSchemaId = contractCheck.structuredArtifact.schemaId;
|
|
880
|
-
}
|
|
881
|
-
if (contractCheck.frontendPlanOutput) {
|
|
882
|
-
node.frontendPlanOutput = contractCheck.frontendPlanOutput;
|
|
883
|
-
}
|
|
884
1054
|
if (contractCheck.normalizedText) {
|
|
885
1055
|
result = {
|
|
886
1056
|
...result,
|
|
@@ -888,6 +1058,9 @@ export async function executeDagNode(input) {
|
|
|
888
1058
|
stdout: contractCheck.normalizedText,
|
|
889
1059
|
};
|
|
890
1060
|
}
|
|
1061
|
+
if (contractCheck.artifact) {
|
|
1062
|
+
pendingStructuredArtifact = contractCheck.artifact;
|
|
1063
|
+
}
|
|
891
1064
|
previousProtocolReason = undefined;
|
|
892
1065
|
}
|
|
893
1066
|
}
|
|
@@ -900,6 +1073,7 @@ export async function executeDagNode(input) {
|
|
|
900
1073
|
const attemptFinishedAt = new Date().toISOString();
|
|
901
1074
|
const attemptWallDurationMs = durationBetween(attemptStartedAt, attemptFinishedAt);
|
|
902
1075
|
totalAttemptWallDurationMs += attemptWallDurationMs;
|
|
1076
|
+
const rawFailureCategory = result.rawFailureCategory ?? result.failureCategory;
|
|
903
1077
|
const attemptRecord = {
|
|
904
1078
|
attempt: attemptNumber,
|
|
905
1079
|
startedAt: attemptStartedAt,
|
|
@@ -911,6 +1085,7 @@ export async function executeDagNode(input) {
|
|
|
911
1085
|
stderr: result.stderr,
|
|
912
1086
|
assistantText: result.assistantText,
|
|
913
1087
|
failureCategory: result.failureCategory,
|
|
1088
|
+
...(rawFailureCategory ? { rawFailureCategory } : {}),
|
|
914
1089
|
backend: result.backend,
|
|
915
1090
|
sdkAttempted: result.sdkAttempted,
|
|
916
1091
|
tokensUsed: result.tokensUsed,
|
|
@@ -925,6 +1100,60 @@ export async function executeDagNode(input) {
|
|
|
925
1100
|
await writeDagNodeJsonArtifact(runDir, nodeId, `attempt-${attemptNumber}.json`, attemptRecord);
|
|
926
1101
|
node.attempts = attempts;
|
|
927
1102
|
}
|
|
1103
|
+
// Frontend plan retry ladder: project the 7-value protocol failure
|
|
1104
|
+
// reason, detect new committed facts, and escalate instead of re-running.
|
|
1105
|
+
if (frontendPlanLadderEnabled && !result.ok) {
|
|
1106
|
+
const afterSnapshot = await readFrontendPlanCommittedSnapshot(runDir, nodeId);
|
|
1107
|
+
const hasNewCommittedFact = beforeCommittedDigest !== undefined &&
|
|
1108
|
+
afterSnapshot.digest !== beforeCommittedDigest;
|
|
1109
|
+
const protocolReason = projectFrontendNodeProtocolFailureReason({
|
|
1110
|
+
failureCategory: result.failureCategory,
|
|
1111
|
+
stopReason: result.stopReason,
|
|
1112
|
+
assistantText: result.assistantText ?? result.stdout,
|
|
1113
|
+
hasTerminalFact: afterSnapshot.hasTerminalFact,
|
|
1114
|
+
});
|
|
1115
|
+
const fingerprint = computeNormalizedFailureFingerprint({
|
|
1116
|
+
failureOwner: "plan",
|
|
1117
|
+
protocolFailureReason: protocolReason ?? "",
|
|
1118
|
+
});
|
|
1119
|
+
const backupRoute = resolveFrontendPlanBackupRoute(protocolReason);
|
|
1120
|
+
const nextStep = resolveFrontendPlanRetryStep({
|
|
1121
|
+
currentStep: frontendPlanRetryStep,
|
|
1122
|
+
protocolFailureReason: protocolReason,
|
|
1123
|
+
hasNewCommittedFact,
|
|
1124
|
+
fingerprint,
|
|
1125
|
+
priorFingerprints: frontendPriorFingerprints,
|
|
1126
|
+
backupRouteAvailable: backupRoute.ok,
|
|
1127
|
+
});
|
|
1128
|
+
frontendPriorFingerprints.push(fingerprint);
|
|
1129
|
+
frontendPlanRetryStep = nextStep;
|
|
1130
|
+
if (nextStep === "unsupported-provider-capability") {
|
|
1131
|
+
result = {
|
|
1132
|
+
...result,
|
|
1133
|
+
ok: false,
|
|
1134
|
+
failureCategory: "unsupported-provider-capability",
|
|
1135
|
+
stderr: [
|
|
1136
|
+
result.stderr,
|
|
1137
|
+
"frontend plan retry ladder: no compatible provider capability route for the protocol failure reason",
|
|
1138
|
+
]
|
|
1139
|
+
.filter(Boolean)
|
|
1140
|
+
.join("\n"),
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
else if (nextStep === "non-converging") {
|
|
1144
|
+
result = {
|
|
1145
|
+
...result,
|
|
1146
|
+
ok: false,
|
|
1147
|
+
failureCategory: "non-converging",
|
|
1148
|
+
stderr: [
|
|
1149
|
+
result.stderr,
|
|
1150
|
+
"frontend plan retry ladder: repeated failure fingerprint with no new committed fact",
|
|
1151
|
+
]
|
|
1152
|
+
.filter(Boolean)
|
|
1153
|
+
.join("\n"),
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
928
1157
|
// Reflect the latest attempt on the node so progress is observable,
|
|
929
1158
|
// but keep node.status RUNNING while retry is still possible.
|
|
930
1159
|
node.durationMs = durationBetween(node.startedAt, attemptFinishedAt);
|
|
@@ -958,13 +1187,26 @@ export async function executeDagNode(input) {
|
|
|
958
1187
|
break;
|
|
959
1188
|
const canRetry = retryPolicy !== undefined && attemptNumber < maxAttempts;
|
|
960
1189
|
const retryable = retryPolicy !== undefined &&
|
|
1190
|
+
result.failureCategory !== GOVERNANCE_BLOCKED_CATEGORY &&
|
|
961
1191
|
(isRetryablePiFailureCategory(result.failureCategory, {
|
|
962
1192
|
retryCategories: retryPolicy.retryCategories,
|
|
963
1193
|
}) ||
|
|
964
1194
|
(result.failureCategory === "protocol-invalid" &&
|
|
965
1195
|
task.outputProtocol?.retryOnInvalid === true));
|
|
966
|
-
if (!canRetry || !retryable)
|
|
1196
|
+
if (!canRetry || !retryable) {
|
|
1197
|
+
if (!canRetry &&
|
|
1198
|
+
isFrontendStructuredRepairSchemaId(task.structuredContractOutput?.schemaId) &&
|
|
1199
|
+
isStructuredRepairableFailureCategory(result.failureCategory)) {
|
|
1200
|
+
result = {
|
|
1201
|
+
...result,
|
|
1202
|
+
failureCategory: STRUCTURED_REPAIR_EXHAUSTED_CATEGORY,
|
|
1203
|
+
};
|
|
1204
|
+
terminalResult = result;
|
|
1205
|
+
previousFailureCategory = result.failureCategory;
|
|
1206
|
+
node.failureCategory = result.failureCategory;
|
|
1207
|
+
}
|
|
967
1208
|
break;
|
|
1209
|
+
}
|
|
968
1210
|
const delayMs = computeBackoffDelayMs(attemptNumber + 1, retryPolicy);
|
|
969
1211
|
if (delayMs > 0) {
|
|
970
1212
|
const backoffStartedAt = new Date().toISOString();
|
|
@@ -1113,6 +1355,11 @@ export async function executeDagNode(input) {
|
|
|
1113
1355
|
node.structuredArtifactSchemaId = "backend-test-result-v1";
|
|
1114
1356
|
}
|
|
1115
1357
|
}
|
|
1358
|
+
else if (pendingStructuredArtifact) {
|
|
1359
|
+
node.structuredArtifactPath = pendingStructuredArtifact.path;
|
|
1360
|
+
node.structuredArtifactSha256 = pendingStructuredArtifact.sha256;
|
|
1361
|
+
node.structuredArtifactSchemaId = pendingStructuredArtifact.schemaId;
|
|
1362
|
+
}
|
|
1116
1363
|
}
|
|
1117
1364
|
const finishedAt = new Date().toISOString();
|
|
1118
1365
|
node.finishedAt = finishedAt;
|
|
@@ -67,7 +67,6 @@ export function buildUpstreamContext(task, upstream, maxChars = MAX_UPSTREAM_CHA
|
|
|
67
67
|
const toleratedFailures = new Set(task.failureAwareDependsOn ?? []);
|
|
68
68
|
for (const depId of task.depends_on) {
|
|
69
69
|
const record = upstream[depId];
|
|
70
|
-
const artifactFirst = task.artifactFirstUpstreamNodeIds?.includes(depId);
|
|
71
70
|
const stdout = record?.stdout?.trim() ? record.stdout : "";
|
|
72
71
|
const assistantText = record?.assistantText?.trim()
|
|
73
72
|
? record.assistantText
|
|
@@ -119,23 +118,16 @@ export function buildUpstreamContext(task, upstream, maxChars = MAX_UPSTREAM_CHA
|
|
|
119
118
|
}
|
|
120
119
|
if (!record || record.status !== "FINISHED")
|
|
121
120
|
continue;
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
record.
|
|
129
|
-
record.structuredArtifactSchemaId
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
lines.push(`- payload kind: ${record.frontendPlanOutput.payloadKind}`, `- citations: ${record.frontendPlanOutput.citationsArtifactPath}`, `- citations sha256: ${record.frontendPlanOutput.citationsArtifactSha256}`);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
else {
|
|
136
|
-
lines.push("- artifact: unavailable (do not reconstruct the contract from node prose; treat this as an upstream artifact integrity failure)");
|
|
137
|
-
}
|
|
138
|
-
sections.push(lines.join("\n"));
|
|
121
|
+
if (record.structuredArtifactPath &&
|
|
122
|
+
record.structuredArtifactSchemaId ===
|
|
123
|
+
"frontend-implementation-contract-v1") {
|
|
124
|
+
sections.push([
|
|
125
|
+
`## Upstream output: ${depId}`,
|
|
126
|
+
"Validated structured artifact (read-only runner evidence — use read tool to fetch; do not edit). Do not infer the contract from stdout.",
|
|
127
|
+
`- path: ${record.structuredArtifactPath}`,
|
|
128
|
+
`- schema: ${record.structuredArtifactSchemaId}`,
|
|
129
|
+
`- sha256: ${record.structuredArtifactSha256 ?? "unknown"}`,
|
|
130
|
+
].join("\n"));
|
|
139
131
|
continue;
|
|
140
132
|
}
|
|
141
133
|
if (!upstreamText)
|