@tea-agent/loop-agent 0.41.1-beta.0 → 0.42.0-next.1
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 +61 -207
- package/dist/adapters/context-transfer/optional-pi-handoff.js +31 -0
- package/dist/adapters/context-transfer/pi-session.js +61 -0
- package/dist/application/dag/generate-task-dag.js +19 -75
- package/dist/application/task-lifecycle/advance.js +5 -24
- package/dist/application/task-lifecycle/observe.js +17 -171
- package/dist/application/task-lifecycle/plan-transitions.js +7 -42
- package/dist/application/task-lifecycle/recommendations.js +2 -13
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +12 -7
- package/dist/cli/program.js +23 -6
- package/dist/commands/client-recovery.js +0 -3
- package/dist/commands/dag-artifact.js +284 -0
- package/dist/commands/dag-context.js +184 -0
- package/dist/commands/dag-rerun.js +206 -1
- package/dist/commands/init.js +1 -27
- package/dist/commands/task-advance.js +0 -19
- package/dist/executors/dag-pi-executor.js +266 -3792
- package/dist/executors/pi-executor.js +4 -15
- package/dist/executors/pi-sdk-executor.js +3 -0
- package/dist/executors/shell-executor.js +217 -945
- package/dist/executors/shell-write-guard.js +0 -7
- package/dist/{worker → infrastructure}/console/app-data.js +4 -0
- package/dist/infrastructure/console/artifact-revision-store.js +430 -0
- package/dist/infrastructure/console/context-export-store.js +160 -0
- package/dist/infrastructure/console/dir-lock.js +132 -0
- package/dist/infrastructure/console/operation-store.js +197 -0
- package/dist/infrastructure/harness/artifact-store.js +10 -1
- package/dist/infrastructure/harness/atomic-write.js +12 -2
- package/dist/shared/context-transfer/artifact-revision.js +172 -0
- package/dist/shared/context-transfer.js +418 -0
- package/dist/shared/dag-failure-category.js +0 -12
- package/dist/shared/openspec-spec.js +4 -70
- package/dist/shared/operator/capabilities.js +14 -7
- package/dist/shared/operator/safe-run-summary.js +1 -0
- package/dist/shared/path-safety.js +93 -0
- package/dist/shared/preview.js +28 -4
- package/dist/task/config-types.js +9 -35
- package/dist/task/contract/adopt.js +0 -4
- package/dist/task/contract/import-revision.js +0 -4
- package/dist/task/contract/project.js +0 -3
- package/dist/task/contract/schema.js +1 -2
- package/dist/task/frontend-project-capability.js +20 -203
- package/dist/task/runtime.js +2 -5
- package/dist/task/source-prepare/build-draft.js +3 -3
- package/dist/task/source-prepare/fragment-inventory.js +9 -15
- package/dist/task/source-prepare/prepare.js +1 -89
- package/dist/task/source-prepare/semantic-intake.js +2 -6
- package/dist/task/source-references.js +1 -22
- package/dist/task/task-demand-routing.js +3 -0
- package/dist/worker/console/chat/chat-event-store.js +2 -2
- package/dist/worker/console/chat/pi-runtime.js +3 -2
- package/dist/worker/console/chat/resource-preferences-store.js +1 -1
- package/dist/worker/console/chat/routes.js +13 -4
- package/dist/worker/console/chat/semantic-activity.js +12 -11
- package/dist/worker/console/chat/session-stats.js +96 -0
- package/dist/worker/console/chat/session-store.js +1 -1
- package/dist/worker/console/chat/turn-process.js +28 -4
- package/dist/worker/console/chat/user-questions.js +1 -1
- package/dist/worker/console/console-update-runtime.js +1 -1
- package/dist/worker/console/context-transfer-diagnostics.js +198 -0
- package/dist/worker/console/dag-confirmation.js +1 -1
- package/dist/worker/console/dag-execution-receipt.js +1 -1
- package/dist/worker/console/doctor.js +1 -1
- package/dist/worker/console/draft-store.js +1 -1
- package/dist/worker/console/human-gate-token.js +1 -1
- package/dist/worker/console/index.js +3 -6
- package/dist/worker/console/interview/assessment.js +1 -1
- package/dist/worker/console/interview/session.js +1 -1
- package/dist/worker/console/operation-runner.js +45 -4
- package/dist/worker/console/operation-sse.js +1 -1
- package/dist/worker/console/operation-wait.js +1 -1
- package/dist/worker/console/operator-actions.js +107 -168
- package/dist/worker/console/operator-surface-health.js +1 -1
- package/dist/worker/console/operator-user-error.js +169 -0
- package/dist/worker/console/pi-plugins.js +60 -1
- package/dist/worker/console/routes.js +637 -8
- package/dist/worker/console/security.js +35 -0
- package/dist/worker/console/server.js +6 -6
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Bho4we4c.js → abnfDiagram-N423BO3Z-DZZ8m3PO.js} +1 -1
- package/dist/worker/console/static/assets/{arc-TBDkTeK0.js → arc-D6PvaVd-.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BF_fZrn2.js → architectureDiagram-T3A2C74G-B_OTOiI8.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CCnPgUe2.js → blockDiagram-VBNYF7ZC-Bv6rqHBg.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BCYZJLAT.js → c4Diagram-5PPSVZJV-B8eHr0oz.js} +1 -1
- package/dist/worker/console/static/assets/channel-BU5gOilw.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-D8K6adhU.js → chunk-2GRJ4B5K-DYwR0im2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DkT1Q78b.js → chunk-2Q5K7J3B-D2WPGqXt.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DuhvpIr7.js → chunk-5RXB4S5H-CQISJ_I7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BnK6dC49.js → chunk-5VM5RSS4-C0o2Du1e.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DPz__z7r.js → chunk-6Q2QTUOP-4f8kr-U7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-0kkppM2j.js → chunk-GF5L2VYU-D_OWvXzX.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BB41iK5t.js → chunk-JWPE2WC7-CasdPz5X.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-CiVrxKHU.js → chunk-KBJHAD2P-Cj8lRrla.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-C2h9Xje1.js → chunk-RYQCIY6F-CUvD1FSd.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-3iOSHdbM.js → chunk-XXDRQBXY-CqLw_eqB.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DIzKJGHr.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DIzKJGHr.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BN3A7tkk.js → cose-bilkent-JH36ORCC-ku-WqJPx.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CTuBy0Or.js → cynefin-VYW2F7L2-Bq_PYDhl.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-wb6tUlMN.js → cynefinDiagram-MW4NZA55-BiGZV641.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-GP1Ivx8B.js → dagre-VZM6K2ZE-C4JRlglF.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-ejrcK6L0.js → diagram-7IWD3JNH-B0dNeWYG.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-YFkCCvUU.js → diagram-B4RE2ZJO-Cj35YvYM.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-B4rrvIaL.js → diagram-LBJQPF4R-uzQoJ2-8.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-C-YIZcUf.js → diagram-Q27KOJAE-D1a-Buoz.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPF5wcL1.js → diagram-UB23O5K3-XjRrLRSs.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-CU5SzXjO.js → ebnfDiagram-BXEA7PRR-Da_O24cW.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-CpL414DE.js → erDiagram-JOGREHBK-BLJ8jrYU.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-MrhLzNau.js → flowDiagram-UKHOOZJN-B9GrLjM0.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Bg4qFuUo.js → ganttDiagram-PKOTCBZU-CeJ0TqiK.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-ColxSXLR.js → gitGraphDiagram-DS77QQ5N-Bm2eNNFX.js} +1 -1
- package/dist/worker/console/static/assets/index-H9rFJiGL.css +1 -0
- package/dist/worker/console/static/assets/index-xwu9GxEc.js +451 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Czvtm6gc.js → infoDiagram-6WML65LV-DB26i3d8.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-B7Xp8HyJ.js → ishikawaDiagram-WSZJBQD7-BOGRyeZT.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DWYZcvph.js → journeyDiagram-NVQOT4AX-CZRoBO_6.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-KTLZRUjx.js → kanban-definition-27J2QSJJ-qlWhJyoB.js} +1 -1
- package/dist/worker/console/static/assets/{linear-_Exn7bfl.js → linear-CfUiDDB3.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-Cxg8kX8V.js → mermaid.core-BQe6fpqj.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-oKrlo2Bt.js → mindmap-definition-FAOFIHXS-zFzWHw64.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-XAeteiNd.js → pegDiagram-VL7TDLO6-DFGUqjZO.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BTf1K-lp.js → pieDiagram-7S7Q4E2Y-BB5i0l8Z.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-C0IlY0Jf.js → quadrantDiagram-CIZ2JOQS-obbVZ_X5.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BVbKtZhW.js → railroadDiagram-AXF67PYL-BwdaNzc1.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-C5e4JFcr.js → requirementDiagram-LRYGKXZP-CkTJaugh.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Cz30hc29.js → sankeyDiagram-W5VNT64P-BwJ-hIgq.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DS33n6d0.js → sequenceDiagram-SI44F4Z6-BzSgmm7w.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-46w7v-9T.js → sizeCapture-X5ZJPWSS-Bn3oj5Q1.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-pbcD5SsZ.js → stateDiagram-OKZ733FA-dxhGtL8K.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cld2qK9v.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DuivYBSj.js → swimlanes-SLNWSIFB-BLpEInya.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-TIHpiT7w.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Cb9VZVIl.js → timeline-definition-Z64GVDOM-ChJGYcXy.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-D2-mfOgR.js → vennDiagram-T6HMQDX7-DK-qjRer.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DeS2aC76.js → wardleyDiagram-T6FBY63Y-3qeaWAg-.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-WGsrFjxH.js → xychartDiagram-ELKLHX3M-CootlyP9.js} +1 -1
- package/dist/worker/console/static/index.html +7 -2
- package/dist/worker/console/static-src/app/useOperatorActions.js +3 -2
- package/dist/worker/console/static-src/app/useRunProgress.js +9 -2
- package/dist/worker/console/static-src/operator-chat/dag-progress-link.js +22 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +2 -0
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +6 -0
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +28 -3
- package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +106 -0
- package/dist/worker/console/static-src/shell/console-update-reload.js +25 -0
- package/dist/worker/console/workspace-context.js +28 -112
- package/dist/worker/console/workspace-registry.js +2 -2
- package/dist/worker/continuation/worker-continuation.js +278 -0
- package/dist/worker/observability/read-model.js +4 -0
- package/dist/worker/observe/health.js +1 -1
- package/dist/worker/observe/node-transparency.js +572 -0
- package/dist/worker/observe/routes.js +51 -1
- package/dist/worker/observe/static/api.js +69 -5
- package/dist/worker/observe/static/dag-context-reason-labels.d.ts +9 -0
- package/dist/worker/observe/static/dag-context-reason-labels.js +120 -0
- package/dist/worker/observe/static/dag-node-purpose.js +0 -5
- package/dist/worker/observe/static/state.js +3 -1
- package/dist/worker/observe/static/styles.css +1037 -126
- package/dist/worker/observe/static/views/dag-inspector.js +1217 -65
- package/dist/worker/observe/static/views/session-timeline.js +121 -25
- package/dist/worker/outcomes/adapters.js +19 -0
- package/dist/worker/outcomes/types.js +1 -0
- package/dist/worker/pool/attempt-lease.js +97 -66
- package/dist/worker/pool/begin-attempt-with-lease.js +1 -0
- package/dist/worker/pool/reconcile.js +46 -1
- package/dist/worker/pool/run-store.js +2 -0
- package/dist/worker/pool/state-projection.js +2 -0
- package/dist/worker/task-spec/workflow-routing.js +8 -3
- package/dist/workflows/dag/artifact-bindings.js +149 -0
- package/dist/workflows/dag/artifact-revision-schema-registry.js +31 -0
- package/dist/workflows/dag/backend-test-result-contract.js +1 -1
- package/dist/workflows/dag/budget-enforcement.js +10 -0
- package/dist/workflows/dag/context-receipt.js +305 -0
- package/dist/workflows/dag/context-transfer/context-bundle.js +423 -0
- package/dist/workflows/dag/context-transfer/operator-actions.js +61 -0
- package/dist/workflows/dag/context-transfer/renderers.js +93 -0
- package/dist/workflows/dag/contract-validator-registrations.js +2 -1
- package/dist/workflows/dag/frontend-implementation-contract.js +162 -932
- package/dist/workflows/dag/frontend-plan-render.js +1 -2
- package/dist/workflows/dag/frontend-prewrite-gate.js +349 -255
- package/dist/workflows/dag/frontend-recovery-plan.js +10 -17
- package/dist/workflows/dag/frontend-recovery-run.js +20 -33
- package/dist/workflows/dag/frontend-repair.js +432 -1
- package/dist/workflows/dag/frontend-review-context.js +15 -261
- package/dist/workflows/dag/frontend-verification-trace.js +24 -249
- package/dist/workflows/dag/frontend-worktree-diff.js +17 -250
- package/dist/workflows/dag/frontend-writer-rollback.js +32 -0
- package/dist/workflows/dag/init-hybrid.js +640 -685
- package/dist/workflows/dag/interrupt-request.js +0 -7
- package/dist/workflows/dag/node-execution.js +116 -759
- package/dist/workflows/dag/path-safety.js +1 -0
- package/dist/workflows/dag/prompt.js +30 -38
- package/dist/workflows/dag/report.js +1 -37
- package/dist/workflows/dag/rerun-feedback.js +1 -256
- package/dist/workflows/dag/rerun-plan.js +557 -11
- package/dist/workflows/dag/rerun-run.js +601 -38
- package/dist/workflows/dag/rerun-task.js +0 -29
- package/dist/workflows/dag/retry-policy.js +104 -214
- package/dist/workflows/dag/runner.js +142 -430
- package/dist/workflows/dag/scheduler.js +16 -114
- package/dist/workflows/dag/skill-snapshot.js +17 -0
- package/dist/workflows/dag/types.js +195 -246
- package/dist/workflows/dag/validate.js +12 -28
- package/docs/architecture/runtime-boundaries.md +6 -6
- package/docs/architecture/worker-and-feature.md +1 -1
- package/docs/init-surface.manifest.json +12 -30
- package/docs/skills/vetted-skill-registry.md +2 -4
- package/docs/templates/README.md +0 -2
- package/docs/templates/agent-dag-report.schema.json +2 -8
- package/docs/templates/agent-dag.schema.json +33 -1
- package/docs/templates/frontend-implementation-contract.schema.json +1 -4
- package/docs/templates/product-line/task.yaml +1 -1
- package/harness.json +3 -3
- package/package.json +3 -1
- package/skills/frontend-bounded-implement/SKILL.md +14 -15
- package/skills/frontend-design-review/SKILL.md +41 -22
- package/skills/frontend-implementation/SKILL.md +52 -0
- package/skills/frontend-implementation/references/code-standards.md +33 -0
- package/skills/frontend-implementation/references/design-spec.md +56 -0
- package/skills/frontend-implementation/references/node-contracts.md +31 -0
- package/skills/frontend-review/SKILL.md +15 -20
- package/skills/frontend-review/references/review-findings.md +7 -6
- package/skills/frontend-verification/SKILL.md +1 -1
- package/skills/loop-agent/references/command-reference.md +6 -1
- package/skills/loop-agent/references/hybrid-dag.md +2 -2
- package/dist/commands/dag-follow-up.js +0 -138
- package/dist/executors/pi-read-budget-policy.js +0 -239
- package/dist/worker/console/frontend-human-decision-adapter.js +0 -19
- package/dist/worker/console/frontend-split-operation-adapter.js +0 -20
- package/dist/worker/console/operation-store.js +0 -389
- package/dist/worker/console/static/assets/channel-bnXW0U3C.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-rZpq_twR.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-rZpq_twR.js +0 -1
- package/dist/worker/console/static/assets/index-D3CC4eUz.css +0 -1
- package/dist/worker/console/static/assets/index-DLt4ZFvD.js +0 -437
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-zBMYiUyi.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-d_rms-5i.js +0 -8
- package/dist/worker/console/static-src/active-run-badge.js +0 -17
- package/dist/worker/materialize/frontend-split-task-materializer.js +0 -72
- package/dist/workflows/dag/dag-retry-schema.js +0 -138
- package/dist/workflows/dag/frontend-closeout.js +0 -221
- package/dist/workflows/dag/frontend-design-policy.js +0 -400
- package/dist/workflows/dag/frontend-human-decision.js +0 -182
- package/dist/workflows/dag/frontend-provider-capability-matrix.js +0 -159
- package/dist/workflows/dag/frontend-read-budgets.js +0 -12
- package/dist/workflows/dag/frontend-recovery-capsule.js +0 -455
- package/dist/workflows/dag/frontend-recovery-controller.js +0 -202
- package/dist/workflows/dag/frontend-recovery-lineage.js +0 -178
- package/dist/workflows/dag/frontend-review-findings.js +0 -270
- package/dist/workflows/dag/frontend-shadow-dual-write.js +0 -914
- package/dist/workflows/dag/frontend-shape-capsule-store.js +0 -191
- package/dist/workflows/dag/frontend-shape-facts.js +0 -409
- package/dist/workflows/dag/frontend-shape.js +0 -427
- package/dist/workflows/dag/frontend-source-fidelity-ledger.js +0 -108
- package/dist/workflows/dag/frontend-split-application-service.js +0 -203
- package/dist/workflows/dag/frontend-split-orchestrator.js +0 -899
- package/dist/workflows/dag/frontend-typed-event-store.js +0 -452
- package/dist/workflows/dag/frontend-typed-event-transaction.js +0 -180
- package/dist/workflows/dag/frontend-writer-admission.js +0 -285
- package/dist/workflows/dag/frontend-writer-status.js +0 -256
- package/dist/workflows/dag/recovery-lease.js +0 -80
- package/docs/templates/frontend-implementation-dag.json +0 -89
- package/docs/templates/spec-registry.schema.json +0 -45
- package/skills/frontend-bounded-implement/references/code-standards.md +0 -19
- package/skills/frontend-contract/SKILL.md +0 -23
- package/skills/frontend-contract/references/contract-protocol.md +0 -34
- package/skills/frontend-plan/SKILL.md +0 -26
- package/skills/frontend-plan/references/decision-contract.md +0 -37
- package/skills/frontend-plan/references/design-decisions.md +0 -17
- package/skills/frontend-scout/SKILL.md +0 -25
- package/skills/frontend-scout/references/design-evidence.md +0 -16
- package/skills/frontend-scout/references/scout-evidence.md +0 -23
- /package/dist/{worker → infrastructure}/console/repo-fingerprint.js +0 -0
|
@@ -10,15 +10,13 @@ import { writeNodeRecord, writeNodeSkillArtifacts } from "./run-store.js";
|
|
|
10
10
|
import { resolveContextPolicy } from "./context-policy.js";
|
|
11
11
|
import { buildDagNodePromptEnvelope, formatConvergenceFeedbackBlock, } from "./prompt.js";
|
|
12
12
|
import { persistLongNodeOutputArtifacts } from "./upstream-artifacts.js";
|
|
13
|
+
import { materializeDeclaredArtifactFacts } from "./artifact-bindings.js";
|
|
13
14
|
import { buildOutputLimitRecoverySection, loadBackendTestWriterProgressForRetry, } from "./backend-test-writer-completeness.js";
|
|
14
|
-
import { computeBackoffDelayMs, isCanonicalFinalVerifyShellRetryCandidate, isRetryablePiFailureCategory, isSafeReadOnlyPiRetryCandidate, isTargetTemplateTransientRetryNode, isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate,
|
|
15
|
+
import { computeBackoffDelayMs, isCanonicalFinalVerifyShellRetryCandidate, isRetryablePiFailureCategory, isSafeReadOnlyPiRetryCandidate, isTargetTemplateTransientRetryNode, isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, VERIFY_SHELL_RETRY_POLICY, } from "./retry-policy.js";
|
|
15
16
|
import { applyNodeActivity, evaluateNodeLiveness, resolveLivenessPolicy, } from "./liveness-policy.js";
|
|
16
17
|
import { buildProtocolRetryInstruction, normalizeReviewVerdictAfterRetries, parseJsonReviewVerdict, validateOutputProtocol, } from "./output-protocol.js";
|
|
17
18
|
import { getStructuredContractValidator } from "./contract-output-registry.js";
|
|
18
19
|
import "./contract-validator-registrations.js";
|
|
19
|
-
import { computeNormalizedFailureFingerprint } from "./frontend-recovery-lineage.js";
|
|
20
|
-
import { parseLedgerJson } from "../../task/source-prepare/ledger.js";
|
|
21
|
-
import { readTypedEventStoreFromJsonl } from "./frontend-typed-event-store.js";
|
|
22
20
|
import { allowedRepairReadPaths, auditRepairAttemptToolUse, buildStructuredOutputRepairPrompt, freezeStructuredOutputRepairContext, hasNonEmptyStructuredCandidate, isFrontendStructuredRepairSchemaId, isStructuredRepairableFailureCategory, persistStructuredAttemptRaw, sessionEventsByteLength, GOVERNANCE_BLOCKED_CATEGORY, STRUCTURED_REPAIR_EXHAUSTED_CATEGORY, } from "./structured-output-repair.js";
|
|
23
21
|
import { readFrontendCanonicalCandidate } from "./frontend-implementation-contract.js";
|
|
24
22
|
import { writeDagNodeJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
|
|
@@ -45,7 +43,11 @@ function canonicalApprovalJson(value) {
|
|
|
45
43
|
export function parseAndValidateFinalWriteSetApproval(input) {
|
|
46
44
|
const binding = input.task.finalWriteSetApproval;
|
|
47
45
|
if (!binding) {
|
|
48
|
-
return {
|
|
46
|
+
return {
|
|
47
|
+
ok: false,
|
|
48
|
+
reason: "missing final write-set approval binding",
|
|
49
|
+
approvalSourceNodeId: "(missing)",
|
|
50
|
+
};
|
|
49
51
|
}
|
|
50
52
|
const source = input.state.nodes[binding.approvalSourceNodeId];
|
|
51
53
|
if (source?.status !== "FINISHED" || parseProcessVerdict(source) !== "pass") {
|
|
@@ -56,20 +58,44 @@ export function parseAndValidateFinalWriteSetApproval(input) {
|
|
|
56
58
|
};
|
|
57
59
|
}
|
|
58
60
|
const raw = canonicalNodeOutput(source);
|
|
59
|
-
const blocks = [
|
|
61
|
+
const blocks = [
|
|
62
|
+
...raw.matchAll(/```FINAL_WRITE_SET_APPROVAL_JSON\s*\r?\n([\s\S]*?)\r?\n```/g),
|
|
63
|
+
];
|
|
60
64
|
if (blocks.length !== 1) {
|
|
61
|
-
return {
|
|
65
|
+
return {
|
|
66
|
+
ok: false,
|
|
67
|
+
reason: `expected exactly one FINAL_WRITE_SET_APPROVAL_JSON block, found ${blocks.length}`,
|
|
68
|
+
approvalSourceNodeId: binding.approvalSourceNodeId,
|
|
69
|
+
};
|
|
62
70
|
}
|
|
63
71
|
let approval;
|
|
64
72
|
try {
|
|
65
73
|
approval = JSON.parse(blocks[0][1]);
|
|
66
74
|
}
|
|
67
75
|
catch {
|
|
68
|
-
return {
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
reason: "final write-set approval is not valid JSON",
|
|
79
|
+
approvalSourceNodeId: binding.approvalSourceNodeId,
|
|
80
|
+
};
|
|
69
81
|
}
|
|
70
|
-
const required = [
|
|
71
|
-
|
|
72
|
-
|
|
82
|
+
const required = [
|
|
83
|
+
"schemaVersion",
|
|
84
|
+
"writerNodeId",
|
|
85
|
+
"approvalSourceNodeId",
|
|
86
|
+
"auditedPlanNodeId",
|
|
87
|
+
"approvedWriteSet",
|
|
88
|
+
"taskContractSha256",
|
|
89
|
+
"auditedPlanSha256",
|
|
90
|
+
"approvalDigest",
|
|
91
|
+
];
|
|
92
|
+
if (Object.keys(approval).length !== required.length ||
|
|
93
|
+
required.some((key) => !(key in approval))) {
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
reason: "final write-set approval has an invalid schema",
|
|
97
|
+
approvalSourceNodeId: binding.approvalSourceNodeId,
|
|
98
|
+
};
|
|
73
99
|
}
|
|
74
100
|
const approved = approval.approvedWriteSet;
|
|
75
101
|
if (approval.schemaVersion !== 1 ||
|
|
@@ -82,12 +108,20 @@ export function parseAndValidateFinalWriteSetApproval(input) {
|
|
|
82
108
|
typeof approval.taskContractSha256 !== "string" ||
|
|
83
109
|
typeof approval.auditedPlanSha256 !== "string" ||
|
|
84
110
|
typeof approval.approvalDigest !== "string") {
|
|
85
|
-
return {
|
|
111
|
+
return {
|
|
112
|
+
ok: false,
|
|
113
|
+
reason: "final write-set approval binding or field types are invalid",
|
|
114
|
+
approvalSourceNodeId: binding.approvalSourceNodeId,
|
|
115
|
+
};
|
|
86
116
|
}
|
|
87
117
|
if (!/^[a-f0-9]{64}$/.test(approval.taskContractSha256) ||
|
|
88
118
|
!/^[a-f0-9]{64}$/.test(approval.auditedPlanSha256) ||
|
|
89
119
|
!/^[a-f0-9]{64}$/.test(approval.approvalDigest)) {
|
|
90
|
-
return {
|
|
120
|
+
return {
|
|
121
|
+
ok: false,
|
|
122
|
+
reason: "final write-set approval digest fields are invalid",
|
|
123
|
+
approvalSourceNodeId: binding.approvalSourceNodeId,
|
|
124
|
+
};
|
|
91
125
|
}
|
|
92
126
|
const canonicalPayload = { ...approval };
|
|
93
127
|
delete canonicalPayload.approvalDigest;
|
|
@@ -95,33 +129,70 @@ export function parseAndValidateFinalWriteSetApproval(input) {
|
|
|
95
129
|
.update(canonicalApprovalJson(canonicalPayload))
|
|
96
130
|
.digest("hex");
|
|
97
131
|
if (approval.approvalDigest !== expectedDigest) {
|
|
98
|
-
return {
|
|
132
|
+
return {
|
|
133
|
+
ok: false,
|
|
134
|
+
reason: "final write-set approval digest mismatch",
|
|
135
|
+
approvalSourceNodeId: binding.approvalSourceNodeId,
|
|
136
|
+
};
|
|
99
137
|
}
|
|
100
138
|
const expectedTaskDigest = input.spec.taskContractBinding?.canonicalHash;
|
|
101
139
|
const plan = input.state.nodes[binding.auditedPlanNodeId];
|
|
102
140
|
const expectedPlanDigest = createHash("sha256")
|
|
103
141
|
.update(canonicalNodeOutput(plan))
|
|
104
142
|
.digest("hex");
|
|
105
|
-
if (!expectedTaskDigest ||
|
|
106
|
-
|
|
143
|
+
if (!expectedTaskDigest ||
|
|
144
|
+
approval.taskContractSha256 !== expectedTaskDigest ||
|
|
145
|
+
approval.auditedPlanSha256 !== expectedPlanDigest) {
|
|
146
|
+
return {
|
|
147
|
+
ok: false,
|
|
148
|
+
reason: "final write-set approval is stale for the task contract or audited plan",
|
|
149
|
+
approvalSourceNodeId: binding.approvalSourceNodeId,
|
|
150
|
+
};
|
|
107
151
|
}
|
|
108
152
|
const effectiveWriteSet = approved;
|
|
109
|
-
if (effectiveWriteSet.length === 0 ||
|
|
110
|
-
|
|
153
|
+
if (effectiveWriteSet.length === 0 ||
|
|
154
|
+
new Set(effectiveWriteSet).size !== effectiveWriteSet.length) {
|
|
155
|
+
return {
|
|
156
|
+
ok: false,
|
|
157
|
+
reason: "final write-set approval must contain a non-empty ordered unique path set",
|
|
158
|
+
approvalSourceNodeId: binding.approvalSourceNodeId,
|
|
159
|
+
};
|
|
111
160
|
}
|
|
112
161
|
for (const entry of effectiveWriteSet) {
|
|
113
162
|
const normalized = entry.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
114
|
-
if (!normalized ||
|
|
115
|
-
|
|
163
|
+
if (!normalized ||
|
|
164
|
+
normalized === "." ||
|
|
165
|
+
normalized === ".." ||
|
|
166
|
+
normalized.includes("*") ||
|
|
167
|
+
normalized.includes("?") ||
|
|
168
|
+
normalized.includes("REPLACE/") ||
|
|
169
|
+
normalized.includes("PLACEHOLDER")) {
|
|
170
|
+
return {
|
|
171
|
+
ok: false,
|
|
172
|
+
reason: `final write-set approval contains a broad or placeholder path: ${entry}`,
|
|
173
|
+
approvalSourceNodeId: binding.approvalSourceNodeId,
|
|
174
|
+
};
|
|
116
175
|
}
|
|
117
176
|
if (!input.task.allowedPaths.some((allowed) => pathMatchesPattern(normalized, allowed))) {
|
|
118
|
-
return {
|
|
177
|
+
return {
|
|
178
|
+
ok: false,
|
|
179
|
+
reason: `final write-set approval exceeds allowedPaths: ${entry}`,
|
|
180
|
+
approvalSourceNodeId: binding.approvalSourceNodeId,
|
|
181
|
+
};
|
|
119
182
|
}
|
|
120
183
|
if (input.task.forbiddenPaths.some((forbidden) => pathMatchesPattern(normalized, forbidden))) {
|
|
121
|
-
return {
|
|
184
|
+
return {
|
|
185
|
+
ok: false,
|
|
186
|
+
reason: `final write-set approval overlaps forbiddenPaths: ${entry}`,
|
|
187
|
+
approvalSourceNodeId: binding.approvalSourceNodeId,
|
|
188
|
+
};
|
|
122
189
|
}
|
|
123
190
|
}
|
|
124
|
-
return {
|
|
191
|
+
return {
|
|
192
|
+
ok: true,
|
|
193
|
+
effectiveWriteSet,
|
|
194
|
+
approvalDigest: approval.approvalDigest,
|
|
195
|
+
};
|
|
125
196
|
}
|
|
126
197
|
export function buildNodePrompt(spec, task, upstream, options) {
|
|
127
198
|
const policy = resolveContextPolicy(spec);
|
|
@@ -129,6 +200,7 @@ export function buildNodePrompt(spec, task, upstream, options) {
|
|
|
129
200
|
spec,
|
|
130
201
|
task,
|
|
131
202
|
upstream,
|
|
203
|
+
runDir: options?.runDir,
|
|
132
204
|
resolvedSkills: policy.resolveSkills(spec, task),
|
|
133
205
|
maxUpstreamChars: policy.resolveMaxUpstreamChars(task),
|
|
134
206
|
projectGovernanceContext: options?.projectGovernanceContext,
|
|
@@ -165,518 +237,9 @@ function frontendStructuredArtifactRetryGuidance(schemaId) {
|
|
|
165
237
|
"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.",
|
|
166
238
|
];
|
|
167
239
|
}
|
|
168
|
-
|
|
169
|
-
const COMPACT_RETRY_SECTION_MAX_CHARS = 1_200;
|
|
170
|
-
const COMPACT_RETRY_TASK_MAX_CHARS = 4_800;
|
|
171
|
-
/** Keep alias/field-shape remediation out of the initial Plan prompt. */
|
|
172
|
-
function frontendPlanValidationRetryGuidance(reason) {
|
|
173
|
-
const guidance = [];
|
|
174
|
-
if (/notApplicableReason|uiState.*reason/i.test(reason)) {
|
|
175
|
-
guidance.push("For this retry, use uiStates[].notApplicableReason for a non-applicable state; omit expectedBehavior instead of supplying an empty value.");
|
|
176
|
-
}
|
|
177
|
-
if (/interaction.*(?:\bid\b|\bname\b)|requires non-empty interaction\.name/i.test(reason)) {
|
|
178
|
-
guidance.push("For this retry, use interactions[].name with non-empty trigger and expectedBehavior.");
|
|
179
|
-
}
|
|
180
|
-
if (/verificationTargets.*uiStates|uiStates.*verificationTargets/i.test(reason)) {
|
|
181
|
-
guidance.push("For this retry, provide verificationTargets[].uiStates as an array; use [] when the target has no named UI state.");
|
|
182
|
-
}
|
|
183
|
-
return guidance;
|
|
184
|
-
}
|
|
185
|
-
function compactRetryText(text, maxChars) {
|
|
186
|
-
if (text.length <= maxChars)
|
|
187
|
-
return text;
|
|
188
|
-
const headChars = Math.floor(maxChars * 0.78);
|
|
189
|
-
const tailChars = Math.max(0, maxChars - headChars - 52);
|
|
190
|
-
return `${text.slice(0, headChars)}\n… [omitted after context-overflow] …\n${text.slice(-tailChars)}`;
|
|
191
|
-
}
|
|
192
|
-
function compactRetrySection(prompt, name, maxChars) {
|
|
193
|
-
const match = prompt.match(new RegExp(`<${name}>([\\s\\S]*?)</${name}>`));
|
|
194
|
-
return compactRetryText(match?.[1]?.trim() ?? "(not available)", maxChars);
|
|
195
|
-
}
|
|
196
|
-
/**
|
|
197
|
-
* A context-overflow retry must not resend the full resolved skills, governance
|
|
198
|
-
* payload, repeated source excerpts, and upstream previews that caused the
|
|
199
|
-
* previous session to overflow. Keep the immutable node contract plus bounded
|
|
200
|
-
* objective/criteria/task excerpts; canonical artifacts remain readable by
|
|
201
|
-
* pointer from the retained task instructions.
|
|
202
|
-
*/
|
|
203
|
-
export function buildContextOverflowRetryPrompt(task, basePrompt) {
|
|
204
|
-
return [
|
|
205
|
-
"<compact_retry_context>",
|
|
206
|
-
"This is a fresh, compact retry after provider context-overflow. The original full prompt remains audit evidence but is intentionally not resent.",
|
|
207
|
-
"Do not re-read task sources, upstream stdout, skills, or full diff artifacts that are already represented by canonical artifacts. Start from the smallest retained artifact and expand only the exact file or diff fragment needed.",
|
|
208
|
-
"</compact_retry_context>",
|
|
209
|
-
`<dag_objective>\n${compactRetrySection(basePrompt, "dag_objective", COMPACT_RETRY_SECTION_MAX_CHARS)}\n</dag_objective>`,
|
|
210
|
-
`<success_criteria>\n${compactRetrySection(basePrompt, "success_criteria", COMPACT_RETRY_SECTION_MAX_CHARS)}\n</success_criteria>`,
|
|
211
|
-
`<global_constraints>\n${compactRetrySection(basePrompt, "global_constraints", COMPACT_RETRY_SECTION_MAX_CHARS)}\n</global_constraints>`,
|
|
212
|
-
`<node_contract>\n${compactRetrySection(basePrompt, "node_contract", COMPACT_RETRY_SECTION_MAX_CHARS)}\n</node_contract>`,
|
|
213
|
-
`<upstream_context>\n${compactRetrySection(basePrompt, "upstream_context", COMPACT_RETRY_SECTION_MAX_CHARS)}\n</upstream_context>`,
|
|
214
|
-
[
|
|
215
|
-
"<task>",
|
|
216
|
-
`Original task id: ${task.id}`,
|
|
217
|
-
compactRetrySection(basePrompt, "task", COMPACT_RETRY_TASK_MAX_CHARS),
|
|
218
|
-
"</task>",
|
|
219
|
-
].join("\n"),
|
|
220
|
-
].join("\n\n");
|
|
221
|
-
}
|
|
222
|
-
function isFrontendPlanLadderTask(task) {
|
|
223
|
-
return task.id === FRONTEND_PLAN_NODE_ID;
|
|
224
|
-
}
|
|
225
|
-
/** Contract node consumes the compiled ledger input; local predicate avoids a
|
|
226
|
-
* dag-pi-executor import cycle. */
|
|
227
|
-
function isFrontendContractTypedNode(task) {
|
|
228
|
-
return task.id === "frontend-contract-pi";
|
|
229
|
-
}
|
|
230
|
-
/** Resolve the source-fidelity ledger path from a v2 source binding, refusing
|
|
231
|
-
* any path that escapes the workspace root (mirrors dag-pi-executor). */
|
|
232
|
-
function resolveFrontendLedgerPath(sourceBinding, cwd) {
|
|
233
|
-
if (!sourceBinding || sourceBinding.schemaVersion !== 2) {
|
|
234
|
-
throw new Error(`frontend-contract-input-unavailable: sourceBinding v2 ledger required (got ${sourceBinding?.schemaVersion ?? "none"})`);
|
|
235
|
-
}
|
|
236
|
-
const absolutePath = path.resolve(cwd, sourceBinding.ledgerPath);
|
|
237
|
-
const workspaceRoot = path.resolve(cwd);
|
|
238
|
-
if (absolutePath !== workspaceRoot &&
|
|
239
|
-
!absolutePath.startsWith(`${workspaceRoot}${path.sep}`)) {
|
|
240
|
-
throw new Error(`frontend-contract-input-unavailable: ledger path escapes workspace root: ${sourceBinding.ledgerPath}`);
|
|
241
|
-
}
|
|
242
|
-
return absolutePath;
|
|
243
|
-
}
|
|
244
|
-
/** record_* submissions observed for the contract node in its session events. */
|
|
245
|
-
const FRONTEND_CONTRACT_RECORD_TOOL_NAMES_LOCAL = new Set([
|
|
246
|
-
"record_requirement",
|
|
247
|
-
"record_constraint",
|
|
248
|
-
"record_evidence_expectation",
|
|
249
|
-
"record_handoff_intent",
|
|
250
|
-
"record_open_question",
|
|
251
|
-
"record_split_proposal",
|
|
252
|
-
]);
|
|
253
|
-
async function countContractRecordSubmissions(runDir, nodeId) {
|
|
254
|
-
const eventsPath = path.join(runDir, nodeId, "session-events.jsonl");
|
|
255
|
-
let count = 0;
|
|
256
|
-
try {
|
|
257
|
-
for (const line of (await readFile(eventsPath, "utf8")).split("\n")) {
|
|
258
|
-
if (!line.trim())
|
|
259
|
-
continue;
|
|
260
|
-
let event;
|
|
261
|
-
try {
|
|
262
|
-
event = JSON.parse(line);
|
|
263
|
-
}
|
|
264
|
-
catch {
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
267
|
-
if (event !== null &&
|
|
268
|
-
typeof event === "object" &&
|
|
269
|
-
event.type === "tool_execution_start" &&
|
|
270
|
-
typeof event.toolName === "string" &&
|
|
271
|
-
FRONTEND_CONTRACT_RECORD_TOOL_NAMES_LOCAL.has(event.toolName)) {
|
|
272
|
-
count += 1;
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
catch {
|
|
277
|
-
// Missing events file = zero submissions.
|
|
278
|
-
}
|
|
279
|
-
return count;
|
|
280
|
-
}
|
|
281
|
-
/**
|
|
282
|
-
* Read the committed plan ledger snapshot for the ladder's "no new committed
|
|
283
|
-
* fact" check. The digest is over the sorted committed payload hashes, and
|
|
284
|
-
* `hasTerminalFact` reflects a committed `finalize_plan` terminal fact.
|
|
285
|
-
*/
|
|
286
|
-
async function readFrontendPlanCommittedSnapshot(runDir, nodeId) {
|
|
287
|
-
const records = await readTypedEventStoreFromJsonl(path.join(runDir, nodeId, "plan-typed-facts.jsonl"));
|
|
288
|
-
const committed = records.filter((record) => record.phase === "committed");
|
|
289
|
-
const digest = createHash("sha256")
|
|
290
|
-
.update(committed
|
|
291
|
-
.map((record) => record.payloadSha256)
|
|
292
|
-
.sort()
|
|
293
|
-
.join("\n"))
|
|
294
|
-
.digest("hex");
|
|
295
|
-
const hasTerminalFact = committed.some((record) => record.fact.kind === "finalize_plan");
|
|
296
|
-
return { digest, hasTerminalFact };
|
|
297
|
-
}
|
|
298
|
-
function planInputRecord(value) {
|
|
299
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
300
|
-
}
|
|
301
|
-
function planInputText(value, maxChars = 240) {
|
|
302
|
-
if (typeof value !== "string" || value.trim().length === 0)
|
|
303
|
-
return undefined;
|
|
304
|
-
const normalized = value.trim();
|
|
305
|
-
return normalized.length <= maxChars
|
|
306
|
-
? normalized
|
|
307
|
-
: `${normalized.slice(0, maxChars - 1)}…`;
|
|
308
|
-
}
|
|
309
|
-
function planInputStrings(value) {
|
|
310
|
-
return Array.isArray(value)
|
|
311
|
-
? value.filter((item) => typeof item === "string")
|
|
312
|
-
: [];
|
|
313
|
-
}
|
|
314
|
-
/** Input bound for the planner evidence block; protects the model input budget. */
|
|
315
|
-
const FRONTEND_PLAN_INPUT_MAX_CHARS = 12_000;
|
|
316
|
-
const FRONTEND_PLAN_INPUT_CAP_LADDER = [
|
|
317
|
-
{ text: 240, array: 40 },
|
|
318
|
-
{ text: 120, array: 20 },
|
|
319
|
-
{ text: 60, array: 10 },
|
|
320
|
-
{ text: 24, array: 4 },
|
|
321
|
-
];
|
|
322
|
-
/**
|
|
323
|
-
* Frozen requirement→PRD citation map for the plan review checklist: lets the
|
|
324
|
-
* model declare sourceRequirementIds whose section/line match the component
|
|
325
|
-
* purpose, so the runtime-derived specReference survives reviewer scrutiny
|
|
326
|
-
* (r12: 4 of 9 decision=new choices cited a mismatched PRD section).
|
|
327
|
-
*/
|
|
328
|
-
async function resolveComponentSourceCitations(spec, cwd) {
|
|
329
|
-
const binding = spec.sourceBinding;
|
|
330
|
-
if (!binding || binding.schemaVersion !== 2 || !binding.ledgerPath)
|
|
331
|
-
return new Map();
|
|
332
|
-
const absolutePath = path.resolve(cwd, binding.ledgerPath);
|
|
333
|
-
const workspaceRoot = path.resolve(cwd);
|
|
334
|
-
if (absolutePath !== workspaceRoot &&
|
|
335
|
-
!absolutePath.startsWith(`${workspaceRoot}${path.sep}`))
|
|
336
|
-
return new Map();
|
|
337
|
-
try {
|
|
338
|
-
const ledger = JSON.parse(await readFile(absolutePath, "utf8"));
|
|
339
|
-
const fragmentsById = new Map((ledger.fragments ?? []).map((fragment) => [fragment.id, fragment]));
|
|
340
|
-
const references = new Map();
|
|
341
|
-
for (const requirement of ledger.canonicalRequirements ?? []) {
|
|
342
|
-
const id = typeof requirement.id === "string" ? requirement.id : "";
|
|
343
|
-
if (!id)
|
|
344
|
-
continue;
|
|
345
|
-
const citations = (requirement.sourceFragmentIds ?? [])
|
|
346
|
-
.map((fragmentId) => fragmentsById.get(fragmentId))
|
|
347
|
-
.filter((fragment) => fragment !== undefined)
|
|
348
|
-
.map((fragment) => ({
|
|
349
|
-
fragmentId: fragment.id,
|
|
350
|
-
path: typeof fragment.path === "string" ? fragment.path : "",
|
|
351
|
-
section: typeof fragment.headingPath === "string"
|
|
352
|
-
? fragment.headingPath
|
|
353
|
-
: "",
|
|
354
|
-
line: typeof fragment.lineRange?.start === "number"
|
|
355
|
-
? fragment.lineRange.start
|
|
356
|
-
: undefined,
|
|
357
|
-
}));
|
|
358
|
-
if (citations.length > 0)
|
|
359
|
-
references.set(id, citations);
|
|
360
|
-
}
|
|
361
|
-
return references;
|
|
362
|
-
}
|
|
363
|
-
catch {
|
|
364
|
-
return new Map();
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
/** Input bound for the contract node's compiled ledger block. */
|
|
368
|
-
const FRONTEND_CONTRACT_INPUT_MAX_CHARS = 12_000;
|
|
369
|
-
/**
|
|
370
|
-
* Render the contract node's complete-but-bounded ledger handoff. The
|
|
371
|
-
* source-fidelity ledger already extracted canonical requirements with source
|
|
372
|
-
* spans; the contract node confirms and commits them incrementally through
|
|
373
|
-
* record_requirement instead of re-reading the raw source (extreme-environment:
|
|
374
|
-
* a small output window cannot absorb a full source re-read).
|
|
375
|
-
*
|
|
376
|
-
* Same shape guarantees as the plan input block: always valid JSON under the
|
|
377
|
-
* char bound, ids never drop, texts degrade through the cap ladder.
|
|
378
|
-
*/
|
|
379
|
-
export function renderFrontendContractInputContext(input) {
|
|
380
|
-
// Fragment bindings are ids, not prose: they are the one thing the contract
|
|
381
|
-
// must never lose. r6 regression — the last-resort degradation dropped
|
|
382
|
-
// sourceFragmentIds entirely, the model (correctly refusing to invent ids)
|
|
383
|
-
// committed empty bindings, and the plan compile failed the ledger-binding
|
|
384
|
-
// gate for every requirement. Bindings therefore bypass the cap ladder and
|
|
385
|
-
// every degradation level; only requirement TEXTS and fragment CONTEXT
|
|
386
|
-
// (path/headingPath) may degrade. Fragment context is rendered only for
|
|
387
|
-
// fragments actually referenced by a requirement and shrinks first.
|
|
388
|
-
const referencedFragmentIds = new Set(input.canonicalRequirements.flatMap((requirement) => planInputStrings(requirement.sourceFragmentIds)));
|
|
389
|
-
const referencedFragments = input.fragments.filter((fragment) => referencedFragmentIds.has(fragment.id));
|
|
390
|
-
const serializeAtCap = (cap) => JSON.stringify({
|
|
391
|
-
requirements: input.canonicalRequirements.map((requirement) => ({
|
|
392
|
-
id: requirement.id,
|
|
393
|
-
text: planInputText(requirement.text, cap.text),
|
|
394
|
-
sourceFragmentIds: planInputStrings(requirement.sourceFragmentIds),
|
|
395
|
-
})),
|
|
396
|
-
fragments: referencedFragments.map((fragment) => ({
|
|
397
|
-
id: fragment.id,
|
|
398
|
-
path: planInputText(fragment.path, 200),
|
|
399
|
-
headingPath: planInputText(fragment.headingPath, 120),
|
|
400
|
-
lineRange: fragment.lineRange,
|
|
401
|
-
})),
|
|
402
|
-
});
|
|
403
|
-
let serialized = serializeAtCap(FRONTEND_PLAN_INPUT_CAP_LADDER[0]);
|
|
404
|
-
for (const cap of FRONTEND_PLAN_INPUT_CAP_LADDER.slice(1)) {
|
|
405
|
-
if (serialized.length <= FRONTEND_CONTRACT_INPUT_MAX_CHARS)
|
|
406
|
-
break;
|
|
407
|
-
serialized = serializeAtCap(cap);
|
|
408
|
-
}
|
|
409
|
-
if (serialized.length > FRONTEND_CONTRACT_INPUT_MAX_CHARS) {
|
|
410
|
-
// Last resort: keep every requirement id AND its fragment bindings,
|
|
411
|
-
// degrade texts, and shrink referenced fragment context first (halve,
|
|
412
|
-
// then drop context fields, then drop the fragment list entirely).
|
|
413
|
-
// Requirement ids and sourceFragmentIds are never dropped.
|
|
414
|
-
let fragments = referencedFragments.map((fragment) => ({
|
|
415
|
-
id: fragment.id,
|
|
416
|
-
path: planInputText(fragment.path, 120),
|
|
417
|
-
}));
|
|
418
|
-
let requirements = input.canonicalRequirements.map((requirement) => {
|
|
419
|
-
const sourceFragmentIds = planInputStrings(requirement.sourceFragmentIds);
|
|
420
|
-
return {
|
|
421
|
-
id: requirement.id,
|
|
422
|
-
text: "(truncated)",
|
|
423
|
-
// Empty bindings carry no information; omit them so the payload
|
|
424
|
-
// stays inside the char bound when no requirement is bound.
|
|
425
|
-
...(sourceFragmentIds.length > 0 ? { sourceFragmentIds } : {}),
|
|
426
|
-
};
|
|
427
|
-
});
|
|
428
|
-
let bounded = JSON.stringify({ degraded: "requirement-texts-truncated", requirements, fragments });
|
|
429
|
-
while (bounded.length > FRONTEND_CONTRACT_INPUT_MAX_CHARS && fragments.length > 0) {
|
|
430
|
-
fragments = fragments.slice(0, Math.floor(fragments.length / 2));
|
|
431
|
-
bounded = JSON.stringify({
|
|
432
|
-
degraded: "requirement-texts-truncated",
|
|
433
|
-
requirements,
|
|
434
|
-
fragments,
|
|
435
|
-
});
|
|
436
|
-
}
|
|
437
|
-
serialized = bounded;
|
|
438
|
-
}
|
|
439
|
-
return [
|
|
440
|
-
"<frontend_contract_input>",
|
|
441
|
-
"Canonical requirements extracted by the source-fidelity ledger, compiled by the runner. Treat them as the authoritative requirement inventory: confirm and commit each requirement through record_requirement (one per tool call); the ledger already binds source fragments, so do NOT re-read the raw source files.",
|
|
442
|
-
serialized,
|
|
443
|
-
"</frontend_contract_input>",
|
|
444
|
-
].join("\n");
|
|
445
|
-
}
|
|
446
|
-
export async function buildFrontendContractInputContext(input) {
|
|
447
|
-
const ledger = parseLedgerJson(await readFile(input.ledgerPath, "utf8"));
|
|
448
|
-
return renderFrontendContractInputContext({
|
|
449
|
-
canonicalRequirements: ledger.canonicalRequirements.map((requirement) => ({
|
|
450
|
-
id: requirement.id,
|
|
451
|
-
text: requirement.text,
|
|
452
|
-
sourceFragmentIds: requirement.sourceFragmentIds ?? [],
|
|
453
|
-
})),
|
|
454
|
-
fragments: ledger.fragments.map((fragment) => ({
|
|
455
|
-
id: fragment.id,
|
|
456
|
-
path: fragment.path,
|
|
457
|
-
headingPath: fragment.headingPath,
|
|
458
|
-
lineRange: fragment.lineRange,
|
|
459
|
-
})),
|
|
460
|
-
});
|
|
461
|
-
}
|
|
462
|
-
/**
|
|
463
|
-
* Render the planner's complete-but-bounded evidence handoff from committed
|
|
464
|
-
* typed facts. It deliberately excludes upstream response prose and artifact
|
|
465
|
-
* paths: Contract and Scout have already established these facts, so Plan
|
|
466
|
-
* should decide and commit rather than spend another model turn reading them.
|
|
467
|
-
*
|
|
468
|
-
* The block is always valid JSON under the char bound: field texts shrink
|
|
469
|
-
* through a cap ladder before any fact is dropped, and the last-resort
|
|
470
|
-
* fallback keeps every requirement id (with `text: "(truncated)"`) while
|
|
471
|
-
* declaring the degradation, so the planner records targeted evidence gaps
|
|
472
|
-
* instead of receiving a silently corrupted tail.
|
|
473
|
-
*/
|
|
474
|
-
export function renderFrontendPlanInputContext(input) {
|
|
475
|
-
const committedFacts = (records) => records.flatMap((record) => record.phase === "committed" && planInputRecord(record.fact)
|
|
476
|
-
? [record.fact]
|
|
477
|
-
: []);
|
|
478
|
-
const contractFacts = committedFacts(input.contractRecords);
|
|
479
|
-
const scoutFacts = committedFacts(input.scoutRecords);
|
|
480
|
-
const requirements = contractFacts
|
|
481
|
-
.filter((fact) => fact.kind === "requirement" && fact.origin === "contract")
|
|
482
|
-
.map((fact) => ({
|
|
483
|
-
id: planInputText(fact.id, 80),
|
|
484
|
-
text: planInputText(fact.text),
|
|
485
|
-
sourceFragmentIds: planInputStrings(fact.sourceFragmentIds),
|
|
486
|
-
}))
|
|
487
|
-
.filter((fact) => fact.id !== undefined);
|
|
488
|
-
const targetSurface = scoutFacts
|
|
489
|
-
.filter((fact) => fact.kind === "target-surface" && fact.origin === "scout")
|
|
490
|
-
.map((fact) => ({
|
|
491
|
-
completeness: fact.completeness,
|
|
492
|
-
entrypoint: fact.entrypoint,
|
|
493
|
-
routeOrMount: fact.routeOrMount,
|
|
494
|
-
implementationPaths: fact.implementationPaths,
|
|
495
|
-
testPaths: fact.testPaths,
|
|
496
|
-
dataSource: fact.dataSource,
|
|
497
|
-
allowedPathConflicts: fact.allowedPathConflicts,
|
|
498
|
-
unresolvedPaths: fact.unresolvedPaths,
|
|
499
|
-
}));
|
|
500
|
-
const designEvidence = scoutFacts
|
|
501
|
-
.filter((fact) => fact.kind === "design-evidence" && fact.origin === "scout")
|
|
502
|
-
.map((fact) => ({
|
|
503
|
-
source: fact.source,
|
|
504
|
-
paths: fact.paths,
|
|
505
|
-
conflicts: fact.conflicts,
|
|
506
|
-
}));
|
|
507
|
-
// Reviewer-rubric scaffold: the design reviewer re-runs the design-policy
|
|
508
|
-
// checks on the committed facts, so publish the checklist to the producer.
|
|
509
|
-
// Requirements whose contract evidence expects behavioural verification are
|
|
510
|
-
// enumerated explicitly — those are the slots the reviewer finds missing
|
|
511
|
-
// when the plan models interactions ad hoc (r8/r9 findings).
|
|
512
|
-
const behaviorRequiredIds = requirements
|
|
513
|
-
.filter((requirement) => {
|
|
514
|
-
const fact = contractFacts.find((candidate) => candidate.kind === "requirement" &&
|
|
515
|
-
candidate.origin === "contract" &&
|
|
516
|
-
candidate.id === requirement.id);
|
|
517
|
-
const evidence = fact?.evidence;
|
|
518
|
-
return evidence?.behavior === "required";
|
|
519
|
-
})
|
|
520
|
-
.map((requirement) => requirement.id);
|
|
521
|
-
const serializeAtCap = (cap) => JSON.stringify({
|
|
522
|
-
requirements: requirements.map((requirement) => ({
|
|
523
|
-
id: requirement.id,
|
|
524
|
-
text: planInputText(requirement.text, cap.text),
|
|
525
|
-
sourceFragmentIds: planInputStrings(requirement.sourceFragmentIds).slice(0, cap.array),
|
|
526
|
-
})),
|
|
527
|
-
targetSurface: targetSurface.map((surface) => ({
|
|
528
|
-
completeness: planInputText(surface.completeness, 32),
|
|
529
|
-
entrypoint: planInputText(surface.entrypoint, cap.text),
|
|
530
|
-
routeOrMount: planInputText(surface.routeOrMount, cap.text),
|
|
531
|
-
implementationPaths: planInputStrings(surface.implementationPaths).slice(0, cap.array),
|
|
532
|
-
testPaths: planInputStrings(surface.testPaths).slice(0, cap.array),
|
|
533
|
-
dataSource: planInputText(surface.dataSource, cap.text),
|
|
534
|
-
allowedPathConflicts: planInputStrings(surface.allowedPathConflicts).slice(0, cap.array),
|
|
535
|
-
unresolvedPaths: planInputStrings(surface.unresolvedPaths).slice(0, cap.array),
|
|
536
|
-
})),
|
|
537
|
-
designEvidence: designEvidence.map((evidence) => ({
|
|
538
|
-
source: planInputText(evidence.source, cap.text),
|
|
539
|
-
paths: planInputStrings(evidence.paths).slice(0, cap.array),
|
|
540
|
-
conflicts: planInputStrings(evidence.conflicts).slice(0, cap.array),
|
|
541
|
-
})),
|
|
542
|
-
});
|
|
543
|
-
let serialized = serializeAtCap(FRONTEND_PLAN_INPUT_CAP_LADDER[0]);
|
|
544
|
-
for (const cap of FRONTEND_PLAN_INPUT_CAP_LADDER.slice(1)) {
|
|
545
|
-
if (serialized.length <= FRONTEND_PLAN_INPUT_MAX_CHARS)
|
|
546
|
-
break;
|
|
547
|
-
serialized = serializeAtCap(cap);
|
|
548
|
-
}
|
|
549
|
-
if (serialized.length > FRONTEND_PLAN_INPUT_MAX_CHARS) {
|
|
550
|
-
// Last resort: keep every requirement id (ids are short and the plan
|
|
551
|
-
// prompt separately lists them) but drop their texts, shrink scout facts
|
|
552
|
-
// to the minimum, and declare the degradation instead of corrupting JSON.
|
|
553
|
-
let fallback = {
|
|
554
|
-
degraded: "requirement-texts-truncated",
|
|
555
|
-
requirements: requirements.map((requirement) => ({
|
|
556
|
-
id: requirement.id,
|
|
557
|
-
text: "(truncated)",
|
|
558
|
-
})),
|
|
559
|
-
targetSurface: targetSurface.map((surface) => ({
|
|
560
|
-
completeness: planInputText(surface.completeness, 32),
|
|
561
|
-
implementationPaths: planInputStrings(surface.implementationPaths).slice(0, FRONTEND_PLAN_INPUT_CAP_LADDER[3].array),
|
|
562
|
-
})),
|
|
563
|
-
designEvidence: [],
|
|
564
|
-
};
|
|
565
|
-
let bounded = JSON.stringify(fallback);
|
|
566
|
-
let keep = fallback.requirements.length;
|
|
567
|
-
while (bounded.length > FRONTEND_PLAN_INPUT_MAX_CHARS &&
|
|
568
|
-
keep > 0) {
|
|
569
|
-
keep = Math.max(0, Math.floor(keep / 2));
|
|
570
|
-
fallback = { ...fallback, requirements: fallback.requirements.slice(0, keep) };
|
|
571
|
-
bounded = JSON.stringify({
|
|
572
|
-
...fallback,
|
|
573
|
-
requirementIdsTruncated: keep < requirements.length,
|
|
574
|
-
});
|
|
575
|
-
}
|
|
576
|
-
serialized = bounded;
|
|
577
|
-
}
|
|
578
|
-
const checklistLines = [
|
|
579
|
-
"1. Every interaction you record needs a uiComponentChoices entry whose purpose equals the interaction name, or one decision=reuse-existing choice covering behavioural interactions.",
|
|
580
|
-
"2. Every applicable UI state needs a purpose-matching component choice or a stylingStrategy.",
|
|
581
|
-
"3. Every requirement marked (behavior) below needs modelled interactions plus at least one verification target that references it.",
|
|
582
|
-
"4. targets.files must name the concrete deliverable files; never leave the scope broader than the frozen requirements state.",
|
|
583
|
-
"5. Verification targets may only reference UI states and requirements you actually recorded (the record_* tools reject unknown references).",
|
|
584
|
-
`Requirements requiring behavioural coverage: ${behaviorRequiredIds.length > 0 ? behaviorRequiredIds.join(", ") : "(none)"}`,
|
|
585
|
-
"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.",
|
|
586
|
-
...[...input.componentSourceCitations ?? []]
|
|
587
|
-
.filter(([id]) => behaviorRequiredIds.includes(id))
|
|
588
|
-
.flatMap(([id, citations]) => citations.map((citation) => ` ${id} + ${citation.fragmentId} → ${citation.section}${citation.line ? ` (line ${citation.line})` : ""}`)),
|
|
589
|
-
];
|
|
590
|
-
return [
|
|
591
|
-
"<frontend_plan_input>",
|
|
592
|
-
"Committed Contract/Scout facts, compiled by the runner. Treat them as the complete planning evidence.",
|
|
593
|
-
serialized,
|
|
594
|
-
"Do not read upstream artifacts, task sources, or repository files. If this input cannot support a decision, record a genuine evidence gap.",
|
|
595
|
-
"</frontend_plan_input>",
|
|
596
|
-
"<plan_review_checklist>",
|
|
597
|
-
"The design reviewer re-runs these exact checks on the committed facts; satisfy every line before finalize_plan:",
|
|
598
|
-
...checklistLines,
|
|
599
|
-
"</plan_review_checklist>",
|
|
600
|
-
].join("\n");
|
|
601
|
-
}
|
|
602
|
-
export async function buildFrontendPlanInputContext(runDir, componentSourceCitations) {
|
|
603
|
-
const contractFactsPath = path.join(runDir, "frontend-contract-pi", "contract-typed-facts.jsonl");
|
|
604
|
-
const scoutFactsPath = path.join(runDir, "frontend-scout-pi", "scout-typed-facts.jsonl");
|
|
605
|
-
let contractRecords;
|
|
606
|
-
let scoutRecords;
|
|
607
|
-
try {
|
|
608
|
-
[contractRecords, scoutRecords] = await Promise.all([
|
|
609
|
-
readTypedEventStoreFromJsonl(contractFactsPath),
|
|
610
|
-
readTypedEventStoreFromJsonl(scoutFactsPath),
|
|
611
|
-
]);
|
|
612
|
-
}
|
|
613
|
-
catch (error) {
|
|
614
|
-
// A missing/unreadable upstream store is a broken pipeline, not an
|
|
615
|
-
// evidence gap; fail before spending a model turn on a prompt that
|
|
616
|
-
// forbids reading anything.
|
|
617
|
-
throw new Error(`frontend-plan-input-unavailable: cannot read committed typed facts (${error instanceof Error ? error.message : String(error)})`);
|
|
618
|
-
}
|
|
619
|
-
const committedCount = [...contractRecords, ...scoutRecords].filter((record) => record.phase === "committed").length;
|
|
620
|
-
if (committedCount === 0) {
|
|
621
|
-
throw new Error(`frontend-plan-input-unavailable: no committed Contract/Scout facts in ${contractFactsPath} / ${scoutFactsPath}`);
|
|
622
|
-
}
|
|
623
|
-
return renderFrontendPlanInputContext({
|
|
624
|
-
contractRecords,
|
|
625
|
-
scoutRecords,
|
|
626
|
-
componentSourceCitations,
|
|
627
|
-
});
|
|
628
|
-
}
|
|
629
|
-
export function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCategory, previousProtocolReason, recoveryTargetPaths, recoveryDiagnostics, frontendPlanRetryStep) {
|
|
240
|
+
function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCategory, previousProtocolReason, recoveryTargetPaths, recoveryDiagnostics) {
|
|
630
241
|
if (attemptNumber <= 1)
|
|
631
242
|
return basePrompt;
|
|
632
|
-
// Context overflow is a transport/session failure, not a plan-protocol
|
|
633
|
-
// failure. It must take precedence over the plan retry ladder so every
|
|
634
|
-
// retry, including a repeated plan overflow, starts from the compact
|
|
635
|
-
// envelope rather than re-sending the original prompt.
|
|
636
|
-
if (previousFailureCategory === "context-overflow") {
|
|
637
|
-
return [
|
|
638
|
-
buildContextOverflowRetryPrompt(task, basePrompt),
|
|
639
|
-
"",
|
|
640
|
-
"<retry_instruction>",
|
|
641
|
-
"The previous attempt exceeded the provider context window. This retry starts a fresh session: keep the evidence surface narrow.",
|
|
642
|
-
"Do not re-read task sources, upstream stdout, skills, or full diff artifacts that are already represented by the canonical contract/review context. Read the smallest relevant artifact first, then only the specific source file or diff fragment needed to decide a finding.",
|
|
643
|
-
"Do not read artifacts/diff_patch.patch in full. For frontend review, read the bounded diff summary and then at most the specific per-file diff fragments it identifies, in part order.",
|
|
644
|
-
"Use concise tool calls and finish the required terminal/output protocol as soon as the evidence is sufficient.",
|
|
645
|
-
"</retry_instruction>",
|
|
646
|
-
].join("\n");
|
|
647
|
-
}
|
|
648
|
-
if (frontendPlanRetryStep === "compact-terminal-first") {
|
|
649
|
-
return [
|
|
650
|
-
basePrompt,
|
|
651
|
-
"",
|
|
652
|
-
"<retry_instruction>",
|
|
653
|
-
"Frontend plan retry ladder step: compact-terminal-first.",
|
|
654
|
-
"Do NOT rewrite the narrative. Only adopt already staged/quarantined fresh facts, fill the missing required facts, then call finalize_plan exactly once.",
|
|
655
|
-
"Keep the existing committed ledger intact; do not re-derive already committed facts.",
|
|
656
|
-
"</retry_instruction>",
|
|
657
|
-
].join("\n");
|
|
658
|
-
}
|
|
659
|
-
if (frontendPlanRetryStep === "bounded-tool-only") {
|
|
660
|
-
return [
|
|
661
|
-
basePrompt,
|
|
662
|
-
"",
|
|
663
|
-
"<retry_instruction>",
|
|
664
|
-
"Frontend plan retry ladder step: bounded-tool-only.",
|
|
665
|
-
"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.",
|
|
666
|
-
"Do not expand scope or re-derive already committed facts.",
|
|
667
|
-
"</retry_instruction>",
|
|
668
|
-
].join("\n");
|
|
669
|
-
}
|
|
670
|
-
if (frontendPlanRetryStep === "backup-model") {
|
|
671
|
-
return [
|
|
672
|
-
basePrompt,
|
|
673
|
-
"",
|
|
674
|
-
"<retry_instruction>",
|
|
675
|
-
"Frontend plan retry ladder step: backup-model.",
|
|
676
|
-
"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.",
|
|
677
|
-
"</retry_instruction>",
|
|
678
|
-
].join("\n");
|
|
679
|
-
}
|
|
680
243
|
if (previousFailureCategory === "protocol-invalid" &&
|
|
681
244
|
task.outputProtocol &&
|
|
682
245
|
previousProtocolReason) {
|
|
@@ -686,65 +249,9 @@ export function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFail
|
|
|
686
249
|
buildProtocolRetryInstruction(task.outputProtocol, previousProtocolReason),
|
|
687
250
|
].join("\n");
|
|
688
251
|
}
|
|
689
|
-
if (previousFailureCategory === "review-terminal-missing") {
|
|
690
|
-
return [
|
|
691
|
-
basePrompt,
|
|
692
|
-
"",
|
|
693
|
-
"<retry_instruction>",
|
|
694
|
-
"The review emitted a verdict in response text but never committed the authoritative typed terminal tool call (approve_review / request_review_changes). The response text is NOT the authority: no branch or gate reads it.",
|
|
695
|
-
"Call exactly one typed terminal tool to finish: approve_review (implementation passes, no Critical/Important findings) or request_review_changes (with typed issueCategory, at least one evidenceRef, and non-empty findings). Do not repeat the review analysis; commit the terminal tool once and stop.",
|
|
696
|
-
"</retry_instruction>",
|
|
697
|
-
].join("\n");
|
|
698
|
-
}
|
|
699
|
-
if (previousFailureCategory === "read-burst") {
|
|
700
|
-
if (task.id !== FRONTEND_PLAN_NODE_ID) {
|
|
701
|
-
return [
|
|
702
|
-
basePrompt,
|
|
703
|
-
"",
|
|
704
|
-
"<retry_instruction>",
|
|
705
|
-
"The previous attempt exceeded its read budget. Start a fresh, evidence-minimal pass: do not re-read task sources, upstream stdout, skills, or full diff artifacts already represented by a canonical context artifact.",
|
|
706
|
-
"Read the smallest relevant summary first, then only the specific source file or diff fragment needed for the required decision. Finish the required terminal/output protocol as soon as evidence is sufficient.",
|
|
707
|
-
"</retry_instruction>",
|
|
708
|
-
].join("\n");
|
|
709
|
-
}
|
|
710
|
-
return [
|
|
711
|
-
basePrompt,
|
|
712
|
-
"",
|
|
713
|
-
"<retry_instruction>",
|
|
714
|
-
"Previous plan attempt issued too many read-only tool calls (read/grep/ls/find) and blew up the context window. Trust the upstream frontend-contract-pi typed requirement facts and frontend-scout-pi target surface already provided — do NOT re-read contract/scout stdout, PRD/source files, or component sources you already inspected.",
|
|
715
|
-
"Minimize discovery reads: only read what you genuinely need, once. Commit record_plan_requirement / record_plan_verification_target / record_* facts directly from the facts already in context (one tool call per message), then call finalize_plan exactly once.",
|
|
716
|
-
"</retry_instruction>",
|
|
717
|
-
].join("\n");
|
|
718
|
-
}
|
|
719
252
|
if (previousFailureCategory === "invalid-output" &&
|
|
720
253
|
task.structuredContractOutput &&
|
|
721
254
|
previousProtocolReason) {
|
|
722
|
-
// The frontend plan node's compile authority is the committed typed
|
|
723
|
-
// ledger, not a fenced JSON text artifact: its retry guidance must
|
|
724
|
-
// direct the model to re-commit corrected record_* facts and
|
|
725
|
-
// finalize_plan. The legacy full-contract JSON guidance below applies
|
|
726
|
-
// only to nodes whose authority is still a text contract artifact.
|
|
727
|
-
if (task.structuredContractOutput.schemaId ===
|
|
728
|
-
"frontend-implementation-contract-plan-patch-v1") {
|
|
729
|
-
const splitGuidance = /write-set-too-large|split the task/.test(previousProtocolReason ?? "")
|
|
730
|
-
? [
|
|
731
|
-
"",
|
|
732
|
-
"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.",
|
|
733
|
-
]
|
|
734
|
-
: [];
|
|
735
|
-
return [
|
|
736
|
-
basePrompt,
|
|
737
|
-
"",
|
|
738
|
-
"<retry_instruction>",
|
|
739
|
-
"Previous plan ledger facts failed canonical contract validation:",
|
|
740
|
-
previousProtocolReason,
|
|
741
|
-
"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.",
|
|
742
|
-
"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.",
|
|
743
|
-
...frontendPlanValidationRetryGuidance(previousProtocolReason),
|
|
744
|
-
...splitGuidance,
|
|
745
|
-
"</retry_instruction>",
|
|
746
|
-
].join("\n");
|
|
747
|
-
}
|
|
748
255
|
return [
|
|
749
256
|
basePrompt,
|
|
750
257
|
"",
|
|
@@ -756,31 +263,8 @@ export function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFail
|
|
|
756
263
|
"</retry_instruction>",
|
|
757
264
|
].join("\n");
|
|
758
265
|
}
|
|
759
|
-
if (previousFailureCategory === "invalid-output" &&
|
|
760
|
-
task.id === "frontend-scout-pi") {
|
|
761
|
-
return [
|
|
762
|
-
basePrompt,
|
|
763
|
-
"",
|
|
764
|
-
"<retry_instruction>",
|
|
765
|
-
"The previous Scout attempt did not commit a complete, runtime-evidenced target surface.",
|
|
766
|
-
"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. If ownership truly cannot be established, record completeness=blocked with each unresolved path; do not make Plan discover it.",
|
|
767
|
-
"</retry_instruction>",
|
|
768
|
-
].join("\n");
|
|
769
|
-
}
|
|
770
266
|
if (previousFailureCategory === "structured-output-truncated" &&
|
|
771
267
|
task.structuredContractOutput) {
|
|
772
|
-
if (task.structuredContractOutput.schemaId ===
|
|
773
|
-
"frontend-implementation-contract-plan-patch-v1") {
|
|
774
|
-
return [
|
|
775
|
-
basePrompt,
|
|
776
|
-
"",
|
|
777
|
-
"<retry_instruction>",
|
|
778
|
-
"Previous attempt was truncated by the provider (stopReason=length) before the plan facts were fully committed.",
|
|
779
|
-
"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.",
|
|
780
|
-
"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.",
|
|
781
|
-
"</retry_instruction>",
|
|
782
|
-
].join("\n");
|
|
783
|
-
}
|
|
784
268
|
return [
|
|
785
269
|
basePrompt,
|
|
786
270
|
"",
|
|
@@ -867,18 +351,6 @@ export function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFail
|
|
|
867
351
|
}
|
|
868
352
|
if (task.outputMode === "structured-required") {
|
|
869
353
|
if (task.structuredContractOutput) {
|
|
870
|
-
if (task.structuredContractOutput.schemaId ===
|
|
871
|
-
"frontend-implementation-contract-plan-patch-v1") {
|
|
872
|
-
return [
|
|
873
|
-
basePrompt,
|
|
874
|
-
"",
|
|
875
|
-
"<retry_instruction>",
|
|
876
|
-
"Previous attempt exceeded the structured output size limit.",
|
|
877
|
-
"Re-commit corrected record_* facts and call finalize_plan exactly once; the committed typed ledger is the only compile authority.",
|
|
878
|
-
"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.",
|
|
879
|
-
"</retry_instruction>",
|
|
880
|
-
].join("\n");
|
|
881
|
-
}
|
|
882
354
|
return [
|
|
883
355
|
basePrompt,
|
|
884
356
|
"",
|
|
@@ -917,10 +389,7 @@ function promptRestartCandidateForAttempt(input) {
|
|
|
917
389
|
}
|
|
918
390
|
const appendedPrefix = `${input.baseRuntimePrompt}\n\n`;
|
|
919
391
|
if (input.attemptPrompt.startsWith(appendedPrefix)) {
|
|
920
|
-
return [
|
|
921
|
-
input.taskPrompt,
|
|
922
|
-
input.attemptPrompt.slice(appendedPrefix.length),
|
|
923
|
-
]
|
|
392
|
+
return [input.taskPrompt, input.attemptPrompt.slice(appendedPrefix.length)]
|
|
924
393
|
.filter(Boolean)
|
|
925
394
|
.join("\n\n");
|
|
926
395
|
}
|
|
@@ -956,6 +425,7 @@ export async function buildNodePromptWithResolvedSkillInstructions(spec, task, u
|
|
|
956
425
|
spec,
|
|
957
426
|
task,
|
|
958
427
|
upstream,
|
|
428
|
+
runDir: options?.runDir,
|
|
959
429
|
resolvedSkills: skillNames,
|
|
960
430
|
resolvedSkillInstructions,
|
|
961
431
|
maxUpstreamChars: policy.resolveMaxUpstreamChars(task),
|
|
@@ -1136,7 +606,11 @@ export async function executeDagNode(input) {
|
|
|
1136
606
|
await notifyNodeObserver(input.observer, "onNodeFinish", nodeId, state);
|
|
1137
607
|
};
|
|
1138
608
|
if (task.finalWriteSetApproval) {
|
|
1139
|
-
const authorization = parseAndValidateFinalWriteSetApproval({
|
|
609
|
+
const authorization = parseAndValidateFinalWriteSetApproval({
|
|
610
|
+
task,
|
|
611
|
+
spec,
|
|
612
|
+
state,
|
|
613
|
+
});
|
|
1140
614
|
if (!authorization.ok) {
|
|
1141
615
|
node.runtimeWriteAuthorization = {
|
|
1142
616
|
schemaVersion: 1,
|
|
@@ -1225,6 +699,7 @@ export async function executeDagNode(input) {
|
|
|
1225
699
|
spec,
|
|
1226
700
|
task,
|
|
1227
701
|
upstream: state.nodes,
|
|
702
|
+
runDir,
|
|
1228
703
|
snapshot: skillSnapshot,
|
|
1229
704
|
projectGovernanceContext,
|
|
1230
705
|
});
|
|
@@ -1315,6 +790,7 @@ export async function executeDagNode(input) {
|
|
|
1315
790
|
else {
|
|
1316
791
|
({ prompt, resolvedSkills } =
|
|
1317
792
|
await buildNodePromptWithResolvedSkillInstructions(spec, task, state.nodes, cwd, {
|
|
793
|
+
runDir,
|
|
1318
794
|
projectGovernanceContext,
|
|
1319
795
|
convergenceFeedback: deriveConvergenceFeedback(state),
|
|
1320
796
|
}));
|
|
@@ -1326,41 +802,9 @@ export async function executeDagNode(input) {
|
|
|
1326
802
|
await failSkillSnapshot(error);
|
|
1327
803
|
return;
|
|
1328
804
|
}
|
|
1329
|
-
if (isFrontendPlanLadderTask(task)) {
|
|
1330
|
-
let planInputContext;
|
|
1331
|
-
try {
|
|
1332
|
-
const componentSourceCitations = await resolveComponentSourceCitations(spec, cwd);
|
|
1333
|
-
planInputContext = await buildFrontendPlanInputContext(runDir, componentSourceCitations);
|
|
1334
|
-
}
|
|
1335
|
-
catch (error) {
|
|
1336
|
-
await failBeforePrompt(error, "frontend-plan-input-unavailable");
|
|
1337
|
-
return;
|
|
1338
|
-
}
|
|
1339
|
-
prompt = `${prompt}\n\n${planInputContext}`;
|
|
1340
|
-
}
|
|
1341
|
-
if (isFrontendContractTypedNode(task)) {
|
|
1342
|
-
// Extreme-environment input handoff: compile the ledger's canonical
|
|
1343
|
-
// requirements into a bounded block so the contract node never re-reads
|
|
1344
|
-
// the raw source (mirrors the plan node's compiled input; its tool set
|
|
1345
|
-
// is record_* + finalize_contract, no read tools).
|
|
1346
|
-
let contractInputContext;
|
|
1347
|
-
try {
|
|
1348
|
-
const ledgerPath = resolveFrontendLedgerPath(spec.sourceBinding, cwd);
|
|
1349
|
-
contractInputContext = await buildFrontendContractInputContext({
|
|
1350
|
-
ledgerPath,
|
|
1351
|
-
});
|
|
1352
|
-
prompt = `${prompt}\n\n${contractInputContext}`;
|
|
1353
|
-
}
|
|
1354
|
-
catch (error) {
|
|
1355
|
-
// Ledger unreadable is a broken pipeline: fail before spending a
|
|
1356
|
-
// model turn on a prompt that forbids reading anything.
|
|
1357
|
-
await failBeforePrompt(error, "frontend-contract-input-unavailable");
|
|
1358
|
-
return;
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
805
|
node.resolvedSkills = resolvedSkills;
|
|
1362
806
|
await writeNodeSkillArtifacts(runDir, nodeId, resolvedSkills);
|
|
1363
|
-
|
|
807
|
+
const model = resolveModelForTask(task, spec.executorModels);
|
|
1364
808
|
let thinking;
|
|
1365
809
|
if (task.executor === "pi") {
|
|
1366
810
|
try {
|
|
@@ -1387,9 +831,6 @@ export async function executeDagNode(input) {
|
|
|
1387
831
|
const attempts = [];
|
|
1388
832
|
let totalAttemptWallDurationMs = 0;
|
|
1389
833
|
let totalBackoffMs = 0;
|
|
1390
|
-
const frontendPlanLadderEnabled = isFrontendPlanLadderTask(task);
|
|
1391
|
-
let frontendPlanRetryStep = "normal";
|
|
1392
|
-
const frontendPriorFingerprints = [];
|
|
1393
834
|
const livenessPolicy = resolveLivenessPolicy(spec.defaults?.livenessPolicy, task.livenessPolicy);
|
|
1394
835
|
/**
|
|
1395
836
|
* Attempt-fenced, throttled activity sink. Late events from a previous
|
|
@@ -1433,13 +874,6 @@ export async function executeDagNode(input) {
|
|
|
1433
874
|
const attemptStartedAt = new Date().toISOString();
|
|
1434
875
|
node.currentAttempt = attemptNumber;
|
|
1435
876
|
node.livenessStatus = "active";
|
|
1436
|
-
const beforeCommittedDigest = frontendPlanLadderEnabled
|
|
1437
|
-
? (await readFrontendPlanCommittedSnapshot(runDir, nodeId)).digest
|
|
1438
|
-
: undefined;
|
|
1439
|
-
if (frontendPlanLadderEnabled &&
|
|
1440
|
-
frontendPlanRetryStep === "backup-model") {
|
|
1441
|
-
model = resolveModelForTask(task, spec.executorModels);
|
|
1442
|
-
}
|
|
1443
877
|
// Capture the session-events.jsonl length BEFORE the executor appends this
|
|
1444
878
|
// attempt's events, so the post-attempt repair tool audit can be scoped to
|
|
1445
879
|
// exactly this attempt's segment (B1: earlier attempts legitimately use
|
|
@@ -1468,15 +902,8 @@ export async function executeDagNode(input) {
|
|
|
1468
902
|
(acc[i.path] ??= []).push(i.detail);
|
|
1469
903
|
return acc;
|
|
1470
904
|
}, {});
|
|
1471
|
-
let attemptPrompt = buildAttemptPrompt(task, prompt, attemptNumber, previousFailureCategory, previousProtocolReason, recoveryTargetPaths, recoveryDiagnostics
|
|
905
|
+
let attemptPrompt = buildAttemptPrompt(task, prompt, attemptNumber, previousFailureCategory, previousProtocolReason, recoveryTargetPaths, recoveryDiagnostics);
|
|
1472
906
|
if (attemptNumber > 1 &&
|
|
1473
|
-
// The plan node (frontend-plan-pi) is a typed-facts ladder task:
|
|
1474
|
-
// its retry is driven by the §5.1 ladder (compact-terminal-first
|
|
1475
|
-
// / backup-model) and its compile authority is the committed
|
|
1476
|
-
// ledger — never the legacy frozen fenced-JSON repair. The
|
|
1477
|
-
// frozen repair path below applies only to non-ladder
|
|
1478
|
-
// structured nodes whose output authority is a text artifact.
|
|
1479
|
-
!frontendPlanLadderEnabled &&
|
|
1480
907
|
isFrontendStructuredRepairSchemaId(task.structuredContractOutput?.schemaId) &&
|
|
1481
908
|
isStructuredRepairableFailureCategory(previousFailureCategory) &&
|
|
1482
909
|
(await hasNonEmptyStructuredCandidate({
|
|
@@ -1527,6 +954,7 @@ export async function executeDagNode(input) {
|
|
|
1527
954
|
model,
|
|
1528
955
|
...(thinking ? { thinking } : {}),
|
|
1529
956
|
prompt: attemptPrompt,
|
|
957
|
+
resolvedSkills: resolvedSkills.map((skill) => skill.name),
|
|
1530
958
|
attempt: attemptNumber,
|
|
1531
959
|
reportActivity,
|
|
1532
960
|
timeoutMs: livenessPolicy.absoluteMaxWallClockMs,
|
|
@@ -1564,9 +992,7 @@ export async function executeDagNode(input) {
|
|
|
1564
992
|
...result,
|
|
1565
993
|
ok: false,
|
|
1566
994
|
failureCategory: GOVERNANCE_BLOCKED_CATEGORY,
|
|
1567
|
-
stderr: [result.stderr, audit.reason]
|
|
1568
|
-
.filter(Boolean)
|
|
1569
|
-
.join("\n"),
|
|
995
|
+
stderr: [result.stderr, audit.reason].filter(Boolean).join("\n"),
|
|
1570
996
|
};
|
|
1571
997
|
}
|
|
1572
998
|
}
|
|
@@ -1657,28 +1083,6 @@ export async function executeDagNode(input) {
|
|
|
1657
1083
|
prompt: promptRestartCandidate,
|
|
1658
1084
|
});
|
|
1659
1085
|
}
|
|
1660
|
-
// C: contract incremental-progress guard. A failed contract attempt that
|
|
1661
|
-
// committed zero record_* submissions is a "no-progress" failure, not an
|
|
1662
|
-
// opaque timeout/error: normalize it to empty-output so the retryPolicy
|
|
1663
|
-
// retries with the incremental-commit discipline instead of burning the
|
|
1664
|
-
// remaining attempts on the same stalled behavior.
|
|
1665
|
-
if (isFrontendContractTypedNode(task) &&
|
|
1666
|
-
!result.ok &&
|
|
1667
|
-
retryPolicy !== undefined) {
|
|
1668
|
-
const submissions = await countContractRecordSubmissions(runDir, nodeId);
|
|
1669
|
-
if (submissions === 0) {
|
|
1670
|
-
result = {
|
|
1671
|
-
...result,
|
|
1672
|
-
failureCategory: "empty-output",
|
|
1673
|
-
stderr: [
|
|
1674
|
-
result.stderr,
|
|
1675
|
-
"contract attempt failed with zero record_* submissions; retrying with incremental-commit discipline (one record_* call per message, starting from the first tool call)",
|
|
1676
|
-
]
|
|
1677
|
-
.filter(Boolean)
|
|
1678
|
-
.join("\n"),
|
|
1679
|
-
};
|
|
1680
|
-
}
|
|
1681
|
-
}
|
|
1682
1086
|
const attemptRecord = {
|
|
1683
1087
|
attempt: attemptNumber,
|
|
1684
1088
|
startedAt: attemptStartedAt,
|
|
@@ -1705,60 +1109,6 @@ export async function executeDagNode(input) {
|
|
|
1705
1109
|
await writeDagNodeJsonArtifact(runDir, nodeId, `attempt-${attemptNumber}.json`, attemptRecord);
|
|
1706
1110
|
node.attempts = attempts;
|
|
1707
1111
|
}
|
|
1708
|
-
// Frontend plan retry ladder: project the 7-value protocol failure
|
|
1709
|
-
// reason, detect new committed facts, and escalate instead of re-running.
|
|
1710
|
-
if (frontendPlanLadderEnabled && !result.ok) {
|
|
1711
|
-
const afterSnapshot = await readFrontendPlanCommittedSnapshot(runDir, nodeId);
|
|
1712
|
-
const hasNewCommittedFact = beforeCommittedDigest !== undefined &&
|
|
1713
|
-
afterSnapshot.digest !== beforeCommittedDigest;
|
|
1714
|
-
const protocolReason = projectFrontendNodeProtocolFailureReason({
|
|
1715
|
-
failureCategory: result.failureCategory,
|
|
1716
|
-
stopReason: result.stopReason,
|
|
1717
|
-
assistantText: result.assistantText ?? result.stdout,
|
|
1718
|
-
hasTerminalFact: afterSnapshot.hasTerminalFact,
|
|
1719
|
-
});
|
|
1720
|
-
const fingerprint = computeNormalizedFailureFingerprint({
|
|
1721
|
-
failureOwner: "plan",
|
|
1722
|
-
protocolFailureReason: protocolReason ?? "",
|
|
1723
|
-
});
|
|
1724
|
-
const backupRoute = resolveFrontendPlanBackupRoute(protocolReason);
|
|
1725
|
-
const nextStep = resolveFrontendPlanRetryStep({
|
|
1726
|
-
currentStep: frontendPlanRetryStep,
|
|
1727
|
-
protocolFailureReason: protocolReason,
|
|
1728
|
-
hasNewCommittedFact,
|
|
1729
|
-
fingerprint,
|
|
1730
|
-
priorFingerprints: frontendPriorFingerprints,
|
|
1731
|
-
backupRouteAvailable: backupRoute.ok,
|
|
1732
|
-
});
|
|
1733
|
-
frontendPriorFingerprints.push(fingerprint);
|
|
1734
|
-
frontendPlanRetryStep = nextStep;
|
|
1735
|
-
if (nextStep === "unsupported-provider-capability") {
|
|
1736
|
-
result = {
|
|
1737
|
-
...result,
|
|
1738
|
-
ok: false,
|
|
1739
|
-
failureCategory: "unsupported-provider-capability",
|
|
1740
|
-
stderr: [
|
|
1741
|
-
result.stderr,
|
|
1742
|
-
"frontend plan retry ladder: no compatible provider capability route for the protocol failure reason",
|
|
1743
|
-
]
|
|
1744
|
-
.filter(Boolean)
|
|
1745
|
-
.join("\n"),
|
|
1746
|
-
};
|
|
1747
|
-
}
|
|
1748
|
-
else if (nextStep === "non-converging") {
|
|
1749
|
-
result = {
|
|
1750
|
-
...result,
|
|
1751
|
-
ok: false,
|
|
1752
|
-
failureCategory: "non-converging",
|
|
1753
|
-
stderr: [
|
|
1754
|
-
result.stderr,
|
|
1755
|
-
"frontend plan retry ladder: repeated failure fingerprint with no new committed fact",
|
|
1756
|
-
]
|
|
1757
|
-
.filter(Boolean)
|
|
1758
|
-
.join("\n"),
|
|
1759
|
-
};
|
|
1760
|
-
}
|
|
1761
|
-
}
|
|
1762
1112
|
// Reflect the latest attempt on the node so progress is observable,
|
|
1763
1113
|
// but keep node.status RUNNING while retry is still possible.
|
|
1764
1114
|
node.durationMs = durationBetween(node.startedAt, attemptFinishedAt);
|
|
@@ -1965,6 +1315,13 @@ export async function executeDagNode(input) {
|
|
|
1965
1315
|
node.structuredArtifactSha256 = pendingStructuredArtifact.sha256;
|
|
1966
1316
|
node.structuredArtifactSchemaId = pendingStructuredArtifact.schemaId;
|
|
1967
1317
|
}
|
|
1318
|
+
if ((task.producesArtifacts?.length ?? 0) > 0) {
|
|
1319
|
+
node.declaredArtifacts = await materializeDeclaredArtifactFacts({
|
|
1320
|
+
runDir,
|
|
1321
|
+
task,
|
|
1322
|
+
node,
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1968
1325
|
}
|
|
1969
1326
|
const finishedAt = new Date().toISOString();
|
|
1970
1327
|
node.finishedAt = finishedAt;
|