@tea-agent/loop-agent 0.43.0-next.1 → 0.43.0-next.11
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 +144 -9
- package/dist/application/dag/run-dag.js +5 -0
- package/dist/application/evaluation/budget.js +19 -1
- package/dist/application/task-lifecycle/advance.js +17 -4
- package/dist/application/task-lifecycle/observe.js +43 -29
- package/dist/application/task-lifecycle/plan-transitions.js +5 -4
- package/dist/build-stamp.json +3 -3
- package/dist/executors/dag-pi-executor.js +1237 -586
- package/dist/executors/pi-executor.js +22 -1
- package/dist/executors/pi-sdk-executor.js +140 -39
- package/dist/executors/shell-executor.js +8 -4
- package/dist/shared/frontend-execution-policy.js +23 -0
- package/dist/task/config-types.js +4 -0
- package/dist/task/source-prepare/ledger-reconciliation.js +2 -2
- package/dist/task/source-prepare/ledger-review.js +6 -9
- package/dist/task/source-prepare/semantic-intake.js +16 -26
- package/dist/task/source-prepare/source-fidelity-pi.js +28 -9
- package/dist/task/source-references.js +48 -23
- package/dist/worker/console/chat/assistant-content.js +23 -2
- package/dist/worker/console/chat/browser-policy.js +113 -0
- package/dist/worker/console/chat/browser-routes.js +140 -0
- package/dist/worker/console/chat/explore-tools.js +13 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/ask-user-question.js +44 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/explore-tools.js +202 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/goal-tools.js +79 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +86 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/todo-write.js +38 -0
- package/dist/worker/console/chat/pi-runtime/inspection-types.js +28 -0
- package/dist/worker/console/chat/pi-runtime/inspection.js +439 -0
- package/dist/worker/console/chat/pi-runtime/sdk-bindings.js +153 -0
- package/dist/worker/console/chat/pi-runtime/sdk-session.js +5 -0
- package/dist/worker/console/chat/pi-runtime.js +207 -1178
- package/dist/worker/console/chat/routes.js +208 -71
- package/dist/worker/console/chat/scm-routes.js +217 -0
- package/dist/worker/console/chat/scm-service.js +283 -0
- package/dist/worker/console/chat/scm-tools.js +111 -0
- package/dist/worker/console/chat/sdd-data-alignment.js +65 -9
- package/dist/worker/console/chat/session-catalog.js +32 -0
- package/dist/worker/console/chat/session-mode-view.js +49 -0
- package/dist/worker/console/chat/session-mode.js +219 -0
- package/dist/worker/console/chat/session-store.js +22 -6
- package/dist/worker/console/chat/shortcuts.js +6 -0
- package/dist/worker/console/chat/terminal-routes.js +216 -0
- package/dist/worker/console/chat/terminal-sessions.js +348 -0
- package/dist/worker/console/chat/terminal-tools.js +211 -0
- package/dist/worker/console/chat/tool-preview.js +11 -0
- package/dist/worker/console/chat/tools.js +2 -0
- package/dist/worker/console/server.js +2 -28
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-B0Q0nClf.js → abnfDiagram-N423BO3Z-CTSK0xbU.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DCPjC19G.js → arc-_2MfctFP.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CP5n9jmG.js → architectureDiagram-T3A2C74G-DsDe2wTg.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CmaWi_Wg.js → blockDiagram-VBNYF7ZC-Dtz_779U.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Ct5tcMfZ.js → c4Diagram-5PPSVZJV-LaULi-gw.js} +1 -1
- package/dist/worker/console/static/assets/channel-CqS4yBWT.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-C0osm1Zf.js → chunk-2GRJ4B5K-DuHBqqX8.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CDWviED7.js → chunk-2Q5K7J3B-BZgZs1dn.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-mtSk1-j7.js → chunk-5RXB4S5H-tzwmlIO_.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D5J9PHC7.js → chunk-5VM5RSS4-CeYrLkSn.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Begg4WAa.js → chunk-6Q2QTUOP-C92cgilm.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-sF1AEy7T.js → chunk-GF5L2VYU-XOXpGC0F.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BqbsWXZb.js → chunk-JWPE2WC7-CGHbJyCH.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BKJrPcJ6.js → chunk-KBJHAD2P-Bs2-LTzf.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CcwNMRho.js → chunk-RYQCIY6F-rN7up7E4.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BEJi3iIs.js → chunk-XXDRQBXY-B_fWjKpR.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DVOxNOq0.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DVOxNOq0.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-DRLgTvVu.js → cose-bilkent-JH36ORCC-DE3SdJvB.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BoAVG3cs.js → cynefin-VYW2F7L2-B-4yuRAX.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bzol06hR.js → cynefinDiagram-MW4NZA55-TiCPFuO8.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-UmliJM77.js → dagre-VZM6K2ZE-7crzLMzA.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BWLcJkFo.js → diagram-7IWD3JNH-Cvr-x6Fd.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CgApOm-O.js → diagram-B4RE2ZJO-BzcRHUjH.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-Bh3RgTLs.js → diagram-LBJQPF4R-BjkAXilh.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-Dsh-D5nE.js → diagram-Q27KOJAE-BkzyATK7.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-Dkbbpcpb.js → diagram-UB23O5K3-rZuTarYP.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-3EBaA3t3.js → ebnfDiagram-BXEA7PRR-CAYENaN1.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Bq2Dc5ok.js → erDiagram-JOGREHBK-DCEam0Uq.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-De7y55ha.js → flowDiagram-UKHOOZJN-e_dH4wVO.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-fLiDbQRh.js → ganttDiagram-PKOTCBZU-CvZa2zOK.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Du617mp1.js → gitGraphDiagram-DS77QQ5N-CbT5TuKp.js} +1 -1
- package/dist/worker/console/static/assets/index-CUPeoE8W.js +468 -0
- package/dist/worker/console/static/assets/index-SAANIK8g.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BwmlF-XP.js → infoDiagram-6WML65LV-BWzHQa_Z.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-zsGtS59u.js → ishikawaDiagram-WSZJBQD7-BTjlovtU.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DTTPTe3f.js → journeyDiagram-NVQOT4AX-DkvQoCc1.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BkXg9aP-.js → kanban-definition-27J2QSJJ-CHSzGi3W.js} +1 -1
- package/dist/worker/console/static/assets/{linear-7U2ue5IE.js → linear-DZMkTCWH.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BUuGHmWO.js → mermaid.core-BIMhty7A.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-7q6fX0Sv.js → mindmap-definition-FAOFIHXS-Dx-tyMl3.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CkQPJN55.js → pegDiagram-VL7TDLO6-C6G34rbY.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CCal_tfX.js → pieDiagram-7S7Q4E2Y-CHK9XOfm.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B7bpxyAZ.js → quadrantDiagram-CIZ2JOQS-CDARkI0w.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-CJmww7D-.js → railroadDiagram-AXF67PYL-DiuUe0ZQ.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D6ldWJEv.js → requirementDiagram-LRYGKXZP-RE61UR56.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-VGh33I9e.js → sankeyDiagram-W5VNT64P-KDNLHMEG.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CCkDrIjU.js → sequenceDiagram-SI44F4Z6-CeVgXnD9.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-NC8f5Otb.js → sizeCapture-X5ZJPWSS-CXwGyjjG.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-_92ezZdF.js → stateDiagram-OKZ733FA-BjTfozab.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BXoYsyr2.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BdUCGtbP.js → swimlanes-SLNWSIFB-BaXOB8GT.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DfKliS4e.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CdD1H8ia.js → timeline-definition-Z64GVDOM-C3r2hqUU.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-Dkdk3oFo.js → vennDiagram-T6HMQDX7-MWKgl1S9.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CKu2uPPY.js → wardleyDiagram-T6FBY63Y-C9iFcZPk.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-ClqAnuG8.js → xychartDiagram-ELKLHX3M-DqnM24Jp.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/active-turn-tail.js +10 -0
- package/dist/worker/console/static-src/operator-chat/chat-link.js +94 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +122 -10
- package/dist/worker/console/static-src/operator-chat/compaction-message.js +7 -0
- package/dist/worker/console/static-src/operator-chat/pending-user-message.js +27 -15
- package/dist/worker/console/static-src/operator-chat/process-label.js +41 -0
- package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +13 -0
- package/dist/worker/console/static-src/operator-chat/turn-process-disclosure.js +10 -0
- package/dist/worker/console/static-src/operator-chat/use-searchable-hidden.js +51 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +44 -24
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +18 -2
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +41 -8
- package/dist/worker/console/static-src/operator-chat/useComposer.js +26 -11
- package/dist/worker/console/static-src/operator-chat/user-turn-anchor-equality.js +21 -0
- package/dist/worker/console/workspace-context.js +15 -1
- package/dist/worker/observe/node-transparency.js +81 -72
- package/dist/worker/observe/routes.js +20 -1
- package/dist/worker/observe/static/dag-inspector-humanize.js +3 -0
- package/dist/worker/observe/static/dom.js +20 -1
- package/dist/worker/observe/static/format-pool.d.ts +2 -0
- package/dist/worker/observe/static/format-pool.js +6 -0
- package/dist/worker/observe/static/format.js +7 -0
- package/dist/worker/observe/static/inspect-workspace.js +34 -7
- package/dist/worker/observe/static/inspector-submission.js +32 -0
- package/dist/worker/observe/static/kpi.js +1 -0
- package/dist/worker/observe/static/relations.js +2 -0
- package/dist/worker/observe/static/router.js +13 -0
- package/dist/worker/observe/static/run-processing.js +2 -0
- package/dist/worker/observe/static/shell-chrome.js +36 -3
- package/dist/worker/observe/static/state.js +35 -2
- package/dist/worker/observe/static/styles.css +263 -39
- package/dist/worker/observe/static/task-failure-labels.d.ts +4 -0
- package/dist/worker/observe/static/task-failure-labels.js +67 -0
- package/dist/worker/observe/static/task-history.js +12 -0
- package/dist/worker/observe/static/views/batch.js +6 -13
- package/dist/worker/observe/static/views/dag-graph.js +50 -3
- package/dist/worker/observe/static/views/dag-inspector.js +742 -268
- package/dist/worker/observe/static/views/dag-trajectory.js +3 -0
- package/dist/worker/observe/static/views/dag.d.ts +6 -0
- package/dist/worker/observe/static/views/dag.js +49 -11
- package/dist/worker/observe/static/views/dags.js +2 -0
- package/dist/worker/observe/static/views/dashboard.js +21 -12
- package/dist/worker/observe/static/views/failures.js +21 -11
- package/dist/worker/observe/static/views/feature.js +11 -29
- package/dist/worker/observe/static/views/pool.js +37 -28
- package/dist/worker/observe/static/views/run.js +48 -5
- package/dist/worker/observe/static/views/session-timeline.js +189 -240
- package/dist/worker/observe/static/views/task.js +81 -62
- package/dist/workflows/dag/budget-enforcement.js +53 -3
- package/dist/workflows/dag/frontend-capacity.js +9 -0
- package/dist/workflows/dag/frontend-closeout.js +1 -1
- package/dist/workflows/dag/frontend-design-policy.js +2 -1
- package/dist/workflows/dag/frontend-durable-tools.js +193 -0
- package/dist/workflows/dag/frontend-execution-groups.js +24 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +13 -0
- package/dist/workflows/dag/frontend-input-projection.js +76 -0
- package/dist/workflows/dag/frontend-plan-completeness.js +186 -0
- package/dist/workflows/dag/frontend-plan-recovery-policy.js +18 -0
- package/dist/workflows/dag/frontend-plan-render.js +10 -3
- package/dist/workflows/dag/frontend-prewrite-gate.js +3 -3
- package/dist/workflows/dag/frontend-recovery-controller.js +7 -7
- package/dist/workflows/dag/frontend-recovery-lineage.js +13 -0
- package/dist/workflows/dag/frontend-recovery-run.js +4 -0
- package/dist/workflows/dag/frontend-review-context.js +2 -1
- package/dist/workflows/dag/frontend-review-scopes.js +139 -0
- package/dist/workflows/dag/frontend-session-budget.js +249 -0
- package/dist/workflows/dag/frontend-shadow-dual-write.js +20 -2
- package/dist/workflows/dag/frontend-shape-facts.js +12 -2
- package/dist/workflows/dag/frontend-shape.js +30 -1
- package/dist/workflows/dag/frontend-test-execution-evidence.js +3 -2
- package/dist/workflows/dag/frontend-typed-event-store.js +11 -0
- package/dist/workflows/dag/frontend-verification-trace.js +1 -1
- package/dist/workflows/dag/init-hybrid.js +65 -167
- package/dist/workflows/dag/node-execution.js +38 -155
- package/dist/workflows/dag/prompt.js +4 -0
- package/dist/workflows/dag/rerun-plan.js +7 -1
- package/dist/workflows/dag/runner.js +26 -1
- package/dist/workflows/dag/types.js +6 -0
- package/docs/operations/README.md +1 -0
- package/docs/templates/frontend-design-contract.md +4 -4
- package/docs/templates/frontend-implementation-contract.schema.json +34 -2
- package/docs/templates/frontend-implementation-dag.json +14 -13
- package/package.json +6 -3
- package/skills/frontend-bounded-implement/SKILL.md +3 -3
- package/skills/frontend-bounded-implement/references/code-standards.md +3 -3
- package/skills/frontend-contract/SKILL.md +2 -1
- package/skills/frontend-contract/references/contract-protocol.md +22 -4
- package/skills/frontend-design-review/SKILL.md +22 -16
- package/skills/frontend-design-review/references/review-checklist.md +7 -7
- package/skills/frontend-plan/SKILL.md +21 -2
- package/skills/frontend-plan/references/decision-contract.md +42 -23
- package/skills/frontend-review/SKILL.md +22 -29
- package/skills/frontend-review/references/review-findings.md +24 -43
- package/skills/frontend-scout/references/scout-evidence.md +4 -0
- package/dist/worker/console/static/assets/channel-DAS07MdS.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Cwk-5jiW.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Cwk-5jiW.js +0 -1
- package/dist/worker/console/static/assets/index-C0O48S_P.js +0 -449
- package/dist/worker/console/static/assets/index-CzKf4U8P.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DyJg5gzW.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B1cw_0Uy.js +0 -8
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
import { collectFrontendPlanMissingFacts, collectFrontendPlanPhaseMissingFacts, committedFactFromPlanRecord, planFactStringList, planFactScopeIntersects } from "../workflows/dag/frontend-plan-completeness.js";
|
|
2
|
+
export { collectFrontendPlanMissingFacts, collectFrontendPlanPhaseMissingFacts } from "../workflows/dag/frontend-plan-completeness.js";
|
|
3
|
+
import { classifyFrontendPlanRecovery } from "../workflows/dag/frontend-plan-recovery-policy.js";
|
|
4
|
+
import { collectFrontendExecutionGroups, frontendExecutionSchema } from "../workflows/dag/frontend-execution-groups.js";
|
|
5
|
+
import { FRONTEND_SCOPE_TARGET_BYTES, packFrontendInputUnits, parseFrontendInputBlock, projectFrontendContractPrompt, projectFrontendInputScope } from "../workflows/dag/frontend-input-projection.js";
|
|
6
|
+
import { createDurableFrontendTools } from "../workflows/dag/frontend-durable-tools.js";
|
|
7
|
+
import { sha256OfCanonicalJson } from "../task/contract/hash.js";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { createFrontendReviewScopeProtocol, loadFrontendReviewScopes } from "../workflows/dag/frontend-review-scopes.js";
|
|
10
|
+
import { observeFrontendSession } from "../workflows/dag/frontend-session-budget.js";
|
|
1
11
|
import path from "node:path";
|
|
2
12
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
13
|
import { readFile, stat } from "node:fs/promises";
|
|
@@ -454,6 +464,7 @@ export const FRONTEND_CONTRACT_RECORD_TOOL_NAMES = [
|
|
|
454
464
|
"record_split_proposal",
|
|
455
465
|
"record_ui_state",
|
|
456
466
|
"record_required_deliverables",
|
|
467
|
+
"complete_contract_scope",
|
|
457
468
|
];
|
|
458
469
|
export const FRONTEND_CONTRACT_TERMINAL_TOOL_NAMES = [
|
|
459
470
|
"finalize_contract",
|
|
@@ -471,9 +482,11 @@ export const FRONTEND_PLAN_RECORD_TOOL_NAMES = [
|
|
|
471
482
|
"record_state_flow",
|
|
472
483
|
"record_data_flow",
|
|
473
484
|
"record_mock_api",
|
|
485
|
+
"record_mock_endpoint",
|
|
474
486
|
"record_design_deviation",
|
|
475
487
|
"record_dependency",
|
|
476
488
|
"record_plan_requirement",
|
|
489
|
+
"record_plan_group_coverage",
|
|
477
490
|
"record_plan_verification_target",
|
|
478
491
|
"record_plan_evidence_gap",
|
|
479
492
|
];
|
|
@@ -548,6 +561,8 @@ export function resolveDagPiToolNames(task) {
|
|
|
548
561
|
if (isFrontendReviewTypedTerminalNode(task)) {
|
|
549
562
|
return [
|
|
550
563
|
...DAG_PI_READONLY_TOOLS,
|
|
564
|
+
"complete_review_scope",
|
|
565
|
+
"record_review_finding",
|
|
551
566
|
"approve_review",
|
|
552
567
|
"request_review_changes",
|
|
553
568
|
];
|
|
@@ -555,6 +570,8 @@ export function resolveDagPiToolNames(task) {
|
|
|
555
570
|
if (isFrontendDesignTypedTerminalNode(task)) {
|
|
556
571
|
return [
|
|
557
572
|
...DAG_PI_READONLY_TOOLS,
|
|
573
|
+
"complete_review_scope",
|
|
574
|
+
"record_design_finding",
|
|
558
575
|
"approve_design",
|
|
559
576
|
"request_design_changes",
|
|
560
577
|
];
|
|
@@ -580,6 +597,7 @@ export function resolveDagPiToolNames(task) {
|
|
|
580
597
|
...FRONTEND_PLAN_RECORD_TOOL_NAMES,
|
|
581
598
|
...FRONTEND_PLAN_TERMINAL_TOOL_NAMES,
|
|
582
599
|
...FRONTEND_PLAN_ADOPT_TOOL_NAMES,
|
|
600
|
+
"read_plan_facts",
|
|
583
601
|
];
|
|
584
602
|
}
|
|
585
603
|
if ((task.readSet?.length ?? 0) > 0) {
|
|
@@ -759,21 +777,35 @@ export async function createFrontendReviewTerminalTools(input) {
|
|
|
759
777
|
]);
|
|
760
778
|
const { approveReviewFactSchema, readCommittedEvents, requestReviewChangesFactSchema, writeTypedEventStoreJsonl, } = await import("../workflows/dag/frontend-typed-event-store.js");
|
|
761
779
|
const { adoptTypedEventFact, stageTypedEventFact } = await import("../workflows/dag/frontend-typed-event-transaction.js");
|
|
762
|
-
|
|
780
|
+
let store = input.store;
|
|
763
781
|
const attemptId = input.attemptId;
|
|
764
782
|
const findingSchema = Type.Object({
|
|
765
|
-
severity: Type.
|
|
766
|
-
|
|
767
|
-
}),
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
issue: Type.String({}),
|
|
771
|
-
requiredChange: Type.Optional(Type.String({})),
|
|
783
|
+
severity: Type.Enum({ Critical: "Critical", Important: "Important", Minor: "Minor", Info: "Info" }),
|
|
784
|
+
file: Type.Optional(Type.String({ minLength: 1 })),
|
|
785
|
+
line: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
786
|
+
issue: Type.String({ minLength: 1 }),
|
|
787
|
+
requiredChange: Type.Optional(Type.String({ minLength: 1 })),
|
|
772
788
|
}, { additionalProperties: false });
|
|
789
|
+
const scopeProtocol = createFrontendReviewScopeProtocol({ phase: "review", inventory: input.inventory, getStore: () => store, attemptId });
|
|
790
|
+
const savedFindings = () => [...new Map(readCommittedEvents(store, attemptId).filter(r => r.fact.kind === "review-finding").map(r => [r.fact.id, r.fact.finding])).values()];
|
|
791
|
+
const allFindings = (direct) => [...new Map([...savedFindings(), ...(Array.isArray(direct) ? direct : [])].map(finding => [JSON.stringify(finding), finding])).values()];
|
|
792
|
+
const recordFindingTool = defineTool({
|
|
793
|
+
name: "record_review_finding", label: "record_review_finding",
|
|
794
|
+
description: "Save one finding with a stable id. Submit findings incrementally, then finalize without repeating the findings array. Saved blocking findings cannot be omitted from approval; correct a finding explicitly with replace:true.",
|
|
795
|
+
parameters: Type.Object({ id: Type.String({ minLength: 1 }), finding: findingSchema }, { additionalProperties: false }),
|
|
796
|
+
async execute(_callId, params) {
|
|
797
|
+
const fact = { kind: "review-finding", id: params.id, finding: params.finding };
|
|
798
|
+
const requestId = `${attemptId}:finding:${randomUUID()}`;
|
|
799
|
+
const staged = stageTypedEventFact({ store, requestId, attemptId, fact });
|
|
800
|
+
const adopted = await adoptTypedEventFact({ store, requestId, attemptId, fact, eventId: staged.eventId, expectedRevision: store.revision });
|
|
801
|
+
const details = { ok: true, eventId: adopted.eventId, revision: adopted.revision };
|
|
802
|
+
return { content: [{ type: "text", text: JSON.stringify(details) }], details };
|
|
803
|
+
},
|
|
804
|
+
});
|
|
773
805
|
const approveParameters = Type.Object({
|
|
774
|
-
findings: Type.Array(findingSchema, {
|
|
806
|
+
findings: Type.Optional(Type.Array(findingSchema, {
|
|
775
807
|
description: "Optional informational findings (Minor/Info only; no Critical/Important on approval)",
|
|
776
|
-
}),
|
|
808
|
+
})),
|
|
777
809
|
}, { additionalProperties: false });
|
|
778
810
|
const requestParameters = Type.Object({
|
|
779
811
|
issueCategory: Type.Enum({
|
|
@@ -783,16 +815,17 @@ export async function createFrontendReviewTerminalTools(input) {
|
|
|
783
815
|
"contract-requirement-gap": "contract-requirement-gap",
|
|
784
816
|
"unknown": "unknown",
|
|
785
817
|
}, { description: "Typed issue category (five-value enum)" }),
|
|
786
|
-
evidenceRefs: Type.Array(Type.String({}), {
|
|
787
|
-
description: "Evidence refs (paths or artifact ids); at least one",
|
|
788
|
-
}),
|
|
789
|
-
findings: Type.Array(findingSchema, {
|
|
790
|
-
description: "At least one finding",
|
|
818
|
+
evidenceRefs: Type.Array(Type.String({ minLength: 1 }), {
|
|
819
|
+
description: "Evidence refs (paths or artifact ids); at least one", minItems: 1,
|
|
791
820
|
}),
|
|
821
|
+
findings: Type.Optional(Type.Array(findingSchema, {
|
|
822
|
+
description: "At least one finding", minItems: 1,
|
|
823
|
+
})),
|
|
792
824
|
}, { additionalProperties: false });
|
|
793
825
|
async function adoptReviewFact(kind, fact) {
|
|
794
826
|
const requestId = randomUUID();
|
|
795
827
|
try {
|
|
828
|
+
scopeProtocol.assertComplete();
|
|
796
829
|
const parsed = kind === "approve_review"
|
|
797
830
|
? approveReviewFactSchema.parse(fact)
|
|
798
831
|
: requestReviewChangesFactSchema.parse(fact);
|
|
@@ -854,7 +887,7 @@ export async function createFrontendReviewTerminalTools(input) {
|
|
|
854
887
|
return adoptReviewFact("approve_review", {
|
|
855
888
|
kind: "approve_review",
|
|
856
889
|
verdict: "approve_review",
|
|
857
|
-
findings: params?.findings
|
|
890
|
+
findings: allFindings(params?.findings),
|
|
858
891
|
});
|
|
859
892
|
},
|
|
860
893
|
});
|
|
@@ -870,17 +903,15 @@ export async function createFrontendReviewTerminalTools(input) {
|
|
|
870
903
|
verdict: "request_review_changes",
|
|
871
904
|
issueCategory: params?.issueCategory,
|
|
872
905
|
evidenceRefs: params?.evidenceRefs,
|
|
873
|
-
findings: params?.findings,
|
|
906
|
+
findings: allFindings(params?.findings),
|
|
874
907
|
});
|
|
875
908
|
},
|
|
876
909
|
});
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
},
|
|
883
|
-
};
|
|
910
|
+
const durable = await createDurableFrontendTools({
|
|
911
|
+
file: path.join(input.runDir, input.nodeId, "review-typed-facts.jsonl"), attemptId, store: input.store,
|
|
912
|
+
binding: { inputDigest: input.inputDigest, scopeDigest: input.inventory?.digest }, setWorkingStore: next => { store = next; }, tools: [...scopeProtocol.customTools, recordFindingTool, approveReviewTool, requestReviewChangesTool], validateRestored: async () => { await input.inventory?.validate(); },
|
|
913
|
+
});
|
|
914
|
+
return { ...durable, scopeProtocol };
|
|
884
915
|
}
|
|
885
916
|
/**
|
|
886
917
|
* M8: build the two committed typed design terminal tools (approve_design /
|
|
@@ -897,21 +928,35 @@ export async function createFrontendDesignTerminalTools(input) {
|
|
|
897
928
|
]);
|
|
898
929
|
const { approveDesignFactSchema, readCommittedEvents, requestDesignChangesFactSchema, writeTypedEventStoreJsonl, } = await import("../workflows/dag/frontend-typed-event-store.js");
|
|
899
930
|
const { adoptTypedEventFact, stageTypedEventFact } = await import("../workflows/dag/frontend-typed-event-transaction.js");
|
|
900
|
-
|
|
931
|
+
let store = input.store;
|
|
901
932
|
const attemptId = input.attemptId;
|
|
902
933
|
const findingSchema = Type.Object({
|
|
903
|
-
severity: Type.
|
|
904
|
-
|
|
905
|
-
}),
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
issue: Type.String({}),
|
|
909
|
-
requiredChange: Type.Optional(Type.String({})),
|
|
934
|
+
severity: Type.Enum({ Critical: "Critical", Important: "Important", Minor: "Minor", Info: "Info" }),
|
|
935
|
+
file: Type.Optional(Type.String({ minLength: 1 })),
|
|
936
|
+
line: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
937
|
+
issue: Type.String({ minLength: 1 }),
|
|
938
|
+
requiredChange: Type.Optional(Type.String({ minLength: 1 })),
|
|
910
939
|
}, { additionalProperties: false });
|
|
940
|
+
const scopeProtocol = createFrontendReviewScopeProtocol({ phase: "design", inventory: input.inventory, getStore: () => store, attemptId });
|
|
941
|
+
const savedFindings = () => [...new Map(readCommittedEvents(store, attemptId).filter(r => r.fact.kind === "design-finding").map(r => [r.fact.id, r.fact.finding])).values()];
|
|
942
|
+
const allFindings = (direct) => [...new Map([...savedFindings(), ...(Array.isArray(direct) ? direct : [])].map(finding => [JSON.stringify(finding), finding])).values()];
|
|
943
|
+
const recordFindingTool = defineTool({
|
|
944
|
+
name: "record_design_finding", label: "record_design_finding",
|
|
945
|
+
description: "Save one finding with a stable id. Submit findings incrementally, then finalize without repeating the findings array. Saved blocking findings cannot be omitted from approval; correct a finding explicitly with replace:true.",
|
|
946
|
+
parameters: Type.Object({ id: Type.String({ minLength: 1 }), finding: findingSchema }, { additionalProperties: false }),
|
|
947
|
+
async execute(_callId, params) {
|
|
948
|
+
const fact = { kind: "design-finding", id: params.id, finding: params.finding };
|
|
949
|
+
const requestId = `${attemptId}:finding:${randomUUID()}`;
|
|
950
|
+
const staged = stageTypedEventFact({ store, requestId, attemptId, fact });
|
|
951
|
+
const adopted = await adoptTypedEventFact({ store, requestId, attemptId, fact, eventId: staged.eventId, expectedRevision: store.revision });
|
|
952
|
+
const details = { ok: true, eventId: adopted.eventId, revision: adopted.revision };
|
|
953
|
+
return { content: [{ type: "text", text: JSON.stringify(details) }], details };
|
|
954
|
+
},
|
|
955
|
+
});
|
|
911
956
|
const approveParameters = Type.Object({
|
|
912
|
-
findings: Type.Array(findingSchema, {
|
|
957
|
+
findings: Type.Optional(Type.Array(findingSchema, {
|
|
913
958
|
description: "Optional informational findings (Minor/Info only; no Critical/Important on approval)",
|
|
914
|
-
}),
|
|
959
|
+
})),
|
|
915
960
|
}, { additionalProperties: false });
|
|
916
961
|
const requestParameters = Type.Object({
|
|
917
962
|
issueCategory: Type.Enum({
|
|
@@ -921,16 +966,17 @@ export async function createFrontendDesignTerminalTools(input) {
|
|
|
921
966
|
"contract-requirement-gap": "contract-requirement-gap",
|
|
922
967
|
"unknown": "unknown",
|
|
923
968
|
}, { description: "Typed issue category (five-value enum)" }),
|
|
924
|
-
evidenceRefs: Type.Array(Type.String({}), {
|
|
925
|
-
description: "Evidence refs (paths or artifact ids); at least one",
|
|
926
|
-
}),
|
|
927
|
-
findings: Type.Array(findingSchema, {
|
|
928
|
-
description: "At least one finding",
|
|
969
|
+
evidenceRefs: Type.Array(Type.String({ minLength: 1 }), {
|
|
970
|
+
description: "Evidence refs (paths or artifact ids); at least one", minItems: 1,
|
|
929
971
|
}),
|
|
972
|
+
findings: Type.Optional(Type.Array(findingSchema, {
|
|
973
|
+
description: "At least one finding", minItems: 1,
|
|
974
|
+
})),
|
|
930
975
|
}, { additionalProperties: false });
|
|
931
976
|
async function adoptDesignFact(kind, fact) {
|
|
932
977
|
const requestId = randomUUID();
|
|
933
978
|
try {
|
|
979
|
+
scopeProtocol.assertComplete();
|
|
934
980
|
const parsed = kind === "approve_design"
|
|
935
981
|
? approveDesignFactSchema.parse(fact)
|
|
936
982
|
: requestDesignChangesFactSchema.parse(fact);
|
|
@@ -992,7 +1038,7 @@ export async function createFrontendDesignTerminalTools(input) {
|
|
|
992
1038
|
return adoptDesignFact("approve_design", {
|
|
993
1039
|
kind: "approve_design",
|
|
994
1040
|
verdict: "approve_design",
|
|
995
|
-
findings: params?.findings
|
|
1041
|
+
findings: allFindings(params?.findings),
|
|
996
1042
|
});
|
|
997
1043
|
},
|
|
998
1044
|
});
|
|
@@ -1008,17 +1054,15 @@ export async function createFrontendDesignTerminalTools(input) {
|
|
|
1008
1054
|
verdict: "request_design_changes",
|
|
1009
1055
|
issueCategory: params?.issueCategory,
|
|
1010
1056
|
evidenceRefs: params?.evidenceRefs,
|
|
1011
|
-
findings: params?.findings,
|
|
1057
|
+
findings: allFindings(params?.findings),
|
|
1012
1058
|
});
|
|
1013
1059
|
},
|
|
1014
1060
|
});
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
},
|
|
1021
|
-
};
|
|
1061
|
+
const durable = await createDurableFrontendTools({
|
|
1062
|
+
file: path.join(input.runDir, input.nodeId, "design-typed-facts.jsonl"), attemptId, store: input.store,
|
|
1063
|
+
binding: { inputDigest: input.inputDigest, scopeDigest: input.inventory?.digest }, setWorkingStore: next => { store = next; }, tools: [...scopeProtocol.customTools, recordFindingTool, approveDesignTool, requestDesignChangesTool], validateRestored: async () => { await input.inventory?.validate(); },
|
|
1064
|
+
});
|
|
1065
|
+
return { ...durable, scopeProtocol };
|
|
1022
1066
|
}
|
|
1023
1067
|
/**
|
|
1024
1068
|
* Source fidelity ledger (AC-005/AC-006): load the contract node's committed
|
|
@@ -1063,6 +1107,8 @@ async function loadContractRequirementInheritance(runDir) {
|
|
|
1063
1107
|
: undefined;
|
|
1064
1108
|
if (sourceFragmentIds || sourceRefs) {
|
|
1065
1109
|
byId.set(id, {
|
|
1110
|
+
...(typeof recordFact.text === "string" ? { text: recordFact.text } : {}),
|
|
1111
|
+
...(recordFact.execution !== undefined ? { execution: frontendExecutionSchema.parse(recordFact.execution) } : {}),
|
|
1066
1112
|
...(sourceFragmentIds ? { sourceFragmentIds } : {}),
|
|
1067
1113
|
...(sourceRefs ? { sourceRefs } : {}),
|
|
1068
1114
|
});
|
|
@@ -1228,26 +1274,12 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1228
1274
|
const { loadTypedEventStore, readCommittedEvents, writeTypedEventStoreJsonl, } = await import("../workflows/dag/frontend-typed-event-store.js");
|
|
1229
1275
|
const { adoptStagedFact, adoptTypedEventFact, stageTypedEventFact } = await import("../workflows/dag/frontend-typed-event-transaction.js");
|
|
1230
1276
|
const { assemblePlanPatchFromCommittedFacts } = await import("../workflows/dag/frontend-shadow-dual-write.js");
|
|
1231
|
-
|
|
1277
|
+
let store = input.store;
|
|
1232
1278
|
const attemptId = input.attemptId;
|
|
1233
1279
|
let activeRequirementScope = [];
|
|
1234
1280
|
const scopedRequirementIds = () => [...activeRequirementScope];
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
// tools; otherwise the first flush of a retry can overwrite facts that the
|
|
1238
|
-
// previous attempt had already committed. The on-disk file contains only
|
|
1239
|
-
// committed records, so loading it is also fail-closed with respect to
|
|
1240
|
-
// staged/quarantined facts.
|
|
1241
|
-
const persisted = await loadTypedEventStore(path.join(input.runDir, input.nodeId, "plan-typed-facts.jsonl"));
|
|
1242
|
-
if (persisted.records.length > 0) {
|
|
1243
|
-
const existingEventIds = new Set(store.records.map((record) => record.eventId));
|
|
1244
|
-
for (const record of persisted.records) {
|
|
1245
|
-
if (!existingEventIds.has(record.eventId)) {
|
|
1246
|
-
store.records.push(record);
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
store.revision = Math.max(store.revision, persisted.revision);
|
|
1250
|
-
}
|
|
1281
|
+
const contractInheritance = await loadContractRequirementInheritance(input.runDir);
|
|
1282
|
+
const executionGroups = collectFrontendExecutionGroups([...contractInheritance].map(([id, r]) => ({ id, ...r })));
|
|
1251
1283
|
const stringArray = Type.Array(Type.String({}));
|
|
1252
1284
|
const optionalString = Type.Optional(Type.String({}));
|
|
1253
1285
|
const optionalStringArray = Type.Optional(stringArray);
|
|
@@ -1282,9 +1314,7 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1282
1314
|
verificationTargetIds: stringArray,
|
|
1283
1315
|
}, { additionalProperties: false });
|
|
1284
1316
|
const mockEndpointSchema = Type.Object({
|
|
1285
|
-
method: Type.
|
|
1286
|
-
description: "GET | POST | PUT | PATCH | DELETE | HEAD | OPTIONS",
|
|
1287
|
-
}),
|
|
1317
|
+
method: Type.Enum({ GET: "GET", POST: "POST", PUT: "PUT", PATCH: "PATCH", DELETE: "DELETE", HEAD: "HEAD", OPTIONS: "OPTIONS" }),
|
|
1288
1318
|
path: Type.String({}),
|
|
1289
1319
|
fixture: optionalString,
|
|
1290
1320
|
consumer: optionalString,
|
|
@@ -1346,9 +1376,9 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1346
1376
|
specReference: Type.Optional(Type.Object({
|
|
1347
1377
|
path: Type.String({}),
|
|
1348
1378
|
section: Type.String({}),
|
|
1349
|
-
line: Type.Optional(Type.
|
|
1379
|
+
line: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
1350
1380
|
}, { additionalProperties: false })),
|
|
1351
|
-
rationale: Type.String({}),
|
|
1381
|
+
rationale: Type.Optional(Type.String({ minLength: 1 })),
|
|
1352
1382
|
covers: Type.Optional(Type.Array(Type.String({}), {
|
|
1353
1383
|
description: "UI state and/or interaction names this single component choice covers (one choice may cover many ids).",
|
|
1354
1384
|
})),
|
|
@@ -1787,6 +1817,44 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1787
1817
|
});
|
|
1788
1818
|
}
|
|
1789
1819
|
}
|
|
1820
|
+
// Named entries are last-wins at assembly. A scoped correction may
|
|
1821
|
+
// replace its own bindings, but must retain other scopes' coverage.
|
|
1822
|
+
const scope = new Set(scopedRequirementIds());
|
|
1823
|
+
const committed = readCommittedEvents(store, attemptId).map(event => event.fact);
|
|
1824
|
+
const targetOwners = new Map();
|
|
1825
|
+
for (const fact of committed) {
|
|
1826
|
+
if (fact.kind === "plan-verification-target" && isRecordObject(fact.entry) && typeof fact.entry.id === "string") {
|
|
1827
|
+
targetOwners.set(fact.entry.id, stringList(fact.entry.requirementIds));
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
for (const [field, removals] of [["uiStates", removeUiStateNames], ["interactions", removeInteractionNames]]) {
|
|
1831
|
+
const live = new Map();
|
|
1832
|
+
for (const fact of committed) {
|
|
1833
|
+
if (fact.kind !== "state-flow")
|
|
1834
|
+
continue;
|
|
1835
|
+
for (const name of stringList(fact[field === "uiStates" ? "removeUiStateNames" : "removeInteractionNames"]))
|
|
1836
|
+
live.delete(name);
|
|
1837
|
+
for (const entry of Array.isArray(fact[field]) ? fact[field] : []) {
|
|
1838
|
+
if (isRecordObject(entry) && typeof entry.name === "string")
|
|
1839
|
+
live.set(entry.name, entry);
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
const foreignBindings = (entry) => scope.size === 0 ? [] : stringList(entry.verificationTargetIds).filter(id => {
|
|
1843
|
+
const owners = targetOwners.get(id);
|
|
1844
|
+
return !owners?.length || owners.some(owner => !scope.has(owner));
|
|
1845
|
+
});
|
|
1846
|
+
for (const name of removals) {
|
|
1847
|
+
const previous = live.get(name);
|
|
1848
|
+
if (previous && foreignBindings(previous).length)
|
|
1849
|
+
return planToolReceipt({ ok: false, kind: "state-flow",
|
|
1850
|
+
error: `Cannot remove shared ${name} from this requirement scope; retain it and update its scoped bindings instead` });
|
|
1851
|
+
}
|
|
1852
|
+
for (const entry of field === "uiStates" ? uiStates : interactions) {
|
|
1853
|
+
const previous = live.get(String(entry.name));
|
|
1854
|
+
if (previous)
|
|
1855
|
+
entry.verificationTargetIds = [...new Set([...foreignBindings(previous), ...stringList(entry.verificationTargetIds)])];
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1790
1858
|
const result = await adoptPlanFact("state-flow", `${attemptId}:record_state_flow:${randomUUID()}`, {
|
|
1791
1859
|
kind: "state-flow",
|
|
1792
1860
|
origin: "plan",
|
|
@@ -1805,13 +1873,14 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1805
1873
|
label: "record_data_flow",
|
|
1806
1874
|
description: "Record interaction/endpoint data flow as an origin=plan data-flow fact. Example: {\"interactions\": [\"<interaction name>\"], \"endpoints\": [\"GET <path>\"]}",
|
|
1807
1875
|
promptSnippet: "Record the plan data-flow fact.",
|
|
1808
|
-
parameters: Type.Object({ interactions: stringArray, endpoints: stringArray }, { additionalProperties: false }),
|
|
1876
|
+
parameters: Type.Object({ interactions: stringArray, endpoints: stringArray, replace: Type.Optional(Type.Boolean()) }, { additionalProperties: false }),
|
|
1809
1877
|
async execute(_toolCallId, params) {
|
|
1878
|
+
const previous = params.replace ? undefined : readCommittedEvents(store, attemptId).filter(r => r.fact.kind === "data-flow").at(-1)?.fact;
|
|
1810
1879
|
const result = await adoptPlanFact("data-flow", `${attemptId}:record_data_flow:${randomUUID()}`, {
|
|
1811
1880
|
kind: "data-flow",
|
|
1812
1881
|
origin: "plan",
|
|
1813
|
-
interactions: stringList(params?.interactions),
|
|
1814
|
-
endpoints: stringList(params?.endpoints),
|
|
1882
|
+
interactions: [...new Set([...stringList(previous?.interactions), ...stringList(params?.interactions)])],
|
|
1883
|
+
endpoints: [...new Set([...stringList(previous?.endpoints), ...stringList(params?.endpoints)])],
|
|
1815
1884
|
});
|
|
1816
1885
|
return planToolReceipt(result);
|
|
1817
1886
|
},
|
|
@@ -1839,6 +1908,16 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1839
1908
|
return planToolReceipt(result);
|
|
1840
1909
|
},
|
|
1841
1910
|
});
|
|
1911
|
+
const recordMockEndpointTool = defineTool({
|
|
1912
|
+
name: "record_mock_endpoint", label: "record_mock_endpoint",
|
|
1913
|
+
description: "Record one Mock/API endpoint. First record_mock_api with the policy and endpoints: []; then submit each endpoint separately. Never regenerate the whole endpoint collection. Use replace:true to revise an existing method/path, or replace:true plus remove:true to withdraw it.",
|
|
1914
|
+
parameters: Type.Object({ endpoint: mockEndpointSchema, remove: Type.Optional(Type.Boolean()) }, { additionalProperties: false }),
|
|
1915
|
+
async execute(_callId, params) {
|
|
1916
|
+
if (!readCommittedEvents(store, attemptId).some(r => r.fact.kind === "mock-api"))
|
|
1917
|
+
return planToolReceipt({ ok: false, kind: "mock-endpoint", code: "MOCK_POLICY_MISSING", error: "Record the mock policy before its endpoints" });
|
|
1918
|
+
return planToolReceipt(await adoptPlanFact("mock-endpoint", `${attemptId}:endpoint:${randomUUID()}`, { kind: "mock-endpoint", origin: "plan", endpoint: params.endpoint, ...(params.remove ? { removed: true } : {}) }));
|
|
1919
|
+
},
|
|
1920
|
+
});
|
|
1842
1921
|
const recordDesignDeviationTool = defineTool({
|
|
1843
1922
|
name: "record_design_deviation",
|
|
1844
1923
|
label: "record_design_deviation",
|
|
@@ -1914,6 +1993,8 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1914
1993
|
// canonical-coverage gate rejects with no in-node cure. Reject here
|
|
1915
1994
|
// and name the allowed ids.
|
|
1916
1995
|
const id = typeof entry.id === "string" ? entry.id : "";
|
|
1996
|
+
if (activeRequirementScope.length && !activeRequirementScope.includes(id))
|
|
1997
|
+
return planToolReceipt({ ok: false, kind: "plan-requirement", code: "FRONTEND_INPUT_SCOPE_VIOLATION", error: `Requirement ${id} is outside this session` });
|
|
1917
1998
|
if (id &&
|
|
1918
1999
|
input.requirementIds &&
|
|
1919
2000
|
input.requirementIds.length > 0 &&
|
|
@@ -1963,6 +2044,29 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1963
2044
|
return planToolReceipt(result);
|
|
1964
2045
|
},
|
|
1965
2046
|
});
|
|
2047
|
+
const recordPlanGroupCoverageTool = defineTool({
|
|
2048
|
+
name: "record_plan_group_coverage", label: "record_plan_group_coverage",
|
|
2049
|
+
description: "Submit shared implementation/verification references for one declared execution group. Runtime expands to every canonical member and retains its full outcome and source bindings. A shared VT must actually verify each independent condition. Use per-requirement records for differences; never create UI for constraints or exclusions. replace:true explicitly revises the group.",
|
|
2050
|
+
parameters: Type.Object({ id: Type.String({ minLength: 1 }), implementationTargets: stringArray, verificationTargetIds: stringArray, replace: Type.Optional(Type.Boolean()) }, { additionalProperties: false }),
|
|
2051
|
+
async execute(callId, params, signal, onUpdate, ctx) {
|
|
2052
|
+
const group = executionGroups.find(g => g.id === params.id && g.kind !== "unclassified");
|
|
2053
|
+
if (!group || (activeRequirementScope.length && group.requirementIds.some(id => !activeRequirementScope.includes(id))))
|
|
2054
|
+
return planToolReceipt({ ok: false, kind: "plan-requirement", code: "EXECUTION_GROUP_SCOPE_INVALID", error: "A known complete group must be present in this session; submit individual member records when the group spans scopes" });
|
|
2055
|
+
let last;
|
|
2056
|
+
for (const id of group.requirementIds) {
|
|
2057
|
+
const existing = readCommittedEvents(store, attemptId).filter(r => r.fact.kind === "plan-requirement" && r.fact.entry?.id === id).at(-1)?.fact.entry;
|
|
2058
|
+
if (existing && !params.replace) {
|
|
2059
|
+
if (JSON.stringify(existing.implementationTargets) !== JSON.stringify(params.implementationTargets) || JSON.stringify(existing.verificationTargetIds) !== JSON.stringify(params.verificationTargetIds))
|
|
2060
|
+
return planToolReceipt({ ok: false, kind: "plan-requirement", code: "FACT_IDENTITY_CONFLICT", error: `${id}: existing coverage differs; use replace:true to revise explicitly` });
|
|
2061
|
+
continue;
|
|
2062
|
+
}
|
|
2063
|
+
last = await recordPlanRequirementTool.execute(`${callId}:${id}`, { entry: { id, implementationTargets: params.implementationTargets, verificationTargetIds: params.verificationTargetIds }, ...(params.replace ? { replace: true } : {}) }, signal, onUpdate, ctx);
|
|
2064
|
+
if (!last.details?.ok)
|
|
2065
|
+
return last;
|
|
2066
|
+
}
|
|
2067
|
+
return planToolReceipt(await adoptPlanFact("plan-group-coverage", `${attemptId}:group:${randomUUID()}`, { kind: "plan-group-coverage", origin: "plan", id: group.id, requirementIds: group.requirementIds }));
|
|
2068
|
+
},
|
|
2069
|
+
});
|
|
1966
2070
|
const recordPlanVerificationTargetTool = defineTool({
|
|
1967
2071
|
name: "record_plan_verification_target",
|
|
1968
2072
|
label: "record_plan_verification_target",
|
|
@@ -2182,9 +2286,13 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
2182
2286
|
const finalizePlanTool = defineTool({
|
|
2183
2287
|
name: "finalize_plan",
|
|
2184
2288
|
label: "finalize_plan",
|
|
2185
|
-
description: "Commit the finalize_plan terminal. Requirements, verification targets, and evidence gaps (optional) were already committed incrementally through record_plan_requirement / record_plan_verification_target / record_plan_evidence_gap; finalize_plan assembles them
|
|
2186
|
-
promptSnippet: "Commit
|
|
2289
|
+
description: "Commit the finalize_plan terminal. Requirements, verification targets, and evidence gaps (optional) were already committed incrementally through record_plan_requirement / record_plan_verification_target / record_plan_evidence_gap; finalize_plan requires implementationSteps: [{requirementIds, action}] covering every frozen requirement (shared steps allowed), and assembles them with the ledger and optional remaining fields, publishes the canonical editable patch on a target-surface fact, and commits the terminal. A successful terminal commit occurs exactly once. If validation fails, correct only the reported facts and retry finalize.",
|
|
2290
|
+
promptSnippet: "Commit finalize_plan with implementationSteps: [{requirementIds, action}] covering every frozen requirement; optional residualRisks / realIntegrationGap.",
|
|
2187
2291
|
parameters: Type.Object({
|
|
2292
|
+
implementationSteps: Type.Optional(Type.Array(Type.Object({
|
|
2293
|
+
requirementIds: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }),
|
|
2294
|
+
action: Type.String({ minLength: 1 }),
|
|
2295
|
+
}, { additionalProperties: false }))),
|
|
2188
2296
|
residualRisks: optionalStringArray,
|
|
2189
2297
|
realIntegrationGap: optionalString,
|
|
2190
2298
|
}, { additionalProperties: false }),
|
|
@@ -2197,9 +2305,27 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
2197
2305
|
// when the plan did not re-declare them. The contract node is the
|
|
2198
2306
|
// sole synthesis point; the plan inherits by requirement id.
|
|
2199
2307
|
const contractInheritance = await loadContractRequirementInheritance(input.runDir);
|
|
2308
|
+
const missingData = collectFrontendPlanPhaseMissingFacts({ phase: "global-mock-data", requirementIds: input.requirementIds ?? [...contractInheritance.keys()], committedFacts: committed }).filter(f => f.kind === "data-flow");
|
|
2309
|
+
if (missingData.length)
|
|
2310
|
+
return planToolReceipt({ ok: false, kind: "finalize_plan", code: "PLAN_DATA_FLOW_INCOMPLETE", error: missingData.map(f => f.reason).join("; ") });
|
|
2200
2311
|
const fragment = assemblePlanPatchFromCommittedFacts(committed, contractInheritance) ?? {};
|
|
2312
|
+
const requiredIds = new Set([
|
|
2313
|
+
...(input.requirementIds ?? []),
|
|
2314
|
+
...contractInheritance.keys(),
|
|
2315
|
+
...(Array.isArray(fragment.requirements) ? fragment.requirements.flatMap((entry) => isRecordObject(entry) && typeof entry.id === "string" ? [entry.id] : []) : []),
|
|
2316
|
+
]);
|
|
2317
|
+
const steps = params?.implementationSteps ?? [];
|
|
2318
|
+
const coveredIds = new Set(steps.flatMap(step => step.requirementIds));
|
|
2319
|
+
const missingRequirementIds = [...requiredIds].filter(id => !coveredIds.has(id));
|
|
2320
|
+
const unknownRequirementIds = [...coveredIds].filter(id => !requiredIds.has(id));
|
|
2321
|
+
if (missingRequirementIds.length || unknownRequirementIds.length || steps.some(step => !step.action.trim())) {
|
|
2322
|
+
return planToolReceipt({ ok: false, kind: "finalize_plan", code: "PLAN_IMPLEMENTATION_STEPS_INCOMPLETE",
|
|
2323
|
+
error: `Submit finalize_plan with non-empty implementationSteps: [{requirementIds: [exact frozen IDs], action: "what to change and where"}]. Cover every requirement; shared steps may cover several IDs. Missing: ${missingRequirementIds.join(", ") || "none"}; unknown: ${unknownRequirementIds.join(", ") || "none"}. Actions must not be blank. Do not add a test plan.`,
|
|
2324
|
+
});
|
|
2325
|
+
}
|
|
2201
2326
|
const patch = {
|
|
2202
2327
|
...fragment,
|
|
2328
|
+
...(steps.length ? { implementationSteps: steps.map(step => `[${[...new Set(step.requirementIds)].join(", ")}] ${step.action.trim()}`) } : {}),
|
|
2203
2329
|
...(params?.residualRisks
|
|
2204
2330
|
? { residualRisks: params.residualRisks }
|
|
2205
2331
|
: {}),
|
|
@@ -2352,23 +2478,99 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
2352
2478
|
}
|
|
2353
2479
|
},
|
|
2354
2480
|
});
|
|
2355
|
-
|
|
2356
|
-
|
|
2481
|
+
const readPlanFactsTool = defineTool({
|
|
2482
|
+
name: "read_plan_facts",
|
|
2483
|
+
label: "read_plan_facts",
|
|
2484
|
+
description: "Read the LIVE committed plan ledger without repository access. Filter by kind and optional entryId or eventId. Use field (dot path, e.g. entry.requirementIds or uiStates) to page large arrays/strings. offset/limit paginate results; follow nextOffset until null before replacing any full-array fact. expectedRevision pins all pages to one ledger revision; restart if it changes. No writes.",
|
|
2485
|
+
parameters: Type.Object({
|
|
2486
|
+
kind: Type.String(), entryId: Type.Optional(Type.String()), eventId: Type.Optional(Type.String()),
|
|
2487
|
+
field: Type.Optional(Type.String()), offset: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
2488
|
+
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 50 })),
|
|
2489
|
+
stringOffset: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
2490
|
+
expectedRevision: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
2491
|
+
}, { additionalProperties: false }),
|
|
2492
|
+
async execute(_id, params) {
|
|
2493
|
+
const reply = (details) => ({ content: [{ type: "text", text: JSON.stringify(details) }], details });
|
|
2494
|
+
if (params.expectedRevision !== undefined && params.expectedRevision !== store.revision) {
|
|
2495
|
+
return reply({ ok: false, revision: store.revision, error: "ledger revision changed; restart pagination" });
|
|
2496
|
+
}
|
|
2497
|
+
let records = readCommittedEvents(store, attemptId).filter(record => {
|
|
2498
|
+
const fact = record.fact;
|
|
2499
|
+
return fact.kind === params.kind && (!params.eventId || record.eventId === params.eventId) &&
|
|
2500
|
+
(!params.entryId || fact.entry?.id === params.entryId);
|
|
2501
|
+
});
|
|
2502
|
+
// Full-entry corrections and registries are last-wins. Other kinds
|
|
2503
|
+
// remain an ordered event log (state removals must remain visible).
|
|
2504
|
+
if (params.entryId || params.kind === "state-registry")
|
|
2505
|
+
records = records.slice(-1);
|
|
2506
|
+
let values = records.map(record => ({ eventId: record.eventId, ...record.fact }));
|
|
2507
|
+
if (params.field) {
|
|
2508
|
+
values = records.flatMap(record => {
|
|
2509
|
+
let value = record.fact;
|
|
2510
|
+
for (const key of params.field.split(".")) {
|
|
2511
|
+
if (!value || typeof value !== "object" || !Object.hasOwn(value, key))
|
|
2512
|
+
return [];
|
|
2513
|
+
value = value[key];
|
|
2514
|
+
}
|
|
2515
|
+
return Array.isArray(value) ? value : [value];
|
|
2516
|
+
});
|
|
2517
|
+
}
|
|
2518
|
+
const offset = params.offset ?? 0;
|
|
2519
|
+
if (params.stringOffset !== undefined) {
|
|
2520
|
+
const value = values[offset];
|
|
2521
|
+
if (typeof value !== "string")
|
|
2522
|
+
return reply({ ok: false, error: "stringOffset requires a string field item" });
|
|
2523
|
+
// Unicode code points prevent a page boundary splitting a surrogate pair.
|
|
2524
|
+
const characters = Array.from(value);
|
|
2525
|
+
const chunk = characters.slice(params.stringOffset, params.stringOffset + 1000).join("");
|
|
2526
|
+
const next = params.stringOffset + Array.from(chunk).length;
|
|
2527
|
+
return reply({ ok: true, revision: store.revision, offset, stringOffset: params.stringOffset,
|
|
2528
|
+
chunk, totalCharacters: characters.length, nextStringOffset: next < characters.length ? next : null });
|
|
2529
|
+
}
|
|
2530
|
+
const items = [];
|
|
2531
|
+
let bytes = 0;
|
|
2532
|
+
for (const value of values.slice(offset, offset + (params.limit ?? 10))) {
|
|
2533
|
+
const size = Buffer.byteLength(JSON.stringify(value));
|
|
2534
|
+
if (bytes + size > 12_000)
|
|
2535
|
+
break;
|
|
2536
|
+
bytes += size;
|
|
2537
|
+
items.push(value);
|
|
2538
|
+
}
|
|
2539
|
+
if (!items.length && offset < values.length)
|
|
2540
|
+
return reply({
|
|
2541
|
+
ok: false, revision: store.revision, total: values.length, offset,
|
|
2542
|
+
error: "item exceeds the page budget; select an eventId/entryId and a narrower field path; for a string item set stringOffset: 0 and follow nextStringOffset",
|
|
2543
|
+
fields: typeof values[offset] === "object" && values[offset] !== null ? Object.keys(values[offset]) : [],
|
|
2544
|
+
});
|
|
2545
|
+
return reply({ ok: true, revision: store.revision, total: values.length, offset, items,
|
|
2546
|
+
nextOffset: offset + items.length < values.length ? offset + items.length : null });
|
|
2547
|
+
},
|
|
2548
|
+
});
|
|
2549
|
+
const durable = await createDurableFrontendTools({
|
|
2550
|
+
file: path.join(input.runDir, input.nodeId, "plan-typed-facts.jsonl"), attemptId, store: input.store,
|
|
2551
|
+
setWorkingStore: next => { store = next; },
|
|
2552
|
+
binding: { sourceBinding: input.sourceBinding, skeleton: input.skeleton, requirementIds: input.requirementIds, writeSet: input.writeSetPatterns, declaredUiStateIds: input.declaredUiStateIds, citations: input.componentNewSourceReferences, contractInheritance },
|
|
2553
|
+
tools: [
|
|
2357
2554
|
recordRouteSelectionTool,
|
|
2358
2555
|
recordComponentChoiceTool,
|
|
2359
2556
|
recordStateRegistryTool,
|
|
2360
2557
|
recordStateFlowTool,
|
|
2361
2558
|
recordDataFlowTool,
|
|
2362
2559
|
recordMockApiTool,
|
|
2560
|
+
recordMockEndpointTool,
|
|
2363
2561
|
recordDesignDeviationTool,
|
|
2364
2562
|
recordDependencyTool,
|
|
2365
2563
|
recordPlanRequirementTool,
|
|
2564
|
+
recordPlanGroupCoverageTool,
|
|
2366
2565
|
recordPlanVerificationTargetTool,
|
|
2367
2566
|
recordPlanEvidenceGapTool,
|
|
2368
2567
|
adoptStagedFactTool,
|
|
2369
2568
|
finalizePlanTool,
|
|
2370
2569
|
],
|
|
2371
|
-
|
|
2570
|
+
});
|
|
2571
|
+
return {
|
|
2572
|
+
customTools: [...durable.customTools, { ...readPlanFactsTool, execute: async (...args) => { await durable.flush(); return readPlanFactsTool.execute(...args); } }],
|
|
2573
|
+
adoptCommittedFacts: async (records) => durable.commitExternal(async () => {
|
|
2372
2574
|
for (const record of records) {
|
|
2373
2575
|
if (record.phase !== "committed")
|
|
2374
2576
|
continue;
|
|
@@ -2427,16 +2629,13 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
2427
2629
|
throw new Error(`frontend plan shard fact merge failed: ${result.error}`);
|
|
2428
2630
|
}
|
|
2429
2631
|
}
|
|
2430
|
-
},
|
|
2632
|
+
}),
|
|
2431
2633
|
setActiveRequirementScope: (requirementIds) => {
|
|
2432
2634
|
activeRequirementScope = [
|
|
2433
2635
|
...new Set(requirementIds.filter((id) => id.trim().length > 0)),
|
|
2434
2636
|
];
|
|
2435
2637
|
},
|
|
2436
|
-
flush:
|
|
2437
|
-
const committed = readCommittedEvents(store, attemptId);
|
|
2438
|
-
await writeTypedEventStoreJsonl(path.join(input.runDir, input.nodeId, "plan-typed-facts.jsonl"), committed);
|
|
2439
|
-
},
|
|
2638
|
+
flush: durable.flush,
|
|
2440
2639
|
committedFactCount: () => readCommittedEvents(store, attemptId).length,
|
|
2441
2640
|
committedRequirementIds: () => {
|
|
2442
2641
|
const ids = new Set();
|
|
@@ -2538,8 +2737,11 @@ export async function createFrontendContractTools(input) {
|
|
|
2538
2737
|
]);
|
|
2539
2738
|
const { readCommittedEvents, writeTypedEventStoreJsonl } = await import("../workflows/dag/frontend-typed-event-store.js");
|
|
2540
2739
|
const { adoptTypedEventFact, stageTypedEventFact } = await import("../workflows/dag/frontend-typed-event-transaction.js");
|
|
2541
|
-
|
|
2740
|
+
let store = input.store;
|
|
2542
2741
|
const attemptId = input.attemptId;
|
|
2742
|
+
let activeScope = null;
|
|
2743
|
+
const committedRequirementIds = () => new Set(readCommittedEvents(store, attemptId).filter(r => r.fact.kind === "requirement").map(r => String(r.fact.id)));
|
|
2744
|
+
const completedScopeRequirementIds = () => new Set(readCommittedEvents(store, attemptId).filter(r => r.fact.kind === "contract-scope-completed").flatMap(r => Array.isArray(r.fact.requirementIds) ? r.fact.requirementIds.filter((id) => typeof id === "string") : []));
|
|
2543
2745
|
const receipt = (details) => ({
|
|
2544
2746
|
content: [{ type: "text", text: JSON.stringify(details) }],
|
|
2545
2747
|
details,
|
|
@@ -2589,12 +2791,24 @@ export async function createFrontendContractTools(input) {
|
|
|
2589
2791
|
label: name,
|
|
2590
2792
|
description: `Commit an origin=contract ${kind} fact. IMPORTANT: submit incrementally — batch up to 5 record_* calls per message, starting from the FIRST message; never attempt to emit the whole contract in one response (a single large dump will be truncated and rejected). Every message must make progress by committing at least one record_* fact.`,
|
|
2591
2793
|
promptSnippet: `Commit 1-5 origin=contract ${kind} facts (up to 5 per message).`,
|
|
2592
|
-
parameters: Type.Object({
|
|
2794
|
+
parameters: Type.Object({
|
|
2795
|
+
text: Type.String({ minLength: 1 }),
|
|
2796
|
+
requirementIds: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
2797
|
+
sourceFragmentIds: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
2798
|
+
...(kind === "constraint" ? { category: Type.Optional(Type.Enum({ constraint: "constraint", "non-goal": "non-goal", risk: "risk" })) } : {}),
|
|
2799
|
+
...(kind === "handoff-intent" ? { taskKind: Type.Literal("frontend-test"), blocking: Type.Optional(Type.Boolean()) } : {}),
|
|
2800
|
+
}, { additionalProperties: false }),
|
|
2593
2801
|
async execute(_toolCallId, params) {
|
|
2802
|
+
const data = params;
|
|
2803
|
+
if (!data.text?.trim())
|
|
2804
|
+
return receipt({ ok: false, code: "TOOL_SCHEMA_INVALID", error: `${name}: text must be non-empty` });
|
|
2805
|
+
const knownFragments = new Set([...(input.canonicalRequirements?.values() ?? [])].flatMap(r => r.sourceFragmentIds));
|
|
2806
|
+
if (data.requirementIds?.some(id => !input.canonicalRequirements?.has(id)) || data.sourceFragmentIds?.some(id => !knownFragments.has(id)))
|
|
2807
|
+
return receipt({ ok: false, code: "CONTRACT_REFERENCE_UNKNOWN", error: `${name}: reference is outside the frozen source inventory` });
|
|
2594
2808
|
const result = await adoptContractFact(kind, {
|
|
2809
|
+
...(params ?? {}),
|
|
2595
2810
|
kind,
|
|
2596
2811
|
origin: "contract",
|
|
2597
|
-
...(params ?? {}),
|
|
2598
2812
|
});
|
|
2599
2813
|
return receipt(result);
|
|
2600
2814
|
},
|
|
@@ -2608,9 +2822,9 @@ export async function createFrontendContractTools(input) {
|
|
|
2608
2822
|
const recordRequirementTool = defineTool({
|
|
2609
2823
|
name: "record_requirement",
|
|
2610
2824
|
label: "record_requirement",
|
|
2611
|
-
description: 'Confirm one canonical ledger requirement (origin=contract requirement fact). Pass
|
|
2825
|
+
description: 'Confirm one canonical ledger requirement (origin=contract requirement fact). Pass the canonical id and optional execution:{groupId,kind,summary} only. Reuse a group only when its behavior and all permission/threshold/error conditions agree; retain separate groups for differences. Constraints/exclusions do not require invented UI. The canonical id is listed in the <frontend_contract_input> inventory, e.g. {"id": "AC-001"} — the runtime commits the authoritative text and sourceFragmentIds from the frozen ledger. Never pass text/statement/sourceFragmentIds yourself: free-form rewrites and JSON-stringified fragment arrays are rejected. IMPORTANT: batch up to 5 record_* calls per message, starting from the FIRST message.',
|
|
2612
2826
|
promptSnippet: "Confirm 1-5 canonical requirements by id (up to 5 per message).",
|
|
2613
|
-
parameters: Type.Object({ id: Type.String({ description: "Canonical ledger requirement id (e.g. AC-001)" }) }, { additionalProperties: false }),
|
|
2827
|
+
parameters: Type.Object({ id: Type.String({ description: "Canonical ledger requirement id (e.g. AC-001)" }), execution: Type.Optional(Type.Object({ groupId: Type.String({ minLength: 1 }), kind: Type.Union([Type.Literal("behavior"), Type.Literal("constraint"), Type.Literal("exclusion")]), summary: Type.String({ minLength: 1 }) }, { additionalProperties: false })) }, { additionalProperties: false }),
|
|
2614
2828
|
async execute(_toolCallId, params) {
|
|
2615
2829
|
const id = typeof params?.id === "string" ? params.id.trim() : "";
|
|
2616
2830
|
if (!id) {
|
|
@@ -2620,6 +2834,8 @@ export async function createFrontendContractTools(input) {
|
|
|
2620
2834
|
error: "record_requirement requires the canonical requirement id",
|
|
2621
2835
|
});
|
|
2622
2836
|
}
|
|
2837
|
+
if (activeScope && !activeScope.has(id))
|
|
2838
|
+
return receipt({ ok: false, code: "FRONTEND_INPUT_SCOPE_VIOLATION", error: `requirement ${id} is outside the complete input scope of this session` });
|
|
2623
2839
|
const canonical = input.canonicalRequirements?.get(id);
|
|
2624
2840
|
if (!canonical) {
|
|
2625
2841
|
const known = [...(input.canonicalRequirements?.keys() ?? [])];
|
|
@@ -2629,6 +2845,14 @@ export async function createFrontendContractTools(input) {
|
|
|
2629
2845
|
error: `record_requirement id "${id}" is not a canonical ledger requirement; canonical ids are: ${known.join(", ") || "(none)"}`,
|
|
2630
2846
|
});
|
|
2631
2847
|
}
|
|
2848
|
+
if (params.execution) {
|
|
2849
|
+
try {
|
|
2850
|
+
collectFrontendExecutionGroups([...readCommittedEvents(store, attemptId).filter(r => r.fact.kind === "requirement" && r.fact.id !== id).map(r => ({ id: String(r.fact.id), execution: r.fact.execution })), { id, execution: params.execution }]);
|
|
2851
|
+
}
|
|
2852
|
+
catch (error) {
|
|
2853
|
+
return receipt({ ok: false, code: "EXECUTION_GROUP_CONFLICT", error: String(error) });
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2632
2856
|
const result = await adoptContractFact("requirement", {
|
|
2633
2857
|
kind: "requirement",
|
|
2634
2858
|
origin: "contract",
|
|
@@ -2636,6 +2860,7 @@ export async function createFrontendContractTools(input) {
|
|
|
2636
2860
|
id,
|
|
2637
2861
|
text: canonical.text,
|
|
2638
2862
|
sourceFragmentIds: canonical.sourceFragmentIds,
|
|
2863
|
+
...(params.execution ? { execution: params.execution } : {}),
|
|
2639
2864
|
});
|
|
2640
2865
|
return receipt(result);
|
|
2641
2866
|
},
|
|
@@ -2715,15 +2940,17 @@ export async function createFrontendContractTools(input) {
|
|
|
2715
2940
|
const recordRequiredDeliverablesTool = defineTool({
|
|
2716
2941
|
name: "record_required_deliverables",
|
|
2717
2942
|
label: "record_required_deliverables",
|
|
2718
|
-
description: 'Declare the complete source-required file deliverables as {items:[{path,requirementId,sourceFragmentId}]}. Interpret obligations from the original source, including lists/tables: permissions (allowedPaths/only allowed to modify), prohibitions, examples and read-only references are NOT delivery obligations. Each path must appear exactly in its frozen requirement-bound source fragment. Submit {items:[]} explicitly if no files are mandatory.
|
|
2719
|
-
parameters: Type.Object({ items: Type.Array(Type.Object({
|
|
2943
|
+
description: 'Declare the complete source-required file deliverables as {items:[{path,requirementId,sourceFragmentId}]}. Interpret obligations from the original source, including lists/tables: permissions (allowedPaths/only allowed to modify), prohibitions, examples and read-only references are NOT delivery obligations. Each path must appear exactly in its frozen requirement-bound source fragment. Submit {items:[]} explicitly if no files are mandatory. Each call appends complete source-bound items; replace:true explicitly replaces the inventory. Required before finalize_contract ready.',
|
|
2944
|
+
parameters: Type.Object({ replace: Type.Optional(Type.Boolean()), items: Type.Array(Type.Object({
|
|
2720
2945
|
path: Type.String(), requirementId: Type.String(), sourceFragmentId: Type.String(),
|
|
2721
2946
|
}, { additionalProperties: false })) }, { additionalProperties: false }),
|
|
2722
2947
|
async execute(_toolCallId, params) {
|
|
2723
2948
|
try {
|
|
2724
|
-
const declaration = validateFrontendRequiredDeliverables(params, input.canonicalRequirements ?? new Map());
|
|
2949
|
+
const declaration = validateFrontendRequiredDeliverables({ items: params.items }, input.canonicalRequirements ?? new Map());
|
|
2950
|
+
const previous = params.replace ? [] : readCommittedEvents(store, attemptId).filter(r => r.fact.kind === "required-deliverables").at(-1)?.fact.items;
|
|
2951
|
+
const items = [...new Map([...(Array.isArray(previous) ? previous : []), ...declaration.items].map(item => [JSON.stringify(item), item])).values()];
|
|
2725
2952
|
return receipt(await adoptContractFact("required-deliverables", {
|
|
2726
|
-
kind: "required-deliverables", origin: "contract",
|
|
2953
|
+
kind: "required-deliverables", origin: "contract", items,
|
|
2727
2954
|
}));
|
|
2728
2955
|
}
|
|
2729
2956
|
catch (error) {
|
|
@@ -2779,7 +3006,7 @@ export async function createFrontendContractTools(input) {
|
|
|
2779
3006
|
const finalizeContractTool = defineTool({
|
|
2780
3007
|
name: "finalize_contract",
|
|
2781
3008
|
label: "finalize_contract",
|
|
2782
|
-
description: "Commit the contract-finalized terminal fact with a disposition of ready | ready-with-assumptions | blocked (blocked requires blockingOwner).
|
|
3009
|
+
description: "Commit the contract-finalized terminal fact with a disposition of ready | ready-with-assumptions | blocked (blocked requires blockingOwner). A successful terminal commit occurs exactly once. If validation fails, correct only the reported facts and retry finalize.",
|
|
2783
3010
|
promptSnippet: "Commit the contract-finalized terminal (disposition + optional blockingOwner).",
|
|
2784
3011
|
parameters: Type.Object({
|
|
2785
3012
|
disposition: Type.Enum({
|
|
@@ -2811,6 +3038,9 @@ export async function createFrontendContractTools(input) {
|
|
|
2811
3038
|
error: "call record_required_deliverables with the complete source-bound inventory (or items:[] when none) before finalizing",
|
|
2812
3039
|
});
|
|
2813
3040
|
}
|
|
3041
|
+
const missing = [...(input.canonicalRequirements?.keys() ?? [])].filter(id => !committedRequirementIds().has(id) || (activeScope !== null && !completedScopeRequirementIds().has(id)));
|
|
3042
|
+
if (disposition !== "blocked" && missing.length)
|
|
3043
|
+
return receipt({ ok: false, code: "CONTRACT_REQUIREMENT_COVERAGE_MISSING", error: `Confirm all complete source obligations before finalizing: ${missing.join(", ")}` });
|
|
2814
3044
|
const blockedOwner = mapContractBlockedOwner({
|
|
2815
3045
|
disposition: disposition ?? "",
|
|
2816
3046
|
blockingOwner,
|
|
@@ -2831,19 +3061,41 @@ export async function createFrontendContractTools(input) {
|
|
|
2831
3061
|
return receipt(result);
|
|
2832
3062
|
},
|
|
2833
3063
|
});
|
|
2834
|
-
|
|
2835
|
-
|
|
3064
|
+
const completeScopeTool = defineTool({
|
|
3065
|
+
name: "complete_contract_scope", label: "complete_contract_scope",
|
|
3066
|
+
description: "After recording all requirements AND their evidence, constraints, questions and deliverables for this session, mark the scope complete. Confirming an ID alone does not complete its analysis. Do this before finalize_contract.",
|
|
3067
|
+
parameters: Type.Object({ requirementIds: Type.Array(Type.String({ minLength: 1 }), { uniqueItems: true }) }, { additionalProperties: false }),
|
|
3068
|
+
async execute(_id, params) {
|
|
3069
|
+
const ids = params.requirementIds;
|
|
3070
|
+
if (activeScope === null || ids.length !== activeScope.size || ids.some(id => !activeScope?.has(id) || !committedRequirementIds().has(id)))
|
|
3071
|
+
return receipt({ ok: false, code: "CONTRACT_SCOPE_INCOMPLETE", error: "Complete exactly the active scope after confirming all its obligations" });
|
|
3072
|
+
return receipt(await adoptContractFact("contract-scope-completed", { kind: "contract-scope-completed", origin: "contract", requirementIds: ids }));
|
|
3073
|
+
},
|
|
3074
|
+
});
|
|
3075
|
+
const durable = await createDurableFrontendTools({
|
|
3076
|
+
file: path.join(input.runDir, input.nodeId, "contract-typed-facts.jsonl"), attemptId, store: input.store,
|
|
3077
|
+
setWorkingStore: next => { store = next; }, binding: { canonicalRequirements: input.canonicalRequirements, sourceDigest: input.sourceDigest },
|
|
3078
|
+
tools: [
|
|
2836
3079
|
...recordTools,
|
|
2837
3080
|
recordRequirementTool,
|
|
2838
3081
|
recordEvidenceExpectationTool,
|
|
2839
3082
|
recordUiStateTool,
|
|
2840
3083
|
recordRequiredDeliverablesTool,
|
|
2841
3084
|
recordOpenspecSelectionTool,
|
|
3085
|
+
completeScopeTool,
|
|
2842
3086
|
finalizeContractTool,
|
|
2843
3087
|
],
|
|
3088
|
+
});
|
|
3089
|
+
return {
|
|
3090
|
+
customTools: durable.customTools,
|
|
3091
|
+
inputRequirements: () => [...(input.canonicalRequirements ?? [])].map(([id, value]) => ({ id, text: value.text, sourceFragmentIds: [...value.sourceFragmentIds] })),
|
|
3092
|
+
completedScopeRequirementIds,
|
|
3093
|
+
setActiveRequirementScope: ids => { activeScope = ids === null ? null : new Set(ids); },
|
|
3094
|
+
committedRequirementIds,
|
|
3095
|
+
committedFacts: () => readCommittedEvents(input.store, attemptId),
|
|
2844
3096
|
flush: async () => {
|
|
2845
|
-
|
|
2846
|
-
|
|
3097
|
+
await durable.flush();
|
|
3098
|
+
const committed = readCommittedEvents(input.store, attemptId);
|
|
2847
3099
|
await writeJsonAtomic(path.join(input.runDir, input.nodeId, "frontend-task-contract-vNext.json"), buildFrontendTaskContractVNext(committed));
|
|
2848
3100
|
},
|
|
2849
3101
|
};
|
|
@@ -2895,7 +3147,7 @@ export async function createFrontendScoutEvidenceTools(input) {
|
|
|
2895
3147
|
]);
|
|
2896
3148
|
const { readCommittedEvents, writeTypedEventStoreJsonl } = await import("../workflows/dag/frontend-typed-event-store.js");
|
|
2897
3149
|
const { adoptTypedEventFact, stageTypedEventFact } = await import("../workflows/dag/frontend-typed-event-transaction.js");
|
|
2898
|
-
|
|
3150
|
+
let store = input.store;
|
|
2899
3151
|
const attemptId = input.attemptId;
|
|
2900
3152
|
const stringArray = Type.Array(Type.String({}));
|
|
2901
3153
|
const optionalString = Type.Optional(Type.String({}));
|
|
@@ -2909,6 +3161,18 @@ export async function createFrontendScoutEvidenceTools(input) {
|
|
|
2909
3161
|
});
|
|
2910
3162
|
const sourceDeclaredPaths = (input.sourceDeclaredPaths ?? []).map((value) => value.replaceAll("\\", "/").replace(/^\.\//, "").replace(/\/$/, ""));
|
|
2911
3163
|
const hasSourceDeclarations = input.sourceDeclaredPaths !== undefined;
|
|
3164
|
+
let activeScope;
|
|
3165
|
+
const scopeIdentity = (ids) => createHash("sha256").update(JSON.stringify([...ids].sort())).digest("hex");
|
|
3166
|
+
const latestScopes = () => {
|
|
3167
|
+
const byRequirement = new Map();
|
|
3168
|
+
for (const record of readCommittedEvents(store, attemptId))
|
|
3169
|
+
if (record.fact.kind === "scout-scope" && Array.isArray(record.fact.requirementIds)) {
|
|
3170
|
+
for (const id of record.fact.requirementIds)
|
|
3171
|
+
if (typeof id === "string")
|
|
3172
|
+
byRequirement.set(id, record.fact);
|
|
3173
|
+
}
|
|
3174
|
+
return byRequirement;
|
|
3175
|
+
};
|
|
2912
3176
|
const isSourceDeclared = (candidate) => {
|
|
2913
3177
|
const normalized = candidate.replaceAll("\\", "/").replace(/^\.\//, "").replace(/\/$/, "");
|
|
2914
3178
|
return sourceDeclaredPaths.some((declared) => declared === normalized || declared.startsWith(`${normalized}/`));
|
|
@@ -3001,6 +3265,7 @@ export async function createFrontendScoutEvidenceTools(input) {
|
|
|
3001
3265
|
description: "Commit an origin=scout target-surface fact with complete/blocked discovery status. A complete surface needs a proven target path and no unresolved paths; blocked surfaces name the unresolved paths instead of guessing. Example: {\"completeness\": \"complete\", \"entrypoint\": \"<file>\", \"implementationPaths\": [\"<dir or file>\"], \"testPaths\": [\"<file>\"], \"allowedPathConflicts\": [], \"unresolvedPaths\": []}",
|
|
3002
3266
|
promptSnippet: "Commit an origin=scout target-surface fact.",
|
|
3003
3267
|
parameters: Type.Object({
|
|
3268
|
+
scopeId: Type.Optional(Type.String({ minLength: 1 })),
|
|
3004
3269
|
completeness: scoutCompleteness,
|
|
3005
3270
|
entrypoint: optionalString,
|
|
3006
3271
|
routeOrMount: optionalString,
|
|
@@ -3011,6 +3276,8 @@ export async function createFrontendScoutEvidenceTools(input) {
|
|
|
3011
3276
|
unresolvedPaths: stringArray,
|
|
3012
3277
|
}, { additionalProperties: false }),
|
|
3013
3278
|
async execute(_toolCallId, params) {
|
|
3279
|
+
if (input.requirementIds && (!activeScope?.length || params.scopeId !== scopeIdentity(activeScope)))
|
|
3280
|
+
return receipt({ ok: false, code: "FRONTEND_INPUT_SCOPE_VIOLATION", error: "Use exactly the runtime Scout scopeId; discovery may complete only the supplied obligations" });
|
|
3014
3281
|
const implementationPaths = params?.implementationPaths ?? [];
|
|
3015
3282
|
const testPaths = params?.testPaths ?? [];
|
|
3016
3283
|
const pathEvidence = await enrichScoutPathEvidence([
|
|
@@ -3018,7 +3285,7 @@ export async function createFrontendScoutEvidenceTools(input) {
|
|
|
3018
3285
|
...implementationPaths,
|
|
3019
3286
|
...testPaths,
|
|
3020
3287
|
]);
|
|
3021
|
-
const
|
|
3288
|
+
const surface = {
|
|
3022
3289
|
kind: "target-surface",
|
|
3023
3290
|
origin: "scout",
|
|
3024
3291
|
completeness: params?.completeness ?? "blocked",
|
|
@@ -3031,7 +3298,27 @@ export async function createFrontendScoutEvidenceTools(input) {
|
|
|
3031
3298
|
unresolvedPaths: params?.unresolvedPaths ?? [],
|
|
3032
3299
|
...(hasSourceDeclarations ? { sourceDeclaredPaths } : {}),
|
|
3033
3300
|
...(pathEvidence.length > 0 ? { pathEvidence } : {}),
|
|
3034
|
-
}
|
|
3301
|
+
};
|
|
3302
|
+
if (activeScope) {
|
|
3303
|
+
const { readCompleteScoutTargetSurface } = await import("../workflows/dag/frontend-shadow-dual-write.js");
|
|
3304
|
+
if (surface.completeness === "complete") {
|
|
3305
|
+
const check = readCompleteScoutTargetSurface([{ phase: "committed", fact: surface }]);
|
|
3306
|
+
if (!check.ok)
|
|
3307
|
+
return receipt({ ok: false, code: "SCOUT_SCOPE_INCOMPLETE", error: check.reason });
|
|
3308
|
+
}
|
|
3309
|
+
const saved = await adoptScoutFact("scout-scope", { kind: "scout-scope", origin: "scout", id: params.scopeId, requirementIds: activeScope, surface });
|
|
3310
|
+
if (!saved.ok)
|
|
3311
|
+
return receipt(saved);
|
|
3312
|
+
const current = latestScopes();
|
|
3313
|
+
if (input.requirementIds?.every(id => current.get(id)?.surface?.completeness === "complete")) {
|
|
3314
|
+
const surfaces = [...new Set(input.requirementIds.map(id => current.get(id)))].map(f => f.surface);
|
|
3315
|
+
const union = (key) => [...new Set(surfaces.flatMap(s => Array.isArray(s[key]) ? s[key] : []))];
|
|
3316
|
+
const entries = [...new Set(surfaces.map(s => String(s.entrypoint ?? "")).filter(Boolean))];
|
|
3317
|
+
return receipt(await adoptScoutFact("target-surface", { kind: "target-surface", origin: "scout", completeness: "complete", entrypoint: entries[0] ?? "", implementationPaths: [...new Set([...entries, ...union("implementationPaths")])], testPaths: union("testPaths"), allowedPathConflicts: union("allowedPathConflicts"), unresolvedPaths: union("unresolvedPaths"), routeOrMount: [...new Set(surfaces.map(s => s.routeOrMount).filter(Boolean))].join("\n"), dataSource: [...new Set(surfaces.map(s => s.dataSource).filter(Boolean))].join("\n"), pathEvidence: surfaces.flatMap(s => s.pathEvidence ?? []), ...(hasSourceDeclarations ? { sourceDeclaredPaths } : {}) }));
|
|
3318
|
+
}
|
|
3319
|
+
return receipt(saved);
|
|
3320
|
+
}
|
|
3321
|
+
const result = await adoptScoutFact("target-surface", surface);
|
|
3035
3322
|
return receipt(result);
|
|
3036
3323
|
},
|
|
3037
3324
|
});
|
|
@@ -3055,10 +3342,30 @@ export async function createFrontendScoutEvidenceTools(input) {
|
|
|
3055
3342
|
return receipt(result);
|
|
3056
3343
|
},
|
|
3057
3344
|
});
|
|
3345
|
+
const durable = await createDurableFrontendTools({
|
|
3346
|
+
file: path.join(input.runDir, input.nodeId, "scout-typed-facts.jsonl"), attemptId, store: input.store,
|
|
3347
|
+
setWorkingStore: next => { store = next; }, binding: { requirementIds: input.requirementIds, sourceDeclaredPaths: input.sourceDeclaredPaths, sourceDigest: input.sourceDigest, workspaceRoot: input.workspaceRoot },
|
|
3348
|
+
tools: [recordTargetSurfaceTool, recordDesignEvidenceTool],
|
|
3349
|
+
validateRestored: async (records) => {
|
|
3350
|
+
if (!input.workspaceRoot)
|
|
3351
|
+
return;
|
|
3352
|
+
for (const record of records) {
|
|
3353
|
+
const evidence = record.fact.kind === "scout-scope" ? record.fact.surface?.pathEvidence : record.fact.pathEvidence;
|
|
3354
|
+
if (!Array.isArray(evidence))
|
|
3355
|
+
continue;
|
|
3356
|
+
for (const previous of evidence) {
|
|
3357
|
+
if (!isRecordObject(previous) || typeof previous.path !== "string")
|
|
3358
|
+
throw Error("scout path evidence is malformed");
|
|
3359
|
+
const current = (await enrichScoutPathEvidence([previous.path]))[0];
|
|
3360
|
+
if (!current || current.sha256 !== previous.sha256 || current.fresh !== previous.fresh)
|
|
3361
|
+
throw Error(`scout evidence drift: ${previous.path}; refresh Scout before reusing facts`);
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
},
|
|
3365
|
+
});
|
|
3058
3366
|
return {
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
adoptCommittedFacts: async (records) => {
|
|
3367
|
+
...durable,
|
|
3368
|
+
adoptCommittedFacts: async (records) => durable.commitExternal(async () => {
|
|
3062
3369
|
for (const record of records) {
|
|
3063
3370
|
if (record.phase !== "committed")
|
|
3064
3371
|
continue;
|
|
@@ -3072,11 +3379,16 @@ export async function createFrontendScoutEvidenceTools(input) {
|
|
|
3072
3379
|
throw new Error(`frontend scout shard fact merge failed: ${result.error}`);
|
|
3073
3380
|
}
|
|
3074
3381
|
}
|
|
3382
|
+
}),
|
|
3383
|
+
setActiveScope: ids => {
|
|
3384
|
+
if (!ids.length || ids.some(id => !input.requirementIds?.includes(id)))
|
|
3385
|
+
throw Error("FRONTEND_INPUT_SCOPE_VIOLATION");
|
|
3386
|
+
activeScope = [...new Set(ids)];
|
|
3387
|
+
return scopeIdentity(activeScope);
|
|
3075
3388
|
},
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
},
|
|
3389
|
+
completedRequirementIds: () => new Set([...latestScopes()].filter(([, fact]) => fact.surface?.completeness === "complete").map(([id]) => id)),
|
|
3390
|
+
completedScopeFacts: () => [...new Set(latestScopes().values())].filter(f => f.surface?.completeness === "complete"),
|
|
3391
|
+
committedFacts: () => readCommittedEvents(store, attemptId),
|
|
3080
3392
|
};
|
|
3081
3393
|
}
|
|
3082
3394
|
export function buildDagPiUserMessage(task, persona, step) {
|
|
@@ -3327,15 +3639,14 @@ async function runFrontendReviewTerminalShadow(input) {
|
|
|
3327
3639
|
return input.mapped;
|
|
3328
3640
|
const { compareTypedReviewToLegacyJsonVerdict } = await import("../workflows/dag/frontend-review-context.js");
|
|
3329
3641
|
const { parseJsonReviewVerdict } = await import("../workflows/dag/output-protocol.js");
|
|
3330
|
-
const sessionEventsPath = path.join(input.meta.runDir, input.task.id, "session-events.jsonl");
|
|
3331
3642
|
let typedKinds = [];
|
|
3332
3643
|
try {
|
|
3333
|
-
|
|
3334
|
-
|
|
3644
|
+
await input.tools?.flush();
|
|
3645
|
+
input.tools?.scopeProtocol?.assertComplete();
|
|
3646
|
+
typedKinds = input.tools?.scopeProtocol?.committedFacts().filter(r => ["approve_review", "request_review_changes"].includes(String(r.fact.kind))).map(r => String(r.fact.kind)) ?? [];
|
|
3335
3647
|
}
|
|
3336
|
-
catch {
|
|
3337
|
-
|
|
3338
|
-
typedKinds = [];
|
|
3648
|
+
catch (error) {
|
|
3649
|
+
return { ...input.mapped, ok: false, failureCategory: "frontend-ledger-invalid", stderr: `FRONTEND_REVIEW_SCOPE_INCOMPLETE: ${error instanceof Error ? error.message : String(error)}` };
|
|
3339
3650
|
}
|
|
3340
3651
|
let legacyVerdict;
|
|
3341
3652
|
try {
|
|
@@ -3361,13 +3672,7 @@ async function runFrontendReviewTerminalShadow(input) {
|
|
|
3361
3672
|
reason: `typed review equivalence comparison crashed: ${error instanceof Error ? error.message : String(error)}`,
|
|
3362
3673
|
};
|
|
3363
3674
|
}
|
|
3364
|
-
//
|
|
3365
|
-
try {
|
|
3366
|
-
await input.tools?.flush?.();
|
|
3367
|
-
}
|
|
3368
|
-
catch {
|
|
3369
|
-
// best-effort
|
|
3370
|
-
}
|
|
3675
|
+
// The durable ledger was validated above; this artifact is audit-only.
|
|
3371
3676
|
try {
|
|
3372
3677
|
await writeDagNodeJsonArtifact(input.meta.runDir, input.task.id, "fact-review-status.json", {
|
|
3373
3678
|
schemaVersion: 1,
|
|
@@ -3409,23 +3714,16 @@ async function runFrontendDesignTerminalShadow(input) {
|
|
|
3409
3714
|
// diagnosed as an omitted terminal tool call.
|
|
3410
3715
|
if (!input.mapped.ok)
|
|
3411
3716
|
return input.mapped;
|
|
3412
|
-
const sessionEventsPath = path.join(input.meta.runDir, input.task.id, "session-events.jsonl");
|
|
3413
3717
|
let typedKinds = [];
|
|
3414
3718
|
try {
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
catch {
|
|
3419
|
-
// Missing/unreadable session log → fail-closed at zero terminal facts.
|
|
3420
|
-
typedKinds = [];
|
|
3719
|
+
await input.tools?.flush();
|
|
3720
|
+
input.tools?.scopeProtocol?.assertComplete();
|
|
3721
|
+
typedKinds = input.tools?.scopeProtocol?.committedFacts().filter(r => ["approve_design", "request_design_changes"].includes(String(r.fact.kind))).map(r => String(r.fact.kind)) ?? [];
|
|
3421
3722
|
}
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
await input.tools?.flush?.();
|
|
3425
|
-
}
|
|
3426
|
-
catch {
|
|
3427
|
-
// best-effort
|
|
3723
|
+
catch (error) {
|
|
3724
|
+
return { ...input.mapped, ok: false, failureCategory: "frontend-ledger-invalid", stderr: `FRONTEND_REVIEW_SCOPE_INCOMPLETE: ${error instanceof Error ? error.message : String(error)}` };
|
|
3428
3725
|
}
|
|
3726
|
+
// The durable ledger was validated above; this artifact is audit-only.
|
|
3429
3727
|
try {
|
|
3430
3728
|
await writeDagNodeJsonArtifact(input.meta.runDir, input.task.id, "fact-design-status.json", {
|
|
3431
3729
|
schemaVersion: 1,
|
|
@@ -3458,6 +3756,7 @@ const FRONTEND_PLAN_SEGMENTS = [
|
|
|
3458
3756
|
id: "coverage",
|
|
3459
3757
|
toolNames: new Set([
|
|
3460
3758
|
"record_plan_requirement",
|
|
3759
|
+
"record_plan_group_coverage",
|
|
3461
3760
|
"record_plan_verification_target",
|
|
3462
3761
|
"record_plan_evidence_gap",
|
|
3463
3762
|
"adopt_staged_fact",
|
|
@@ -3474,13 +3773,14 @@ const FRONTEND_PLAN_SEGMENTS = [
|
|
|
3474
3773
|
toolNames: new Set(["record_state_registry", "adopt_staged_fact"]),
|
|
3475
3774
|
instruction: [
|
|
3476
3775
|
"PLAN PHASE — global UX vocabulary.",
|
|
3477
|
-
"
|
|
3776
|
+
"Bootstrap the global UX vocabulary from the execution-group index and authoritative declared states. This is navigation, not permission to decide unseen behavior. Detailed complete scopes may extend the registry with replace:true while preserving live names. UI states use declaredUiStates ids when present. Interaction names are stable kebab-case behavior domains; merge requirements that describe the same behavior instead of renaming it per AC slice. Empty arrays explicitly declare that no UX vocabulary applies. Do not record component choices or state-flow details in this phase.",
|
|
3478
3777
|
"Do not call finalize_plan; it is not available in this phase.",
|
|
3479
3778
|
].join(" "),
|
|
3480
3779
|
},
|
|
3481
3780
|
{
|
|
3482
3781
|
id: "ux-local",
|
|
3483
3782
|
toolNames: new Set([
|
|
3783
|
+
"record_state_registry",
|
|
3484
3784
|
"record_component_choice",
|
|
3485
3785
|
"record_state_flow",
|
|
3486
3786
|
"record_plan_verification_target",
|
|
@@ -3488,7 +3788,7 @@ const FRONTEND_PLAN_SEGMENTS = [
|
|
|
3488
3788
|
]),
|
|
3489
3789
|
instruction: [
|
|
3490
3790
|
"PLAN PHASE — global UX decisions.",
|
|
3491
|
-
"Requirements and verification targets are already committed in the ledger. Review the complete
|
|
3791
|
+
"Requirements and verification targets are already committed in the ledger; do not re-record unchanged facts. Review the current complete execution-group scope and the committed global UX registry together, then record each component choice, UI state and interaction exactly once. Bind each applicable state to its verificationTargetIds; the runtime derives the reverse VT.uiStates relation. If a VT requires correction, record_plan_verification_target with replace:true is available after declaring its states; preserve its requirement coverage. Multiple requirements describing one behavior share one registry name and state-flow entry; never repeat or rename it per AC. Cross-cutting data flow belongs to the global Mock/data phase. Do not record routes, Mock/API policy, dependencies, or design deviations here.",
|
|
3492
3792
|
"Do not call finalize_plan; it is not available in this phase.",
|
|
3493
3793
|
].join(" "),
|
|
3494
3794
|
},
|
|
@@ -3502,7 +3802,7 @@ const FRONTEND_PLAN_SEGMENTS = [
|
|
|
3502
3802
|
},
|
|
3503
3803
|
{
|
|
3504
3804
|
id: "global-mock-data",
|
|
3505
|
-
toolNames: new Set(["record_data_flow", "record_mock_api", "adopt_staged_fact"]),
|
|
3805
|
+
toolNames: new Set(["record_data_flow", "record_mock_api", "record_mock_endpoint", "adopt_staged_fact"]),
|
|
3506
3806
|
instruction: [
|
|
3507
3807
|
"PLAN PHASE — global Mock/API and data policy.",
|
|
3508
3808
|
"Record the cross-cutting interaction-to-endpoint data flow and Mock/API strategy only. Keep this decision set separate from route, component, state, dependency, and deviation facts. Do not call finalize_plan.",
|
|
@@ -3521,177 +3821,10 @@ const FRONTEND_PLAN_SEGMENTS = [
|
|
|
3521
3821
|
toolNames: null,
|
|
3522
3822
|
instruction: [
|
|
3523
3823
|
"PLAN PHASE — finalize.",
|
|
3524
|
-
"All record_* tools are available only for a narrowly named correction if the finalize receipt reports missing or invalid facts. Otherwise call finalize_plan
|
|
3824
|
+
"All record_* tools are available only for a narrowly named correction if the finalize receipt reports missing or invalid facts. Otherwise call finalize_plan with implementationSteps: [{requirementIds, action}] covering every frozen requirement. Describe concrete changes within approved paths, including what constraints preserve; shared steps may cover related IDs. Do not add tests or a test plan.",
|
|
3525
3825
|
].join(" "),
|
|
3526
3826
|
},
|
|
3527
3827
|
];
|
|
3528
|
-
function committedFactFromPlanRecord(value) {
|
|
3529
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
3530
|
-
return undefined;
|
|
3531
|
-
const record = value;
|
|
3532
|
-
if (record.phase !== undefined && record.phase !== "committed")
|
|
3533
|
-
return undefined;
|
|
3534
|
-
const fact = record.fact;
|
|
3535
|
-
return fact && typeof fact === "object" && !Array.isArray(fact)
|
|
3536
|
-
? fact
|
|
3537
|
-
: typeof record.kind === "string"
|
|
3538
|
-
? record
|
|
3539
|
-
: undefined;
|
|
3540
|
-
}
|
|
3541
|
-
function planFactStringList(value) {
|
|
3542
|
-
if (!Array.isArray(value))
|
|
3543
|
-
return [];
|
|
3544
|
-
return value.filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
3545
|
-
}
|
|
3546
|
-
function planFactScopeIntersects(fact, requirementIds) {
|
|
3547
|
-
return planFactStringList(fact.scopeRequirementIds).some((id) => requirementIds.has(id));
|
|
3548
|
-
}
|
|
3549
|
-
/** Compute the authoritative coverage queue from the committed plan ledger. */
|
|
3550
|
-
export function collectFrontendPlanMissingFacts(input) {
|
|
3551
|
-
const requirements = new Map();
|
|
3552
|
-
const standaloneEvidenceGaps = new Set();
|
|
3553
|
-
const verificationTargetIds = new Set();
|
|
3554
|
-
const verificationTargetRequirements = new Map();
|
|
3555
|
-
for (const value of input.committedFacts) {
|
|
3556
|
-
const fact = committedFactFromPlanRecord(value);
|
|
3557
|
-
if (!fact || fact.origin !== "plan")
|
|
3558
|
-
continue;
|
|
3559
|
-
if (fact.kind === "plan-requirement" && fact.entry && typeof fact.entry === "object") {
|
|
3560
|
-
const entry = fact.entry;
|
|
3561
|
-
if (typeof entry.id === "string" && entry.id.trim())
|
|
3562
|
-
requirements.set(entry.id, entry);
|
|
3563
|
-
}
|
|
3564
|
-
if (fact.kind === "plan-verification-target" && fact.entry && typeof fact.entry === "object") {
|
|
3565
|
-
const entry = fact.entry;
|
|
3566
|
-
const id = entry.id;
|
|
3567
|
-
if (typeof id === "string" && id.trim()) {
|
|
3568
|
-
verificationTargetIds.add(id);
|
|
3569
|
-
verificationTargetRequirements.set(id, new Set(Array.isArray(entry.requirementIds)
|
|
3570
|
-
? entry.requirementIds.filter((value) => typeof value === "string")
|
|
3571
|
-
: []));
|
|
3572
|
-
}
|
|
3573
|
-
}
|
|
3574
|
-
if (fact.kind === "plan-evidence-gap" && fact.entry && typeof fact.entry === "object") {
|
|
3575
|
-
const entry = fact.entry;
|
|
3576
|
-
const requirementId = entry.requirementId;
|
|
3577
|
-
const description = entry.description;
|
|
3578
|
-
if (typeof requirementId === "string" && requirementId.trim() && typeof description === "string" && description.trim()) {
|
|
3579
|
-
standaloneEvidenceGaps.add(requirementId);
|
|
3580
|
-
}
|
|
3581
|
-
}
|
|
3582
|
-
}
|
|
3583
|
-
const missing = [];
|
|
3584
|
-
for (const id of input.requirementIds) {
|
|
3585
|
-
const entry = requirements.get(id);
|
|
3586
|
-
if (!entry) {
|
|
3587
|
-
missing.push({
|
|
3588
|
-
kind: "plan-requirement",
|
|
3589
|
-
id,
|
|
3590
|
-
requirementIds: [id],
|
|
3591
|
-
reason: `requirement ${id} has no committed plan-requirement fact`,
|
|
3592
|
-
});
|
|
3593
|
-
continue;
|
|
3594
|
-
}
|
|
3595
|
-
const targetIds = Array.isArray(entry.verificationTargetIds)
|
|
3596
|
-
? entry.verificationTargetIds.filter((value) => typeof value === "string" && value.trim().length > 0)
|
|
3597
|
-
: [];
|
|
3598
|
-
const gap = entry.evidenceGap && typeof entry.evidenceGap === "object"
|
|
3599
|
-
? entry.evidenceGap
|
|
3600
|
-
: undefined;
|
|
3601
|
-
const hasEvidenceGap = (typeof gap?.description === "string" && gap.description.trim().length > 0) ||
|
|
3602
|
-
standaloneEvidenceGaps.has(id);
|
|
3603
|
-
if (targetIds.length === 0 && !hasEvidenceGap) {
|
|
3604
|
-
missing.push({
|
|
3605
|
-
kind: "plan-verification-target",
|
|
3606
|
-
requirementIds: [id],
|
|
3607
|
-
reason: `requirement ${id} declares neither a verification target nor a non-empty evidenceGap`,
|
|
3608
|
-
});
|
|
3609
|
-
continue;
|
|
3610
|
-
}
|
|
3611
|
-
for (const targetId of targetIds) {
|
|
3612
|
-
if (!verificationTargetIds.has(targetId) ||
|
|
3613
|
-
!verificationTargetRequirements.get(targetId)?.has(id)) {
|
|
3614
|
-
missing.push({
|
|
3615
|
-
kind: "plan-verification-target",
|
|
3616
|
-
id: targetId,
|
|
3617
|
-
requirementIds: [id],
|
|
3618
|
-
reason: `requirement ${id} references verification target ${targetId}, but that target is not committed`,
|
|
3619
|
-
});
|
|
3620
|
-
}
|
|
3621
|
-
}
|
|
3622
|
-
}
|
|
3623
|
-
return missing;
|
|
3624
|
-
}
|
|
3625
|
-
/** Completeness checks for phases whose facts are committed incrementally. */
|
|
3626
|
-
export function collectFrontendPlanPhaseMissingFacts(input) {
|
|
3627
|
-
const facts = input.committedFacts
|
|
3628
|
-
.map(committedFactFromPlanRecord)
|
|
3629
|
-
.filter((fact) => Boolean(fact && fact.origin === "plan"));
|
|
3630
|
-
if (input.phase === "ux-registry") {
|
|
3631
|
-
return facts.some((fact) => fact.kind === "state-registry")
|
|
3632
|
-
? []
|
|
3633
|
-
: [
|
|
3634
|
-
{
|
|
3635
|
-
kind: "state-registry",
|
|
3636
|
-
requirementIds: [...input.requirementIds],
|
|
3637
|
-
reason: "global UX vocabulary phase has no committed state-registry fact",
|
|
3638
|
-
},
|
|
3639
|
-
];
|
|
3640
|
-
}
|
|
3641
|
-
if (input.phase === "ux-local") {
|
|
3642
|
-
const needsUx = input.requirementIds.some((id) => input.behaviorRequiredRequirementIds?.includes(id));
|
|
3643
|
-
if (!needsUx)
|
|
3644
|
-
return [];
|
|
3645
|
-
const requirementSlice = new Set(input.requirementIds);
|
|
3646
|
-
const scopedFacts = facts.filter((fact) => planFactScopeIntersects(fact, requirementSlice));
|
|
3647
|
-
const hasChoice = scopedFacts.some((fact) => fact.kind === "component-choice" &&
|
|
3648
|
-
Array.isArray(fact.uiComponentChoices) &&
|
|
3649
|
-
fact.uiComponentChoices.length > 0);
|
|
3650
|
-
const canonicalStateFlow = collectCanonicalStateFlowNames(scopedFacts);
|
|
3651
|
-
const hasStateFlow = canonicalStateFlow.uiStateNames.size > 0 ||
|
|
3652
|
-
canonicalStateFlow.interactionNames.size > 0;
|
|
3653
|
-
const missing = [];
|
|
3654
|
-
if (!hasChoice) {
|
|
3655
|
-
missing.push({
|
|
3656
|
-
kind: "component-choice",
|
|
3657
|
-
requirementIds: [...input.requirementIds],
|
|
3658
|
-
reason: "behaviour-required UX slice has no committed component-choice fact",
|
|
3659
|
-
});
|
|
3660
|
-
}
|
|
3661
|
-
if (!hasStateFlow) {
|
|
3662
|
-
missing.push({
|
|
3663
|
-
kind: "state-flow",
|
|
3664
|
-
requirementIds: [...input.requirementIds],
|
|
3665
|
-
reason: "behaviour-required UX slice has no committed state-flow fact",
|
|
3666
|
-
});
|
|
3667
|
-
}
|
|
3668
|
-
return missing;
|
|
3669
|
-
}
|
|
3670
|
-
const hasMockApi = facts.some((fact) => fact.kind === "mock-api");
|
|
3671
|
-
const liveInteractions = collectCanonicalStateFlowNames(input.committedFacts).interactionNames;
|
|
3672
|
-
const coveredInteractions = new Set(facts
|
|
3673
|
-
.filter((fact) => fact.kind === "data-flow")
|
|
3674
|
-
.flatMap((fact) => planFactStringList(fact.interactions)));
|
|
3675
|
-
const missing = [];
|
|
3676
|
-
if (!hasMockApi) {
|
|
3677
|
-
missing.push({
|
|
3678
|
-
kind: "mock-api",
|
|
3679
|
-
requirementIds: [...input.requirementIds],
|
|
3680
|
-
reason: "global Mock/data phase has no committed mock-api fact",
|
|
3681
|
-
});
|
|
3682
|
-
}
|
|
3683
|
-
for (const interaction of liveInteractions) {
|
|
3684
|
-
if (coveredInteractions.has(interaction))
|
|
3685
|
-
continue;
|
|
3686
|
-
missing.push({
|
|
3687
|
-
kind: "data-flow",
|
|
3688
|
-
id: interaction,
|
|
3689
|
-
requirementIds: [...input.requirementIds],
|
|
3690
|
-
reason: `interaction ${interaction} has no committed data-flow fact`,
|
|
3691
|
-
});
|
|
3692
|
-
}
|
|
3693
|
-
return missing;
|
|
3694
|
-
}
|
|
3695
3828
|
/** Estimate calls conservatively: requirement + one VT, with a second VT
|
|
3696
3829
|
* reserved for behaviour-required requirements. Explicit declarations win. */
|
|
3697
3830
|
export function estimateFrontendPlanRequirementRecordCalls(fact) {
|
|
@@ -3733,42 +3866,47 @@ export function batchFrontendPlanRequirements(input) {
|
|
|
3733
3866
|
}
|
|
3734
3867
|
const FRONTEND_PLAN_COVERAGE_MAX_RECORD_CALLS = 10;
|
|
3735
3868
|
const FRONTEND_PLAN_COVERAGE_MAX_CONCURRENCY = 4;
|
|
3736
|
-
// A
|
|
3737
|
-
//
|
|
3738
|
-
// reinvented per AC batch. Keep a safety bound for adaptive coverage retries
|
|
3869
|
+
// A single registry owns global names; large UX work uses bounded serial
|
|
3870
|
+
// scopes against that registry. Keep a safety bound for adaptive retries
|
|
3739
3871
|
// without letting the old 32-session ceiling skip finalize.
|
|
3740
3872
|
const FRONTEND_PLAN_BATCH_MAX_SESSIONS = 128;
|
|
3741
3873
|
const FRONTEND_PLAN_SMALL_MAX_REQUIREMENTS = 8;
|
|
3742
3874
|
const FRONTEND_PLAN_SMALL_MAX_ESTIMATED_CALLS = 24;
|
|
3743
|
-
async function mapWithConcurrency(items, limit, worker) {
|
|
3875
|
+
async function mapWithConcurrency(items, limit, worker, shouldReduceConcurrency) {
|
|
3744
3876
|
const results = new Array(items.length);
|
|
3745
3877
|
let nextIndex = 0;
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3878
|
+
let concurrency = Math.max(1, limit);
|
|
3879
|
+
const running = new Map();
|
|
3880
|
+
try {
|
|
3881
|
+
while (nextIndex < items.length || running.size > 0) {
|
|
3882
|
+
while (nextIndex < items.length && running.size < concurrency) {
|
|
3883
|
+
const index = nextIndex++;
|
|
3884
|
+
running.set(index, worker(items[index], index).then(result => ({ index, result })));
|
|
3885
|
+
}
|
|
3886
|
+
const { index, result } = await Promise.race(running.values());
|
|
3887
|
+
running.delete(index);
|
|
3888
|
+
results[index] = result;
|
|
3889
|
+
// Drain existing work; only pending shards use the reduced cap.
|
|
3890
|
+
// Failed shards remain failed and receive no extra retry allowance.
|
|
3891
|
+
if (shouldReduceConcurrency(result))
|
|
3892
|
+
concurrency = Math.max(1, Math.floor(concurrency / 2));
|
|
3753
3893
|
}
|
|
3754
|
-
}
|
|
3894
|
+
}
|
|
3895
|
+
catch (error) {
|
|
3896
|
+
await Promise.allSettled(running.values());
|
|
3897
|
+
throw error;
|
|
3898
|
+
}
|
|
3755
3899
|
return results;
|
|
3756
3900
|
}
|
|
3757
|
-
function compactPromptString(value,
|
|
3758
|
-
|
|
3759
|
-
return undefined;
|
|
3760
|
-
const normalized = value.trim();
|
|
3761
|
-
return normalized.length <= maxChars
|
|
3762
|
-
? normalized
|
|
3763
|
-
: `${normalized.slice(0, maxChars - 1)}…`;
|
|
3901
|
+
function compactPromptString(value, _maxChars) {
|
|
3902
|
+
return typeof value === "string" && value.trim().length ? value.trim() : undefined;
|
|
3764
3903
|
}
|
|
3765
|
-
function compactPromptStringArray(value,
|
|
3904
|
+
function compactPromptStringArray(value, _maxEntries = 12, maxChars = 180) {
|
|
3766
3905
|
if (!Array.isArray(value))
|
|
3767
3906
|
return [];
|
|
3768
3907
|
return value
|
|
3769
3908
|
.map((item) => compactPromptString(item, maxChars))
|
|
3770
|
-
.filter((item) => item !== undefined)
|
|
3771
|
-
.slice(0, maxEntries);
|
|
3909
|
+
.filter((item) => item !== undefined);
|
|
3772
3910
|
}
|
|
3773
3911
|
function countFrontendPlanTargetSurfaces(basePrompt) {
|
|
3774
3912
|
const match = /<frontend_plan_input>[\s\S]*?<\/frontend_plan_input>/.exec(basePrompt);
|
|
@@ -3841,7 +3979,7 @@ export function compactFrontendPlanPromptForRequirementSlice(basePrompt, require
|
|
|
3841
3979
|
}
|
|
3842
3980
|
}
|
|
3843
3981
|
if (!payload || !Array.isArray(payload.requirements))
|
|
3844
|
-
|
|
3982
|
+
throw Error("FRONTEND_INPUT_INVALID: plan inventory is not parseable");
|
|
3845
3983
|
const requirementsById = new Map();
|
|
3846
3984
|
for (const value of payload.requirements) {
|
|
3847
3985
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
@@ -3853,7 +3991,7 @@ export function compactFrontendPlanPromptForRequirementSlice(basePrompt, require
|
|
|
3853
3991
|
}
|
|
3854
3992
|
const requirements = slice.map((id) => requirementsById.get(id));
|
|
3855
3993
|
if (requirements.some((requirement) => requirement === undefined)) {
|
|
3856
|
-
|
|
3994
|
+
throw Error(`FRONTEND_INPUT_SCOPE_MISSING: ${slice.filter(id => !requirementsById.has(id)).join(", ")}`);
|
|
3857
3995
|
}
|
|
3858
3996
|
const compactRequirements = requirements.map((requirement) => ({
|
|
3859
3997
|
id: compactPromptString(requirement.id, 80),
|
|
@@ -3870,26 +4008,28 @@ export function compactFrontendPlanPromptForRequirementSlice(basePrompt, require
|
|
|
3870
4008
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
3871
4009
|
return [];
|
|
3872
4010
|
const target = value;
|
|
3873
|
-
|
|
4011
|
+
if (options.verificationTargetIds && !options.verificationTargetIds.includes(String(target.id)))
|
|
4012
|
+
return [];
|
|
4013
|
+
const targetRequirementIds = planFactStringList(target.requirementIds);
|
|
3874
4014
|
const relatedRequirementIds = targetRequirementIds.filter((id) => sliceSet.has(id));
|
|
3875
4015
|
if (relatedRequirementIds.length === 0)
|
|
3876
4016
|
return [];
|
|
3877
4017
|
return [
|
|
3878
4018
|
{
|
|
3879
|
-
...(
|
|
3880
|
-
? { id:
|
|
4019
|
+
...(typeof target.id === "string"
|
|
4020
|
+
? { id: target.id }
|
|
3881
4021
|
: {}),
|
|
3882
|
-
...(
|
|
3883
|
-
? { commandId:
|
|
4022
|
+
...(typeof target.commandId === "string"
|
|
4023
|
+
? { commandId: target.commandId }
|
|
3884
4024
|
: {}),
|
|
3885
4025
|
...(compactPromptString(target.commandLabel, 180)
|
|
3886
4026
|
? { commandLabel: compactPromptString(target.commandLabel, 180) }
|
|
3887
4027
|
: {}),
|
|
3888
|
-
...(
|
|
3889
|
-
? { file:
|
|
4028
|
+
...(typeof target.file === "string"
|
|
4029
|
+
? { file: target.file }
|
|
3890
4030
|
: {}),
|
|
3891
4031
|
requirementIds: relatedRequirementIds,
|
|
3892
|
-
uiStates:
|
|
4032
|
+
uiStates: planFactStringList(target.uiStates),
|
|
3893
4033
|
},
|
|
3894
4034
|
];
|
|
3895
4035
|
})
|
|
@@ -3941,7 +4081,7 @@ export function compactFrontendPlanPromptForRequirementSlice(basePrompt, require
|
|
|
3941
4081
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
3942
4082
|
return [];
|
|
3943
4083
|
const state = value;
|
|
3944
|
-
const id =
|
|
4084
|
+
const id = typeof state.id === "string" ? state.id : undefined;
|
|
3945
4085
|
if (!id)
|
|
3946
4086
|
return [];
|
|
3947
4087
|
return [{
|
|
@@ -3961,6 +4101,9 @@ export function compactFrontendPlanPromptForRequirementSlice(basePrompt, require
|
|
|
3961
4101
|
? payload.committedUx
|
|
3962
4102
|
: undefined;
|
|
3963
4103
|
const compactPayload = {
|
|
4104
|
+
inputManifest: { ...projectFrontendInputScope({ ...payload, requirements: [...requirementsById.values()] }, slice).inputManifest, semantics: options.includeRequirementText === false ? "navigation-only" : "full" },
|
|
4105
|
+
constraints: payload.constraints,
|
|
4106
|
+
executionGroups: Array.isArray(payload.executionGroups) ? payload.executionGroups.filter(g => isRecordObject(g) && Array.isArray(g.requirementIds) && g.requirementIds.some(id => slice.includes(String(id)))) : [],
|
|
3964
4107
|
requirements: compactRequirements,
|
|
3965
4108
|
requiredDeliverables: Array.isArray(payload.requiredDeliverables) ? payload.requiredDeliverables : [],
|
|
3966
4109
|
...(compactTargetSurface.length > 0
|
|
@@ -4038,9 +4181,9 @@ function compactFrontendPlanLedgerContext(input) {
|
|
|
4038
4181
|
compactFacts.push({
|
|
4039
4182
|
kind: fact.kind,
|
|
4040
4183
|
entry: {
|
|
4041
|
-
id:
|
|
4042
|
-
implementationTargets:
|
|
4043
|
-
verificationTargetIds:
|
|
4184
|
+
id: entry.id,
|
|
4185
|
+
implementationTargets: planFactStringList(entry.implementationTargets).slice(0, 12),
|
|
4186
|
+
verificationTargetIds: planFactStringList(entry.verificationTargetIds).slice(0, 12),
|
|
4044
4187
|
...(compactPromptString(entry.expectedOutcome, 240)
|
|
4045
4188
|
? { expectedOutcome: compactPromptString(entry.expectedOutcome, 240) }
|
|
4046
4189
|
: {}),
|
|
@@ -4054,17 +4197,19 @@ function compactFrontendPlanLedgerContext(input) {
|
|
|
4054
4197
|
if (fact.kind === "plan-verification-target") {
|
|
4055
4198
|
if (!entry)
|
|
4056
4199
|
continue;
|
|
4057
|
-
|
|
4058
|
-
|
|
4200
|
+
// Scope against the complete canonical binding before compacting.
|
|
4201
|
+
// A shared target may bind a late requirement beyond the preview cap.
|
|
4202
|
+
const requirementIds = planFactStringList(entry.requirementIds).filter((id) => slice.has(id));
|
|
4203
|
+
if (requirementIds.length === 0)
|
|
4059
4204
|
continue;
|
|
4060
4205
|
compactFacts.push({
|
|
4061
4206
|
kind: fact.kind,
|
|
4062
4207
|
entry: {
|
|
4063
|
-
id:
|
|
4064
|
-
commandId:
|
|
4065
|
-
file:
|
|
4208
|
+
id: entry.id,
|
|
4209
|
+
commandId: entry.commandId,
|
|
4210
|
+
file: entry.file,
|
|
4066
4211
|
requirementIds,
|
|
4067
|
-
uiStates:
|
|
4212
|
+
uiStates: planFactStringList(entry.uiStates).slice(0, 12),
|
|
4068
4213
|
},
|
|
4069
4214
|
});
|
|
4070
4215
|
continue;
|
|
@@ -4082,7 +4227,7 @@ function compactFrontendPlanLedgerContext(input) {
|
|
|
4082
4227
|
covers: compactPromptStringArray(item.covers, 40, 120),
|
|
4083
4228
|
evidencePath: compactPromptString(item.evidencePath, 180),
|
|
4084
4229
|
}];
|
|
4085
|
-
})
|
|
4230
|
+
})
|
|
4086
4231
|
: [];
|
|
4087
4232
|
if (choices.length > 0)
|
|
4088
4233
|
compactFacts.push({ kind: fact.kind, uiComponentChoices: choices });
|
|
@@ -4091,16 +4236,16 @@ function compactFrontendPlanLedgerContext(input) {
|
|
|
4091
4236
|
if (fact.kind === "state-registry") {
|
|
4092
4237
|
compactFacts.push({
|
|
4093
4238
|
kind: fact.kind,
|
|
4094
|
-
uiStateNames:
|
|
4095
|
-
interactionNames:
|
|
4239
|
+
uiStateNames: planFactStringList(fact.uiStateNames).slice(0, 40),
|
|
4240
|
+
interactionNames: planFactStringList(fact.interactionNames).slice(0, 60),
|
|
4096
4241
|
});
|
|
4097
4242
|
continue;
|
|
4098
4243
|
}
|
|
4099
4244
|
if (fact.kind === "state-flow") {
|
|
4100
4245
|
compactFacts.push({
|
|
4101
4246
|
kind: fact.kind,
|
|
4102
|
-
uiStates: Array.isArray(fact.uiStates) ? fact.uiStates
|
|
4103
|
-
interactions: Array.isArray(fact.interactions) ? fact.interactions
|
|
4247
|
+
uiStates: Array.isArray(fact.uiStates) ? fact.uiStates : [],
|
|
4248
|
+
interactions: Array.isArray(fact.interactions) ? fact.interactions : [],
|
|
4104
4249
|
removeUiStateNames: compactPromptStringArray(fact.removeUiStateNames, 24, 100),
|
|
4105
4250
|
removeInteractionNames: compactPromptStringArray(fact.removeInteractionNames, 24, 100),
|
|
4106
4251
|
});
|
|
@@ -4123,7 +4268,7 @@ function compactFrontendPlanLedgerContext(input) {
|
|
|
4123
4268
|
mockApi: {
|
|
4124
4269
|
strategy: compactPromptString(mockApi.strategy, 40),
|
|
4125
4270
|
activation: compactPromptString(mockApi.activation, 180),
|
|
4126
|
-
endpoints: Array.isArray(mockApi.endpoints) ? mockApi.endpoints
|
|
4271
|
+
endpoints: Array.isArray(mockApi.endpoints) ? mockApi.endpoints : [],
|
|
4127
4272
|
},
|
|
4128
4273
|
});
|
|
4129
4274
|
continue;
|
|
@@ -4144,17 +4289,334 @@ function compactFrontendPlanLedgerContext(input) {
|
|
|
4144
4289
|
return "";
|
|
4145
4290
|
const priorityFacts = compactFacts.filter((fact) => fact.kind === "plan-requirement" || fact.kind === "plan-verification-target");
|
|
4146
4291
|
const otherFacts = compactFacts.filter((fact) => fact.kind !== "plan-requirement" && fact.kind !== "plan-verification-target");
|
|
4292
|
+
let previewBytes = 0;
|
|
4147
4293
|
const boundedFacts = [
|
|
4148
4294
|
...priorityFacts.slice(0, 64),
|
|
4149
4295
|
...otherFacts.slice(-32),
|
|
4150
|
-
].slice(0, 96)
|
|
4296
|
+
].slice(0, 96).filter(fact => {
|
|
4297
|
+
const bytes = Buffer.byteLength(JSON.stringify(fact));
|
|
4298
|
+
if (previewBytes + bytes > 24_000)
|
|
4299
|
+
return false;
|
|
4300
|
+
previewBytes += bytes;
|
|
4301
|
+
return true;
|
|
4302
|
+
});
|
|
4151
4303
|
return [
|
|
4152
4304
|
"<frontend_plan_ledger>",
|
|
4153
|
-
"
|
|
4305
|
+
"Preview of committed plan facts; not a replacement payload. Fields and lists may be omitted or shortened. Use read_plan_facts with kind, entryId/eventId and optional field to retrieve complete values in bounded pages before corrections; preserve all other bindings. Absence from this preview is not absence from the ledger.",
|
|
4306
|
+
JSON.stringify({ complete: false, matchingFacts: compactFacts.length, omittedFacts: compactFacts.length - boundedFacts.length }),
|
|
4154
4307
|
JSON.stringify(boundedFacts),
|
|
4155
4308
|
"</frontend_plan_ledger>",
|
|
4156
4309
|
].join("\n");
|
|
4157
4310
|
}
|
|
4311
|
+
export async function runFrontendReviewSegmentedSessions(input) {
|
|
4312
|
+
const protocol = input.tools.scopeProtocol;
|
|
4313
|
+
const terminalKinds = input.phase === "review" ? ["approve_review", "request_review_changes"] : ["approve_design", "request_design_changes"];
|
|
4314
|
+
const terminal = () => protocol.committedFacts().some(r => terminalKinds.includes(String(r.fact.kind)));
|
|
4315
|
+
const targetBytes = input.sessionOptions.frontendExecutionPolicy?.scopeTargetBytes ?? FRONTEND_SCOPE_TARGET_BYTES;
|
|
4316
|
+
const queue = packFrontendInputUnits(input.inventory.scopes.filter(s => !protocol.completedScopeIds().has(s.id)), { targetBytes, maxUnits: input.sessionOptions.frontendExecutionPolicy?.maxScopeUnits }).map(scopes => ({ scopes, repairs: 0 }));
|
|
4317
|
+
if (!queue.length)
|
|
4318
|
+
queue.push({ scopes: [], repairs: 0 });
|
|
4319
|
+
let calls = 0;
|
|
4320
|
+
let last = { ok: true, assistantText: "", command: [], durationMs: 0, exitCode: 0, failureCategory: "success", modelDisplay: "unknown", parsedEvents: 0, stderr: "", stdout: "", timedOut: false, attemptedModels: [], fallbackUsed: false, tokensUsed: 0 };
|
|
4321
|
+
for (let index = 0; index < queue.length; index++) {
|
|
4322
|
+
await input.tools.flush();
|
|
4323
|
+
await input.inventory.validate();
|
|
4324
|
+
if (terminal()) {
|
|
4325
|
+
protocol.assertComplete();
|
|
4326
|
+
return last;
|
|
4327
|
+
}
|
|
4328
|
+
const item = queue[index];
|
|
4329
|
+
const scopes = item.scopes.filter(s => !protocol.completedScopeIds().has(s.id));
|
|
4330
|
+
protocol.setActiveScope(scopes.map(s => s.id));
|
|
4331
|
+
const finalScope = input.inventory.scopes.every(s => protocol.completedScopeIds().has(s.id) || scopes.some(current => current.id === s.id));
|
|
4332
|
+
const customTools = finalScope ? input.customTools : input.customTools.filter(t => !terminalKinds.includes(String(t.name)));
|
|
4333
|
+
const prompt = `${input.basePrompt}\n<frontend_review_scope>\n${JSON.stringify({ semantics: "full", inventoryDigest: input.inventory.digest, scopes, previouslyCompleted: [...protocol.completedScopeIds()], savedFindings: protocol.committedFacts().filter(r => String(r.fact.kind).endsWith("-finding")).map(r => ({ id: r.fact.id, finding: r.fact.finding })) })}\n</frontend_review_scope>\nReview the complete supplied scopes, saving each finding immediately. Call complete_review_scope for each exact id only after all its independent permissions, thresholds, errors and evidence have been checked. ${finalScope ? "After every scope is complete, make one independent overall approve/request_changes decision; persisted findings cannot be omitted." : "More scopes remain. Do not finalize or reread already completed scopes unless resolving a cross-scope issue."}`;
|
|
4334
|
+
if (Buffer.byteLength(prompt) > targetBytes && scopes.length > 1) {
|
|
4335
|
+
const at = Math.ceil(scopes.length / 2);
|
|
4336
|
+
queue.splice(index, 1, { scopes: scopes.slice(0, at), repairs: 0 }, { scopes: scopes.slice(at), repairs: 0 });
|
|
4337
|
+
index--;
|
|
4338
|
+
continue;
|
|
4339
|
+
}
|
|
4340
|
+
const completionInstruction = `${item.repairs ? "REPAIR: the prior session did not commit all required checkpoints/verdict. Do not repeat the review narrative. " : ""}Complete these exact runtime scope IDs using complete_review_scope: ${JSON.stringify(scopes.map(scope => scope.id))}. These are scope IDs, not node IDs. ${finalScope ? `After those checkpoints succeed, call exactly one terminal tool: ${terminalKinds.join(" or ")}. A prose conclusion is not a committed verdict.` : "More scopes remain; do not submit an overall verdict yet."}`;
|
|
4341
|
+
const userMessage = `${input.sessionOptions.userMessage}\n\n${completionInstruction}`;
|
|
4342
|
+
if (++calls > 128)
|
|
4343
|
+
return { ...last, ok: false, failureCategory: "frontend-input-exhausted", stderr: "FRONTEND_REVIEW_RECOVERY_EXHAUSTED: session quota reached" };
|
|
4344
|
+
last = await observeFrontendSession({ ...input.observation, phase: `${input.phase}/scope`, scopeIds: scopes.map(s => s.id), prompt, userMessage, customTools,
|
|
4345
|
+
artifactPath: input.observation?.artifactPath ? `${input.observation.artifactPath}-${calls}.json` : undefined,
|
|
4346
|
+
committedCount: () => protocol.committedFacts().length, durableCommittedCount: () => protocol.committedFacts().length,
|
|
4347
|
+
}, observer => input.piStepFn({ ...input.sessionOptions, prompt, userMessage, onAttemptObservation: observer, writerToolPolicy: { requireSdk: true, customTools } }));
|
|
4348
|
+
await input.tools.flush();
|
|
4349
|
+
await input.inventory.validate();
|
|
4350
|
+
if (last.timedOut || /interrupt|termination-unconfirmed|budget_breach/.test(last.failureCategory))
|
|
4351
|
+
return { ...last, ok: false };
|
|
4352
|
+
const capacity = readWriterThinkingExhaustionEvidence(last).stopReason === "length" || ["context-overflow", "context-budget-exhausted"].includes(last.failureCategory);
|
|
4353
|
+
const missing = scopes.filter(s => !protocol.completedScopeIds().has(s.id));
|
|
4354
|
+
if (capacity && (missing.length || !terminal() && finalScope)) {
|
|
4355
|
+
if (missing.length === 1 && scopes.length === 1 || !missing.length && !scopes.length)
|
|
4356
|
+
return { ...last, ok: false, failureCategory: "frontend-input-exhausted", stderr: "FRONTEND_INPUT_UNIT_TOO_LARGE: complete review unit or terminal exhausted" };
|
|
4357
|
+
const at = Math.ceil(missing.length / 2);
|
|
4358
|
+
const smaller = missing.length ? [missing.slice(0, at), missing.slice(at)].filter(s => s.length) : [[]];
|
|
4359
|
+
queue.splice(index, 1, ...smaller.map(scopes => ({ scopes, repairs: 0 })));
|
|
4360
|
+
index--;
|
|
4361
|
+
continue;
|
|
4362
|
+
}
|
|
4363
|
+
if (!last.ok && !capacity)
|
|
4364
|
+
return last;
|
|
4365
|
+
if (missing.length || finalScope && !terminal()) {
|
|
4366
|
+
if (item.repairs >= 1)
|
|
4367
|
+
return { ...last, ok: false, failureCategory: "frontend-input-exhausted", stderr: "FRONTEND_REVIEW_SCOPE_INCOMPLETE: required checkpoint or verdict missing" };
|
|
4368
|
+
queue.splice(index, 1, { scopes: missing, repairs: item.repairs + 1 });
|
|
4369
|
+
index--;
|
|
4370
|
+
}
|
|
4371
|
+
}
|
|
4372
|
+
protocol.assertComplete();
|
|
4373
|
+
return terminal() ? { ...last, ok: true, failureCategory: "success" } : { ...last, ok: false, failureCategory: "frontend-input-exhausted", stderr: "FRONTEND_REVIEW_SCOPE_INCOMPLETE: missing independent verdict" };
|
|
4374
|
+
}
|
|
4375
|
+
export async function runFrontendScoutSegmentedSessions(input) {
|
|
4376
|
+
const inventory = parseFrontendInputBlock(input.basePrompt, "scout");
|
|
4377
|
+
if (!inventory)
|
|
4378
|
+
throw Error("FRONTEND_INPUT_MISSING: Scout compiled inventory unavailable");
|
|
4379
|
+
const units = collectFrontendExecutionGroups(inventory.payload.requirements).map(group => ({ ...group, id: `${group.kind === "unclassified" ? "requirement" : "group"}:${group.id}`, requirements: inventory.payload.requirements.filter(r => group.requirementIds.includes(r.id)) }));
|
|
4380
|
+
const targetBytes = input.sessionOptions.frontendExecutionPolicy?.scopeTargetBytes ?? FRONTEND_SCOPE_TARGET_BYTES;
|
|
4381
|
+
const queue = packFrontendInputUnits(units, { targetBytes, maxUnits: input.sessionOptions.frontendExecutionPolicy?.maxScopeUnits }).map(batch => ({ groups: batch, repairs: 0 }));
|
|
4382
|
+
let last = { ok: true, assistantText: "", command: [], durationMs: 0, exitCode: 0, failureCategory: "success", modelDisplay: input.sessionOptions.modelConfig?.model ?? "unknown", parsedEvents: 0, stderr: "", stdout: "", timedOut: false, attemptedModels: [], fallbackUsed: false, tokensUsed: 0 };
|
|
4383
|
+
let calls = 0;
|
|
4384
|
+
for (let index = 0; index < queue.length; index++) {
|
|
4385
|
+
await input.tools.flush();
|
|
4386
|
+
const item = queue[index];
|
|
4387
|
+
const completed = input.tools.completedRequirementIds();
|
|
4388
|
+
const groups = item.groups.filter(g => g.requirementIds.some(id => !completed.has(id)));
|
|
4389
|
+
if (!groups.length)
|
|
4390
|
+
continue;
|
|
4391
|
+
const ids = groups.flatMap(g => g.requirementIds);
|
|
4392
|
+
const scopeId = input.tools.setActiveScope(ids);
|
|
4393
|
+
const prompt = input.basePrompt.replace(inventory.block, `<frontend_scout_input>\n${JSON.stringify(projectFrontendInputScope(inventory.payload, ids))}\n</frontend_scout_input>`) +
|
|
4394
|
+
`\nSCOUT SCOPE ${scopeId}: discover the related surfaces for exactly these complete obligations: ${ids.join(", ")}. Reuse proven paths from completed scope navigation; do not reread their content unless relevant new evidence is needed. Submit record_target_surface with scopeId="${scopeId}" after all discovery/design evidence for this scope. completeness=complete closes only this scope; runtime merges all scopes.\n` +
|
|
4395
|
+
JSON.stringify({ completedScopePaths: input.tools.completedScopeFacts().map(f => ({ id: f.id, requirementIds: f.requirementIds, paths: f.surface?.implementationPaths })) });
|
|
4396
|
+
const envelopeBytes = Buffer.byteLength(prompt + input.sessionOptions.userMessage + JSON.stringify(input.customTools.map(t => { const tool = t; return { name: tool.name, description: tool.description, parameters: tool.parameters }; })));
|
|
4397
|
+
if (envelopeBytes > targetBytes && groups.length > 1) {
|
|
4398
|
+
const at = Math.ceil(groups.length / 2);
|
|
4399
|
+
queue.splice(index, 1, { groups: groups.slice(0, at), repairs: 0 }, { groups: groups.slice(at), repairs: 0 });
|
|
4400
|
+
index--;
|
|
4401
|
+
continue;
|
|
4402
|
+
}
|
|
4403
|
+
if (++calls > 128)
|
|
4404
|
+
return { ...last, ok: false, failureCategory: "frontend-input-exhausted", stderr: "SCOUT_RECOVERY_EXHAUSTED: shared session quota reached" };
|
|
4405
|
+
last = await observeFrontendSession({ ...input.observation, phase: "scout/scope", scopeIds: ids, prompt, userMessage: input.sessionOptions.userMessage, customTools: input.customTools,
|
|
4406
|
+
artifactPath: input.observation?.artifactPath ? `${input.observation.artifactPath}-${calls}.json` : undefined,
|
|
4407
|
+
committedCount: () => input.tools.committedFacts().length, durableCommittedCount: () => input.tools.committedFacts().length,
|
|
4408
|
+
}, observer => input.piStepFn({ ...input.sessionOptions, prompt, onAttemptObservation: observer, writerToolPolicy: { requireSdk: true, customTools: input.customTools } }));
|
|
4409
|
+
await input.tools.flush();
|
|
4410
|
+
const missing = groups.filter(g => g.requirementIds.some(id => !input.tools.completedRequirementIds().has(id)));
|
|
4411
|
+
if (last.timedOut || /interrupt|termination-unconfirmed|budget_breach/.test(last.failureCategory))
|
|
4412
|
+
return { ...last, ok: false };
|
|
4413
|
+
const capacity = readWriterThinkingExhaustionEvidence(last).stopReason === "length" || ["context-overflow", "context-budget-exhausted"].includes(last.failureCategory);
|
|
4414
|
+
if (capacity && missing.length) {
|
|
4415
|
+
if (missing.length === 1 && groups.length === 1)
|
|
4416
|
+
return { ...last, ok: false, failureCategory: "frontend-input-exhausted", stderr: `${last.stderr}\nFRONTEND_INPUT_UNIT_TOO_LARGE: Scout ${missing[0].id}; refine the complete source unit; unchanged retries disabled` };
|
|
4417
|
+
const at = Math.ceil(missing.length / 2);
|
|
4418
|
+
queue.splice(index, 1, ...[missing.slice(0, at), missing.slice(at)].filter(batch => batch.length).map(batch => ({ groups: batch, repairs: 0 })));
|
|
4419
|
+
index--;
|
|
4420
|
+
continue;
|
|
4421
|
+
}
|
|
4422
|
+
if (!last.ok && !capacity)
|
|
4423
|
+
return last;
|
|
4424
|
+
if (missing.length) {
|
|
4425
|
+
if (item.repairs >= 1)
|
|
4426
|
+
return { ...last, ok: false, failureCategory: "frontend-input-exhausted", stderr: "SCOUT_SCOPE_INCOMPLETE: unresolved discovery remains after local correction" };
|
|
4427
|
+
queue.splice(index, 1, { groups: missing, repairs: item.repairs + 1 });
|
|
4428
|
+
index--;
|
|
4429
|
+
continue;
|
|
4430
|
+
}
|
|
4431
|
+
}
|
|
4432
|
+
await input.tools.flush();
|
|
4433
|
+
const { readCompleteScoutTargetSurface } = await import("../workflows/dag/frontend-shadow-dual-write.js");
|
|
4434
|
+
const closure = readCompleteScoutTargetSurface(input.tools.committedFacts());
|
|
4435
|
+
return closure.ok ? { ...last, ok: true, failureCategory: "success" } : { ...last, ok: false, failureCategory: "frontend-input-exhausted", stderr: closure.reason };
|
|
4436
|
+
}
|
|
4437
|
+
export async function runFrontendContractSegmentedSessions(input) {
|
|
4438
|
+
// Build from the frozen runtime inventory if the caller has not rendered it yet.
|
|
4439
|
+
const basePrompt = parseFrontendInputBlock(input.basePrompt, "contract") ? input.basePrompt : input.basePrompt +
|
|
4440
|
+
`\n<frontend_contract_input>\nFrozen complete source obligations.\n${JSON.stringify({ requirements: input.tools.inputRequirements() })}\n</frontend_contract_input>`;
|
|
4441
|
+
const inventory = parseFrontendInputBlock(basePrompt, "contract");
|
|
4442
|
+
const pending = inventory.payload.requirements.filter(r => !input.tools.completedScopeRequirementIds().has(r.id));
|
|
4443
|
+
const batches = packFrontendInputUnits(pending, { targetBytes: input.sessionOptions.frontendExecutionPolicy?.scopeTargetBytes, maxUnits: input.sessionOptions.frontendExecutionPolicy?.maxScopeUnits });
|
|
4444
|
+
if (!batches.length)
|
|
4445
|
+
batches.push([]);
|
|
4446
|
+
let last;
|
|
4447
|
+
let invocation = 0;
|
|
4448
|
+
const maxSessions = batches.length * 3 + 2;
|
|
4449
|
+
const terminal = () => input.tools.committedFacts().some(r => r.fact.kind === "contract-finalized");
|
|
4450
|
+
try {
|
|
4451
|
+
for (let index = 0; index < batches.length; index += 1) {
|
|
4452
|
+
let scopeIds = batches[index].map(r => r.id);
|
|
4453
|
+
const finalScope = index === batches.length - 1;
|
|
4454
|
+
for (let repair = 0; repair < 2; repair += 1) {
|
|
4455
|
+
// Confirmed IDs remain visible until the model commits a scope checkpoint.
|
|
4456
|
+
const completed = input.tools.completedScopeRequirementIds();
|
|
4457
|
+
scopeIds = scopeIds.filter(id => !completed.has(id));
|
|
4458
|
+
input.tools.setActiveRequirementScope(scopeIds);
|
|
4459
|
+
const groupIndex = collectFrontendExecutionGroups(input.tools.committedFacts().filter(r => r.fact.kind === "requirement").map(r => ({ id: String(r.fact.id), execution: r.fact.execution })));
|
|
4460
|
+
const shared = input.tools.committedFacts().filter(r => !["requirement", "contract-finalized", "contract-scope-completed"].includes(String(r.fact.kind))).map(r => r.fact);
|
|
4461
|
+
const prompt = projectFrontendContractPrompt(basePrompt, scopeIds) +
|
|
4462
|
+
`\nCONTRACT SCOPE: analyze only ${scopeIds.join(", ") || "(all scopes complete; verify global facts and terminal)"}. Each obligation is complete. Submit small records immediately, then call complete_contract_scope after ALL decisions for this scope. ` +
|
|
4463
|
+
(finalScope ? "After complete scope coverage and source-bound deliverables, call finalize_contract. Correct rejected calls and retry." : "Do not finalize; subsequent complete scopes remain.") +
|
|
4464
|
+
`\n<committed_contract_facts>\n${JSON.stringify({ facts: shared, executionGroups: groupIndex })}\n</committed_contract_facts>`;
|
|
4465
|
+
const customTools = input.tools.customTools.filter(t => finalScope || t.name !== "finalize_contract");
|
|
4466
|
+
invocation += 1;
|
|
4467
|
+
if (invocation > maxSessions)
|
|
4468
|
+
return { ...last, ok: false, failureCategory: "invalid-output", stderr: "CONTRACT_RECOVERY_EXHAUSTED: session quota exceeded" };
|
|
4469
|
+
last = await observeFrontendSession({
|
|
4470
|
+
...input.observation, phase: "contract/scope", scopeIds, prompt, userMessage: input.sessionOptions.userMessage, customTools,
|
|
4471
|
+
artifactPath: input.observation?.artifactPath ? `${input.observation.artifactPath}-${invocation}.json` : undefined,
|
|
4472
|
+
committedCount: () => input.tools.committedFacts().length, durableCommittedCount: () => input.tools.committedFacts().length,
|
|
4473
|
+
}, observer => input.piStepFn({ ...input.sessionOptions, prompt, onAttemptObservation: observer, writerToolPolicy: { requireSdk: true, customTools } }));
|
|
4474
|
+
await input.tools.flush();
|
|
4475
|
+
if (last.timedOut || /interrupt|termination-unconfirmed|budget_breach/.test(last.failureCategory))
|
|
4476
|
+
return { ...last, ok: false };
|
|
4477
|
+
const capacityExhausted = readWriterThinkingExhaustionEvidence(last).stopReason === "length" || ["context-overflow", "context-budget-exhausted"].includes(last.failureCategory);
|
|
4478
|
+
if (capacityExhausted && !last.timedOut) {
|
|
4479
|
+
const missing = scopeIds.filter(id => !input.tools.completedScopeRequirementIds().has(id));
|
|
4480
|
+
if (!missing.length) {
|
|
4481
|
+
if (finalScope && !terminal()) {
|
|
4482
|
+
if (!scopeIds.length)
|
|
4483
|
+
return { ...last, ok: false, failureCategory: "invalid-output", stderr: `${last.stderr}\nFRONTEND_INPUT_UNIT_TOO_LARGE: contract terminal exhausted; unchanged retry disabled` };
|
|
4484
|
+
batches.push([]);
|
|
4485
|
+
}
|
|
4486
|
+
break;
|
|
4487
|
+
}
|
|
4488
|
+
if (missing.length === 1 && scopeIds.length > 1) {
|
|
4489
|
+
batches.splice(index, 1, inventory.payload.requirements.filter(r => missing.includes(r.id)));
|
|
4490
|
+
index -= 1;
|
|
4491
|
+
break;
|
|
4492
|
+
}
|
|
4493
|
+
if (missing.length <= 1)
|
|
4494
|
+
return { ...last, ok: false, stderr: `${last.stderr}\nFRONTEND_INPUT_UNIT_TOO_LARGE: ${missing[0] ?? "contract terminal"}; atom could not complete; refine the source without dropping conditions` };
|
|
4495
|
+
const smaller = packFrontendInputUnits(inventory.payload.requirements.filter(r => missing.includes(r.id)), { maxUnits: Math.ceil(missing.length / 2) });
|
|
4496
|
+
batches.splice(index, 1, ...smaller);
|
|
4497
|
+
index -= 1;
|
|
4498
|
+
break;
|
|
4499
|
+
}
|
|
4500
|
+
if (!last.ok)
|
|
4501
|
+
return last;
|
|
4502
|
+
const missing = scopeIds.filter(id => !input.tools.completedScopeRequirementIds().has(id));
|
|
4503
|
+
if (!missing.length && (!finalScope || terminal()))
|
|
4504
|
+
break;
|
|
4505
|
+
if (repair === 1)
|
|
4506
|
+
return { ...last, ok: false, failureCategory: "invalid-output", stderr: `CONTRACT_SCOPE_INCOMPLETE: ${missing.join(", ") || "missing finalize_contract"}` };
|
|
4507
|
+
}
|
|
4508
|
+
}
|
|
4509
|
+
return last;
|
|
4510
|
+
}
|
|
4511
|
+
finally {
|
|
4512
|
+
input.tools.setActiveRequirementScope(null);
|
|
4513
|
+
}
|
|
4514
|
+
}
|
|
4515
|
+
/** One workload view for both the outer parallel map and inner session queue.
|
|
4516
|
+
* A declared execution group is an indivisible unit during initial packing. */
|
|
4517
|
+
function buildFrontendPlanWorkload(input) {
|
|
4518
|
+
const allRequirementIds = input.requirementIds;
|
|
4519
|
+
const compiledInput = parseFrontendInputBlock(input.basePrompt, "plan")?.payload;
|
|
4520
|
+
const fullUnits = new Map(compiledInput?.requirements.map(r => [r.id, r]) ?? []);
|
|
4521
|
+
const declaredGroups = Array.isArray(compiledInput?.executionGroups)
|
|
4522
|
+
? compiledInput.executionGroups.filter(isRecordObject) : [];
|
|
4523
|
+
const workGroups = declaredGroups.map(g => ({
|
|
4524
|
+
id: String(g.id), kind: String(g.kind),
|
|
4525
|
+
requirementIds: Array.isArray(g.requirementIds)
|
|
4526
|
+
? g.requirementIds.filter((id) => typeof id === "string" && allRequirementIds.includes(id)) : [],
|
|
4527
|
+
})).filter(g => g.requirementIds.length);
|
|
4528
|
+
const groupedIds = new Set(workGroups.flatMap(g => g.requirementIds));
|
|
4529
|
+
for (const id of allRequirementIds) {
|
|
4530
|
+
if (!groupedIds.has(id))
|
|
4531
|
+
workGroups.push({ id, kind: "unclassified", requirementIds: [id] });
|
|
4532
|
+
}
|
|
4533
|
+
const workCost = (ids) => 1 + ids.reduce((total, id) => total + Math.max(1, (input.requirementCosts?.get(id) ?? 2) - 1), 0);
|
|
4534
|
+
const policy = input.sessionOptions.frontendExecutionPolicy;
|
|
4535
|
+
const targetBytes = policy?.scopeTargetBytes ?? FRONTEND_SCOPE_TARGET_BYTES;
|
|
4536
|
+
const buildWorkBatches = (ids) => {
|
|
4537
|
+
const work = workGroups.flatMap((g, index) => {
|
|
4538
|
+
const members = g.requirementIds.filter(id => ids.includes(id));
|
|
4539
|
+
return members.length ? [{ id: `${index}:${g.id}`, requirementIds: members,
|
|
4540
|
+
requirements: members.map(id => fullUnits.get(id) ?? { id }), estimatedCalls: workCost(members) }] : [];
|
|
4541
|
+
});
|
|
4542
|
+
// Pack complete work units, not the repeated request scaffold. Deducting
|
|
4543
|
+
// fixed context/tools can leave a one-byte budget and force one AC per
|
|
4544
|
+
// session without reducing that overhead. The SDK checks the actual
|
|
4545
|
+
// request against model capacity; capacity recovery splits unfinished work.
|
|
4546
|
+
return packFrontendInputUnits(work, {
|
|
4547
|
+
targetBytes,
|
|
4548
|
+
maxUnits: policy?.maxScopeUnits ?? 4,
|
|
4549
|
+
maxCost: FRONTEND_PLAN_COVERAGE_MAX_RECORD_CALLS, cost: g => g.estimatedCalls,
|
|
4550
|
+
}).map(batch => batch.flatMap(g => g.requirementIds));
|
|
4551
|
+
};
|
|
4552
|
+
// This renderer also carries the Plan's evolving UX checkpoint on resume.
|
|
4553
|
+
// Only the Contract/Scout-derived input participates in ownership binding.
|
|
4554
|
+
const { committedUx: _committedUx, ...frozenInput } = compiledInput ?? {};
|
|
4555
|
+
return {
|
|
4556
|
+
frozenInput, workGroups, buildWorkBatches,
|
|
4557
|
+
compactEligible: input.requirementCosts !== undefined &&
|
|
4558
|
+
workGroups.length <= FRONTEND_PLAN_SMALL_MAX_REQUIREMENTS &&
|
|
4559
|
+
workGroups.reduce((total, group) => total + workCost(group.requirementIds), 0) <= FRONTEND_PLAN_SMALL_MAX_ESTIMATED_CALLS &&
|
|
4560
|
+
countFrontendPlanTargetSurfaces(input.basePrompt) === 1,
|
|
4561
|
+
};
|
|
4562
|
+
}
|
|
4563
|
+
const frontendPlanCoverageLayoutSchema = z.object({
|
|
4564
|
+
schemaVersion: z.literal(1),
|
|
4565
|
+
bindingSha256: z.string(),
|
|
4566
|
+
coverageBatches: z.array(z.array(z.string().min(1)).min(1)).min(1),
|
|
4567
|
+
layoutSha256: z.string(),
|
|
4568
|
+
}).strict();
|
|
4569
|
+
/** Freeze ledger ownership before any provider call. Session packing remains
|
|
4570
|
+
* adaptive inside each owner, but retry instructions cannot rename its scope. */
|
|
4571
|
+
async function loadOrCreateFrontendPlanCoverageLayout(input) {
|
|
4572
|
+
const fileName = "coverage-layout.json";
|
|
4573
|
+
const file = path.join(input.runDir, input.nodeId, fileName);
|
|
4574
|
+
const bindingSha256 = sha256OfCanonicalJson(input.binding);
|
|
4575
|
+
let raw;
|
|
4576
|
+
try {
|
|
4577
|
+
raw = await readFile(file, "utf8");
|
|
4578
|
+
}
|
|
4579
|
+
catch (error) {
|
|
4580
|
+
if (error.code !== "ENOENT")
|
|
4581
|
+
throw error;
|
|
4582
|
+
}
|
|
4583
|
+
let layout;
|
|
4584
|
+
if (raw !== undefined) {
|
|
4585
|
+
layout = frontendPlanCoverageLayoutSchema.parse(JSON.parse(raw));
|
|
4586
|
+
}
|
|
4587
|
+
else {
|
|
4588
|
+
// Losing the ownership receipt must never repartition acknowledged facts.
|
|
4589
|
+
for (const relative of ["plan-typed-facts.jsonl", "parallel"]) {
|
|
4590
|
+
const existing = await stat(path.join(input.runDir, input.nodeId, relative)).catch(error => {
|
|
4591
|
+
if (error.code !== "ENOENT")
|
|
4592
|
+
throw error;
|
|
4593
|
+
return undefined;
|
|
4594
|
+
});
|
|
4595
|
+
if (existing && (existing.isDirectory() || existing.size > 0)) {
|
|
4596
|
+
throw Error("FRONTEND_PLAN_LAYOUT_MISSING: preserve the existing ledger and restart its owning phase");
|
|
4597
|
+
}
|
|
4598
|
+
}
|
|
4599
|
+
const descriptor = {
|
|
4600
|
+
schemaVersion: 1, bindingSha256,
|
|
4601
|
+
coverageBatches: input.workload.compactEligible ? [input.requirementIds] : input.workload.buildWorkBatches(input.requirementIds),
|
|
4602
|
+
};
|
|
4603
|
+
layout = { ...descriptor, layoutSha256: sha256OfCanonicalJson(descriptor) };
|
|
4604
|
+
}
|
|
4605
|
+
const { layoutSha256, ...descriptor } = layout;
|
|
4606
|
+
if (layout.bindingSha256 !== bindingSha256 || layoutSha256 !== sha256OfCanonicalJson(descriptor)) {
|
|
4607
|
+
throw Error("FRONTEND_PLAN_LAYOUT_MISMATCH: frozen coverage ownership or input binding changed");
|
|
4608
|
+
}
|
|
4609
|
+
const members = layout.coverageBatches.flat();
|
|
4610
|
+
const owners = new Map(layout.coverageBatches.flatMap((batch, index) => batch.map(id => [id, index])));
|
|
4611
|
+
if (members.length !== owners.size || members.length !== input.requirementIds.length ||
|
|
4612
|
+
input.requirementIds.some(id => !owners.has(id)) ||
|
|
4613
|
+
input.workload.workGroups.some(group => new Set(group.requirementIds.map(id => owners.get(id))).size !== 1)) {
|
|
4614
|
+
throw Error("FRONTEND_PLAN_LAYOUT_INVALID: coverage must partition complete execution groups exactly once");
|
|
4615
|
+
}
|
|
4616
|
+
if (raw === undefined)
|
|
4617
|
+
await writeDagNodeJsonArtifact(input.runDir, input.nodeId, fileName, layout);
|
|
4618
|
+
return layout.coverageBatches;
|
|
4619
|
+
}
|
|
4158
4620
|
export async function runFrontendPlanSegmentedSessions(input) {
|
|
4159
4621
|
const queue = [];
|
|
4160
4622
|
const coverageSegment = FRONTEND_PLAN_SEGMENTS.find((segment) => segment.id === "coverage");
|
|
@@ -4163,10 +4625,10 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4163
4625
|
const globalMockDataSegment = FRONTEND_PLAN_SEGMENTS.find((segment) => segment.id === "global-mock-data");
|
|
4164
4626
|
const finalizeSegment = FRONTEND_PLAN_SEGMENTS.find((segment) => segment.id === "finalize");
|
|
4165
4627
|
const allRequirementIds = input.requirementIds ?? [];
|
|
4166
|
-
const buildPhasePrompt = (segment, missing = []) => {
|
|
4628
|
+
const buildPhasePrompt = (segment, missing = [], scopeIds = allRequirementIds) => {
|
|
4167
4629
|
const compact = allRequirementIds.length > 0
|
|
4168
|
-
? compactFrontendPlanPromptForRequirementSlice(input.basePrompt,
|
|
4169
|
-
includeRequirementText: segment.id === "global-mock-data" || segment.id === "
|
|
4630
|
+
? compactFrontendPlanPromptForRequirementSlice(input.basePrompt, scopeIds, {
|
|
4631
|
+
includeRequirementText: segment.id === "global-mock-data" || segment.id === "finalize",
|
|
4170
4632
|
includeVerificationTargets: false,
|
|
4171
4633
|
includeDesignEvidence: segment.id === "global-dependency-deviation" || segment.id === "finalize",
|
|
4172
4634
|
includeChecklist: false,
|
|
@@ -4182,8 +4644,9 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4182
4644
|
const ledger = input.committedFacts
|
|
4183
4645
|
? compactFrontendPlanLedgerContext({
|
|
4184
4646
|
committedFacts: input.committedFacts(),
|
|
4185
|
-
requirementIds:
|
|
4647
|
+
requirementIds: scopeIds,
|
|
4186
4648
|
kinds,
|
|
4649
|
+
...(segment.id === "global-mock-data" ? { scopedKinds: ["state-flow"] } : {}),
|
|
4187
4650
|
})
|
|
4188
4651
|
: "";
|
|
4189
4652
|
return [
|
|
@@ -4221,13 +4684,13 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4221
4684
|
"component-choice",
|
|
4222
4685
|
"state-flow",
|
|
4223
4686
|
],
|
|
4224
|
-
|
|
4687
|
+
// Include shared state bindings from other scopes; page full values before correcting.
|
|
4225
4688
|
})
|
|
4226
4689
|
: "";
|
|
4227
4690
|
return [
|
|
4228
4691
|
compactFrontendPlanPromptForRequirementSlice(input.basePrompt, slice),
|
|
4229
4692
|
uxSegment.instruction,
|
|
4230
|
-
`
|
|
4693
|
+
`UX SCOPE: process these complete behavior groups together: ${slice.join(", ")}. Reuse shared registry/component ownership; do not rename a behavior by requirement id. Constraint/exclusion groups do not create UI; preserve genuine verification gaps.`,
|
|
4231
4694
|
ledger,
|
|
4232
4695
|
...(missing.length > 0
|
|
4233
4696
|
? [
|
|
@@ -4251,6 +4714,7 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4251
4714
|
: "";
|
|
4252
4715
|
const compact = allRequirementIds.length > 0
|
|
4253
4716
|
? compactFrontendPlanPromptForRequirementSlice(input.basePrompt, allRequirementIds, {
|
|
4717
|
+
includeRequirementText: false,
|
|
4254
4718
|
includeVerificationTargets: false,
|
|
4255
4719
|
includeDesignEvidence: false,
|
|
4256
4720
|
includeChecklist: false,
|
|
@@ -4296,7 +4760,7 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4296
4760
|
: []),
|
|
4297
4761
|
].filter(Boolean).join("\n\n");
|
|
4298
4762
|
};
|
|
4299
|
-
const compactFinalizeInstruction = "This is a small-request compact pass. Reconcile the committed local facts with route, data-flow, Mock/API, dependency and deviation policy, then call finalize_plan exactly
|
|
4763
|
+
const compactFinalizeInstruction = "This is a small-request compact pass. Reconcile the committed local facts with route, data-flow, Mock/API, dependency and deviation policy, then call finalize_plan; correct rejected facts and retry until exactly one successful terminal commit.";
|
|
4300
4764
|
const buildCompactFinalizePrompt = (missing = []) => [buildPhasePrompt(finalizeSegment, missing), compactFinalizeInstruction].join("\n\n");
|
|
4301
4765
|
const mapPlannerExhaustion = (r, committedAnyFacts) => isPlannerThinkingExhausted(r, committedAnyFacts)
|
|
4302
4766
|
? {
|
|
@@ -4340,8 +4804,10 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4340
4804
|
tokensUsed: 0,
|
|
4341
4805
|
};
|
|
4342
4806
|
}
|
|
4343
|
-
const
|
|
4344
|
-
|
|
4807
|
+
const { workGroups, buildWorkBatches, compactEligible } = buildFrontendPlanWorkload({
|
|
4808
|
+
basePrompt: input.basePrompt, requirementIds: allRequirementIds,
|
|
4809
|
+
requirementCosts: input.requirementCosts, sessionOptions: input.sessionOptions,
|
|
4810
|
+
});
|
|
4345
4811
|
// Small, single-surface requests do not benefit from six isolated Pi
|
|
4346
4812
|
// sessions. Keep the typed ledger as the authority, but let one local
|
|
4347
4813
|
// session establish requirement/UX facts and one final session establish
|
|
@@ -4349,14 +4815,11 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4349
4815
|
// for larger plans and for the unscoped compatibility path.
|
|
4350
4816
|
const useCompactSmallPlan = requirementIdsProvided &&
|
|
4351
4817
|
input.compactSmallPlan === true &&
|
|
4352
|
-
|
|
4353
|
-
estimatedCalls > 12 &&
|
|
4354
|
-
(input.requirementIds?.length ?? 0) <= FRONTEND_PLAN_SMALL_MAX_REQUIREMENTS &&
|
|
4355
|
-
estimatedCalls <= FRONTEND_PLAN_SMALL_MAX_ESTIMATED_CALLS &&
|
|
4356
|
-
targetSurfaceCount === 1;
|
|
4818
|
+
compactEligible;
|
|
4357
4819
|
if (useCompactSmallPlan) {
|
|
4358
4820
|
const compactLocalTools = new Set([
|
|
4359
4821
|
"record_plan_requirement",
|
|
4822
|
+
"record_plan_group_coverage",
|
|
4360
4823
|
"record_plan_verification_target",
|
|
4361
4824
|
"record_plan_evidence_gap",
|
|
4362
4825
|
"record_state_registry",
|
|
@@ -4384,13 +4847,8 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4384
4847
|
});
|
|
4385
4848
|
}
|
|
4386
4849
|
else if (coverageWorkIds.length > 0) {
|
|
4387
|
-
const
|
|
4388
|
-
|
|
4389
|
-
maxEstimatedRecordCalls: FRONTEND_PLAN_COVERAGE_MAX_RECORD_CALLS,
|
|
4390
|
-
maxRequirements: 4,
|
|
4391
|
-
requirementCosts: input.requirementCosts,
|
|
4392
|
-
});
|
|
4393
|
-
coverageBatches.forEach((slice, batchIndex) => queue.push({
|
|
4850
|
+
const completeBatches = buildWorkBatches(coverageWorkIds);
|
|
4851
|
+
completeBatches.forEach((slice, batchIndex) => queue.push({
|
|
4394
4852
|
id: `coverage-batch-${batchIndex + 1}`,
|
|
4395
4853
|
toolNames: coverageSegment.toolNames,
|
|
4396
4854
|
coverageSlice: slice,
|
|
@@ -4409,28 +4867,27 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4409
4867
|
prompt: buildUxRegistryPrompt(),
|
|
4410
4868
|
});
|
|
4411
4869
|
}
|
|
4412
|
-
const
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
: new Set([...(uxSegment.toolNames ?? []), "record_state_registry"]),
|
|
4421
|
-
...(uxSlice.length > 0 ? { requirementSlice: uxSlice } : {}),
|
|
4422
|
-
prompt: uxSlice.length > 0
|
|
4423
|
-
? buildUxPrompt(uxSlice)
|
|
4424
|
-
: buildPhasePrompt(uxSegment),
|
|
4425
|
-
});
|
|
4870
|
+
const uxWorkIds = workGroups.filter(g => !["constraint", "exclusion"].includes(g.kind) || g.requirementIds.some(id => input.behaviorRequiredRequirementIds?.includes(id))).flatMap(g => g.requirementIds);
|
|
4871
|
+
const uxBatches = requirementIdsProvided ? buildWorkBatches(uxWorkIds) : [[]];
|
|
4872
|
+
uxBatches.forEach((slice, batchIndex) => queue.push({
|
|
4873
|
+
id: `ux-local-${batchIndex + 1}`,
|
|
4874
|
+
toolNames: requirementIdsProvided ? uxSegment.toolNames : new Set([...(uxSegment.toolNames ?? []), "record_state_registry"]),
|
|
4875
|
+
...(slice.length ? { requirementSlice: slice } : {}),
|
|
4876
|
+
prompt: slice.length ? buildUxPrompt(slice) : buildPhasePrompt(uxSegment),
|
|
4877
|
+
}));
|
|
4426
4878
|
for (const segment of FRONTEND_PLAN_SEGMENTS) {
|
|
4427
4879
|
if (["coverage", "ux-registry", "ux-local", "finalize"].includes(segment.id))
|
|
4428
4880
|
continue;
|
|
4429
|
-
|
|
4430
|
-
id: segment.
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4881
|
+
if (segment.id === "global-mock-data" && requirementIdsProvided) {
|
|
4882
|
+
buildWorkBatches(allRequirementIds).forEach((slice, i) => queue.push({ id: `global-mock-data-${i + 1}`, toolNames: segment.toolNames, requirementSlice: slice, prompt: buildPhasePrompt(segment, [], slice) }));
|
|
4883
|
+
}
|
|
4884
|
+
else {
|
|
4885
|
+
queue.push({
|
|
4886
|
+
id: segment.id,
|
|
4887
|
+
toolNames: segment.toolNames,
|
|
4888
|
+
prompt: buildPhasePrompt(segment),
|
|
4889
|
+
});
|
|
4890
|
+
}
|
|
4434
4891
|
}
|
|
4435
4892
|
queue.push({
|
|
4436
4893
|
id: finalizeSegment.id,
|
|
@@ -4441,8 +4898,30 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4441
4898
|
let accumulated;
|
|
4442
4899
|
let index = 0;
|
|
4443
4900
|
let invocationCount = 0;
|
|
4901
|
+
let lastDurableCount = input.committedFactCount();
|
|
4444
4902
|
while (index < queue.length) {
|
|
4445
4903
|
const session = queue[index];
|
|
4904
|
+
if (input.attempt > 1 && input.committedFacts && session.id === "ux-registry" &&
|
|
4905
|
+
collectFrontendPlanPhaseMissingFacts({ phase: "ux-registry", requirementIds: allRequirementIds, committedFacts: input.committedFacts() }).length === 0) {
|
|
4906
|
+
index += 1;
|
|
4907
|
+
continue;
|
|
4908
|
+
}
|
|
4909
|
+
// Reuse complete UX scopes on node retry. Require explicit per-requirement
|
|
4910
|
+
// ownership; an unrelated/global fact must not prove a slice complete.
|
|
4911
|
+
if (input.attempt > 1 && input.committedFacts && session.id.startsWith("ux-local-") && session.requirementSlice?.length) {
|
|
4912
|
+
const facts = input.committedFacts();
|
|
4913
|
+
const ownedIds = new Set(facts.flatMap(value => {
|
|
4914
|
+
const fact = committedFactFromPlanRecord(value);
|
|
4915
|
+
return fact ? planFactStringList(fact.scopeRequirementIds) : [];
|
|
4916
|
+
}));
|
|
4917
|
+
const complete = session.requirementSlice.every(id => ownedIds.has(id)) &&
|
|
4918
|
+
collectFrontendPlanPhaseMissingFacts({ phase: "ux-local", requirementIds: session.requirementSlice,
|
|
4919
|
+
committedFacts: facts, behaviorRequiredRequirementIds: input.behaviorRequiredRequirementIds }).length === 0;
|
|
4920
|
+
if (complete) {
|
|
4921
|
+
index += 1;
|
|
4922
|
+
continue;
|
|
4923
|
+
}
|
|
4924
|
+
}
|
|
4446
4925
|
const remaining = (session.coverageOnly ? session.coverageSlice ?? [] : []).filter((id) => !input.committedRequirementIds?.().has(id));
|
|
4447
4926
|
const preexistingMissing = session.coverageOnly && session.coverageSlice && input.committedFacts
|
|
4448
4927
|
? collectFrontendPlanMissingFacts({
|
|
@@ -4460,7 +4939,7 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4460
4939
|
}
|
|
4461
4940
|
let prompt = session.prompt;
|
|
4462
4941
|
if (session.coverageOnly && session.coverageSlice) {
|
|
4463
|
-
const promptSlice = remaining.
|
|
4942
|
+
const promptSlice = [...new Set([...remaining, ...preexistingMissing.flatMap(item => item.requirementIds)])];
|
|
4464
4943
|
prompt = buildCoveragePrompt(promptSlice, session.missingFacts ?? preexistingMissing);
|
|
4465
4944
|
}
|
|
4466
4945
|
else if (session.id === "compact-local") {
|
|
@@ -4480,45 +4959,68 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4480
4959
|
// Global phases and finalize also consume the latest committed ledger;
|
|
4481
4960
|
// constructing their prompt only when the session starts prevents a
|
|
4482
4961
|
// stale queue entry from dropping facts written by earlier phases.
|
|
4483
|
-
const segment = FRONTEND_PLAN_SEGMENTS.find((candidate) => candidate.id === session.id);
|
|
4962
|
+
const segment = FRONTEND_PLAN_SEGMENTS.find((candidate) => candidate.id === session.id || (candidate.id === "global-mock-data" && session.id.startsWith("global-mock-data-")));
|
|
4484
4963
|
if (segment) {
|
|
4485
4964
|
prompt =
|
|
4486
4965
|
session.id === "finalize" && useCompactSmallPlan
|
|
4487
4966
|
? buildCompactFinalizePrompt(session.missingFacts)
|
|
4488
|
-
: buildPhasePrompt(segment, session.missingFacts);
|
|
4967
|
+
: buildPhasePrompt(segment, session.missingFacts, session.requirementSlice ?? allRequirementIds);
|
|
4489
4968
|
}
|
|
4490
4969
|
}
|
|
4970
|
+
const atomicFocus = session.atomicRecovery ? session.missingFacts?.[0] : undefined;
|
|
4971
|
+
if (atomicFocus) {
|
|
4972
|
+
prompt = [
|
|
4973
|
+
compactFrontendPlanPromptForRequirementSlice(input.basePrompt, atomicFocus.requirementIds, {
|
|
4974
|
+
includeChecklist: false,
|
|
4975
|
+
...(atomicFocus.kind === "plan-verification-target" && atomicFocus.id
|
|
4976
|
+
? { verificationTargetIds: [atomicFocus.id] } : {}),
|
|
4977
|
+
}),
|
|
4978
|
+
`ATOMIC FACT: ${atomicFocus.kind}${atomicFocus.id ? ` ${atomicFocus.id}` : ""}`,
|
|
4979
|
+
atomicFocus.reason,
|
|
4980
|
+
"Commit ONLY this missing fact with one record_* call, then end this session. Other facts are queued separately. Preserve canonical IDs and shared bindings; use read_plan_facts for existing values. Do not summarize or plan the entire requirement. Capacity exhaustion is not evidence of a requirement gap.",
|
|
4981
|
+
].join("\n\n");
|
|
4982
|
+
}
|
|
4491
4983
|
const committedBefore = input.committedFactCount();
|
|
4492
|
-
input.setActiveRequirementScope?.(session.coverageOnly
|
|
4493
|
-
session.id === "compact-local" ||
|
|
4494
|
-
session.id.startsWith("ux-local-")
|
|
4495
|
-
? session.coverageSlice ?? session.requirementSlice ?? []
|
|
4496
|
-
: []);
|
|
4984
|
+
input.setActiveRequirementScope?.(session.coverageOnly ? session.coverageSlice ?? [] : session.requirementSlice ?? []);
|
|
4497
4985
|
if (invocationCount >= FRONTEND_PLAN_BATCH_MAX_SESSIONS)
|
|
4498
4986
|
break;
|
|
4499
4987
|
invocationCount += 1;
|
|
4500
|
-
const
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4988
|
+
const atomicTools = {
|
|
4989
|
+
"plan-requirement": "record_plan_requirement", "plan-verification-target": "record_plan_verification_target",
|
|
4990
|
+
"state-registry": "record_state_registry", "component-choice": "record_component_choice",
|
|
4991
|
+
"state-flow": "record_state_flow", "data-flow": "record_data_flow", "mock-api": "record_mock_api",
|
|
4992
|
+
};
|
|
4993
|
+
const customTools = input.segmentCustomTools(atomicFocus ? new Set([atomicTools[atomicFocus.kind]]) : session.toolNames);
|
|
4994
|
+
const result = await observeFrontendSession({
|
|
4995
|
+
...input.observation, phase: `plan/${session.id}`, scopeIds: session.requirementSlice ?? session.coverageSlice ?? allRequirementIds,
|
|
4996
|
+
prompt, userMessage: input.sessionOptions.userMessage, customTools, committedCount: input.committedFactCount, durableCommittedCount: () => lastDurableCount,
|
|
4997
|
+
artifactPath: input.observation?.artifactPath ? `${input.observation.artifactPath}-${invocationCount}.json` : undefined,
|
|
4998
|
+
}, async (observer) => {
|
|
4999
|
+
const result = await input.piStepFn({
|
|
5000
|
+
...input.sessionOptions,
|
|
5001
|
+
onAttemptObservation: observer,
|
|
5002
|
+
prompt,
|
|
5003
|
+
...(customTools.length > 0
|
|
5004
|
+
? {
|
|
5005
|
+
writerToolPolicy: {
|
|
5006
|
+
requireSdk: true,
|
|
5007
|
+
customTools,
|
|
5008
|
+
},
|
|
5009
|
+
}
|
|
5010
|
+
: {}),
|
|
5011
|
+
});
|
|
5012
|
+
try {
|
|
5013
|
+
await input.flushLedger();
|
|
5014
|
+
lastDurableCount = input.committedFactCount();
|
|
5015
|
+
}
|
|
5016
|
+
catch {
|
|
5017
|
+
// best-effort: the node-level flush runs again after the attempt
|
|
5018
|
+
}
|
|
5019
|
+
return result;
|
|
4512
5020
|
});
|
|
4513
5021
|
accumulated = accumulated
|
|
4514
5022
|
? combineSequentialPiResults(accumulated, result)
|
|
4515
5023
|
: result;
|
|
4516
|
-
try {
|
|
4517
|
-
await input.flushLedger();
|
|
4518
|
-
}
|
|
4519
|
-
catch {
|
|
4520
|
-
// best-effort: the node-level flush runs again after the attempt
|
|
4521
|
-
}
|
|
4522
5024
|
const committedAfter = input.committedFactCount();
|
|
4523
5025
|
const committedFactsOnlySuccess = session.id !== "finalize" &&
|
|
4524
5026
|
!(result.assistantText ?? "").trim() &&
|
|
@@ -4565,10 +5067,10 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4565
5067
|
committedFacts: input.committedFacts(),
|
|
4566
5068
|
behaviorRequiredRequirementIds: input.behaviorRequiredRequirementIds,
|
|
4567
5069
|
})
|
|
4568
|
-
: session.id
|
|
5070
|
+
: session.id.startsWith("global-mock-data") && allRequirementIds.length > 0 && input.committedFacts
|
|
4569
5071
|
? collectFrontendPlanPhaseMissingFacts({
|
|
4570
5072
|
phase: "global-mock-data",
|
|
4571
|
-
requirementIds: allRequirementIds,
|
|
5073
|
+
requirementIds: session.requirementSlice ?? allRequirementIds,
|
|
4572
5074
|
committedFacts: input.committedFacts(),
|
|
4573
5075
|
})
|
|
4574
5076
|
: [];
|
|
@@ -4605,7 +5107,96 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4605
5107
|
? buildCompactFinalizePrompt(missing)
|
|
4606
5108
|
: isUxLocalSession
|
|
4607
5109
|
? buildUxPrompt(session.requirementSlice ?? [], missing)
|
|
4608
|
-
: buildPhasePrompt(globalMockDataSegment, missing);
|
|
5110
|
+
: buildPhasePrompt(globalMockDataSegment, missing, session.requirementSlice ?? allRequirementIds);
|
|
5111
|
+
const recovery = classifyFrontendPlanRecovery({ ...result, stopReason: readWriterThinkingExhaustionEvidence(result).stopReason });
|
|
5112
|
+
if (recovery === "stop")
|
|
5113
|
+
return { ...result, ok: false };
|
|
5114
|
+
if ((recovery === "output" || (session.atomicRecovery && result.ok)) && input.committedFacts &&
|
|
5115
|
+
!(missingPhaseFacts.length === 1 && missingPhaseFacts[0].kind === "plan-requirement") &&
|
|
5116
|
+
(session.coverageOnly || isUxRegistrySession || isUxLocalSession || session.id.startsWith("global-mock-data"))) {
|
|
5117
|
+
if (missingPhaseFacts.length === 0) {
|
|
5118
|
+
// A complete validated slice does not need a successful prose turn.
|
|
5119
|
+
// This never accepts finalize or bypasses the final contract validator.
|
|
5120
|
+
accumulated = { ...accumulated, ok: true, failureCategory: "success" };
|
|
5121
|
+
index += 1;
|
|
5122
|
+
continue;
|
|
5123
|
+
}
|
|
5124
|
+
const missingIds = [...new Set(missingPhaseFacts.flatMap(item => item.requirementIds))];
|
|
5125
|
+
const scope = session.coverageSlice ?? session.requirementSlice;
|
|
5126
|
+
if (scope && missingIds.length < scope.length) {
|
|
5127
|
+
queue[index] = { ...session, missingFacts: missingPhaseFacts,
|
|
5128
|
+
...(session.coverageOnly ? { coverageSlice: missingIds } : { requirementSlice: missingIds }),
|
|
5129
|
+
retryCount: 0 };
|
|
5130
|
+
continue;
|
|
5131
|
+
}
|
|
5132
|
+
if (scope && missingIds.length > 1 && (session.coverageOnly || isUxLocalSession)) {
|
|
5133
|
+
const half = Math.ceil(missingIds.length / 2);
|
|
5134
|
+
queue.splice(index, 1, ...[missingIds.slice(0, half), missingIds.slice(half)].map(ids => ({
|
|
5135
|
+
...session, retryCount: 0,
|
|
5136
|
+
...(session.coverageOnly ? { coverageSlice: ids } : { requirementSlice: ids }),
|
|
5137
|
+
missingFacts: missingPhaseFacts.filter(item => item.requirementIds.some(id => ids.includes(id))),
|
|
5138
|
+
})));
|
|
5139
|
+
continue;
|
|
5140
|
+
}
|
|
5141
|
+
const focusStillMissing = atomicFocus && missingPhaseFacts.some(item => item.kind === atomicFocus.kind && item.id === atomicFocus.id &&
|
|
5142
|
+
item.requirementIds.join("\0") === atomicFocus.requirementIds.join("\0"));
|
|
5143
|
+
const retries = focusStillMissing ? (session.retryCount ?? 0) + 1 : 0;
|
|
5144
|
+
if (retries < 2) {
|
|
5145
|
+
queue[index] = { ...session, atomicRecovery: true, missingFacts: missingPhaseFacts, retryCount: retries };
|
|
5146
|
+
continue;
|
|
5147
|
+
}
|
|
5148
|
+
return { ...accumulated, ok: false, failureCategory: OUTPUT_LIMIT_RETRY_CATEGORY,
|
|
5149
|
+
stderr: `${accumulated.stderr}\nfrontend plan capacity recovery exhausted: preserve committed facts; phase=${session.id}; scope=${missingIds.join(",")}; strategy=atomic-fact; missing=${JSON.stringify(missingPhaseFacts)}`.trim() };
|
|
5150
|
+
}
|
|
5151
|
+
const capacityExhausted = recovery === "output" || recovery === "context";
|
|
5152
|
+
if (capacityExhausted && !result.timedOut) {
|
|
5153
|
+
const failure = () => mapPlannerExhaustion({ ...result, ok: false, stderr: `${result.stderr}\nFRONTEND_INPUT_UNIT_TOO_LARGE: ${session.id}; no smaller complete scope can finish; unchanged retries are disabled` }, committedAfter > committedBefore);
|
|
5154
|
+
const missingCoverageIds = input.committedFacts ? [...new Set(collectFrontendPlanMissingFacts({ requirementIds: session.coverageSlice ?? session.requirementSlice ?? allRequirementIds, committedFacts: input.committedFacts() }).flatMap(f => f.requirementIds))] : [...(session.coverageSlice ?? session.requirementSlice ?? allRequirementIds)];
|
|
5155
|
+
const splitScope = (ids, allowMemberSplit) => {
|
|
5156
|
+
const groups = workGroups.map(g => g.requirementIds.filter(id => ids.includes(id))).filter(g => g.length);
|
|
5157
|
+
if (groups.length > 1) {
|
|
5158
|
+
const half = Math.ceil(groups.length / 2);
|
|
5159
|
+
return [groups.slice(0, half).flat(), groups.slice(half).flat()];
|
|
5160
|
+
}
|
|
5161
|
+
if (allowMemberSplit && ids.length > 1) {
|
|
5162
|
+
const half = Math.ceil(ids.length / 2);
|
|
5163
|
+
return [ids.slice(0, half), ids.slice(half)];
|
|
5164
|
+
}
|
|
5165
|
+
return [];
|
|
5166
|
+
};
|
|
5167
|
+
if (isCompactLocalSession) {
|
|
5168
|
+
let scopes = splitScope(missingCoverageIds, true);
|
|
5169
|
+
if (!scopes.length && missingCoverageIds.length) {
|
|
5170
|
+
if (missingCoverageIds.length === allRequirementIds.length && committedAfter === committedBefore)
|
|
5171
|
+
return failure();
|
|
5172
|
+
scopes = [missingCoverageIds];
|
|
5173
|
+
}
|
|
5174
|
+
// UX ownership is independent of coverage completion. Preserve all groups.
|
|
5175
|
+
const recovery = scopes.map((slice, i) => ({ id: `coverage-capacity-${invocationCount}-${i}`, coverageOnly: true, coverageSlice: slice, toolNames: coverageSegment.toolNames, prompt: buildCoveragePrompt(slice) }));
|
|
5176
|
+
recovery.push({ id: "ux-registry", toolNames: uxRegistrySegment.toolNames, prompt: buildUxRegistryPrompt() });
|
|
5177
|
+
recovery.push(...buildWorkBatches([...allRequirementIds]).map((slice, i) => ({ id: `ux-local-capacity-${invocationCount}-${i}`, requirementSlice: slice, toolNames: uxSegment.toolNames, prompt: buildUxPrompt(slice) })));
|
|
5178
|
+
queue.splice(index, 1, ...recovery);
|
|
5179
|
+
continue;
|
|
5180
|
+
}
|
|
5181
|
+
if (session.coverageOnly || isUxLocalSession) {
|
|
5182
|
+
const missingIds = session.coverageOnly ? missingCoverageIds : [...new Set(missingPhase.flatMap(f => f.requirementIds))];
|
|
5183
|
+
if (!missingIds.length) {
|
|
5184
|
+
index += 1;
|
|
5185
|
+
continue;
|
|
5186
|
+
}
|
|
5187
|
+
const scopes = splitScope(missingIds, Boolean(session.coverageOnly));
|
|
5188
|
+
if (scopes.length) {
|
|
5189
|
+
queue.splice(index, 1, ...scopes.map(slice => ({ ...session, ...(session.coverageOnly ? { coverageSlice: slice } : { requirementSlice: slice }), missingFacts: missingPhaseFacts.filter(f => f.requirementIds.some(id => slice.includes(id))), retryCount: 0 })));
|
|
5190
|
+
continue;
|
|
5191
|
+
}
|
|
5192
|
+
const originalIds = session.coverageSlice ?? session.requirementSlice ?? [];
|
|
5193
|
+
if ((missingIds.length < originalIds.length || committedAfter > committedBefore) && (session.retryCount ?? 0) < 2) {
|
|
5194
|
+
queue[index] = { ...session, ...(session.coverageOnly ? { coverageSlice: missingIds } : { requirementSlice: missingIds }), missingFacts: missingPhaseFacts, retryCount: (session.retryCount ?? 0) + 1 };
|
|
5195
|
+
continue;
|
|
5196
|
+
}
|
|
5197
|
+
}
|
|
5198
|
+
return failure();
|
|
5199
|
+
}
|
|
4609
5200
|
if (result.ok) {
|
|
4610
5201
|
if (missingPhaseFacts.length === 0) {
|
|
4611
5202
|
index += 1;
|
|
@@ -4649,24 +5240,6 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4649
5240
|
index += 1;
|
|
4650
5241
|
continue;
|
|
4651
5242
|
}
|
|
4652
|
-
// A length-stopped, fact-less compact session is the planner variant of
|
|
4653
|
-
// writer-thinking-exhausted. Give the same scope exactly one tool-first
|
|
4654
|
-
// retry before the split below re-batches the requirements, because one
|
|
4655
|
-
// reinforced full-scope pass is cheaper than re-planning split halves.
|
|
4656
|
-
const plannerThinkingBurn = isCompactLocalSession &&
|
|
4657
|
-
committedAfter === committedBefore &&
|
|
4658
|
-
!(result.assistantText ?? "").trim() &&
|
|
4659
|
-
!result.stderr.trim() &&
|
|
4660
|
-
!result.timedOut &&
|
|
4661
|
-
readWriterThinkingExhaustionEvidence(result).stopReason === "length";
|
|
4662
|
-
if (plannerThinkingBurn && (session.retryCount ?? 0) < 1) {
|
|
4663
|
-
queue[index] = {
|
|
4664
|
-
...session,
|
|
4665
|
-
retryCount: (session.retryCount ?? 0) + 1,
|
|
4666
|
-
prompt: buildCompactLocalPrompt(),
|
|
4667
|
-
};
|
|
4668
|
-
continue;
|
|
4669
|
-
}
|
|
4670
5243
|
// Option 5: a multi-requirement coverage batch that failed with ZERO
|
|
4671
5244
|
// new facts and no provider stderr is the upfront-reasoning burn —
|
|
4672
5245
|
// halve the slice and retry instead of failing the attempt. The compact
|
|
@@ -4681,20 +5254,24 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4681
5254
|
: undefined;
|
|
4682
5255
|
const zeroProgressBurn = coverageSlice !== undefined &&
|
|
4683
5256
|
coverageSlice.length > 1 &&
|
|
4684
|
-
committedAfter === committedBefore &&
|
|
4685
|
-
!(result.assistantText ?? "").trim() &&
|
|
4686
|
-
!result.stderr.trim() &&
|
|
5257
|
+
(capacityExhausted || (["empty-output", "unknown"].includes(result.failureCategory) && committedAfter === committedBefore && !(result.assistantText ?? "").trim() && !result.stderr.trim())) &&
|
|
4687
5258
|
!result.timedOut;
|
|
4688
5259
|
if (zeroProgressBurn && coverageSlice) {
|
|
4689
|
-
const
|
|
4690
|
-
const
|
|
4691
|
-
|
|
5260
|
+
const missingIds = input.committedFacts ? new Set(collectFrontendPlanMissingFacts({ requirementIds: coverageSlice, committedFacts: input.committedFacts() }).flatMap(f => f.requirementIds)) : undefined;
|
|
5261
|
+
const unfinished = coverageSlice.filter(id => !missingIds || missingIds.has(id));
|
|
5262
|
+
if (unfinished.length <= 1)
|
|
5263
|
+
return mapPlannerExhaustion({ ...result, ok: false, stderr: `${result.stderr}\nFRONTEND_INPUT_UNIT_TOO_LARGE: ${unfinished[0] ?? session.id}; no smaller complete scope can finish` }, committedAfter > committedBefore);
|
|
5264
|
+
const half = Math.ceil(unfinished.length / 2);
|
|
5265
|
+
const firstSlice = unfinished.slice(0, half);
|
|
5266
|
+
const secondSlice = unfinished.slice(half);
|
|
4692
5267
|
if (isCompactLocalSession) {
|
|
5268
|
+
queue.splice(index + 1, 0, { id: "ux-registry", toolNames: uxRegistrySegment.toolNames, prompt: buildUxRegistryPrompt() }, ...buildWorkBatches([...firstSlice, ...secondSlice]).map((slice, i) => ({ id: `ux-local-recovery-${i}`, toolNames: uxSegment.toolNames, requirementSlice: slice, prompt: buildUxPrompt(slice) })));
|
|
4693
5269
|
// The split halves leave compact mode: continue them as ordinary
|
|
4694
5270
|
// coverage sessions so every downstream ladder branch applies.
|
|
4695
5271
|
queue.splice(index, 1, {
|
|
4696
5272
|
...session,
|
|
4697
5273
|
id: `coverage-compact-split-1`,
|
|
5274
|
+
toolNames: coverageSegment.toolNames, retryCount: 0,
|
|
4698
5275
|
coverageOnly: true,
|
|
4699
5276
|
coverageSlice: firstSlice,
|
|
4700
5277
|
requirementSlice: firstSlice,
|
|
@@ -4702,6 +5279,7 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4702
5279
|
}, {
|
|
4703
5280
|
...session,
|
|
4704
5281
|
id: `coverage-compact-split-2`,
|
|
5282
|
+
toolNames: coverageSegment.toolNames, retryCount: 0,
|
|
4705
5283
|
coverageOnly: true,
|
|
4706
5284
|
coverageSlice: secondSlice,
|
|
4707
5285
|
requirementSlice: secondSlice,
|
|
@@ -4720,7 +5298,9 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4720
5298
|
});
|
|
4721
5299
|
continue;
|
|
4722
5300
|
}
|
|
4723
|
-
if ((
|
|
5301
|
+
if (readWriterThinkingExhaustionEvidence(result).stopReason === "length" || ["context-overflow", "context-budget-exhausted"].includes(result.failureCategory))
|
|
5302
|
+
return mapPlannerExhaustion({ ...result, ok: false, stderr: `${result.stderr}\nFRONTEND_INPUT_UNIT_TOO_LARGE: ${session.id}; refine the remaining complete scope; unchanged retries are disabled` }, committedAfter > committedBefore);
|
|
5303
|
+
if ((session.coverageOnly || isUxRegistrySession || isUxLocalSession || session.id.startsWith("global-mock-data")) &&
|
|
4724
5304
|
missingPhaseFacts.length > 0 &&
|
|
4725
5305
|
!result.stderr.trim() &&
|
|
4726
5306
|
!result.timedOut &&
|
|
@@ -4852,14 +5432,27 @@ async function runFrontendScoutParallelSessions(input) {
|
|
|
4852
5432
|
...input.sessionOptions,
|
|
4853
5433
|
sessionEventsPath: path.join(input.runDir, shardNodeId, "session-events.jsonl"),
|
|
4854
5434
|
};
|
|
4855
|
-
|
|
5435
|
+
const prompt = `${input.basePrompt}\n\n${shard.instruction}`;
|
|
5436
|
+
const tools = [...customTools, ...(input.readBudgetTools ?? [])];
|
|
5437
|
+
shardResult = await observeFrontendSession({
|
|
5438
|
+
...input.observation,
|
|
5439
|
+
phase: `scout/parallel/${shard.id}`,
|
|
5440
|
+
scopeIds: [shard.id],
|
|
5441
|
+
prompt,
|
|
5442
|
+
userMessage: sessionOptions.userMessage,
|
|
5443
|
+
customTools: tools,
|
|
5444
|
+
artifactPath: input.observation?.artifactPath ? `${input.observation.artifactPath}-${shard.id}.json` : undefined,
|
|
5445
|
+
committedCount: () => shardTools?.committedFacts().length ?? 0,
|
|
5446
|
+
durableCommittedCount: () => shardTools?.committedFacts().length ?? 0,
|
|
5447
|
+
}, observer => input.piStepFn({
|
|
4856
5448
|
...sessionOptions,
|
|
4857
|
-
|
|
5449
|
+
onAttemptObservation: observer,
|
|
5450
|
+
prompt,
|
|
4858
5451
|
writerToolPolicy: {
|
|
4859
5452
|
requireSdk: true,
|
|
4860
|
-
customTools:
|
|
5453
|
+
customTools: tools,
|
|
4861
5454
|
},
|
|
4862
|
-
});
|
|
5455
|
+
}));
|
|
4863
5456
|
await shardTools.flush();
|
|
4864
5457
|
return { shard, result: shardResult, tools: shardTools };
|
|
4865
5458
|
}
|
|
@@ -5035,6 +5628,8 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
5035
5628
|
let writerToolPolicy;
|
|
5036
5629
|
let reviewTerminalTools;
|
|
5037
5630
|
let designTerminalTools;
|
|
5631
|
+
let reviewInventory;
|
|
5632
|
+
let designInventory;
|
|
5038
5633
|
let planLedgerTools;
|
|
5039
5634
|
let contractTools;
|
|
5040
5635
|
let scoutEvidenceTools;
|
|
@@ -5128,6 +5723,8 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
5128
5723
|
const { createTypedEventStore } = await import("../workflows/dag/frontend-typed-event-store.js");
|
|
5129
5724
|
const store = createTypedEventStore();
|
|
5130
5725
|
reviewTerminalTools = await createFrontendReviewTerminalTools({
|
|
5726
|
+
inventory: reviewInventory = await loadFrontendReviewScopes(meta.runDir, "review", meta.spec?.frontendExecutionPolicy?.scopeTargetBytes ?? FRONTEND_SCOPE_TARGET_BYTES),
|
|
5727
|
+
inputDigest: reviewInventory?.digest ?? createHash("sha256").update(input.prompt).digest("hex"),
|
|
5131
5728
|
attemptId: `${meta.runId}:${input.task.id}`,
|
|
5132
5729
|
store,
|
|
5133
5730
|
runDir: meta.runDir,
|
|
@@ -5153,6 +5750,8 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
5153
5750
|
const { createTypedEventStore } = await import("../workflows/dag/frontend-typed-event-store.js");
|
|
5154
5751
|
const store = createTypedEventStore();
|
|
5155
5752
|
designTerminalTools = await createFrontendDesignTerminalTools({
|
|
5753
|
+
inventory: designInventory = await loadFrontendReviewScopes(meta.runDir, "design", meta.spec?.frontendExecutionPolicy?.scopeTargetBytes ?? FRONTEND_SCOPE_TARGET_BYTES),
|
|
5754
|
+
inputDigest: designInventory?.digest ?? createHash("sha256").update(input.prompt).digest("hex"),
|
|
5156
5755
|
attemptId: `${meta.runId}:${input.task.id}`,
|
|
5157
5756
|
store,
|
|
5158
5757
|
runDir: meta.runDir,
|
|
@@ -5178,6 +5777,7 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
5178
5777
|
const { createTypedEventStore } = await import("../workflows/dag/frontend-typed-event-store.js");
|
|
5179
5778
|
const store = createTypedEventStore();
|
|
5180
5779
|
contractTools = await createFrontendContractTools({
|
|
5780
|
+
sourceDigest: (meta.spec?.sourceBinding?.schemaVersion === 2 ? meta.spec.sourceBinding.inputDigest : undefined),
|
|
5181
5781
|
attemptId: `${meta.runId}:${input.task.id}`,
|
|
5182
5782
|
store,
|
|
5183
5783
|
runDir: meta.runDir,
|
|
@@ -5211,6 +5811,8 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
5211
5811
|
spec: meta.spec,
|
|
5212
5812
|
});
|
|
5213
5813
|
scoutEvidenceTools = await createFrontendScoutEvidenceTools({
|
|
5814
|
+
requirementIds: parseFrontendInputBlock(input.prompt, "scout")?.payload.requirements.map(r => r.id),
|
|
5815
|
+
sourceDigest: (meta.spec?.sourceBinding?.schemaVersion === 2 ? meta.spec.sourceBinding.inputDigest : undefined),
|
|
5214
5816
|
attemptId: `${meta.runId}:${input.task.id}`,
|
|
5215
5817
|
store,
|
|
5216
5818
|
runDir: meta.runDir,
|
|
@@ -5368,8 +5970,11 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
5368
5970
|
activeTools: toolNames,
|
|
5369
5971
|
});
|
|
5370
5972
|
const piSessionOptions = {
|
|
5973
|
+
reserveProviderRequest: input.reserveProviderRequest,
|
|
5974
|
+
frontendExecutionPolicy: meta.spec?.frontendExecutionPolicy,
|
|
5371
5975
|
attachedFiles: [],
|
|
5372
5976
|
modelConfig,
|
|
5977
|
+
outputLimitRecovery: { terminalToolNames: ["finalize_plan", "finalize_contract", "approve_design", "request_design_changes", "approve_review", "request_review_changes"] },
|
|
5373
5978
|
repoRoot: input.cwd,
|
|
5374
5979
|
step,
|
|
5375
5980
|
toolNames,
|
|
@@ -5409,6 +6014,15 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
5409
6014
|
workspaceRoot: input.cwd,
|
|
5410
6015
|
sourceDeclaredPaths: scoutSourceDeclaredPaths,
|
|
5411
6016
|
readBudgetTools: readBudgetTools?.customTools,
|
|
6017
|
+
observation: {
|
|
6018
|
+
artifactPath: path.join(meta.runDir, input.task.id, "session-budget", `attempt-${input.attempt ?? 1}`),
|
|
6019
|
+
runId: meta.runId,
|
|
6020
|
+
nodeId: input.task.id,
|
|
6021
|
+
taskId: meta.spec?.sourceBinding?.taskId,
|
|
6022
|
+
attempt: input.attempt ?? 1,
|
|
6023
|
+
model: input.model,
|
|
6024
|
+
sourceDigest: meta.spec?.sourceBinding?.schemaVersion === 2 ? meta.spec.sourceBinding.inputDigest : undefined,
|
|
6025
|
+
},
|
|
5412
6026
|
});
|
|
5413
6027
|
}
|
|
5414
6028
|
else if (isFrontendPlanLedgerNode(input.task) && planLedgerTools) {
|
|
@@ -5448,6 +6062,16 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
5448
6062
|
// finalize reducer. This avoids revision races while shortening the
|
|
5449
6063
|
// longest coverage phase for large plans.
|
|
5450
6064
|
const runPlanSessions = (options) => runFrontendPlanSegmentedSessions({
|
|
6065
|
+
observation: options.observation ?? {
|
|
6066
|
+
artifactPath: path.join(meta.runDir, input.task.id, "session-budget", `attempt-${input.attempt ?? 1}`),
|
|
6067
|
+
runId: meta.runId,
|
|
6068
|
+
nodeId: input.task.id,
|
|
6069
|
+
taskId: meta.spec?.sourceBinding?.taskId,
|
|
6070
|
+
attempt: input.attempt ?? 1,
|
|
6071
|
+
model: input.model,
|
|
6072
|
+
sourceDigest: meta.spec?.sourceBinding?.schemaVersion === 2 ? meta.spec.sourceBinding.inputDigest : undefined,
|
|
6073
|
+
contractDigest: meta.spec?.taskContractBinding?.canonicalHash,
|
|
6074
|
+
},
|
|
5451
6075
|
piStepFn,
|
|
5452
6076
|
sessionOptions: options.sessionOptions,
|
|
5453
6077
|
basePrompt: options.basePrompt ?? input.prompt,
|
|
@@ -5469,154 +6093,162 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
5469
6093
|
? options.ledgerTools.customTools
|
|
5470
6094
|
: options.ledgerTools.customTools.filter((tool) => typeof tool === "object" &&
|
|
5471
6095
|
tool !== null &&
|
|
5472
|
-
toolNames.has(tool.name)),
|
|
6096
|
+
(toolNames.has(tool.name) || tool.name === "read_plan_facts")),
|
|
5473
6097
|
flushLedger: () => options.ledgerTools.flush(),
|
|
5474
6098
|
});
|
|
5475
6099
|
if (planRequirementIds.length > 1) {
|
|
5476
|
-
const
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
6100
|
+
const workload = buildFrontendPlanWorkload({
|
|
6101
|
+
basePrompt: input.prompt, requirementIds: planRequirementIds,
|
|
6102
|
+
requirementCosts: planRequirementCosts, sessionOptions: piSessionOptions,
|
|
6103
|
+
});
|
|
6104
|
+
let coverageBatches;
|
|
6105
|
+
try {
|
|
6106
|
+
coverageBatches = await loadOrCreateFrontendPlanCoverageLayout({
|
|
6107
|
+
runDir: meta.runDir, nodeId: input.task.id, workload, requirementIds: planRequirementIds,
|
|
6108
|
+
binding: {
|
|
6109
|
+
runId: meta.runId, nodeId: input.task.id, sourceBinding: meta.spec.sourceBinding,
|
|
6110
|
+
skeleton: input.task.structuredContractOutput?.skeleton, writeSet: input.task.writeSet,
|
|
6111
|
+
policy: meta.spec.frontendExecutionPolicy, requirements: [...planRequirementCosts],
|
|
6112
|
+
compiledInput: workload.frozenInput, workGroups: workload.workGroups,
|
|
6113
|
+
},
|
|
5488
6114
|
});
|
|
5489
6115
|
}
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
|
|
6116
|
+
catch (error) {
|
|
6117
|
+
return { ok: false, stdout: "", durationMs: Date.now() - started, failureCategory: "tool-policy",
|
|
6118
|
+
stderr: `frontend plan coverage layout unavailable: ${error instanceof Error ? error.message : String(error)}` };
|
|
6119
|
+
}
|
|
6120
|
+
if (coverageBatches.length > 1) {
|
|
6121
|
+
const shardResults = await mapWithConcurrency(coverageBatches, piSessionOptions.frontendExecutionPolicy?.maxCoverageConcurrency ?? FRONTEND_PLAN_COVERAGE_MAX_CONCURRENCY, async (slice, index) => {
|
|
6122
|
+
let shardTools;
|
|
6123
|
+
let shardResult;
|
|
6124
|
+
try {
|
|
6125
|
+
const { createTypedEventStore } = await import("../workflows/dag/frontend-typed-event-store.js");
|
|
6126
|
+
shardTools = await createFrontendPlanLedgerTools({
|
|
6127
|
+
attemptId: `${meta.runId}:${input.task.id}:parallel:${index + 1}`,
|
|
6128
|
+
store: createTypedEventStore(),
|
|
6129
|
+
runDir: meta.runDir,
|
|
6130
|
+
nodeId: `${input.task.id}/parallel/coverage-${index + 1}`,
|
|
6131
|
+
skeleton: input.task.structuredContractOutput?.skeleton,
|
|
6132
|
+
sourceBinding: meta.spec.sourceBinding,
|
|
6133
|
+
requirementIds: slice,
|
|
6134
|
+
writeSetPatterns: input.task.writeSet,
|
|
6135
|
+
canonicalVerificationTargetIds: await resolveFrontendCanonicalVerificationTargetIds({
|
|
5506
6136
|
runDir: meta.runDir,
|
|
5507
|
-
|
|
5508
|
-
|
|
6137
|
+
}),
|
|
6138
|
+
componentNewSourceReferences: await resolveFrontendPlanNewComponentSourceReferences({
|
|
6139
|
+
cwd: input.cwd,
|
|
5509
6140
|
sourceBinding: meta.spec.sourceBinding,
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
componentNewSourceReferences: await resolveFrontendPlanNewComponentSourceReferences({
|
|
5516
|
-
cwd: input.cwd,
|
|
5517
|
-
sourceBinding: meta.spec.sourceBinding,
|
|
5518
|
-
}),
|
|
5519
|
-
});
|
|
5520
|
-
const shardSessionOptions = {
|
|
5521
|
-
...piSessionOptions,
|
|
5522
|
-
sessionEventsPath: path.join(meta.runDir, input.task.id, "parallel", `coverage-${index + 1}`, "session-events.jsonl"),
|
|
5523
|
-
};
|
|
5524
|
-
shardResult = await runPlanSessions({
|
|
5525
|
-
ledgerTools: shardTools,
|
|
5526
|
-
sessionOptions: shardSessionOptions,
|
|
5527
|
-
requirementIds: slice,
|
|
5528
|
-
basePrompt: `${input.prompt}\n\nPARALLEL COVERAGE SHARD ${index + 1}: use the frozen canonical behavior verification-target ids listed in the record_plan_verification_target tool description — do not prefix ids with a shard namespace or invent variant ids; identical cross-shard targets are deduped, divergent ones fail the merge.`,
|
|
5529
|
-
parallelCoverageOnly: true,
|
|
5530
|
-
});
|
|
5531
|
-
await shardTools.flush();
|
|
5532
|
-
return { index, result: shardResult, tools: shardTools };
|
|
5533
|
-
}
|
|
5534
|
-
catch (error) {
|
|
5535
|
-
const crashMessage = `frontend plan coverage shard ${index + 1} crashed: ${error instanceof Error ? error.message : String(error)}`;
|
|
5536
|
-
return {
|
|
5537
|
-
index,
|
|
5538
|
-
tools: shardTools,
|
|
5539
|
-
result: shardResult
|
|
5540
|
-
? {
|
|
5541
|
-
...shardResult,
|
|
5542
|
-
ok: false,
|
|
5543
|
-
failureCategory: shardResult.ok
|
|
5544
|
-
? "invalid-output"
|
|
5545
|
-
: shardResult.failureCategory,
|
|
5546
|
-
stderr: [shardResult.stderr, crashMessage]
|
|
5547
|
-
.filter(Boolean)
|
|
5548
|
-
.join("\n"),
|
|
5549
|
-
}
|
|
5550
|
-
: {
|
|
5551
|
-
ok: false,
|
|
5552
|
-
assistantText: "",
|
|
5553
|
-
command: [],
|
|
5554
|
-
durationMs: 0,
|
|
5555
|
-
exitCode: null,
|
|
5556
|
-
failureCategory: "tool-policy",
|
|
5557
|
-
modelDisplay: "unknown",
|
|
5558
|
-
parsedEvents: 0,
|
|
5559
|
-
stderr: crashMessage,
|
|
5560
|
-
stdout: "",
|
|
5561
|
-
timedOut: false,
|
|
5562
|
-
attemptedModels: [],
|
|
5563
|
-
fallbackUsed: false,
|
|
5564
|
-
tokensUsed: 0,
|
|
5565
|
-
},
|
|
5566
|
-
};
|
|
5567
|
-
}
|
|
5568
|
-
});
|
|
5569
|
-
const coverageResult = aggregateParallelPiResults(shardResults.map((shard) => shard.result));
|
|
5570
|
-
let mergeFailure;
|
|
5571
|
-
try {
|
|
5572
|
-
// Flatten every shard's facts FIRST, then pre-reduce: shards
|
|
5573
|
-
// partition requirements but a frozen verification target can
|
|
5574
|
-
// span shards, so same-id targets must merge across shards
|
|
5575
|
-
// before adoption, not per shard.
|
|
5576
|
-
await planLedgerTools.adoptCommittedFacts(reduceParallelCoverageShardRecords(shardResults
|
|
5577
|
-
.filter((item) => item.result.ok && item.tools)
|
|
5578
|
-
.flatMap((shard) => normalizeParallelCoverageShardRecords(shard.tools.committedFacts(), shard.index + 1))));
|
|
5579
|
-
}
|
|
5580
|
-
catch (error) {
|
|
5581
|
-
mergeFailure = error;
|
|
5582
|
-
}
|
|
5583
|
-
const failedShard = shardResults.find((shard) => !shard.result.ok);
|
|
5584
|
-
if (mergeFailure) {
|
|
5585
|
-
result = {
|
|
5586
|
-
...coverageResult,
|
|
5587
|
-
ok: false,
|
|
5588
|
-
failureCategory: "invalid-output",
|
|
5589
|
-
stderr: [
|
|
5590
|
-
coverageResult.stderr,
|
|
5591
|
-
`frontend plan coverage shard merge failed: ${mergeFailure instanceof Error ? mergeFailure.message : String(mergeFailure)}`,
|
|
5592
|
-
]
|
|
5593
|
-
.filter(Boolean)
|
|
5594
|
-
.join("\n"),
|
|
6141
|
+
}),
|
|
6142
|
+
});
|
|
6143
|
+
const shardSessionOptions = {
|
|
6144
|
+
...piSessionOptions,
|
|
6145
|
+
sessionEventsPath: path.join(meta.runDir, input.task.id, "parallel", `coverage-${index + 1}`, "session-events.jsonl"),
|
|
5595
6146
|
};
|
|
6147
|
+
shardResult = await runPlanSessions({
|
|
6148
|
+
ledgerTools: shardTools,
|
|
6149
|
+
sessionOptions: shardSessionOptions,
|
|
6150
|
+
requirementIds: slice,
|
|
6151
|
+
basePrompt: `${input.prompt}\n\nPARALLEL COVERAGE SHARD ${index + 1}: use the frozen canonical behavior verification-target ids listed in the record_plan_verification_target tool description — do not prefix ids with a shard namespace or invent variant ids; identical cross-shard targets are deduped, divergent ones fail the merge.`,
|
|
6152
|
+
parallelCoverageOnly: true,
|
|
6153
|
+
observation: {
|
|
6154
|
+
artifactPath: path.join(meta.runDir, input.task.id, "parallel", `coverage-${index + 1}`, "session-budget", `attempt-${input.attempt ?? 1}`),
|
|
6155
|
+
runId: meta.runId,
|
|
6156
|
+
nodeId: `${input.task.id}/parallel/coverage-${index + 1}`,
|
|
6157
|
+
taskId: meta.spec?.sourceBinding?.taskId,
|
|
6158
|
+
attempt: input.attempt ?? 1,
|
|
6159
|
+
model: input.model,
|
|
6160
|
+
sourceDigest: meta.spec?.sourceBinding?.schemaVersion === 2 ? meta.spec.sourceBinding.inputDigest : undefined,
|
|
6161
|
+
contractDigest: meta.spec?.taskContractBinding?.canonicalHash,
|
|
6162
|
+
},
|
|
6163
|
+
});
|
|
6164
|
+
await shardTools.flush();
|
|
6165
|
+
return { index, result: shardResult, tools: shardTools };
|
|
5596
6166
|
}
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
6167
|
+
catch (error) {
|
|
6168
|
+
const crashMessage = `frontend plan coverage shard ${index + 1} crashed: ${error instanceof Error ? error.message : String(error)}`;
|
|
6169
|
+
return {
|
|
6170
|
+
index,
|
|
6171
|
+
tools: shardTools,
|
|
6172
|
+
result: shardResult
|
|
6173
|
+
? {
|
|
6174
|
+
...shardResult,
|
|
6175
|
+
ok: false,
|
|
6176
|
+
failureCategory: shardResult.ok
|
|
6177
|
+
? "invalid-output"
|
|
6178
|
+
: shardResult.failureCategory,
|
|
6179
|
+
stderr: [shardResult.stderr, crashMessage]
|
|
6180
|
+
.filter(Boolean)
|
|
6181
|
+
.join("\n"),
|
|
6182
|
+
}
|
|
6183
|
+
: {
|
|
6184
|
+
ok: false,
|
|
6185
|
+
assistantText: "",
|
|
6186
|
+
command: [],
|
|
6187
|
+
durationMs: 0,
|
|
6188
|
+
exitCode: null,
|
|
6189
|
+
failureCategory: "tool-policy",
|
|
6190
|
+
modelDisplay: "unknown",
|
|
6191
|
+
parsedEvents: 0,
|
|
6192
|
+
stderr: crashMessage,
|
|
6193
|
+
stdout: "",
|
|
6194
|
+
timedOut: false,
|
|
6195
|
+
attemptedModels: [],
|
|
6196
|
+
fallbackUsed: false,
|
|
6197
|
+
tokensUsed: 0,
|
|
6198
|
+
},
|
|
5602
6199
|
};
|
|
5603
6200
|
}
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
6201
|
+
}, shard => shard.result.failureCategory === "rate-limit");
|
|
6202
|
+
const coverageResult = aggregateParallelPiResults(shardResults.map((shard) => shard.result));
|
|
6203
|
+
let mergeFailure;
|
|
6204
|
+
try {
|
|
6205
|
+
// Flatten every shard's facts FIRST, then pre-reduce: shards
|
|
6206
|
+
// partition requirements but a frozen verification target can
|
|
6207
|
+
// span shards, so same-id targets must merge across shards
|
|
6208
|
+
// before adoption, not per shard.
|
|
6209
|
+
await planLedgerTools.adoptCommittedFacts(reduceParallelCoverageShardRecords(shardResults
|
|
6210
|
+
.filter((item) => item.result.ok && item.tools)
|
|
6211
|
+
.flatMap((shard) => normalizeParallelCoverageShardRecords(shard.tools.committedFacts(), shard.index + 1))));
|
|
6212
|
+
}
|
|
6213
|
+
catch (error) {
|
|
6214
|
+
mergeFailure = error;
|
|
6215
|
+
}
|
|
6216
|
+
const failedShard = shardResults.find((shard) => !shard.result.ok);
|
|
6217
|
+
if (mergeFailure) {
|
|
6218
|
+
result = {
|
|
6219
|
+
...coverageResult,
|
|
6220
|
+
ok: false,
|
|
6221
|
+
failureCategory: "invalid-output",
|
|
6222
|
+
stderr: [
|
|
6223
|
+
coverageResult.stderr,
|
|
6224
|
+
`frontend plan coverage shard merge failed: ${mergeFailure instanceof Error ? mergeFailure.message : String(mergeFailure)}`,
|
|
6225
|
+
]
|
|
6226
|
+
.filter(Boolean)
|
|
6227
|
+
.join("\n"),
|
|
6228
|
+
};
|
|
6229
|
+
}
|
|
6230
|
+
else if (failedShard) {
|
|
6231
|
+
result = {
|
|
6232
|
+
...coverageResult,
|
|
6233
|
+
ok: false,
|
|
6234
|
+
stderr: `${coverageResult.stderr}\nfrontend plan coverage shard ${failedShard.index + 1} failed before reduce`.trim(),
|
|
6235
|
+
};
|
|
5611
6236
|
}
|
|
5612
6237
|
else {
|
|
5613
|
-
|
|
6238
|
+
const reducerResult = await runPlanSessions({
|
|
5614
6239
|
ledgerTools: planLedgerTools,
|
|
5615
6240
|
sessionOptions: piSessionOptions,
|
|
5616
|
-
compactSmallPlan: true,
|
|
5617
6241
|
});
|
|
6242
|
+
result = combineSequentialPiResults(coverageResult, reducerResult);
|
|
5618
6243
|
}
|
|
5619
6244
|
}
|
|
6245
|
+
else {
|
|
6246
|
+
result = await runPlanSessions({
|
|
6247
|
+
ledgerTools: planLedgerTools,
|
|
6248
|
+
sessionOptions: piSessionOptions,
|
|
6249
|
+
compactSmallPlan: true,
|
|
6250
|
+
});
|
|
6251
|
+
}
|
|
5620
6252
|
}
|
|
5621
6253
|
else {
|
|
5622
6254
|
result = await runPlanSessions({
|
|
@@ -5626,12 +6258,31 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
5626
6258
|
});
|
|
5627
6259
|
}
|
|
5628
6260
|
}
|
|
6261
|
+
else if (contractTools) {
|
|
6262
|
+
result = await runFrontendContractSegmentedSessions({
|
|
6263
|
+
piStepFn, sessionOptions: piSessionOptions, basePrompt: input.prompt, tools: contractTools,
|
|
6264
|
+
observation: { artifactPath: path.join(meta.runDir, input.task.id, "session-budget", `attempt-${input.attempt ?? 1}`), runId: meta.runId, nodeId: input.task.id, taskId: meta.spec?.sourceBinding?.taskId, attempt: input.attempt ?? 1, model: input.model, sourceDigest: meta.spec?.sourceBinding?.schemaVersion === 2 ? meta.spec.sourceBinding.inputDigest : undefined },
|
|
6265
|
+
});
|
|
6266
|
+
}
|
|
6267
|
+
else if ((reviewInventory && reviewTerminalTools) || (designInventory && designTerminalTools)) {
|
|
6268
|
+
result = await runFrontendReviewSegmentedSessions({ piStepFn, sessionOptions: piSessionOptions, basePrompt: input.prompt, inventory: (reviewInventory ?? designInventory), tools: (reviewTerminalTools ?? designTerminalTools), customTools: writerToolPolicy.customTools, phase: reviewInventory ? "review" : "design",
|
|
6269
|
+
observation: { artifactPath: path.join(meta.runDir, input.task.id, "session-budget", `attempt-${input.attempt ?? 1}`), runId: meta.runId, nodeId: input.task.id, taskId: meta.spec?.sourceBinding?.taskId, attempt: input.attempt ?? 1, model: input.model, contractDigest: (reviewInventory ?? designInventory).digest } });
|
|
6270
|
+
}
|
|
6271
|
+
else if (scoutEvidenceTools && parseFrontendInputBlock(input.prompt, "scout")) {
|
|
6272
|
+
result = await runFrontendScoutSegmentedSessions({ piStepFn, sessionOptions: piSessionOptions, basePrompt: input.prompt, tools: scoutEvidenceTools, customTools: writerToolPolicy.customTools,
|
|
6273
|
+
observation: { artifactPath: path.join(meta.runDir, input.task.id, "session-budget", `attempt-${input.attempt ?? 1}`), runId: meta.runId, nodeId: input.task.id, taskId: meta.spec?.sourceBinding?.taskId, attempt: input.attempt ?? 1, model: input.model, sourceDigest: meta.spec?.sourceBinding?.schemaVersion === 2 ? meta.spec.sourceBinding.inputDigest : undefined } });
|
|
6274
|
+
}
|
|
5629
6275
|
else {
|
|
5630
|
-
result = await
|
|
6276
|
+
result = await observeFrontendSession({
|
|
6277
|
+
artifactPath: input.task.id.startsWith("frontend-") ? path.join(meta.runDir, input.task.id, "session-budget", `attempt-${input.attempt ?? 1}.json`) : undefined,
|
|
6278
|
+
phase: input.task.id, prompt: input.prompt, userMessage: piSessionOptions.userMessage, customTools: writerToolPolicy?.customTools,
|
|
6279
|
+
runId: meta.runId, nodeId: input.task.id, taskId: meta.spec?.sourceBinding?.taskId, attempt: input.attempt ?? 1, model: input.model, sourceDigest: (meta.spec?.sourceBinding?.schemaVersion === 2 ? meta.spec.sourceBinding.inputDigest : undefined), contractDigest: meta.spec?.taskContractBinding?.canonicalHash,
|
|
6280
|
+
}, observer => piStepFn({
|
|
5631
6281
|
...piSessionOptions,
|
|
6282
|
+
onAttemptObservation: observer,
|
|
5632
6283
|
prompt: input.prompt,
|
|
5633
6284
|
...(writerToolPolicy ? { writerToolPolicy } : {}),
|
|
5634
|
-
});
|
|
6285
|
+
}));
|
|
5635
6286
|
}
|
|
5636
6287
|
try {
|
|
5637
6288
|
const verificationCommandFiles = collectFrontendVerificationCommandFiles(input.prompt);
|
|
@@ -5796,8 +6447,8 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
5796
6447
|
try {
|
|
5797
6448
|
await scoutEvidenceTools?.flush?.();
|
|
5798
6449
|
}
|
|
5799
|
-
catch {
|
|
5800
|
-
|
|
6450
|
+
catch (error) {
|
|
6451
|
+
return { ...mapped, ok: false, failureCategory: mapped.ok ? "frontend-ledger-invalid" : mapped.failureCategory, stderr: `${mapped.stderr}\nFRONTEND_LEDGER_INTEGRITY_INVALID: ${error instanceof Error ? error.message : String(error)}` };
|
|
5801
6452
|
}
|
|
5802
6453
|
if (mapped.ok) {
|
|
5803
6454
|
const { checkCommittedOriginFacts, readCommittedOriginFacts } = await import("../workflows/dag/frontend-shadow-dual-write.js");
|