@tea-agent/loop-agent 0.43.0-next.1 → 0.43.0-next.10
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 +129 -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 +1214 -584
- package/dist/executors/pi-executor.js +22 -1
- package/dist/executors/pi-sdk-executor.js +140 -39
- package/dist/executors/shell-executor.js +5 -2
- 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 +143 -0
- package/dist/worker/console/chat/browser-routes.js +148 -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 +78 -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 +213 -1181
- package/dist/worker/console/chat/routes.js +212 -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 +48 -0
- package/dist/worker/console/chat/session-mode.js +218 -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-D_fNbWc_.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DCPjC19G.js → arc-_HuqLW6m.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CP5n9jmG.js → architectureDiagram-T3A2C74G-Djy2qY1m.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CmaWi_Wg.js → blockDiagram-VBNYF7ZC-bB-1TSyY.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Ct5tcMfZ.js → c4Diagram-5PPSVZJV-C5Hmghrz.js} +1 -1
- package/dist/worker/console/static/assets/channel-C0A6UEIa.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-C0osm1Zf.js → chunk-2GRJ4B5K-Duyl39oy.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CDWviED7.js → chunk-2Q5K7J3B-x6mdnSyf.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-mtSk1-j7.js → chunk-5RXB4S5H-WlBuFj7W.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D5J9PHC7.js → chunk-5VM5RSS4-CTF429HX.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Begg4WAa.js → chunk-6Q2QTUOP-Dwd6r-SF.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-sF1AEy7T.js → chunk-GF5L2VYU-C-Br82D1.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BqbsWXZb.js → chunk-JWPE2WC7-CuGp0DeE.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BKJrPcJ6.js → chunk-KBJHAD2P-CJhFX6Lh.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CcwNMRho.js → chunk-RYQCIY6F-DpGOa_jT.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BEJi3iIs.js → chunk-XXDRQBXY-DXJaTynd.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DzguNppy.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DzguNppy.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-DRLgTvVu.js → cose-bilkent-JH36ORCC-CRBZWjE4.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BoAVG3cs.js → cynefin-VYW2F7L2-BdXcxtlb.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bzol06hR.js → cynefinDiagram-MW4NZA55-07UKsOts.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-UmliJM77.js → dagre-VZM6K2ZE-D38p9__S.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BWLcJkFo.js → diagram-7IWD3JNH-nSiBNNjH.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CgApOm-O.js → diagram-B4RE2ZJO-BFOTot5D.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-Bh3RgTLs.js → diagram-LBJQPF4R-DypKaZ6k.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-Dsh-D5nE.js → diagram-Q27KOJAE-B6yoOQeW.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-Dkbbpcpb.js → diagram-UB23O5K3-BHYS6viX.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-3EBaA3t3.js → ebnfDiagram-BXEA7PRR-D2VD5Snj.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Bq2Dc5ok.js → erDiagram-JOGREHBK-E98mWxN1.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-De7y55ha.js → flowDiagram-UKHOOZJN-CT1zxhqP.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-fLiDbQRh.js → ganttDiagram-PKOTCBZU-6GcngNcy.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Du617mp1.js → gitGraphDiagram-DS77QQ5N-DCL6NvKe.js} +1 -1
- package/dist/worker/console/static/assets/index-24jp3aBh.js +468 -0
- package/dist/worker/console/static/assets/index-CY6y6I8M.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BwmlF-XP.js → infoDiagram-6WML65LV-C0627e-9.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-zsGtS59u.js → ishikawaDiagram-WSZJBQD7-C_u3P6Ik.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DTTPTe3f.js → journeyDiagram-NVQOT4AX-DPDPv9HL.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BkXg9aP-.js → kanban-definition-27J2QSJJ-D3OHdWiO.js} +1 -1
- package/dist/worker/console/static/assets/{linear-7U2ue5IE.js → linear-DI88zD7n.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BUuGHmWO.js → mermaid.core-p2l8IrWY.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-7q6fX0Sv.js → mindmap-definition-FAOFIHXS-B1bAX-oS.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CkQPJN55.js → pegDiagram-VL7TDLO6-DwOnWxdD.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CCal_tfX.js → pieDiagram-7S7Q4E2Y-xRkTXnJC.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B7bpxyAZ.js → quadrantDiagram-CIZ2JOQS-_9UC5Zio.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-CJmww7D-.js → railroadDiagram-AXF67PYL-CgEUirWJ.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D6ldWJEv.js → requirementDiagram-LRYGKXZP-Czbpwiyz.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-VGh33I9e.js → sankeyDiagram-W5VNT64P-_j7_KiqY.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CCkDrIjU.js → sequenceDiagram-SI44F4Z6-BNJa6Hyi.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-NC8f5Otb.js → sizeCapture-X5ZJPWSS-DedpKtOc.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-_92ezZdF.js → stateDiagram-OKZ733FA-BBvthr7c.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BABVQnfM.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BdUCGtbP.js → swimlanes-SLNWSIFB-Ds7umgEM.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D0qsLaxR.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CdD1H8ia.js → timeline-definition-Z64GVDOM-CKa-p1nF.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-Dkdk3oFo.js → vennDiagram-T6HMQDX7-LoUNR56o.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CKu2uPPY.js → wardleyDiagram-T6FBY63Y-Bqbl6ym7.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-ClqAnuG8.js → xychartDiagram-ELKLHX3M-B8dUg4lf.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-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-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-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/init-hybrid.js +16 -10
- 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 +5 -5
- package/package.json +6 -3
- package/skills/frontend-contract/SKILL.md +2 -1
- package/skills/frontend-contract/references/contract-protocol.md +19 -3
- package/skills/frontend-design-review/SKILL.md +12 -11
- package/skills/frontend-plan/SKILL.md +9 -2
- package/skills/frontend-plan/references/decision-contract.md +18 -5
- package/skills/frontend-review/SKILL.md +10 -11
- 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,7 +2286,7 @@ 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 from the ledger together with these optional remaining fields, publishes the canonical editable patch on a target-surface fact, and commits the terminal.
|
|
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 assembles them from the ledger together with these 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.",
|
|
2186
2290
|
promptSnippet: "Commit the finalize_plan terminal (ledger fields + optional residualRisks / realIntegrationGap).",
|
|
2187
2291
|
parameters: Type.Object({
|
|
2188
2292
|
residualRisks: optionalStringArray,
|
|
@@ -2197,6 +2301,9 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
2197
2301
|
// when the plan did not re-declare them. The contract node is the
|
|
2198
2302
|
// sole synthesis point; the plan inherits by requirement id.
|
|
2199
2303
|
const contractInheritance = await loadContractRequirementInheritance(input.runDir);
|
|
2304
|
+
const missingData = collectFrontendPlanPhaseMissingFacts({ phase: "global-mock-data", requirementIds: input.requirementIds ?? [...contractInheritance.keys()], committedFacts: committed }).filter(f => f.kind === "data-flow");
|
|
2305
|
+
if (missingData.length)
|
|
2306
|
+
return planToolReceipt({ ok: false, kind: "finalize_plan", code: "PLAN_DATA_FLOW_INCOMPLETE", error: missingData.map(f => f.reason).join("; ") });
|
|
2200
2307
|
const fragment = assemblePlanPatchFromCommittedFacts(committed, contractInheritance) ?? {};
|
|
2201
2308
|
const patch = {
|
|
2202
2309
|
...fragment,
|
|
@@ -2352,23 +2459,99 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
2352
2459
|
}
|
|
2353
2460
|
},
|
|
2354
2461
|
});
|
|
2355
|
-
|
|
2356
|
-
|
|
2462
|
+
const readPlanFactsTool = defineTool({
|
|
2463
|
+
name: "read_plan_facts",
|
|
2464
|
+
label: "read_plan_facts",
|
|
2465
|
+
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.",
|
|
2466
|
+
parameters: Type.Object({
|
|
2467
|
+
kind: Type.String(), entryId: Type.Optional(Type.String()), eventId: Type.Optional(Type.String()),
|
|
2468
|
+
field: Type.Optional(Type.String()), offset: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
2469
|
+
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 50 })),
|
|
2470
|
+
stringOffset: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
2471
|
+
expectedRevision: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
2472
|
+
}, { additionalProperties: false }),
|
|
2473
|
+
async execute(_id, params) {
|
|
2474
|
+
const reply = (details) => ({ content: [{ type: "text", text: JSON.stringify(details) }], details });
|
|
2475
|
+
if (params.expectedRevision !== undefined && params.expectedRevision !== store.revision) {
|
|
2476
|
+
return reply({ ok: false, revision: store.revision, error: "ledger revision changed; restart pagination" });
|
|
2477
|
+
}
|
|
2478
|
+
let records = readCommittedEvents(store, attemptId).filter(record => {
|
|
2479
|
+
const fact = record.fact;
|
|
2480
|
+
return fact.kind === params.kind && (!params.eventId || record.eventId === params.eventId) &&
|
|
2481
|
+
(!params.entryId || fact.entry?.id === params.entryId);
|
|
2482
|
+
});
|
|
2483
|
+
// Full-entry corrections and registries are last-wins. Other kinds
|
|
2484
|
+
// remain an ordered event log (state removals must remain visible).
|
|
2485
|
+
if (params.entryId || params.kind === "state-registry")
|
|
2486
|
+
records = records.slice(-1);
|
|
2487
|
+
let values = records.map(record => ({ eventId: record.eventId, ...record.fact }));
|
|
2488
|
+
if (params.field) {
|
|
2489
|
+
values = records.flatMap(record => {
|
|
2490
|
+
let value = record.fact;
|
|
2491
|
+
for (const key of params.field.split(".")) {
|
|
2492
|
+
if (!value || typeof value !== "object" || !Object.hasOwn(value, key))
|
|
2493
|
+
return [];
|
|
2494
|
+
value = value[key];
|
|
2495
|
+
}
|
|
2496
|
+
return Array.isArray(value) ? value : [value];
|
|
2497
|
+
});
|
|
2498
|
+
}
|
|
2499
|
+
const offset = params.offset ?? 0;
|
|
2500
|
+
if (params.stringOffset !== undefined) {
|
|
2501
|
+
const value = values[offset];
|
|
2502
|
+
if (typeof value !== "string")
|
|
2503
|
+
return reply({ ok: false, error: "stringOffset requires a string field item" });
|
|
2504
|
+
// Unicode code points prevent a page boundary splitting a surrogate pair.
|
|
2505
|
+
const characters = Array.from(value);
|
|
2506
|
+
const chunk = characters.slice(params.stringOffset, params.stringOffset + 1000).join("");
|
|
2507
|
+
const next = params.stringOffset + Array.from(chunk).length;
|
|
2508
|
+
return reply({ ok: true, revision: store.revision, offset, stringOffset: params.stringOffset,
|
|
2509
|
+
chunk, totalCharacters: characters.length, nextStringOffset: next < characters.length ? next : null });
|
|
2510
|
+
}
|
|
2511
|
+
const items = [];
|
|
2512
|
+
let bytes = 0;
|
|
2513
|
+
for (const value of values.slice(offset, offset + (params.limit ?? 10))) {
|
|
2514
|
+
const size = Buffer.byteLength(JSON.stringify(value));
|
|
2515
|
+
if (bytes + size > 12_000)
|
|
2516
|
+
break;
|
|
2517
|
+
bytes += size;
|
|
2518
|
+
items.push(value);
|
|
2519
|
+
}
|
|
2520
|
+
if (!items.length && offset < values.length)
|
|
2521
|
+
return reply({
|
|
2522
|
+
ok: false, revision: store.revision, total: values.length, offset,
|
|
2523
|
+
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",
|
|
2524
|
+
fields: typeof values[offset] === "object" && values[offset] !== null ? Object.keys(values[offset]) : [],
|
|
2525
|
+
});
|
|
2526
|
+
return reply({ ok: true, revision: store.revision, total: values.length, offset, items,
|
|
2527
|
+
nextOffset: offset + items.length < values.length ? offset + items.length : null });
|
|
2528
|
+
},
|
|
2529
|
+
});
|
|
2530
|
+
const durable = await createDurableFrontendTools({
|
|
2531
|
+
file: path.join(input.runDir, input.nodeId, "plan-typed-facts.jsonl"), attemptId, store: input.store,
|
|
2532
|
+
setWorkingStore: next => { store = next; },
|
|
2533
|
+
binding: { sourceBinding: input.sourceBinding, skeleton: input.skeleton, requirementIds: input.requirementIds, writeSet: input.writeSetPatterns, declaredUiStateIds: input.declaredUiStateIds, citations: input.componentNewSourceReferences, contractInheritance },
|
|
2534
|
+
tools: [
|
|
2357
2535
|
recordRouteSelectionTool,
|
|
2358
2536
|
recordComponentChoiceTool,
|
|
2359
2537
|
recordStateRegistryTool,
|
|
2360
2538
|
recordStateFlowTool,
|
|
2361
2539
|
recordDataFlowTool,
|
|
2362
2540
|
recordMockApiTool,
|
|
2541
|
+
recordMockEndpointTool,
|
|
2363
2542
|
recordDesignDeviationTool,
|
|
2364
2543
|
recordDependencyTool,
|
|
2365
2544
|
recordPlanRequirementTool,
|
|
2545
|
+
recordPlanGroupCoverageTool,
|
|
2366
2546
|
recordPlanVerificationTargetTool,
|
|
2367
2547
|
recordPlanEvidenceGapTool,
|
|
2368
2548
|
adoptStagedFactTool,
|
|
2369
2549
|
finalizePlanTool,
|
|
2370
2550
|
],
|
|
2371
|
-
|
|
2551
|
+
});
|
|
2552
|
+
return {
|
|
2553
|
+
customTools: [...durable.customTools, { ...readPlanFactsTool, execute: async (...args) => { await durable.flush(); return readPlanFactsTool.execute(...args); } }],
|
|
2554
|
+
adoptCommittedFacts: async (records) => durable.commitExternal(async () => {
|
|
2372
2555
|
for (const record of records) {
|
|
2373
2556
|
if (record.phase !== "committed")
|
|
2374
2557
|
continue;
|
|
@@ -2427,16 +2610,13 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
2427
2610
|
throw new Error(`frontend plan shard fact merge failed: ${result.error}`);
|
|
2428
2611
|
}
|
|
2429
2612
|
}
|
|
2430
|
-
},
|
|
2613
|
+
}),
|
|
2431
2614
|
setActiveRequirementScope: (requirementIds) => {
|
|
2432
2615
|
activeRequirementScope = [
|
|
2433
2616
|
...new Set(requirementIds.filter((id) => id.trim().length > 0)),
|
|
2434
2617
|
];
|
|
2435
2618
|
},
|
|
2436
|
-
flush:
|
|
2437
|
-
const committed = readCommittedEvents(store, attemptId);
|
|
2438
|
-
await writeTypedEventStoreJsonl(path.join(input.runDir, input.nodeId, "plan-typed-facts.jsonl"), committed);
|
|
2439
|
-
},
|
|
2619
|
+
flush: durable.flush,
|
|
2440
2620
|
committedFactCount: () => readCommittedEvents(store, attemptId).length,
|
|
2441
2621
|
committedRequirementIds: () => {
|
|
2442
2622
|
const ids = new Set();
|
|
@@ -2538,8 +2718,11 @@ export async function createFrontendContractTools(input) {
|
|
|
2538
2718
|
]);
|
|
2539
2719
|
const { readCommittedEvents, writeTypedEventStoreJsonl } = await import("../workflows/dag/frontend-typed-event-store.js");
|
|
2540
2720
|
const { adoptTypedEventFact, stageTypedEventFact } = await import("../workflows/dag/frontend-typed-event-transaction.js");
|
|
2541
|
-
|
|
2721
|
+
let store = input.store;
|
|
2542
2722
|
const attemptId = input.attemptId;
|
|
2723
|
+
let activeScope = null;
|
|
2724
|
+
const committedRequirementIds = () => new Set(readCommittedEvents(store, attemptId).filter(r => r.fact.kind === "requirement").map(r => String(r.fact.id)));
|
|
2725
|
+
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
2726
|
const receipt = (details) => ({
|
|
2544
2727
|
content: [{ type: "text", text: JSON.stringify(details) }],
|
|
2545
2728
|
details,
|
|
@@ -2589,12 +2772,24 @@ export async function createFrontendContractTools(input) {
|
|
|
2589
2772
|
label: name,
|
|
2590
2773
|
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
2774
|
promptSnippet: `Commit 1-5 origin=contract ${kind} facts (up to 5 per message).`,
|
|
2592
|
-
parameters: Type.Object({
|
|
2775
|
+
parameters: Type.Object({
|
|
2776
|
+
text: Type.String({ minLength: 1 }),
|
|
2777
|
+
requirementIds: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
2778
|
+
sourceFragmentIds: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
2779
|
+
...(kind === "constraint" ? { category: Type.Optional(Type.Enum({ constraint: "constraint", "non-goal": "non-goal", risk: "risk" })) } : {}),
|
|
2780
|
+
...(kind === "handoff-intent" ? { taskKind: Type.Literal("frontend-test"), blocking: Type.Optional(Type.Boolean()) } : {}),
|
|
2781
|
+
}, { additionalProperties: false }),
|
|
2593
2782
|
async execute(_toolCallId, params) {
|
|
2783
|
+
const data = params;
|
|
2784
|
+
if (!data.text?.trim())
|
|
2785
|
+
return receipt({ ok: false, code: "TOOL_SCHEMA_INVALID", error: `${name}: text must be non-empty` });
|
|
2786
|
+
const knownFragments = new Set([...(input.canonicalRequirements?.values() ?? [])].flatMap(r => r.sourceFragmentIds));
|
|
2787
|
+
if (data.requirementIds?.some(id => !input.canonicalRequirements?.has(id)) || data.sourceFragmentIds?.some(id => !knownFragments.has(id)))
|
|
2788
|
+
return receipt({ ok: false, code: "CONTRACT_REFERENCE_UNKNOWN", error: `${name}: reference is outside the frozen source inventory` });
|
|
2594
2789
|
const result = await adoptContractFact(kind, {
|
|
2790
|
+
...(params ?? {}),
|
|
2595
2791
|
kind,
|
|
2596
2792
|
origin: "contract",
|
|
2597
|
-
...(params ?? {}),
|
|
2598
2793
|
});
|
|
2599
2794
|
return receipt(result);
|
|
2600
2795
|
},
|
|
@@ -2608,9 +2803,9 @@ export async function createFrontendContractTools(input) {
|
|
|
2608
2803
|
const recordRequirementTool = defineTool({
|
|
2609
2804
|
name: "record_requirement",
|
|
2610
2805
|
label: "record_requirement",
|
|
2611
|
-
description: 'Confirm one canonical ledger requirement (origin=contract requirement fact). Pass
|
|
2806
|
+
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
2807
|
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 }),
|
|
2808
|
+
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
2809
|
async execute(_toolCallId, params) {
|
|
2615
2810
|
const id = typeof params?.id === "string" ? params.id.trim() : "";
|
|
2616
2811
|
if (!id) {
|
|
@@ -2620,6 +2815,8 @@ export async function createFrontendContractTools(input) {
|
|
|
2620
2815
|
error: "record_requirement requires the canonical requirement id",
|
|
2621
2816
|
});
|
|
2622
2817
|
}
|
|
2818
|
+
if (activeScope && !activeScope.has(id))
|
|
2819
|
+
return receipt({ ok: false, code: "FRONTEND_INPUT_SCOPE_VIOLATION", error: `requirement ${id} is outside the complete input scope of this session` });
|
|
2623
2820
|
const canonical = input.canonicalRequirements?.get(id);
|
|
2624
2821
|
if (!canonical) {
|
|
2625
2822
|
const known = [...(input.canonicalRequirements?.keys() ?? [])];
|
|
@@ -2629,6 +2826,14 @@ export async function createFrontendContractTools(input) {
|
|
|
2629
2826
|
error: `record_requirement id "${id}" is not a canonical ledger requirement; canonical ids are: ${known.join(", ") || "(none)"}`,
|
|
2630
2827
|
});
|
|
2631
2828
|
}
|
|
2829
|
+
if (params.execution) {
|
|
2830
|
+
try {
|
|
2831
|
+
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 }]);
|
|
2832
|
+
}
|
|
2833
|
+
catch (error) {
|
|
2834
|
+
return receipt({ ok: false, code: "EXECUTION_GROUP_CONFLICT", error: String(error) });
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2632
2837
|
const result = await adoptContractFact("requirement", {
|
|
2633
2838
|
kind: "requirement",
|
|
2634
2839
|
origin: "contract",
|
|
@@ -2636,6 +2841,7 @@ export async function createFrontendContractTools(input) {
|
|
|
2636
2841
|
id,
|
|
2637
2842
|
text: canonical.text,
|
|
2638
2843
|
sourceFragmentIds: canonical.sourceFragmentIds,
|
|
2844
|
+
...(params.execution ? { execution: params.execution } : {}),
|
|
2639
2845
|
});
|
|
2640
2846
|
return receipt(result);
|
|
2641
2847
|
},
|
|
@@ -2715,15 +2921,17 @@ export async function createFrontendContractTools(input) {
|
|
|
2715
2921
|
const recordRequiredDeliverablesTool = defineTool({
|
|
2716
2922
|
name: "record_required_deliverables",
|
|
2717
2923
|
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({
|
|
2924
|
+
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.',
|
|
2925
|
+
parameters: Type.Object({ replace: Type.Optional(Type.Boolean()), items: Type.Array(Type.Object({
|
|
2720
2926
|
path: Type.String(), requirementId: Type.String(), sourceFragmentId: Type.String(),
|
|
2721
2927
|
}, { additionalProperties: false })) }, { additionalProperties: false }),
|
|
2722
2928
|
async execute(_toolCallId, params) {
|
|
2723
2929
|
try {
|
|
2724
|
-
const declaration = validateFrontendRequiredDeliverables(params, input.canonicalRequirements ?? new Map());
|
|
2930
|
+
const declaration = validateFrontendRequiredDeliverables({ items: params.items }, input.canonicalRequirements ?? new Map());
|
|
2931
|
+
const previous = params.replace ? [] : readCommittedEvents(store, attemptId).filter(r => r.fact.kind === "required-deliverables").at(-1)?.fact.items;
|
|
2932
|
+
const items = [...new Map([...(Array.isArray(previous) ? previous : []), ...declaration.items].map(item => [JSON.stringify(item), item])).values()];
|
|
2725
2933
|
return receipt(await adoptContractFact("required-deliverables", {
|
|
2726
|
-
kind: "required-deliverables", origin: "contract",
|
|
2934
|
+
kind: "required-deliverables", origin: "contract", items,
|
|
2727
2935
|
}));
|
|
2728
2936
|
}
|
|
2729
2937
|
catch (error) {
|
|
@@ -2779,7 +2987,7 @@ export async function createFrontendContractTools(input) {
|
|
|
2779
2987
|
const finalizeContractTool = defineTool({
|
|
2780
2988
|
name: "finalize_contract",
|
|
2781
2989
|
label: "finalize_contract",
|
|
2782
|
-
description: "Commit the contract-finalized terminal fact with a disposition of ready | ready-with-assumptions | blocked (blocked requires blockingOwner).
|
|
2990
|
+
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
2991
|
promptSnippet: "Commit the contract-finalized terminal (disposition + optional blockingOwner).",
|
|
2784
2992
|
parameters: Type.Object({
|
|
2785
2993
|
disposition: Type.Enum({
|
|
@@ -2811,6 +3019,9 @@ export async function createFrontendContractTools(input) {
|
|
|
2811
3019
|
error: "call record_required_deliverables with the complete source-bound inventory (or items:[] when none) before finalizing",
|
|
2812
3020
|
});
|
|
2813
3021
|
}
|
|
3022
|
+
const missing = [...(input.canonicalRequirements?.keys() ?? [])].filter(id => !committedRequirementIds().has(id) || (activeScope !== null && !completedScopeRequirementIds().has(id)));
|
|
3023
|
+
if (disposition !== "blocked" && missing.length)
|
|
3024
|
+
return receipt({ ok: false, code: "CONTRACT_REQUIREMENT_COVERAGE_MISSING", error: `Confirm all complete source obligations before finalizing: ${missing.join(", ")}` });
|
|
2814
3025
|
const blockedOwner = mapContractBlockedOwner({
|
|
2815
3026
|
disposition: disposition ?? "",
|
|
2816
3027
|
blockingOwner,
|
|
@@ -2831,19 +3042,41 @@ export async function createFrontendContractTools(input) {
|
|
|
2831
3042
|
return receipt(result);
|
|
2832
3043
|
},
|
|
2833
3044
|
});
|
|
2834
|
-
|
|
2835
|
-
|
|
3045
|
+
const completeScopeTool = defineTool({
|
|
3046
|
+
name: "complete_contract_scope", label: "complete_contract_scope",
|
|
3047
|
+
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.",
|
|
3048
|
+
parameters: Type.Object({ requirementIds: Type.Array(Type.String({ minLength: 1 }), { uniqueItems: true }) }, { additionalProperties: false }),
|
|
3049
|
+
async execute(_id, params) {
|
|
3050
|
+
const ids = params.requirementIds;
|
|
3051
|
+
if (activeScope === null || ids.length !== activeScope.size || ids.some(id => !activeScope?.has(id) || !committedRequirementIds().has(id)))
|
|
3052
|
+
return receipt({ ok: false, code: "CONTRACT_SCOPE_INCOMPLETE", error: "Complete exactly the active scope after confirming all its obligations" });
|
|
3053
|
+
return receipt(await adoptContractFact("contract-scope-completed", { kind: "contract-scope-completed", origin: "contract", requirementIds: ids }));
|
|
3054
|
+
},
|
|
3055
|
+
});
|
|
3056
|
+
const durable = await createDurableFrontendTools({
|
|
3057
|
+
file: path.join(input.runDir, input.nodeId, "contract-typed-facts.jsonl"), attemptId, store: input.store,
|
|
3058
|
+
setWorkingStore: next => { store = next; }, binding: { canonicalRequirements: input.canonicalRequirements, sourceDigest: input.sourceDigest },
|
|
3059
|
+
tools: [
|
|
2836
3060
|
...recordTools,
|
|
2837
3061
|
recordRequirementTool,
|
|
2838
3062
|
recordEvidenceExpectationTool,
|
|
2839
3063
|
recordUiStateTool,
|
|
2840
3064
|
recordRequiredDeliverablesTool,
|
|
2841
3065
|
recordOpenspecSelectionTool,
|
|
3066
|
+
completeScopeTool,
|
|
2842
3067
|
finalizeContractTool,
|
|
2843
3068
|
],
|
|
3069
|
+
});
|
|
3070
|
+
return {
|
|
3071
|
+
customTools: durable.customTools,
|
|
3072
|
+
inputRequirements: () => [...(input.canonicalRequirements ?? [])].map(([id, value]) => ({ id, text: value.text, sourceFragmentIds: [...value.sourceFragmentIds] })),
|
|
3073
|
+
completedScopeRequirementIds,
|
|
3074
|
+
setActiveRequirementScope: ids => { activeScope = ids === null ? null : new Set(ids); },
|
|
3075
|
+
committedRequirementIds,
|
|
3076
|
+
committedFacts: () => readCommittedEvents(input.store, attemptId),
|
|
2844
3077
|
flush: async () => {
|
|
2845
|
-
|
|
2846
|
-
|
|
3078
|
+
await durable.flush();
|
|
3079
|
+
const committed = readCommittedEvents(input.store, attemptId);
|
|
2847
3080
|
await writeJsonAtomic(path.join(input.runDir, input.nodeId, "frontend-task-contract-vNext.json"), buildFrontendTaskContractVNext(committed));
|
|
2848
3081
|
},
|
|
2849
3082
|
};
|
|
@@ -2895,7 +3128,7 @@ export async function createFrontendScoutEvidenceTools(input) {
|
|
|
2895
3128
|
]);
|
|
2896
3129
|
const { readCommittedEvents, writeTypedEventStoreJsonl } = await import("../workflows/dag/frontend-typed-event-store.js");
|
|
2897
3130
|
const { adoptTypedEventFact, stageTypedEventFact } = await import("../workflows/dag/frontend-typed-event-transaction.js");
|
|
2898
|
-
|
|
3131
|
+
let store = input.store;
|
|
2899
3132
|
const attemptId = input.attemptId;
|
|
2900
3133
|
const stringArray = Type.Array(Type.String({}));
|
|
2901
3134
|
const optionalString = Type.Optional(Type.String({}));
|
|
@@ -2909,6 +3142,18 @@ export async function createFrontendScoutEvidenceTools(input) {
|
|
|
2909
3142
|
});
|
|
2910
3143
|
const sourceDeclaredPaths = (input.sourceDeclaredPaths ?? []).map((value) => value.replaceAll("\\", "/").replace(/^\.\//, "").replace(/\/$/, ""));
|
|
2911
3144
|
const hasSourceDeclarations = input.sourceDeclaredPaths !== undefined;
|
|
3145
|
+
let activeScope;
|
|
3146
|
+
const scopeIdentity = (ids) => createHash("sha256").update(JSON.stringify([...ids].sort())).digest("hex");
|
|
3147
|
+
const latestScopes = () => {
|
|
3148
|
+
const byRequirement = new Map();
|
|
3149
|
+
for (const record of readCommittedEvents(store, attemptId))
|
|
3150
|
+
if (record.fact.kind === "scout-scope" && Array.isArray(record.fact.requirementIds)) {
|
|
3151
|
+
for (const id of record.fact.requirementIds)
|
|
3152
|
+
if (typeof id === "string")
|
|
3153
|
+
byRequirement.set(id, record.fact);
|
|
3154
|
+
}
|
|
3155
|
+
return byRequirement;
|
|
3156
|
+
};
|
|
2912
3157
|
const isSourceDeclared = (candidate) => {
|
|
2913
3158
|
const normalized = candidate.replaceAll("\\", "/").replace(/^\.\//, "").replace(/\/$/, "");
|
|
2914
3159
|
return sourceDeclaredPaths.some((declared) => declared === normalized || declared.startsWith(`${normalized}/`));
|
|
@@ -3001,6 +3246,7 @@ export async function createFrontendScoutEvidenceTools(input) {
|
|
|
3001
3246
|
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
3247
|
promptSnippet: "Commit an origin=scout target-surface fact.",
|
|
3003
3248
|
parameters: Type.Object({
|
|
3249
|
+
scopeId: Type.Optional(Type.String({ minLength: 1 })),
|
|
3004
3250
|
completeness: scoutCompleteness,
|
|
3005
3251
|
entrypoint: optionalString,
|
|
3006
3252
|
routeOrMount: optionalString,
|
|
@@ -3011,6 +3257,8 @@ export async function createFrontendScoutEvidenceTools(input) {
|
|
|
3011
3257
|
unresolvedPaths: stringArray,
|
|
3012
3258
|
}, { additionalProperties: false }),
|
|
3013
3259
|
async execute(_toolCallId, params) {
|
|
3260
|
+
if (input.requirementIds && (!activeScope?.length || params.scopeId !== scopeIdentity(activeScope)))
|
|
3261
|
+
return receipt({ ok: false, code: "FRONTEND_INPUT_SCOPE_VIOLATION", error: "Use exactly the runtime Scout scopeId; discovery may complete only the supplied obligations" });
|
|
3014
3262
|
const implementationPaths = params?.implementationPaths ?? [];
|
|
3015
3263
|
const testPaths = params?.testPaths ?? [];
|
|
3016
3264
|
const pathEvidence = await enrichScoutPathEvidence([
|
|
@@ -3018,7 +3266,7 @@ export async function createFrontendScoutEvidenceTools(input) {
|
|
|
3018
3266
|
...implementationPaths,
|
|
3019
3267
|
...testPaths,
|
|
3020
3268
|
]);
|
|
3021
|
-
const
|
|
3269
|
+
const surface = {
|
|
3022
3270
|
kind: "target-surface",
|
|
3023
3271
|
origin: "scout",
|
|
3024
3272
|
completeness: params?.completeness ?? "blocked",
|
|
@@ -3031,7 +3279,27 @@ export async function createFrontendScoutEvidenceTools(input) {
|
|
|
3031
3279
|
unresolvedPaths: params?.unresolvedPaths ?? [],
|
|
3032
3280
|
...(hasSourceDeclarations ? { sourceDeclaredPaths } : {}),
|
|
3033
3281
|
...(pathEvidence.length > 0 ? { pathEvidence } : {}),
|
|
3034
|
-
}
|
|
3282
|
+
};
|
|
3283
|
+
if (activeScope) {
|
|
3284
|
+
const { readCompleteScoutTargetSurface } = await import("../workflows/dag/frontend-shadow-dual-write.js");
|
|
3285
|
+
if (surface.completeness === "complete") {
|
|
3286
|
+
const check = readCompleteScoutTargetSurface([{ phase: "committed", fact: surface }]);
|
|
3287
|
+
if (!check.ok)
|
|
3288
|
+
return receipt({ ok: false, code: "SCOUT_SCOPE_INCOMPLETE", error: check.reason });
|
|
3289
|
+
}
|
|
3290
|
+
const saved = await adoptScoutFact("scout-scope", { kind: "scout-scope", origin: "scout", id: params.scopeId, requirementIds: activeScope, surface });
|
|
3291
|
+
if (!saved.ok)
|
|
3292
|
+
return receipt(saved);
|
|
3293
|
+
const current = latestScopes();
|
|
3294
|
+
if (input.requirementIds?.every(id => current.get(id)?.surface?.completeness === "complete")) {
|
|
3295
|
+
const surfaces = [...new Set(input.requirementIds.map(id => current.get(id)))].map(f => f.surface);
|
|
3296
|
+
const union = (key) => [...new Set(surfaces.flatMap(s => Array.isArray(s[key]) ? s[key] : []))];
|
|
3297
|
+
const entries = [...new Set(surfaces.map(s => String(s.entrypoint ?? "")).filter(Boolean))];
|
|
3298
|
+
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 } : {}) }));
|
|
3299
|
+
}
|
|
3300
|
+
return receipt(saved);
|
|
3301
|
+
}
|
|
3302
|
+
const result = await adoptScoutFact("target-surface", surface);
|
|
3035
3303
|
return receipt(result);
|
|
3036
3304
|
},
|
|
3037
3305
|
});
|
|
@@ -3055,10 +3323,30 @@ export async function createFrontendScoutEvidenceTools(input) {
|
|
|
3055
3323
|
return receipt(result);
|
|
3056
3324
|
},
|
|
3057
3325
|
});
|
|
3326
|
+
const durable = await createDurableFrontendTools({
|
|
3327
|
+
file: path.join(input.runDir, input.nodeId, "scout-typed-facts.jsonl"), attemptId, store: input.store,
|
|
3328
|
+
setWorkingStore: next => { store = next; }, binding: { requirementIds: input.requirementIds, sourceDeclaredPaths: input.sourceDeclaredPaths, sourceDigest: input.sourceDigest, workspaceRoot: input.workspaceRoot },
|
|
3329
|
+
tools: [recordTargetSurfaceTool, recordDesignEvidenceTool],
|
|
3330
|
+
validateRestored: async (records) => {
|
|
3331
|
+
if (!input.workspaceRoot)
|
|
3332
|
+
return;
|
|
3333
|
+
for (const record of records) {
|
|
3334
|
+
const evidence = record.fact.kind === "scout-scope" ? record.fact.surface?.pathEvidence : record.fact.pathEvidence;
|
|
3335
|
+
if (!Array.isArray(evidence))
|
|
3336
|
+
continue;
|
|
3337
|
+
for (const previous of evidence) {
|
|
3338
|
+
if (!isRecordObject(previous) || typeof previous.path !== "string")
|
|
3339
|
+
throw Error("scout path evidence is malformed");
|
|
3340
|
+
const current = (await enrichScoutPathEvidence([previous.path]))[0];
|
|
3341
|
+
if (!current || current.sha256 !== previous.sha256 || current.fresh !== previous.fresh)
|
|
3342
|
+
throw Error(`scout evidence drift: ${previous.path}; refresh Scout before reusing facts`);
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3345
|
+
},
|
|
3346
|
+
});
|
|
3058
3347
|
return {
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
adoptCommittedFacts: async (records) => {
|
|
3348
|
+
...durable,
|
|
3349
|
+
adoptCommittedFacts: async (records) => durable.commitExternal(async () => {
|
|
3062
3350
|
for (const record of records) {
|
|
3063
3351
|
if (record.phase !== "committed")
|
|
3064
3352
|
continue;
|
|
@@ -3072,11 +3360,16 @@ export async function createFrontendScoutEvidenceTools(input) {
|
|
|
3072
3360
|
throw new Error(`frontend scout shard fact merge failed: ${result.error}`);
|
|
3073
3361
|
}
|
|
3074
3362
|
}
|
|
3363
|
+
}),
|
|
3364
|
+
setActiveScope: ids => {
|
|
3365
|
+
if (!ids.length || ids.some(id => !input.requirementIds?.includes(id)))
|
|
3366
|
+
throw Error("FRONTEND_INPUT_SCOPE_VIOLATION");
|
|
3367
|
+
activeScope = [...new Set(ids)];
|
|
3368
|
+
return scopeIdentity(activeScope);
|
|
3075
3369
|
},
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
},
|
|
3370
|
+
completedRequirementIds: () => new Set([...latestScopes()].filter(([, fact]) => fact.surface?.completeness === "complete").map(([id]) => id)),
|
|
3371
|
+
completedScopeFacts: () => [...new Set(latestScopes().values())].filter(f => f.surface?.completeness === "complete"),
|
|
3372
|
+
committedFacts: () => readCommittedEvents(store, attemptId),
|
|
3080
3373
|
};
|
|
3081
3374
|
}
|
|
3082
3375
|
export function buildDagPiUserMessage(task, persona, step) {
|
|
@@ -3327,15 +3620,14 @@ async function runFrontendReviewTerminalShadow(input) {
|
|
|
3327
3620
|
return input.mapped;
|
|
3328
3621
|
const { compareTypedReviewToLegacyJsonVerdict } = await import("../workflows/dag/frontend-review-context.js");
|
|
3329
3622
|
const { parseJsonReviewVerdict } = await import("../workflows/dag/output-protocol.js");
|
|
3330
|
-
const sessionEventsPath = path.join(input.meta.runDir, input.task.id, "session-events.jsonl");
|
|
3331
3623
|
let typedKinds = [];
|
|
3332
3624
|
try {
|
|
3333
|
-
|
|
3334
|
-
|
|
3625
|
+
await input.tools?.flush();
|
|
3626
|
+
input.tools?.scopeProtocol?.assertComplete();
|
|
3627
|
+
typedKinds = input.tools?.scopeProtocol?.committedFacts().filter(r => ["approve_review", "request_review_changes"].includes(String(r.fact.kind))).map(r => String(r.fact.kind)) ?? [];
|
|
3335
3628
|
}
|
|
3336
|
-
catch {
|
|
3337
|
-
|
|
3338
|
-
typedKinds = [];
|
|
3629
|
+
catch (error) {
|
|
3630
|
+
return { ...input.mapped, ok: false, failureCategory: "frontend-ledger-invalid", stderr: `FRONTEND_REVIEW_SCOPE_INCOMPLETE: ${error instanceof Error ? error.message : String(error)}` };
|
|
3339
3631
|
}
|
|
3340
3632
|
let legacyVerdict;
|
|
3341
3633
|
try {
|
|
@@ -3361,13 +3653,7 @@ async function runFrontendReviewTerminalShadow(input) {
|
|
|
3361
3653
|
reason: `typed review equivalence comparison crashed: ${error instanceof Error ? error.message : String(error)}`,
|
|
3362
3654
|
};
|
|
3363
3655
|
}
|
|
3364
|
-
//
|
|
3365
|
-
try {
|
|
3366
|
-
await input.tools?.flush?.();
|
|
3367
|
-
}
|
|
3368
|
-
catch {
|
|
3369
|
-
// best-effort
|
|
3370
|
-
}
|
|
3656
|
+
// The durable ledger was validated above; this artifact is audit-only.
|
|
3371
3657
|
try {
|
|
3372
3658
|
await writeDagNodeJsonArtifact(input.meta.runDir, input.task.id, "fact-review-status.json", {
|
|
3373
3659
|
schemaVersion: 1,
|
|
@@ -3409,23 +3695,16 @@ async function runFrontendDesignTerminalShadow(input) {
|
|
|
3409
3695
|
// diagnosed as an omitted terminal tool call.
|
|
3410
3696
|
if (!input.mapped.ok)
|
|
3411
3697
|
return input.mapped;
|
|
3412
|
-
const sessionEventsPath = path.join(input.meta.runDir, input.task.id, "session-events.jsonl");
|
|
3413
3698
|
let typedKinds = [];
|
|
3414
3699
|
try {
|
|
3415
|
-
|
|
3416
|
-
|
|
3700
|
+
await input.tools?.flush();
|
|
3701
|
+
input.tools?.scopeProtocol?.assertComplete();
|
|
3702
|
+
typedKinds = input.tools?.scopeProtocol?.committedFacts().filter(r => ["approve_design", "request_design_changes"].includes(String(r.fact.kind))).map(r => String(r.fact.kind)) ?? [];
|
|
3417
3703
|
}
|
|
3418
|
-
catch {
|
|
3419
|
-
|
|
3420
|
-
typedKinds = [];
|
|
3421
|
-
}
|
|
3422
|
-
// Audit-only flush + artifact. Neither blocks the node.
|
|
3423
|
-
try {
|
|
3424
|
-
await input.tools?.flush?.();
|
|
3425
|
-
}
|
|
3426
|
-
catch {
|
|
3427
|
-
// best-effort
|
|
3704
|
+
catch (error) {
|
|
3705
|
+
return { ...input.mapped, ok: false, failureCategory: "frontend-ledger-invalid", stderr: `FRONTEND_REVIEW_SCOPE_INCOMPLETE: ${error instanceof Error ? error.message : String(error)}` };
|
|
3428
3706
|
}
|
|
3707
|
+
// The durable ledger was validated above; this artifact is audit-only.
|
|
3429
3708
|
try {
|
|
3430
3709
|
await writeDagNodeJsonArtifact(input.meta.runDir, input.task.id, "fact-design-status.json", {
|
|
3431
3710
|
schemaVersion: 1,
|
|
@@ -3458,6 +3737,7 @@ const FRONTEND_PLAN_SEGMENTS = [
|
|
|
3458
3737
|
id: "coverage",
|
|
3459
3738
|
toolNames: new Set([
|
|
3460
3739
|
"record_plan_requirement",
|
|
3740
|
+
"record_plan_group_coverage",
|
|
3461
3741
|
"record_plan_verification_target",
|
|
3462
3742
|
"record_plan_evidence_gap",
|
|
3463
3743
|
"adopt_staged_fact",
|
|
@@ -3474,13 +3754,14 @@ const FRONTEND_PLAN_SEGMENTS = [
|
|
|
3474
3754
|
toolNames: new Set(["record_state_registry", "adopt_staged_fact"]),
|
|
3475
3755
|
instruction: [
|
|
3476
3756
|
"PLAN PHASE — global UX vocabulary.",
|
|
3477
|
-
"
|
|
3757
|
+
"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
3758
|
"Do not call finalize_plan; it is not available in this phase.",
|
|
3479
3759
|
].join(" "),
|
|
3480
3760
|
},
|
|
3481
3761
|
{
|
|
3482
3762
|
id: "ux-local",
|
|
3483
3763
|
toolNames: new Set([
|
|
3764
|
+
"record_state_registry",
|
|
3484
3765
|
"record_component_choice",
|
|
3485
3766
|
"record_state_flow",
|
|
3486
3767
|
"record_plan_verification_target",
|
|
@@ -3488,7 +3769,7 @@ const FRONTEND_PLAN_SEGMENTS = [
|
|
|
3488
3769
|
]),
|
|
3489
3770
|
instruction: [
|
|
3490
3771
|
"PLAN PHASE — global UX decisions.",
|
|
3491
|
-
"Requirements and verification targets are already committed in the ledger. Review the complete
|
|
3772
|
+
"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
3773
|
"Do not call finalize_plan; it is not available in this phase.",
|
|
3493
3774
|
].join(" "),
|
|
3494
3775
|
},
|
|
@@ -3502,7 +3783,7 @@ const FRONTEND_PLAN_SEGMENTS = [
|
|
|
3502
3783
|
},
|
|
3503
3784
|
{
|
|
3504
3785
|
id: "global-mock-data",
|
|
3505
|
-
toolNames: new Set(["record_data_flow", "record_mock_api", "adopt_staged_fact"]),
|
|
3786
|
+
toolNames: new Set(["record_data_flow", "record_mock_api", "record_mock_endpoint", "adopt_staged_fact"]),
|
|
3506
3787
|
instruction: [
|
|
3507
3788
|
"PLAN PHASE — global Mock/API and data policy.",
|
|
3508
3789
|
"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.",
|
|
@@ -3525,173 +3806,6 @@ const FRONTEND_PLAN_SEGMENTS = [
|
|
|
3525
3806
|
].join(" "),
|
|
3526
3807
|
},
|
|
3527
3808
|
];
|
|
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
3809
|
/** Estimate calls conservatively: requirement + one VT, with a second VT
|
|
3696
3810
|
* reserved for behaviour-required requirements. Explicit declarations win. */
|
|
3697
3811
|
export function estimateFrontendPlanRequirementRecordCalls(fact) {
|
|
@@ -3733,42 +3847,47 @@ export function batchFrontendPlanRequirements(input) {
|
|
|
3733
3847
|
}
|
|
3734
3848
|
const FRONTEND_PLAN_COVERAGE_MAX_RECORD_CALLS = 10;
|
|
3735
3849
|
const FRONTEND_PLAN_COVERAGE_MAX_CONCURRENCY = 4;
|
|
3736
|
-
// A
|
|
3737
|
-
//
|
|
3738
|
-
// reinvented per AC batch. Keep a safety bound for adaptive coverage retries
|
|
3850
|
+
// A single registry owns global names; large UX work uses bounded serial
|
|
3851
|
+
// scopes against that registry. Keep a safety bound for adaptive retries
|
|
3739
3852
|
// without letting the old 32-session ceiling skip finalize.
|
|
3740
3853
|
const FRONTEND_PLAN_BATCH_MAX_SESSIONS = 128;
|
|
3741
3854
|
const FRONTEND_PLAN_SMALL_MAX_REQUIREMENTS = 8;
|
|
3742
3855
|
const FRONTEND_PLAN_SMALL_MAX_ESTIMATED_CALLS = 24;
|
|
3743
|
-
async function mapWithConcurrency(items, limit, worker) {
|
|
3856
|
+
async function mapWithConcurrency(items, limit, worker, shouldReduceConcurrency) {
|
|
3744
3857
|
const results = new Array(items.length);
|
|
3745
3858
|
let nextIndex = 0;
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3859
|
+
let concurrency = Math.max(1, limit);
|
|
3860
|
+
const running = new Map();
|
|
3861
|
+
try {
|
|
3862
|
+
while (nextIndex < items.length || running.size > 0) {
|
|
3863
|
+
while (nextIndex < items.length && running.size < concurrency) {
|
|
3864
|
+
const index = nextIndex++;
|
|
3865
|
+
running.set(index, worker(items[index], index).then(result => ({ index, result })));
|
|
3866
|
+
}
|
|
3867
|
+
const { index, result } = await Promise.race(running.values());
|
|
3868
|
+
running.delete(index);
|
|
3869
|
+
results[index] = result;
|
|
3870
|
+
// Drain existing work; only pending shards use the reduced cap.
|
|
3871
|
+
// Failed shards remain failed and receive no extra retry allowance.
|
|
3872
|
+
if (shouldReduceConcurrency(result))
|
|
3873
|
+
concurrency = Math.max(1, Math.floor(concurrency / 2));
|
|
3753
3874
|
}
|
|
3754
|
-
}
|
|
3875
|
+
}
|
|
3876
|
+
catch (error) {
|
|
3877
|
+
await Promise.allSettled(running.values());
|
|
3878
|
+
throw error;
|
|
3879
|
+
}
|
|
3755
3880
|
return results;
|
|
3756
3881
|
}
|
|
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)}…`;
|
|
3882
|
+
function compactPromptString(value, _maxChars) {
|
|
3883
|
+
return typeof value === "string" && value.trim().length ? value.trim() : undefined;
|
|
3764
3884
|
}
|
|
3765
|
-
function compactPromptStringArray(value,
|
|
3885
|
+
function compactPromptStringArray(value, _maxEntries = 12, maxChars = 180) {
|
|
3766
3886
|
if (!Array.isArray(value))
|
|
3767
3887
|
return [];
|
|
3768
3888
|
return value
|
|
3769
3889
|
.map((item) => compactPromptString(item, maxChars))
|
|
3770
|
-
.filter((item) => item !== undefined)
|
|
3771
|
-
.slice(0, maxEntries);
|
|
3890
|
+
.filter((item) => item !== undefined);
|
|
3772
3891
|
}
|
|
3773
3892
|
function countFrontendPlanTargetSurfaces(basePrompt) {
|
|
3774
3893
|
const match = /<frontend_plan_input>[\s\S]*?<\/frontend_plan_input>/.exec(basePrompt);
|
|
@@ -3841,7 +3960,7 @@ export function compactFrontendPlanPromptForRequirementSlice(basePrompt, require
|
|
|
3841
3960
|
}
|
|
3842
3961
|
}
|
|
3843
3962
|
if (!payload || !Array.isArray(payload.requirements))
|
|
3844
|
-
|
|
3963
|
+
throw Error("FRONTEND_INPUT_INVALID: plan inventory is not parseable");
|
|
3845
3964
|
const requirementsById = new Map();
|
|
3846
3965
|
for (const value of payload.requirements) {
|
|
3847
3966
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
@@ -3853,7 +3972,7 @@ export function compactFrontendPlanPromptForRequirementSlice(basePrompt, require
|
|
|
3853
3972
|
}
|
|
3854
3973
|
const requirements = slice.map((id) => requirementsById.get(id));
|
|
3855
3974
|
if (requirements.some((requirement) => requirement === undefined)) {
|
|
3856
|
-
|
|
3975
|
+
throw Error(`FRONTEND_INPUT_SCOPE_MISSING: ${slice.filter(id => !requirementsById.has(id)).join(", ")}`);
|
|
3857
3976
|
}
|
|
3858
3977
|
const compactRequirements = requirements.map((requirement) => ({
|
|
3859
3978
|
id: compactPromptString(requirement.id, 80),
|
|
@@ -3870,26 +3989,28 @@ export function compactFrontendPlanPromptForRequirementSlice(basePrompt, require
|
|
|
3870
3989
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
3871
3990
|
return [];
|
|
3872
3991
|
const target = value;
|
|
3873
|
-
|
|
3992
|
+
if (options.verificationTargetIds && !options.verificationTargetIds.includes(String(target.id)))
|
|
3993
|
+
return [];
|
|
3994
|
+
const targetRequirementIds = planFactStringList(target.requirementIds);
|
|
3874
3995
|
const relatedRequirementIds = targetRequirementIds.filter((id) => sliceSet.has(id));
|
|
3875
3996
|
if (relatedRequirementIds.length === 0)
|
|
3876
3997
|
return [];
|
|
3877
3998
|
return [
|
|
3878
3999
|
{
|
|
3879
|
-
...(
|
|
3880
|
-
? { id:
|
|
4000
|
+
...(typeof target.id === "string"
|
|
4001
|
+
? { id: target.id }
|
|
3881
4002
|
: {}),
|
|
3882
|
-
...(
|
|
3883
|
-
? { commandId:
|
|
4003
|
+
...(typeof target.commandId === "string"
|
|
4004
|
+
? { commandId: target.commandId }
|
|
3884
4005
|
: {}),
|
|
3885
4006
|
...(compactPromptString(target.commandLabel, 180)
|
|
3886
4007
|
? { commandLabel: compactPromptString(target.commandLabel, 180) }
|
|
3887
4008
|
: {}),
|
|
3888
|
-
...(
|
|
3889
|
-
? { file:
|
|
4009
|
+
...(typeof target.file === "string"
|
|
4010
|
+
? { file: target.file }
|
|
3890
4011
|
: {}),
|
|
3891
4012
|
requirementIds: relatedRequirementIds,
|
|
3892
|
-
uiStates:
|
|
4013
|
+
uiStates: planFactStringList(target.uiStates),
|
|
3893
4014
|
},
|
|
3894
4015
|
];
|
|
3895
4016
|
})
|
|
@@ -3941,7 +4062,7 @@ export function compactFrontendPlanPromptForRequirementSlice(basePrompt, require
|
|
|
3941
4062
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
3942
4063
|
return [];
|
|
3943
4064
|
const state = value;
|
|
3944
|
-
const id =
|
|
4065
|
+
const id = typeof state.id === "string" ? state.id : undefined;
|
|
3945
4066
|
if (!id)
|
|
3946
4067
|
return [];
|
|
3947
4068
|
return [{
|
|
@@ -3961,6 +4082,9 @@ export function compactFrontendPlanPromptForRequirementSlice(basePrompt, require
|
|
|
3961
4082
|
? payload.committedUx
|
|
3962
4083
|
: undefined;
|
|
3963
4084
|
const compactPayload = {
|
|
4085
|
+
inputManifest: { ...projectFrontendInputScope({ ...payload, requirements: [...requirementsById.values()] }, slice).inputManifest, semantics: options.includeRequirementText === false ? "navigation-only" : "full" },
|
|
4086
|
+
constraints: payload.constraints,
|
|
4087
|
+
executionGroups: Array.isArray(payload.executionGroups) ? payload.executionGroups.filter(g => isRecordObject(g) && Array.isArray(g.requirementIds) && g.requirementIds.some(id => slice.includes(String(id)))) : [],
|
|
3964
4088
|
requirements: compactRequirements,
|
|
3965
4089
|
requiredDeliverables: Array.isArray(payload.requiredDeliverables) ? payload.requiredDeliverables : [],
|
|
3966
4090
|
...(compactTargetSurface.length > 0
|
|
@@ -4038,9 +4162,9 @@ function compactFrontendPlanLedgerContext(input) {
|
|
|
4038
4162
|
compactFacts.push({
|
|
4039
4163
|
kind: fact.kind,
|
|
4040
4164
|
entry: {
|
|
4041
|
-
id:
|
|
4042
|
-
implementationTargets:
|
|
4043
|
-
verificationTargetIds:
|
|
4165
|
+
id: entry.id,
|
|
4166
|
+
implementationTargets: planFactStringList(entry.implementationTargets).slice(0, 12),
|
|
4167
|
+
verificationTargetIds: planFactStringList(entry.verificationTargetIds).slice(0, 12),
|
|
4044
4168
|
...(compactPromptString(entry.expectedOutcome, 240)
|
|
4045
4169
|
? { expectedOutcome: compactPromptString(entry.expectedOutcome, 240) }
|
|
4046
4170
|
: {}),
|
|
@@ -4054,17 +4178,19 @@ function compactFrontendPlanLedgerContext(input) {
|
|
|
4054
4178
|
if (fact.kind === "plan-verification-target") {
|
|
4055
4179
|
if (!entry)
|
|
4056
4180
|
continue;
|
|
4057
|
-
|
|
4058
|
-
|
|
4181
|
+
// Scope against the complete canonical binding before compacting.
|
|
4182
|
+
// A shared target may bind a late requirement beyond the preview cap.
|
|
4183
|
+
const requirementIds = planFactStringList(entry.requirementIds).filter((id) => slice.has(id));
|
|
4184
|
+
if (requirementIds.length === 0)
|
|
4059
4185
|
continue;
|
|
4060
4186
|
compactFacts.push({
|
|
4061
4187
|
kind: fact.kind,
|
|
4062
4188
|
entry: {
|
|
4063
|
-
id:
|
|
4064
|
-
commandId:
|
|
4065
|
-
file:
|
|
4189
|
+
id: entry.id,
|
|
4190
|
+
commandId: entry.commandId,
|
|
4191
|
+
file: entry.file,
|
|
4066
4192
|
requirementIds,
|
|
4067
|
-
uiStates:
|
|
4193
|
+
uiStates: planFactStringList(entry.uiStates).slice(0, 12),
|
|
4068
4194
|
},
|
|
4069
4195
|
});
|
|
4070
4196
|
continue;
|
|
@@ -4082,7 +4208,7 @@ function compactFrontendPlanLedgerContext(input) {
|
|
|
4082
4208
|
covers: compactPromptStringArray(item.covers, 40, 120),
|
|
4083
4209
|
evidencePath: compactPromptString(item.evidencePath, 180),
|
|
4084
4210
|
}];
|
|
4085
|
-
})
|
|
4211
|
+
})
|
|
4086
4212
|
: [];
|
|
4087
4213
|
if (choices.length > 0)
|
|
4088
4214
|
compactFacts.push({ kind: fact.kind, uiComponentChoices: choices });
|
|
@@ -4091,16 +4217,16 @@ function compactFrontendPlanLedgerContext(input) {
|
|
|
4091
4217
|
if (fact.kind === "state-registry") {
|
|
4092
4218
|
compactFacts.push({
|
|
4093
4219
|
kind: fact.kind,
|
|
4094
|
-
uiStateNames:
|
|
4095
|
-
interactionNames:
|
|
4220
|
+
uiStateNames: planFactStringList(fact.uiStateNames).slice(0, 40),
|
|
4221
|
+
interactionNames: planFactStringList(fact.interactionNames).slice(0, 60),
|
|
4096
4222
|
});
|
|
4097
4223
|
continue;
|
|
4098
4224
|
}
|
|
4099
4225
|
if (fact.kind === "state-flow") {
|
|
4100
4226
|
compactFacts.push({
|
|
4101
4227
|
kind: fact.kind,
|
|
4102
|
-
uiStates: Array.isArray(fact.uiStates) ? fact.uiStates
|
|
4103
|
-
interactions: Array.isArray(fact.interactions) ? fact.interactions
|
|
4228
|
+
uiStates: Array.isArray(fact.uiStates) ? fact.uiStates : [],
|
|
4229
|
+
interactions: Array.isArray(fact.interactions) ? fact.interactions : [],
|
|
4104
4230
|
removeUiStateNames: compactPromptStringArray(fact.removeUiStateNames, 24, 100),
|
|
4105
4231
|
removeInteractionNames: compactPromptStringArray(fact.removeInteractionNames, 24, 100),
|
|
4106
4232
|
});
|
|
@@ -4123,7 +4249,7 @@ function compactFrontendPlanLedgerContext(input) {
|
|
|
4123
4249
|
mockApi: {
|
|
4124
4250
|
strategy: compactPromptString(mockApi.strategy, 40),
|
|
4125
4251
|
activation: compactPromptString(mockApi.activation, 180),
|
|
4126
|
-
endpoints: Array.isArray(mockApi.endpoints) ? mockApi.endpoints
|
|
4252
|
+
endpoints: Array.isArray(mockApi.endpoints) ? mockApi.endpoints : [],
|
|
4127
4253
|
},
|
|
4128
4254
|
});
|
|
4129
4255
|
continue;
|
|
@@ -4144,17 +4270,332 @@ function compactFrontendPlanLedgerContext(input) {
|
|
|
4144
4270
|
return "";
|
|
4145
4271
|
const priorityFacts = compactFacts.filter((fact) => fact.kind === "plan-requirement" || fact.kind === "plan-verification-target");
|
|
4146
4272
|
const otherFacts = compactFacts.filter((fact) => fact.kind !== "plan-requirement" && fact.kind !== "plan-verification-target");
|
|
4273
|
+
let previewBytes = 0;
|
|
4147
4274
|
const boundedFacts = [
|
|
4148
4275
|
...priorityFacts.slice(0, 64),
|
|
4149
4276
|
...otherFacts.slice(-32),
|
|
4150
|
-
].slice(0, 96)
|
|
4277
|
+
].slice(0, 96).filter(fact => {
|
|
4278
|
+
const bytes = Buffer.byteLength(JSON.stringify(fact));
|
|
4279
|
+
if (previewBytes + bytes > 24_000)
|
|
4280
|
+
return false;
|
|
4281
|
+
previewBytes += bytes;
|
|
4282
|
+
return true;
|
|
4283
|
+
});
|
|
4151
4284
|
return [
|
|
4152
4285
|
"<frontend_plan_ledger>",
|
|
4153
|
-
"
|
|
4286
|
+
"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.",
|
|
4287
|
+
JSON.stringify({ complete: false, matchingFacts: compactFacts.length, omittedFacts: compactFacts.length - boundedFacts.length }),
|
|
4154
4288
|
JSON.stringify(boundedFacts),
|
|
4155
4289
|
"</frontend_plan_ledger>",
|
|
4156
4290
|
].join("\n");
|
|
4157
4291
|
}
|
|
4292
|
+
export async function runFrontendReviewSegmentedSessions(input) {
|
|
4293
|
+
const protocol = input.tools.scopeProtocol;
|
|
4294
|
+
const terminalKinds = input.phase === "review" ? ["approve_review", "request_review_changes"] : ["approve_design", "request_design_changes"];
|
|
4295
|
+
const terminal = () => protocol.committedFacts().some(r => terminalKinds.includes(String(r.fact.kind)));
|
|
4296
|
+
const targetBytes = input.sessionOptions.frontendExecutionPolicy?.scopeTargetBytes ?? FRONTEND_SCOPE_TARGET_BYTES;
|
|
4297
|
+
const queue = packFrontendInputUnits(input.inventory.scopes.filter(s => !protocol.completedScopeIds().has(s.id)), { targetBytes, maxUnits: input.sessionOptions.frontendExecutionPolicy?.maxScopeUnits }).map(scopes => ({ scopes, repairs: 0 }));
|
|
4298
|
+
if (!queue.length)
|
|
4299
|
+
queue.push({ scopes: [], repairs: 0 });
|
|
4300
|
+
let calls = 0;
|
|
4301
|
+
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 };
|
|
4302
|
+
for (let index = 0; index < queue.length; index++) {
|
|
4303
|
+
await input.tools.flush();
|
|
4304
|
+
await input.inventory.validate();
|
|
4305
|
+
if (terminal()) {
|
|
4306
|
+
protocol.assertComplete();
|
|
4307
|
+
return last;
|
|
4308
|
+
}
|
|
4309
|
+
const item = queue[index];
|
|
4310
|
+
const scopes = item.scopes.filter(s => !protocol.completedScopeIds().has(s.id));
|
|
4311
|
+
protocol.setActiveScope(scopes.map(s => s.id));
|
|
4312
|
+
const finalScope = input.inventory.scopes.every(s => protocol.completedScopeIds().has(s.id) || scopes.some(current => current.id === s.id));
|
|
4313
|
+
const customTools = finalScope ? input.customTools : input.customTools.filter(t => !terminalKinds.includes(String(t.name)));
|
|
4314
|
+
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."}`;
|
|
4315
|
+
if (Buffer.byteLength(prompt) > targetBytes && scopes.length > 1) {
|
|
4316
|
+
const at = Math.ceil(scopes.length / 2);
|
|
4317
|
+
queue.splice(index, 1, { scopes: scopes.slice(0, at), repairs: 0 }, { scopes: scopes.slice(at), repairs: 0 });
|
|
4318
|
+
index--;
|
|
4319
|
+
continue;
|
|
4320
|
+
}
|
|
4321
|
+
if (++calls > 128)
|
|
4322
|
+
return { ...last, ok: false, failureCategory: "frontend-input-exhausted", stderr: "FRONTEND_REVIEW_RECOVERY_EXHAUSTED: session quota reached" };
|
|
4323
|
+
last = await observeFrontendSession({ ...input.observation, phase: `${input.phase}/scope`, scopeIds: scopes.map(s => s.id), prompt, userMessage: input.sessionOptions.userMessage, customTools,
|
|
4324
|
+
artifactPath: input.observation?.artifactPath ? `${input.observation.artifactPath}-${calls}.json` : undefined,
|
|
4325
|
+
committedCount: () => protocol.committedFacts().length, durableCommittedCount: () => protocol.committedFacts().length,
|
|
4326
|
+
}, observer => input.piStepFn({ ...input.sessionOptions, prompt, onAttemptObservation: observer, writerToolPolicy: { requireSdk: true, customTools } }));
|
|
4327
|
+
await input.tools.flush();
|
|
4328
|
+
await input.inventory.validate();
|
|
4329
|
+
if (last.timedOut || /interrupt|termination-unconfirmed|budget_breach/.test(last.failureCategory))
|
|
4330
|
+
return { ...last, ok: false };
|
|
4331
|
+
const capacity = readWriterThinkingExhaustionEvidence(last).stopReason === "length" || ["context-overflow", "context-budget-exhausted"].includes(last.failureCategory);
|
|
4332
|
+
const missing = scopes.filter(s => !protocol.completedScopeIds().has(s.id));
|
|
4333
|
+
if (capacity && (missing.length || !terminal() && finalScope)) {
|
|
4334
|
+
if (missing.length === 1 && scopes.length === 1 || !missing.length && !scopes.length)
|
|
4335
|
+
return { ...last, ok: false, failureCategory: "frontend-input-exhausted", stderr: "FRONTEND_INPUT_UNIT_TOO_LARGE: complete review unit or terminal exhausted" };
|
|
4336
|
+
const at = Math.ceil(missing.length / 2);
|
|
4337
|
+
const smaller = missing.length ? [missing.slice(0, at), missing.slice(at)].filter(s => s.length) : [[]];
|
|
4338
|
+
queue.splice(index, 1, ...smaller.map(scopes => ({ scopes, repairs: 0 })));
|
|
4339
|
+
index--;
|
|
4340
|
+
continue;
|
|
4341
|
+
}
|
|
4342
|
+
if (!last.ok && !capacity)
|
|
4343
|
+
return last;
|
|
4344
|
+
if (missing.length || finalScope && !terminal()) {
|
|
4345
|
+
if (item.repairs >= 1)
|
|
4346
|
+
return { ...last, ok: false, failureCategory: "frontend-input-exhausted", stderr: "FRONTEND_REVIEW_SCOPE_INCOMPLETE: required checkpoint or verdict missing" };
|
|
4347
|
+
queue.splice(index, 1, { scopes: missing, repairs: item.repairs + 1 });
|
|
4348
|
+
index--;
|
|
4349
|
+
}
|
|
4350
|
+
}
|
|
4351
|
+
protocol.assertComplete();
|
|
4352
|
+
return terminal() ? { ...last, ok: true, failureCategory: "success" } : { ...last, ok: false, failureCategory: "frontend-input-exhausted", stderr: "FRONTEND_REVIEW_SCOPE_INCOMPLETE: missing independent verdict" };
|
|
4353
|
+
}
|
|
4354
|
+
export async function runFrontendScoutSegmentedSessions(input) {
|
|
4355
|
+
const inventory = parseFrontendInputBlock(input.basePrompt, "scout");
|
|
4356
|
+
if (!inventory)
|
|
4357
|
+
throw Error("FRONTEND_INPUT_MISSING: Scout compiled inventory unavailable");
|
|
4358
|
+
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)) }));
|
|
4359
|
+
const targetBytes = input.sessionOptions.frontendExecutionPolicy?.scopeTargetBytes ?? FRONTEND_SCOPE_TARGET_BYTES;
|
|
4360
|
+
const queue = packFrontendInputUnits(units, { targetBytes, maxUnits: input.sessionOptions.frontendExecutionPolicy?.maxScopeUnits }).map(batch => ({ groups: batch, repairs: 0 }));
|
|
4361
|
+
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 };
|
|
4362
|
+
let calls = 0;
|
|
4363
|
+
for (let index = 0; index < queue.length; index++) {
|
|
4364
|
+
await input.tools.flush();
|
|
4365
|
+
const item = queue[index];
|
|
4366
|
+
const completed = input.tools.completedRequirementIds();
|
|
4367
|
+
const groups = item.groups.filter(g => g.requirementIds.some(id => !completed.has(id)));
|
|
4368
|
+
if (!groups.length)
|
|
4369
|
+
continue;
|
|
4370
|
+
const ids = groups.flatMap(g => g.requirementIds);
|
|
4371
|
+
const scopeId = input.tools.setActiveScope(ids);
|
|
4372
|
+
const prompt = input.basePrompt.replace(inventory.block, `<frontend_scout_input>\n${JSON.stringify(projectFrontendInputScope(inventory.payload, ids))}\n</frontend_scout_input>`) +
|
|
4373
|
+
`\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` +
|
|
4374
|
+
JSON.stringify({ completedScopePaths: input.tools.completedScopeFacts().map(f => ({ id: f.id, requirementIds: f.requirementIds, paths: f.surface?.implementationPaths })) });
|
|
4375
|
+
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 }; })));
|
|
4376
|
+
if (envelopeBytes > targetBytes && groups.length > 1) {
|
|
4377
|
+
const at = Math.ceil(groups.length / 2);
|
|
4378
|
+
queue.splice(index, 1, { groups: groups.slice(0, at), repairs: 0 }, { groups: groups.slice(at), repairs: 0 });
|
|
4379
|
+
index--;
|
|
4380
|
+
continue;
|
|
4381
|
+
}
|
|
4382
|
+
if (++calls > 128)
|
|
4383
|
+
return { ...last, ok: false, failureCategory: "frontend-input-exhausted", stderr: "SCOUT_RECOVERY_EXHAUSTED: shared session quota reached" };
|
|
4384
|
+
last = await observeFrontendSession({ ...input.observation, phase: "scout/scope", scopeIds: ids, prompt, userMessage: input.sessionOptions.userMessage, customTools: input.customTools,
|
|
4385
|
+
artifactPath: input.observation?.artifactPath ? `${input.observation.artifactPath}-${calls}.json` : undefined,
|
|
4386
|
+
committedCount: () => input.tools.committedFacts().length, durableCommittedCount: () => input.tools.committedFacts().length,
|
|
4387
|
+
}, observer => input.piStepFn({ ...input.sessionOptions, prompt, onAttemptObservation: observer, writerToolPolicy: { requireSdk: true, customTools: input.customTools } }));
|
|
4388
|
+
await input.tools.flush();
|
|
4389
|
+
const missing = groups.filter(g => g.requirementIds.some(id => !input.tools.completedRequirementIds().has(id)));
|
|
4390
|
+
if (last.timedOut || /interrupt|termination-unconfirmed|budget_breach/.test(last.failureCategory))
|
|
4391
|
+
return { ...last, ok: false };
|
|
4392
|
+
const capacity = readWriterThinkingExhaustionEvidence(last).stopReason === "length" || ["context-overflow", "context-budget-exhausted"].includes(last.failureCategory);
|
|
4393
|
+
if (capacity && missing.length) {
|
|
4394
|
+
if (missing.length === 1 && groups.length === 1)
|
|
4395
|
+
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` };
|
|
4396
|
+
const at = Math.ceil(missing.length / 2);
|
|
4397
|
+
queue.splice(index, 1, ...[missing.slice(0, at), missing.slice(at)].filter(batch => batch.length).map(batch => ({ groups: batch, repairs: 0 })));
|
|
4398
|
+
index--;
|
|
4399
|
+
continue;
|
|
4400
|
+
}
|
|
4401
|
+
if (!last.ok && !capacity)
|
|
4402
|
+
return last;
|
|
4403
|
+
if (missing.length) {
|
|
4404
|
+
if (item.repairs >= 1)
|
|
4405
|
+
return { ...last, ok: false, failureCategory: "frontend-input-exhausted", stderr: "SCOUT_SCOPE_INCOMPLETE: unresolved discovery remains after local correction" };
|
|
4406
|
+
queue.splice(index, 1, { groups: missing, repairs: item.repairs + 1 });
|
|
4407
|
+
index--;
|
|
4408
|
+
continue;
|
|
4409
|
+
}
|
|
4410
|
+
}
|
|
4411
|
+
await input.tools.flush();
|
|
4412
|
+
const { readCompleteScoutTargetSurface } = await import("../workflows/dag/frontend-shadow-dual-write.js");
|
|
4413
|
+
const closure = readCompleteScoutTargetSurface(input.tools.committedFacts());
|
|
4414
|
+
return closure.ok ? { ...last, ok: true, failureCategory: "success" } : { ...last, ok: false, failureCategory: "frontend-input-exhausted", stderr: closure.reason };
|
|
4415
|
+
}
|
|
4416
|
+
export async function runFrontendContractSegmentedSessions(input) {
|
|
4417
|
+
// Build from the frozen runtime inventory if the caller has not rendered it yet.
|
|
4418
|
+
const basePrompt = parseFrontendInputBlock(input.basePrompt, "contract") ? input.basePrompt : input.basePrompt +
|
|
4419
|
+
`\n<frontend_contract_input>\nFrozen complete source obligations.\n${JSON.stringify({ requirements: input.tools.inputRequirements() })}\n</frontend_contract_input>`;
|
|
4420
|
+
const inventory = parseFrontendInputBlock(basePrompt, "contract");
|
|
4421
|
+
const pending = inventory.payload.requirements.filter(r => !input.tools.completedScopeRequirementIds().has(r.id));
|
|
4422
|
+
const batches = packFrontendInputUnits(pending, { targetBytes: input.sessionOptions.frontendExecutionPolicy?.scopeTargetBytes, maxUnits: input.sessionOptions.frontendExecutionPolicy?.maxScopeUnits });
|
|
4423
|
+
if (!batches.length)
|
|
4424
|
+
batches.push([]);
|
|
4425
|
+
let last;
|
|
4426
|
+
let invocation = 0;
|
|
4427
|
+
const maxSessions = batches.length * 3 + 2;
|
|
4428
|
+
const terminal = () => input.tools.committedFacts().some(r => r.fact.kind === "contract-finalized");
|
|
4429
|
+
try {
|
|
4430
|
+
for (let index = 0; index < batches.length; index += 1) {
|
|
4431
|
+
let scopeIds = batches[index].map(r => r.id);
|
|
4432
|
+
const finalScope = index === batches.length - 1;
|
|
4433
|
+
for (let repair = 0; repair < 2; repair += 1) {
|
|
4434
|
+
// Confirmed IDs remain visible until the model commits a scope checkpoint.
|
|
4435
|
+
const completed = input.tools.completedScopeRequirementIds();
|
|
4436
|
+
scopeIds = scopeIds.filter(id => !completed.has(id));
|
|
4437
|
+
input.tools.setActiveRequirementScope(scopeIds);
|
|
4438
|
+
const groupIndex = collectFrontendExecutionGroups(input.tools.committedFacts().filter(r => r.fact.kind === "requirement").map(r => ({ id: String(r.fact.id), execution: r.fact.execution })));
|
|
4439
|
+
const shared = input.tools.committedFacts().filter(r => !["requirement", "contract-finalized", "contract-scope-completed"].includes(String(r.fact.kind))).map(r => r.fact);
|
|
4440
|
+
const prompt = projectFrontendContractPrompt(basePrompt, scopeIds) +
|
|
4441
|
+
`\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. ` +
|
|
4442
|
+
(finalScope ? "After complete scope coverage and source-bound deliverables, call finalize_contract. Correct rejected calls and retry." : "Do not finalize; subsequent complete scopes remain.") +
|
|
4443
|
+
`\n<committed_contract_facts>\n${JSON.stringify({ facts: shared, executionGroups: groupIndex })}\n</committed_contract_facts>`;
|
|
4444
|
+
const customTools = input.tools.customTools.filter(t => finalScope || t.name !== "finalize_contract");
|
|
4445
|
+
invocation += 1;
|
|
4446
|
+
if (invocation > maxSessions)
|
|
4447
|
+
return { ...last, ok: false, failureCategory: "invalid-output", stderr: "CONTRACT_RECOVERY_EXHAUSTED: session quota exceeded" };
|
|
4448
|
+
last = await observeFrontendSession({
|
|
4449
|
+
...input.observation, phase: "contract/scope", scopeIds, prompt, userMessage: input.sessionOptions.userMessage, customTools,
|
|
4450
|
+
artifactPath: input.observation?.artifactPath ? `${input.observation.artifactPath}-${invocation}.json` : undefined,
|
|
4451
|
+
committedCount: () => input.tools.committedFacts().length, durableCommittedCount: () => input.tools.committedFacts().length,
|
|
4452
|
+
}, observer => input.piStepFn({ ...input.sessionOptions, prompt, onAttemptObservation: observer, writerToolPolicy: { requireSdk: true, customTools } }));
|
|
4453
|
+
await input.tools.flush();
|
|
4454
|
+
if (last.timedOut || /interrupt|termination-unconfirmed|budget_breach/.test(last.failureCategory))
|
|
4455
|
+
return { ...last, ok: false };
|
|
4456
|
+
const capacityExhausted = readWriterThinkingExhaustionEvidence(last).stopReason === "length" || ["context-overflow", "context-budget-exhausted"].includes(last.failureCategory);
|
|
4457
|
+
if (capacityExhausted && !last.timedOut) {
|
|
4458
|
+
const missing = scopeIds.filter(id => !input.tools.completedScopeRequirementIds().has(id));
|
|
4459
|
+
if (!missing.length) {
|
|
4460
|
+
if (finalScope && !terminal()) {
|
|
4461
|
+
if (!scopeIds.length)
|
|
4462
|
+
return { ...last, ok: false, failureCategory: "invalid-output", stderr: `${last.stderr}\nFRONTEND_INPUT_UNIT_TOO_LARGE: contract terminal exhausted; unchanged retry disabled` };
|
|
4463
|
+
batches.push([]);
|
|
4464
|
+
}
|
|
4465
|
+
break;
|
|
4466
|
+
}
|
|
4467
|
+
if (missing.length === 1 && scopeIds.length > 1) {
|
|
4468
|
+
batches.splice(index, 1, inventory.payload.requirements.filter(r => missing.includes(r.id)));
|
|
4469
|
+
index -= 1;
|
|
4470
|
+
break;
|
|
4471
|
+
}
|
|
4472
|
+
if (missing.length <= 1)
|
|
4473
|
+
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` };
|
|
4474
|
+
const smaller = packFrontendInputUnits(inventory.payload.requirements.filter(r => missing.includes(r.id)), { maxUnits: Math.ceil(missing.length / 2) });
|
|
4475
|
+
batches.splice(index, 1, ...smaller);
|
|
4476
|
+
index -= 1;
|
|
4477
|
+
break;
|
|
4478
|
+
}
|
|
4479
|
+
if (!last.ok)
|
|
4480
|
+
return last;
|
|
4481
|
+
const missing = scopeIds.filter(id => !input.tools.completedScopeRequirementIds().has(id));
|
|
4482
|
+
if (!missing.length && (!finalScope || terminal()))
|
|
4483
|
+
break;
|
|
4484
|
+
if (repair === 1)
|
|
4485
|
+
return { ...last, ok: false, failureCategory: "invalid-output", stderr: `CONTRACT_SCOPE_INCOMPLETE: ${missing.join(", ") || "missing finalize_contract"}` };
|
|
4486
|
+
}
|
|
4487
|
+
}
|
|
4488
|
+
return last;
|
|
4489
|
+
}
|
|
4490
|
+
finally {
|
|
4491
|
+
input.tools.setActiveRequirementScope(null);
|
|
4492
|
+
}
|
|
4493
|
+
}
|
|
4494
|
+
/** One workload view for both the outer parallel map and inner session queue.
|
|
4495
|
+
* A declared execution group is an indivisible unit during initial packing. */
|
|
4496
|
+
function buildFrontendPlanWorkload(input) {
|
|
4497
|
+
const allRequirementIds = input.requirementIds;
|
|
4498
|
+
const compiledInput = parseFrontendInputBlock(input.basePrompt, "plan")?.payload;
|
|
4499
|
+
const fullUnits = new Map(compiledInput?.requirements.map(r => [r.id, r]) ?? []);
|
|
4500
|
+
const declaredGroups = Array.isArray(compiledInput?.executionGroups)
|
|
4501
|
+
? compiledInput.executionGroups.filter(isRecordObject) : [];
|
|
4502
|
+
const workGroups = declaredGroups.map(g => ({
|
|
4503
|
+
id: String(g.id), kind: String(g.kind),
|
|
4504
|
+
requirementIds: Array.isArray(g.requirementIds)
|
|
4505
|
+
? g.requirementIds.filter((id) => typeof id === "string" && allRequirementIds.includes(id)) : [],
|
|
4506
|
+
})).filter(g => g.requirementIds.length);
|
|
4507
|
+
const groupedIds = new Set(workGroups.flatMap(g => g.requirementIds));
|
|
4508
|
+
for (const id of allRequirementIds) {
|
|
4509
|
+
if (!groupedIds.has(id))
|
|
4510
|
+
workGroups.push({ id, kind: "unclassified", requirementIds: [id] });
|
|
4511
|
+
}
|
|
4512
|
+
const workCost = (ids) => 1 + ids.reduce((total, id) => total + Math.max(1, (input.requirementCosts?.get(id) ?? 2) - 1), 0);
|
|
4513
|
+
const policy = input.sessionOptions.frontendExecutionPolicy;
|
|
4514
|
+
const targetBytes = policy?.scopeTargetBytes ?? FRONTEND_SCOPE_TARGET_BYTES;
|
|
4515
|
+
const buildWorkBatches = (ids) => {
|
|
4516
|
+
const work = workGroups.flatMap((g, index) => {
|
|
4517
|
+
const members = g.requirementIds.filter(id => ids.includes(id));
|
|
4518
|
+
return members.length ? [{ id: `${index}:${g.id}`, requirementIds: members,
|
|
4519
|
+
requirements: members.map(id => fullUnits.get(id) ?? { id }), estimatedCalls: workCost(members) }] : [];
|
|
4520
|
+
});
|
|
4521
|
+
// Pack complete work units, not the repeated request scaffold. Deducting
|
|
4522
|
+
// fixed context/tools can leave a one-byte budget and force one AC per
|
|
4523
|
+
// session without reducing that overhead. The SDK checks the actual
|
|
4524
|
+
// request against model capacity; capacity recovery splits unfinished work.
|
|
4525
|
+
return packFrontendInputUnits(work, {
|
|
4526
|
+
targetBytes,
|
|
4527
|
+
maxUnits: policy?.maxScopeUnits ?? 4,
|
|
4528
|
+
maxCost: FRONTEND_PLAN_COVERAGE_MAX_RECORD_CALLS, cost: g => g.estimatedCalls,
|
|
4529
|
+
}).map(batch => batch.flatMap(g => g.requirementIds));
|
|
4530
|
+
};
|
|
4531
|
+
// This renderer also carries the Plan's evolving UX checkpoint on resume.
|
|
4532
|
+
// Only the Contract/Scout-derived input participates in ownership binding.
|
|
4533
|
+
const { committedUx: _committedUx, ...frozenInput } = compiledInput ?? {};
|
|
4534
|
+
return {
|
|
4535
|
+
frozenInput, workGroups, buildWorkBatches,
|
|
4536
|
+
compactEligible: input.requirementCosts !== undefined &&
|
|
4537
|
+
workGroups.length <= FRONTEND_PLAN_SMALL_MAX_REQUIREMENTS &&
|
|
4538
|
+
workGroups.reduce((total, group) => total + workCost(group.requirementIds), 0) <= FRONTEND_PLAN_SMALL_MAX_ESTIMATED_CALLS &&
|
|
4539
|
+
countFrontendPlanTargetSurfaces(input.basePrompt) === 1,
|
|
4540
|
+
};
|
|
4541
|
+
}
|
|
4542
|
+
const frontendPlanCoverageLayoutSchema = z.object({
|
|
4543
|
+
schemaVersion: z.literal(1),
|
|
4544
|
+
bindingSha256: z.string(),
|
|
4545
|
+
coverageBatches: z.array(z.array(z.string().min(1)).min(1)).min(1),
|
|
4546
|
+
layoutSha256: z.string(),
|
|
4547
|
+
}).strict();
|
|
4548
|
+
/** Freeze ledger ownership before any provider call. Session packing remains
|
|
4549
|
+
* adaptive inside each owner, but retry instructions cannot rename its scope. */
|
|
4550
|
+
async function loadOrCreateFrontendPlanCoverageLayout(input) {
|
|
4551
|
+
const fileName = "coverage-layout.json";
|
|
4552
|
+
const file = path.join(input.runDir, input.nodeId, fileName);
|
|
4553
|
+
const bindingSha256 = sha256OfCanonicalJson(input.binding);
|
|
4554
|
+
let raw;
|
|
4555
|
+
try {
|
|
4556
|
+
raw = await readFile(file, "utf8");
|
|
4557
|
+
}
|
|
4558
|
+
catch (error) {
|
|
4559
|
+
if (error.code !== "ENOENT")
|
|
4560
|
+
throw error;
|
|
4561
|
+
}
|
|
4562
|
+
let layout;
|
|
4563
|
+
if (raw !== undefined) {
|
|
4564
|
+
layout = frontendPlanCoverageLayoutSchema.parse(JSON.parse(raw));
|
|
4565
|
+
}
|
|
4566
|
+
else {
|
|
4567
|
+
// Losing the ownership receipt must never repartition acknowledged facts.
|
|
4568
|
+
for (const relative of ["plan-typed-facts.jsonl", "parallel"]) {
|
|
4569
|
+
const existing = await stat(path.join(input.runDir, input.nodeId, relative)).catch(error => {
|
|
4570
|
+
if (error.code !== "ENOENT")
|
|
4571
|
+
throw error;
|
|
4572
|
+
return undefined;
|
|
4573
|
+
});
|
|
4574
|
+
if (existing && (existing.isDirectory() || existing.size > 0)) {
|
|
4575
|
+
throw Error("FRONTEND_PLAN_LAYOUT_MISSING: preserve the existing ledger and restart its owning phase");
|
|
4576
|
+
}
|
|
4577
|
+
}
|
|
4578
|
+
const descriptor = {
|
|
4579
|
+
schemaVersion: 1, bindingSha256,
|
|
4580
|
+
coverageBatches: input.workload.compactEligible ? [input.requirementIds] : input.workload.buildWorkBatches(input.requirementIds),
|
|
4581
|
+
};
|
|
4582
|
+
layout = { ...descriptor, layoutSha256: sha256OfCanonicalJson(descriptor) };
|
|
4583
|
+
}
|
|
4584
|
+
const { layoutSha256, ...descriptor } = layout;
|
|
4585
|
+
if (layout.bindingSha256 !== bindingSha256 || layoutSha256 !== sha256OfCanonicalJson(descriptor)) {
|
|
4586
|
+
throw Error("FRONTEND_PLAN_LAYOUT_MISMATCH: frozen coverage ownership or input binding changed");
|
|
4587
|
+
}
|
|
4588
|
+
const members = layout.coverageBatches.flat();
|
|
4589
|
+
const owners = new Map(layout.coverageBatches.flatMap((batch, index) => batch.map(id => [id, index])));
|
|
4590
|
+
if (members.length !== owners.size || members.length !== input.requirementIds.length ||
|
|
4591
|
+
input.requirementIds.some(id => !owners.has(id)) ||
|
|
4592
|
+
input.workload.workGroups.some(group => new Set(group.requirementIds.map(id => owners.get(id))).size !== 1)) {
|
|
4593
|
+
throw Error("FRONTEND_PLAN_LAYOUT_INVALID: coverage must partition complete execution groups exactly once");
|
|
4594
|
+
}
|
|
4595
|
+
if (raw === undefined)
|
|
4596
|
+
await writeDagNodeJsonArtifact(input.runDir, input.nodeId, fileName, layout);
|
|
4597
|
+
return layout.coverageBatches;
|
|
4598
|
+
}
|
|
4158
4599
|
export async function runFrontendPlanSegmentedSessions(input) {
|
|
4159
4600
|
const queue = [];
|
|
4160
4601
|
const coverageSegment = FRONTEND_PLAN_SEGMENTS.find((segment) => segment.id === "coverage");
|
|
@@ -4163,10 +4604,10 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4163
4604
|
const globalMockDataSegment = FRONTEND_PLAN_SEGMENTS.find((segment) => segment.id === "global-mock-data");
|
|
4164
4605
|
const finalizeSegment = FRONTEND_PLAN_SEGMENTS.find((segment) => segment.id === "finalize");
|
|
4165
4606
|
const allRequirementIds = input.requirementIds ?? [];
|
|
4166
|
-
const buildPhasePrompt = (segment, missing = []) => {
|
|
4607
|
+
const buildPhasePrompt = (segment, missing = [], scopeIds = allRequirementIds) => {
|
|
4167
4608
|
const compact = allRequirementIds.length > 0
|
|
4168
|
-
? compactFrontendPlanPromptForRequirementSlice(input.basePrompt,
|
|
4169
|
-
includeRequirementText: segment.id === "global-mock-data"
|
|
4609
|
+
? compactFrontendPlanPromptForRequirementSlice(input.basePrompt, scopeIds, {
|
|
4610
|
+
includeRequirementText: segment.id === "global-mock-data",
|
|
4170
4611
|
includeVerificationTargets: false,
|
|
4171
4612
|
includeDesignEvidence: segment.id === "global-dependency-deviation" || segment.id === "finalize",
|
|
4172
4613
|
includeChecklist: false,
|
|
@@ -4182,8 +4623,9 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4182
4623
|
const ledger = input.committedFacts
|
|
4183
4624
|
? compactFrontendPlanLedgerContext({
|
|
4184
4625
|
committedFacts: input.committedFacts(),
|
|
4185
|
-
requirementIds:
|
|
4626
|
+
requirementIds: scopeIds,
|
|
4186
4627
|
kinds,
|
|
4628
|
+
...(segment.id === "global-mock-data" ? { scopedKinds: ["state-flow"] } : {}),
|
|
4187
4629
|
})
|
|
4188
4630
|
: "";
|
|
4189
4631
|
return [
|
|
@@ -4221,13 +4663,13 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4221
4663
|
"component-choice",
|
|
4222
4664
|
"state-flow",
|
|
4223
4665
|
],
|
|
4224
|
-
|
|
4666
|
+
// Include shared state bindings from other scopes; page full values before correcting.
|
|
4225
4667
|
})
|
|
4226
4668
|
: "";
|
|
4227
4669
|
return [
|
|
4228
4670
|
compactFrontendPlanPromptForRequirementSlice(input.basePrompt, slice),
|
|
4229
4671
|
uxSegment.instruction,
|
|
4230
|
-
`
|
|
4672
|
+
`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
4673
|
ledger,
|
|
4232
4674
|
...(missing.length > 0
|
|
4233
4675
|
? [
|
|
@@ -4251,6 +4693,7 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4251
4693
|
: "";
|
|
4252
4694
|
const compact = allRequirementIds.length > 0
|
|
4253
4695
|
? compactFrontendPlanPromptForRequirementSlice(input.basePrompt, allRequirementIds, {
|
|
4696
|
+
includeRequirementText: false,
|
|
4254
4697
|
includeVerificationTargets: false,
|
|
4255
4698
|
includeDesignEvidence: false,
|
|
4256
4699
|
includeChecklist: false,
|
|
@@ -4296,7 +4739,7 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4296
4739
|
: []),
|
|
4297
4740
|
].filter(Boolean).join("\n\n");
|
|
4298
4741
|
};
|
|
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
|
|
4742
|
+
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
4743
|
const buildCompactFinalizePrompt = (missing = []) => [buildPhasePrompt(finalizeSegment, missing), compactFinalizeInstruction].join("\n\n");
|
|
4301
4744
|
const mapPlannerExhaustion = (r, committedAnyFacts) => isPlannerThinkingExhausted(r, committedAnyFacts)
|
|
4302
4745
|
? {
|
|
@@ -4340,8 +4783,10 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4340
4783
|
tokensUsed: 0,
|
|
4341
4784
|
};
|
|
4342
4785
|
}
|
|
4343
|
-
const
|
|
4344
|
-
|
|
4786
|
+
const { workGroups, buildWorkBatches, compactEligible } = buildFrontendPlanWorkload({
|
|
4787
|
+
basePrompt: input.basePrompt, requirementIds: allRequirementIds,
|
|
4788
|
+
requirementCosts: input.requirementCosts, sessionOptions: input.sessionOptions,
|
|
4789
|
+
});
|
|
4345
4790
|
// Small, single-surface requests do not benefit from six isolated Pi
|
|
4346
4791
|
// sessions. Keep the typed ledger as the authority, but let one local
|
|
4347
4792
|
// session establish requirement/UX facts and one final session establish
|
|
@@ -4349,14 +4794,11 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4349
4794
|
// for larger plans and for the unscoped compatibility path.
|
|
4350
4795
|
const useCompactSmallPlan = requirementIdsProvided &&
|
|
4351
4796
|
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;
|
|
4797
|
+
compactEligible;
|
|
4357
4798
|
if (useCompactSmallPlan) {
|
|
4358
4799
|
const compactLocalTools = new Set([
|
|
4359
4800
|
"record_plan_requirement",
|
|
4801
|
+
"record_plan_group_coverage",
|
|
4360
4802
|
"record_plan_verification_target",
|
|
4361
4803
|
"record_plan_evidence_gap",
|
|
4362
4804
|
"record_state_registry",
|
|
@@ -4384,13 +4826,8 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4384
4826
|
});
|
|
4385
4827
|
}
|
|
4386
4828
|
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({
|
|
4829
|
+
const completeBatches = buildWorkBatches(coverageWorkIds);
|
|
4830
|
+
completeBatches.forEach((slice, batchIndex) => queue.push({
|
|
4394
4831
|
id: `coverage-batch-${batchIndex + 1}`,
|
|
4395
4832
|
toolNames: coverageSegment.toolNames,
|
|
4396
4833
|
coverageSlice: slice,
|
|
@@ -4409,28 +4846,27 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4409
4846
|
prompt: buildUxRegistryPrompt(),
|
|
4410
4847
|
});
|
|
4411
4848
|
}
|
|
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
|
-
});
|
|
4849
|
+
const uxWorkIds = workGroups.filter(g => !["constraint", "exclusion"].includes(g.kind) || g.requirementIds.some(id => input.behaviorRequiredRequirementIds?.includes(id))).flatMap(g => g.requirementIds);
|
|
4850
|
+
const uxBatches = requirementIdsProvided ? buildWorkBatches(uxWorkIds) : [[]];
|
|
4851
|
+
uxBatches.forEach((slice, batchIndex) => queue.push({
|
|
4852
|
+
id: `ux-local-${batchIndex + 1}`,
|
|
4853
|
+
toolNames: requirementIdsProvided ? uxSegment.toolNames : new Set([...(uxSegment.toolNames ?? []), "record_state_registry"]),
|
|
4854
|
+
...(slice.length ? { requirementSlice: slice } : {}),
|
|
4855
|
+
prompt: slice.length ? buildUxPrompt(slice) : buildPhasePrompt(uxSegment),
|
|
4856
|
+
}));
|
|
4426
4857
|
for (const segment of FRONTEND_PLAN_SEGMENTS) {
|
|
4427
4858
|
if (["coverage", "ux-registry", "ux-local", "finalize"].includes(segment.id))
|
|
4428
4859
|
continue;
|
|
4429
|
-
|
|
4430
|
-
id: segment.
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4860
|
+
if (segment.id === "global-mock-data" && requirementIdsProvided) {
|
|
4861
|
+
buildWorkBatches(allRequirementIds).forEach((slice, i) => queue.push({ id: `global-mock-data-${i + 1}`, toolNames: segment.toolNames, requirementSlice: slice, prompt: buildPhasePrompt(segment, [], slice) }));
|
|
4862
|
+
}
|
|
4863
|
+
else {
|
|
4864
|
+
queue.push({
|
|
4865
|
+
id: segment.id,
|
|
4866
|
+
toolNames: segment.toolNames,
|
|
4867
|
+
prompt: buildPhasePrompt(segment),
|
|
4868
|
+
});
|
|
4869
|
+
}
|
|
4434
4870
|
}
|
|
4435
4871
|
queue.push({
|
|
4436
4872
|
id: finalizeSegment.id,
|
|
@@ -4441,8 +4877,30 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4441
4877
|
let accumulated;
|
|
4442
4878
|
let index = 0;
|
|
4443
4879
|
let invocationCount = 0;
|
|
4880
|
+
let lastDurableCount = input.committedFactCount();
|
|
4444
4881
|
while (index < queue.length) {
|
|
4445
4882
|
const session = queue[index];
|
|
4883
|
+
if (input.attempt > 1 && input.committedFacts && session.id === "ux-registry" &&
|
|
4884
|
+
collectFrontendPlanPhaseMissingFacts({ phase: "ux-registry", requirementIds: allRequirementIds, committedFacts: input.committedFacts() }).length === 0) {
|
|
4885
|
+
index += 1;
|
|
4886
|
+
continue;
|
|
4887
|
+
}
|
|
4888
|
+
// Reuse complete UX scopes on node retry. Require explicit per-requirement
|
|
4889
|
+
// ownership; an unrelated/global fact must not prove a slice complete.
|
|
4890
|
+
if (input.attempt > 1 && input.committedFacts && session.id.startsWith("ux-local-") && session.requirementSlice?.length) {
|
|
4891
|
+
const facts = input.committedFacts();
|
|
4892
|
+
const ownedIds = new Set(facts.flatMap(value => {
|
|
4893
|
+
const fact = committedFactFromPlanRecord(value);
|
|
4894
|
+
return fact ? planFactStringList(fact.scopeRequirementIds) : [];
|
|
4895
|
+
}));
|
|
4896
|
+
const complete = session.requirementSlice.every(id => ownedIds.has(id)) &&
|
|
4897
|
+
collectFrontendPlanPhaseMissingFacts({ phase: "ux-local", requirementIds: session.requirementSlice,
|
|
4898
|
+
committedFacts: facts, behaviorRequiredRequirementIds: input.behaviorRequiredRequirementIds }).length === 0;
|
|
4899
|
+
if (complete) {
|
|
4900
|
+
index += 1;
|
|
4901
|
+
continue;
|
|
4902
|
+
}
|
|
4903
|
+
}
|
|
4446
4904
|
const remaining = (session.coverageOnly ? session.coverageSlice ?? [] : []).filter((id) => !input.committedRequirementIds?.().has(id));
|
|
4447
4905
|
const preexistingMissing = session.coverageOnly && session.coverageSlice && input.committedFacts
|
|
4448
4906
|
? collectFrontendPlanMissingFacts({
|
|
@@ -4460,7 +4918,7 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4460
4918
|
}
|
|
4461
4919
|
let prompt = session.prompt;
|
|
4462
4920
|
if (session.coverageOnly && session.coverageSlice) {
|
|
4463
|
-
const promptSlice = remaining.
|
|
4921
|
+
const promptSlice = [...new Set([...remaining, ...preexistingMissing.flatMap(item => item.requirementIds)])];
|
|
4464
4922
|
prompt = buildCoveragePrompt(promptSlice, session.missingFacts ?? preexistingMissing);
|
|
4465
4923
|
}
|
|
4466
4924
|
else if (session.id === "compact-local") {
|
|
@@ -4480,45 +4938,68 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4480
4938
|
// Global phases and finalize also consume the latest committed ledger;
|
|
4481
4939
|
// constructing their prompt only when the session starts prevents a
|
|
4482
4940
|
// stale queue entry from dropping facts written by earlier phases.
|
|
4483
|
-
const segment = FRONTEND_PLAN_SEGMENTS.find((candidate) => candidate.id === session.id);
|
|
4941
|
+
const segment = FRONTEND_PLAN_SEGMENTS.find((candidate) => candidate.id === session.id || (candidate.id === "global-mock-data" && session.id.startsWith("global-mock-data-")));
|
|
4484
4942
|
if (segment) {
|
|
4485
4943
|
prompt =
|
|
4486
4944
|
session.id === "finalize" && useCompactSmallPlan
|
|
4487
4945
|
? buildCompactFinalizePrompt(session.missingFacts)
|
|
4488
|
-
: buildPhasePrompt(segment, session.missingFacts);
|
|
4946
|
+
: buildPhasePrompt(segment, session.missingFacts, session.requirementSlice ?? allRequirementIds);
|
|
4489
4947
|
}
|
|
4490
4948
|
}
|
|
4949
|
+
const atomicFocus = session.atomicRecovery ? session.missingFacts?.[0] : undefined;
|
|
4950
|
+
if (atomicFocus) {
|
|
4951
|
+
prompt = [
|
|
4952
|
+
compactFrontendPlanPromptForRequirementSlice(input.basePrompt, atomicFocus.requirementIds, {
|
|
4953
|
+
includeChecklist: false,
|
|
4954
|
+
...(atomicFocus.kind === "plan-verification-target" && atomicFocus.id
|
|
4955
|
+
? { verificationTargetIds: [atomicFocus.id] } : {}),
|
|
4956
|
+
}),
|
|
4957
|
+
`ATOMIC FACT: ${atomicFocus.kind}${atomicFocus.id ? ` ${atomicFocus.id}` : ""}`,
|
|
4958
|
+
atomicFocus.reason,
|
|
4959
|
+
"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.",
|
|
4960
|
+
].join("\n\n");
|
|
4961
|
+
}
|
|
4491
4962
|
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
|
-
: []);
|
|
4963
|
+
input.setActiveRequirementScope?.(session.coverageOnly ? session.coverageSlice ?? [] : session.requirementSlice ?? []);
|
|
4497
4964
|
if (invocationCount >= FRONTEND_PLAN_BATCH_MAX_SESSIONS)
|
|
4498
4965
|
break;
|
|
4499
4966
|
invocationCount += 1;
|
|
4500
|
-
const
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4967
|
+
const atomicTools = {
|
|
4968
|
+
"plan-requirement": "record_plan_requirement", "plan-verification-target": "record_plan_verification_target",
|
|
4969
|
+
"state-registry": "record_state_registry", "component-choice": "record_component_choice",
|
|
4970
|
+
"state-flow": "record_state_flow", "data-flow": "record_data_flow", "mock-api": "record_mock_api",
|
|
4971
|
+
};
|
|
4972
|
+
const customTools = input.segmentCustomTools(atomicFocus ? new Set([atomicTools[atomicFocus.kind]]) : session.toolNames);
|
|
4973
|
+
const result = await observeFrontendSession({
|
|
4974
|
+
...input.observation, phase: `plan/${session.id}`, scopeIds: session.requirementSlice ?? session.coverageSlice ?? allRequirementIds,
|
|
4975
|
+
prompt, userMessage: input.sessionOptions.userMessage, customTools, committedCount: input.committedFactCount, durableCommittedCount: () => lastDurableCount,
|
|
4976
|
+
artifactPath: input.observation?.artifactPath ? `${input.observation.artifactPath}-${invocationCount}.json` : undefined,
|
|
4977
|
+
}, async (observer) => {
|
|
4978
|
+
const result = await input.piStepFn({
|
|
4979
|
+
...input.sessionOptions,
|
|
4980
|
+
onAttemptObservation: observer,
|
|
4981
|
+
prompt,
|
|
4982
|
+
...(customTools.length > 0
|
|
4983
|
+
? {
|
|
4984
|
+
writerToolPolicy: {
|
|
4985
|
+
requireSdk: true,
|
|
4986
|
+
customTools,
|
|
4987
|
+
},
|
|
4988
|
+
}
|
|
4989
|
+
: {}),
|
|
4990
|
+
});
|
|
4991
|
+
try {
|
|
4992
|
+
await input.flushLedger();
|
|
4993
|
+
lastDurableCount = input.committedFactCount();
|
|
4994
|
+
}
|
|
4995
|
+
catch {
|
|
4996
|
+
// best-effort: the node-level flush runs again after the attempt
|
|
4997
|
+
}
|
|
4998
|
+
return result;
|
|
4512
4999
|
});
|
|
4513
5000
|
accumulated = accumulated
|
|
4514
5001
|
? combineSequentialPiResults(accumulated, result)
|
|
4515
5002
|
: result;
|
|
4516
|
-
try {
|
|
4517
|
-
await input.flushLedger();
|
|
4518
|
-
}
|
|
4519
|
-
catch {
|
|
4520
|
-
// best-effort: the node-level flush runs again after the attempt
|
|
4521
|
-
}
|
|
4522
5003
|
const committedAfter = input.committedFactCount();
|
|
4523
5004
|
const committedFactsOnlySuccess = session.id !== "finalize" &&
|
|
4524
5005
|
!(result.assistantText ?? "").trim() &&
|
|
@@ -4565,10 +5046,10 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4565
5046
|
committedFacts: input.committedFacts(),
|
|
4566
5047
|
behaviorRequiredRequirementIds: input.behaviorRequiredRequirementIds,
|
|
4567
5048
|
})
|
|
4568
|
-
: session.id
|
|
5049
|
+
: session.id.startsWith("global-mock-data") && allRequirementIds.length > 0 && input.committedFacts
|
|
4569
5050
|
? collectFrontendPlanPhaseMissingFacts({
|
|
4570
5051
|
phase: "global-mock-data",
|
|
4571
|
-
requirementIds: allRequirementIds,
|
|
5052
|
+
requirementIds: session.requirementSlice ?? allRequirementIds,
|
|
4572
5053
|
committedFacts: input.committedFacts(),
|
|
4573
5054
|
})
|
|
4574
5055
|
: [];
|
|
@@ -4605,7 +5086,96 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4605
5086
|
? buildCompactFinalizePrompt(missing)
|
|
4606
5087
|
: isUxLocalSession
|
|
4607
5088
|
? buildUxPrompt(session.requirementSlice ?? [], missing)
|
|
4608
|
-
: buildPhasePrompt(globalMockDataSegment, missing);
|
|
5089
|
+
: buildPhasePrompt(globalMockDataSegment, missing, session.requirementSlice ?? allRequirementIds);
|
|
5090
|
+
const recovery = classifyFrontendPlanRecovery({ ...result, stopReason: readWriterThinkingExhaustionEvidence(result).stopReason });
|
|
5091
|
+
if (recovery === "stop")
|
|
5092
|
+
return { ...result, ok: false };
|
|
5093
|
+
if ((recovery === "output" || (session.atomicRecovery && result.ok)) && input.committedFacts &&
|
|
5094
|
+
!(missingPhaseFacts.length === 1 && missingPhaseFacts[0].kind === "plan-requirement") &&
|
|
5095
|
+
(session.coverageOnly || isUxRegistrySession || isUxLocalSession || session.id.startsWith("global-mock-data"))) {
|
|
5096
|
+
if (missingPhaseFacts.length === 0) {
|
|
5097
|
+
// A complete validated slice does not need a successful prose turn.
|
|
5098
|
+
// This never accepts finalize or bypasses the final contract validator.
|
|
5099
|
+
accumulated = { ...accumulated, ok: true, failureCategory: "success" };
|
|
5100
|
+
index += 1;
|
|
5101
|
+
continue;
|
|
5102
|
+
}
|
|
5103
|
+
const missingIds = [...new Set(missingPhaseFacts.flatMap(item => item.requirementIds))];
|
|
5104
|
+
const scope = session.coverageSlice ?? session.requirementSlice;
|
|
5105
|
+
if (scope && missingIds.length < scope.length) {
|
|
5106
|
+
queue[index] = { ...session, missingFacts: missingPhaseFacts,
|
|
5107
|
+
...(session.coverageOnly ? { coverageSlice: missingIds } : { requirementSlice: missingIds }),
|
|
5108
|
+
retryCount: 0 };
|
|
5109
|
+
continue;
|
|
5110
|
+
}
|
|
5111
|
+
if (scope && missingIds.length > 1 && (session.coverageOnly || isUxLocalSession)) {
|
|
5112
|
+
const half = Math.ceil(missingIds.length / 2);
|
|
5113
|
+
queue.splice(index, 1, ...[missingIds.slice(0, half), missingIds.slice(half)].map(ids => ({
|
|
5114
|
+
...session, retryCount: 0,
|
|
5115
|
+
...(session.coverageOnly ? { coverageSlice: ids } : { requirementSlice: ids }),
|
|
5116
|
+
missingFacts: missingPhaseFacts.filter(item => item.requirementIds.some(id => ids.includes(id))),
|
|
5117
|
+
})));
|
|
5118
|
+
continue;
|
|
5119
|
+
}
|
|
5120
|
+
const focusStillMissing = atomicFocus && missingPhaseFacts.some(item => item.kind === atomicFocus.kind && item.id === atomicFocus.id &&
|
|
5121
|
+
item.requirementIds.join("\0") === atomicFocus.requirementIds.join("\0"));
|
|
5122
|
+
const retries = focusStillMissing ? (session.retryCount ?? 0) + 1 : 0;
|
|
5123
|
+
if (retries < 2) {
|
|
5124
|
+
queue[index] = { ...session, atomicRecovery: true, missingFacts: missingPhaseFacts, retryCount: retries };
|
|
5125
|
+
continue;
|
|
5126
|
+
}
|
|
5127
|
+
return { ...accumulated, ok: false, failureCategory: OUTPUT_LIMIT_RETRY_CATEGORY,
|
|
5128
|
+
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() };
|
|
5129
|
+
}
|
|
5130
|
+
const capacityExhausted = recovery === "output" || recovery === "context";
|
|
5131
|
+
if (capacityExhausted && !result.timedOut) {
|
|
5132
|
+
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);
|
|
5133
|
+
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)];
|
|
5134
|
+
const splitScope = (ids, allowMemberSplit) => {
|
|
5135
|
+
const groups = workGroups.map(g => g.requirementIds.filter(id => ids.includes(id))).filter(g => g.length);
|
|
5136
|
+
if (groups.length > 1) {
|
|
5137
|
+
const half = Math.ceil(groups.length / 2);
|
|
5138
|
+
return [groups.slice(0, half).flat(), groups.slice(half).flat()];
|
|
5139
|
+
}
|
|
5140
|
+
if (allowMemberSplit && ids.length > 1) {
|
|
5141
|
+
const half = Math.ceil(ids.length / 2);
|
|
5142
|
+
return [ids.slice(0, half), ids.slice(half)];
|
|
5143
|
+
}
|
|
5144
|
+
return [];
|
|
5145
|
+
};
|
|
5146
|
+
if (isCompactLocalSession) {
|
|
5147
|
+
let scopes = splitScope(missingCoverageIds, true);
|
|
5148
|
+
if (!scopes.length && missingCoverageIds.length) {
|
|
5149
|
+
if (missingCoverageIds.length === allRequirementIds.length && committedAfter === committedBefore)
|
|
5150
|
+
return failure();
|
|
5151
|
+
scopes = [missingCoverageIds];
|
|
5152
|
+
}
|
|
5153
|
+
// UX ownership is independent of coverage completion. Preserve all groups.
|
|
5154
|
+
const recovery = scopes.map((slice, i) => ({ id: `coverage-capacity-${invocationCount}-${i}`, coverageOnly: true, coverageSlice: slice, toolNames: coverageSegment.toolNames, prompt: buildCoveragePrompt(slice) }));
|
|
5155
|
+
recovery.push({ id: "ux-registry", toolNames: uxRegistrySegment.toolNames, prompt: buildUxRegistryPrompt() });
|
|
5156
|
+
recovery.push(...buildWorkBatches([...allRequirementIds]).map((slice, i) => ({ id: `ux-local-capacity-${invocationCount}-${i}`, requirementSlice: slice, toolNames: uxSegment.toolNames, prompt: buildUxPrompt(slice) })));
|
|
5157
|
+
queue.splice(index, 1, ...recovery);
|
|
5158
|
+
continue;
|
|
5159
|
+
}
|
|
5160
|
+
if (session.coverageOnly || isUxLocalSession) {
|
|
5161
|
+
const missingIds = session.coverageOnly ? missingCoverageIds : [...new Set(missingPhase.flatMap(f => f.requirementIds))];
|
|
5162
|
+
if (!missingIds.length) {
|
|
5163
|
+
index += 1;
|
|
5164
|
+
continue;
|
|
5165
|
+
}
|
|
5166
|
+
const scopes = splitScope(missingIds, Boolean(session.coverageOnly));
|
|
5167
|
+
if (scopes.length) {
|
|
5168
|
+
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 })));
|
|
5169
|
+
continue;
|
|
5170
|
+
}
|
|
5171
|
+
const originalIds = session.coverageSlice ?? session.requirementSlice ?? [];
|
|
5172
|
+
if ((missingIds.length < originalIds.length || committedAfter > committedBefore) && (session.retryCount ?? 0) < 2) {
|
|
5173
|
+
queue[index] = { ...session, ...(session.coverageOnly ? { coverageSlice: missingIds } : { requirementSlice: missingIds }), missingFacts: missingPhaseFacts, retryCount: (session.retryCount ?? 0) + 1 };
|
|
5174
|
+
continue;
|
|
5175
|
+
}
|
|
5176
|
+
}
|
|
5177
|
+
return failure();
|
|
5178
|
+
}
|
|
4609
5179
|
if (result.ok) {
|
|
4610
5180
|
if (missingPhaseFacts.length === 0) {
|
|
4611
5181
|
index += 1;
|
|
@@ -4649,24 +5219,6 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4649
5219
|
index += 1;
|
|
4650
5220
|
continue;
|
|
4651
5221
|
}
|
|
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
5222
|
// Option 5: a multi-requirement coverage batch that failed with ZERO
|
|
4671
5223
|
// new facts and no provider stderr is the upfront-reasoning burn —
|
|
4672
5224
|
// halve the slice and retry instead of failing the attempt. The compact
|
|
@@ -4681,20 +5233,24 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4681
5233
|
: undefined;
|
|
4682
5234
|
const zeroProgressBurn = coverageSlice !== undefined &&
|
|
4683
5235
|
coverageSlice.length > 1 &&
|
|
4684
|
-
committedAfter === committedBefore &&
|
|
4685
|
-
!(result.assistantText ?? "").trim() &&
|
|
4686
|
-
!result.stderr.trim() &&
|
|
5236
|
+
(capacityExhausted || (["empty-output", "unknown"].includes(result.failureCategory) && committedAfter === committedBefore && !(result.assistantText ?? "").trim() && !result.stderr.trim())) &&
|
|
4687
5237
|
!result.timedOut;
|
|
4688
5238
|
if (zeroProgressBurn && coverageSlice) {
|
|
4689
|
-
const
|
|
4690
|
-
const
|
|
4691
|
-
|
|
5239
|
+
const missingIds = input.committedFacts ? new Set(collectFrontendPlanMissingFacts({ requirementIds: coverageSlice, committedFacts: input.committedFacts() }).flatMap(f => f.requirementIds)) : undefined;
|
|
5240
|
+
const unfinished = coverageSlice.filter(id => !missingIds || missingIds.has(id));
|
|
5241
|
+
if (unfinished.length <= 1)
|
|
5242
|
+
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);
|
|
5243
|
+
const half = Math.ceil(unfinished.length / 2);
|
|
5244
|
+
const firstSlice = unfinished.slice(0, half);
|
|
5245
|
+
const secondSlice = unfinished.slice(half);
|
|
4692
5246
|
if (isCompactLocalSession) {
|
|
5247
|
+
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
5248
|
// The split halves leave compact mode: continue them as ordinary
|
|
4694
5249
|
// coverage sessions so every downstream ladder branch applies.
|
|
4695
5250
|
queue.splice(index, 1, {
|
|
4696
5251
|
...session,
|
|
4697
5252
|
id: `coverage-compact-split-1`,
|
|
5253
|
+
toolNames: coverageSegment.toolNames, retryCount: 0,
|
|
4698
5254
|
coverageOnly: true,
|
|
4699
5255
|
coverageSlice: firstSlice,
|
|
4700
5256
|
requirementSlice: firstSlice,
|
|
@@ -4702,6 +5258,7 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4702
5258
|
}, {
|
|
4703
5259
|
...session,
|
|
4704
5260
|
id: `coverage-compact-split-2`,
|
|
5261
|
+
toolNames: coverageSegment.toolNames, retryCount: 0,
|
|
4705
5262
|
coverageOnly: true,
|
|
4706
5263
|
coverageSlice: secondSlice,
|
|
4707
5264
|
requirementSlice: secondSlice,
|
|
@@ -4720,7 +5277,9 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4720
5277
|
});
|
|
4721
5278
|
continue;
|
|
4722
5279
|
}
|
|
4723
|
-
if ((
|
|
5280
|
+
if (readWriterThinkingExhaustionEvidence(result).stopReason === "length" || ["context-overflow", "context-budget-exhausted"].includes(result.failureCategory))
|
|
5281
|
+
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);
|
|
5282
|
+
if ((session.coverageOnly || isUxRegistrySession || isUxLocalSession || session.id.startsWith("global-mock-data")) &&
|
|
4724
5283
|
missingPhaseFacts.length > 0 &&
|
|
4725
5284
|
!result.stderr.trim() &&
|
|
4726
5285
|
!result.timedOut &&
|
|
@@ -4852,14 +5411,27 @@ async function runFrontendScoutParallelSessions(input) {
|
|
|
4852
5411
|
...input.sessionOptions,
|
|
4853
5412
|
sessionEventsPath: path.join(input.runDir, shardNodeId, "session-events.jsonl"),
|
|
4854
5413
|
};
|
|
4855
|
-
|
|
5414
|
+
const prompt = `${input.basePrompt}\n\n${shard.instruction}`;
|
|
5415
|
+
const tools = [...customTools, ...(input.readBudgetTools ?? [])];
|
|
5416
|
+
shardResult = await observeFrontendSession({
|
|
5417
|
+
...input.observation,
|
|
5418
|
+
phase: `scout/parallel/${shard.id}`,
|
|
5419
|
+
scopeIds: [shard.id],
|
|
5420
|
+
prompt,
|
|
5421
|
+
userMessage: sessionOptions.userMessage,
|
|
5422
|
+
customTools: tools,
|
|
5423
|
+
artifactPath: input.observation?.artifactPath ? `${input.observation.artifactPath}-${shard.id}.json` : undefined,
|
|
5424
|
+
committedCount: () => shardTools?.committedFacts().length ?? 0,
|
|
5425
|
+
durableCommittedCount: () => shardTools?.committedFacts().length ?? 0,
|
|
5426
|
+
}, observer => input.piStepFn({
|
|
4856
5427
|
...sessionOptions,
|
|
4857
|
-
|
|
5428
|
+
onAttemptObservation: observer,
|
|
5429
|
+
prompt,
|
|
4858
5430
|
writerToolPolicy: {
|
|
4859
5431
|
requireSdk: true,
|
|
4860
|
-
customTools:
|
|
5432
|
+
customTools: tools,
|
|
4861
5433
|
},
|
|
4862
|
-
});
|
|
5434
|
+
}));
|
|
4863
5435
|
await shardTools.flush();
|
|
4864
5436
|
return { shard, result: shardResult, tools: shardTools };
|
|
4865
5437
|
}
|
|
@@ -5035,6 +5607,8 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
5035
5607
|
let writerToolPolicy;
|
|
5036
5608
|
let reviewTerminalTools;
|
|
5037
5609
|
let designTerminalTools;
|
|
5610
|
+
let reviewInventory;
|
|
5611
|
+
let designInventory;
|
|
5038
5612
|
let planLedgerTools;
|
|
5039
5613
|
let contractTools;
|
|
5040
5614
|
let scoutEvidenceTools;
|
|
@@ -5128,6 +5702,8 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
5128
5702
|
const { createTypedEventStore } = await import("../workflows/dag/frontend-typed-event-store.js");
|
|
5129
5703
|
const store = createTypedEventStore();
|
|
5130
5704
|
reviewTerminalTools = await createFrontendReviewTerminalTools({
|
|
5705
|
+
inventory: reviewInventory = await loadFrontendReviewScopes(meta.runDir, "review", meta.spec?.frontendExecutionPolicy?.scopeTargetBytes ?? FRONTEND_SCOPE_TARGET_BYTES),
|
|
5706
|
+
inputDigest: reviewInventory?.digest ?? createHash("sha256").update(input.prompt).digest("hex"),
|
|
5131
5707
|
attemptId: `${meta.runId}:${input.task.id}`,
|
|
5132
5708
|
store,
|
|
5133
5709
|
runDir: meta.runDir,
|
|
@@ -5153,6 +5729,8 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
5153
5729
|
const { createTypedEventStore } = await import("../workflows/dag/frontend-typed-event-store.js");
|
|
5154
5730
|
const store = createTypedEventStore();
|
|
5155
5731
|
designTerminalTools = await createFrontendDesignTerminalTools({
|
|
5732
|
+
inventory: designInventory = await loadFrontendReviewScopes(meta.runDir, "design", meta.spec?.frontendExecutionPolicy?.scopeTargetBytes ?? FRONTEND_SCOPE_TARGET_BYTES),
|
|
5733
|
+
inputDigest: designInventory?.digest ?? createHash("sha256").update(input.prompt).digest("hex"),
|
|
5156
5734
|
attemptId: `${meta.runId}:${input.task.id}`,
|
|
5157
5735
|
store,
|
|
5158
5736
|
runDir: meta.runDir,
|
|
@@ -5178,6 +5756,7 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
5178
5756
|
const { createTypedEventStore } = await import("../workflows/dag/frontend-typed-event-store.js");
|
|
5179
5757
|
const store = createTypedEventStore();
|
|
5180
5758
|
contractTools = await createFrontendContractTools({
|
|
5759
|
+
sourceDigest: (meta.spec?.sourceBinding?.schemaVersion === 2 ? meta.spec.sourceBinding.inputDigest : undefined),
|
|
5181
5760
|
attemptId: `${meta.runId}:${input.task.id}`,
|
|
5182
5761
|
store,
|
|
5183
5762
|
runDir: meta.runDir,
|
|
@@ -5211,6 +5790,8 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
5211
5790
|
spec: meta.spec,
|
|
5212
5791
|
});
|
|
5213
5792
|
scoutEvidenceTools = await createFrontendScoutEvidenceTools({
|
|
5793
|
+
requirementIds: parseFrontendInputBlock(input.prompt, "scout")?.payload.requirements.map(r => r.id),
|
|
5794
|
+
sourceDigest: (meta.spec?.sourceBinding?.schemaVersion === 2 ? meta.spec.sourceBinding.inputDigest : undefined),
|
|
5214
5795
|
attemptId: `${meta.runId}:${input.task.id}`,
|
|
5215
5796
|
store,
|
|
5216
5797
|
runDir: meta.runDir,
|
|
@@ -5368,8 +5949,11 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
5368
5949
|
activeTools: toolNames,
|
|
5369
5950
|
});
|
|
5370
5951
|
const piSessionOptions = {
|
|
5952
|
+
reserveProviderRequest: input.reserveProviderRequest,
|
|
5953
|
+
frontendExecutionPolicy: meta.spec?.frontendExecutionPolicy,
|
|
5371
5954
|
attachedFiles: [],
|
|
5372
5955
|
modelConfig,
|
|
5956
|
+
outputLimitRecovery: { terminalToolNames: ["finalize_plan", "finalize_contract", "approve_design", "request_design_changes", "approve_review", "request_review_changes"] },
|
|
5373
5957
|
repoRoot: input.cwd,
|
|
5374
5958
|
step,
|
|
5375
5959
|
toolNames,
|
|
@@ -5409,6 +5993,15 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
5409
5993
|
workspaceRoot: input.cwd,
|
|
5410
5994
|
sourceDeclaredPaths: scoutSourceDeclaredPaths,
|
|
5411
5995
|
readBudgetTools: readBudgetTools?.customTools,
|
|
5996
|
+
observation: {
|
|
5997
|
+
artifactPath: path.join(meta.runDir, input.task.id, "session-budget", `attempt-${input.attempt ?? 1}`),
|
|
5998
|
+
runId: meta.runId,
|
|
5999
|
+
nodeId: input.task.id,
|
|
6000
|
+
taskId: meta.spec?.sourceBinding?.taskId,
|
|
6001
|
+
attempt: input.attempt ?? 1,
|
|
6002
|
+
model: input.model,
|
|
6003
|
+
sourceDigest: meta.spec?.sourceBinding?.schemaVersion === 2 ? meta.spec.sourceBinding.inputDigest : undefined,
|
|
6004
|
+
},
|
|
5412
6005
|
});
|
|
5413
6006
|
}
|
|
5414
6007
|
else if (isFrontendPlanLedgerNode(input.task) && planLedgerTools) {
|
|
@@ -5448,6 +6041,16 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
5448
6041
|
// finalize reducer. This avoids revision races while shortening the
|
|
5449
6042
|
// longest coverage phase for large plans.
|
|
5450
6043
|
const runPlanSessions = (options) => runFrontendPlanSegmentedSessions({
|
|
6044
|
+
observation: options.observation ?? {
|
|
6045
|
+
artifactPath: path.join(meta.runDir, input.task.id, "session-budget", `attempt-${input.attempt ?? 1}`),
|
|
6046
|
+
runId: meta.runId,
|
|
6047
|
+
nodeId: input.task.id,
|
|
6048
|
+
taskId: meta.spec?.sourceBinding?.taskId,
|
|
6049
|
+
attempt: input.attempt ?? 1,
|
|
6050
|
+
model: input.model,
|
|
6051
|
+
sourceDigest: meta.spec?.sourceBinding?.schemaVersion === 2 ? meta.spec.sourceBinding.inputDigest : undefined,
|
|
6052
|
+
contractDigest: meta.spec?.taskContractBinding?.canonicalHash,
|
|
6053
|
+
},
|
|
5451
6054
|
piStepFn,
|
|
5452
6055
|
sessionOptions: options.sessionOptions,
|
|
5453
6056
|
basePrompt: options.basePrompt ?? input.prompt,
|
|
@@ -5469,154 +6072,162 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
5469
6072
|
? options.ledgerTools.customTools
|
|
5470
6073
|
: options.ledgerTools.customTools.filter((tool) => typeof tool === "object" &&
|
|
5471
6074
|
tool !== null &&
|
|
5472
|
-
toolNames.has(tool.name)),
|
|
6075
|
+
(toolNames.has(tool.name) || tool.name === "read_plan_facts")),
|
|
5473
6076
|
flushLedger: () => options.ledgerTools.flush(),
|
|
5474
6077
|
});
|
|
5475
6078
|
if (planRequirementIds.length > 1) {
|
|
5476
|
-
const
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
6079
|
+
const workload = buildFrontendPlanWorkload({
|
|
6080
|
+
basePrompt: input.prompt, requirementIds: planRequirementIds,
|
|
6081
|
+
requirementCosts: planRequirementCosts, sessionOptions: piSessionOptions,
|
|
6082
|
+
});
|
|
6083
|
+
let coverageBatches;
|
|
6084
|
+
try {
|
|
6085
|
+
coverageBatches = await loadOrCreateFrontendPlanCoverageLayout({
|
|
6086
|
+
runDir: meta.runDir, nodeId: input.task.id, workload, requirementIds: planRequirementIds,
|
|
6087
|
+
binding: {
|
|
6088
|
+
runId: meta.runId, nodeId: input.task.id, sourceBinding: meta.spec.sourceBinding,
|
|
6089
|
+
skeleton: input.task.structuredContractOutput?.skeleton, writeSet: input.task.writeSet,
|
|
6090
|
+
policy: meta.spec.frontendExecutionPolicy, requirements: [...planRequirementCosts],
|
|
6091
|
+
compiledInput: workload.frozenInput, workGroups: workload.workGroups,
|
|
6092
|
+
},
|
|
5488
6093
|
});
|
|
5489
6094
|
}
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
|
|
6095
|
+
catch (error) {
|
|
6096
|
+
return { ok: false, stdout: "", durationMs: Date.now() - started, failureCategory: "tool-policy",
|
|
6097
|
+
stderr: `frontend plan coverage layout unavailable: ${error instanceof Error ? error.message : String(error)}` };
|
|
6098
|
+
}
|
|
6099
|
+
if (coverageBatches.length > 1) {
|
|
6100
|
+
const shardResults = await mapWithConcurrency(coverageBatches, piSessionOptions.frontendExecutionPolicy?.maxCoverageConcurrency ?? FRONTEND_PLAN_COVERAGE_MAX_CONCURRENCY, async (slice, index) => {
|
|
6101
|
+
let shardTools;
|
|
6102
|
+
let shardResult;
|
|
6103
|
+
try {
|
|
6104
|
+
const { createTypedEventStore } = await import("../workflows/dag/frontend-typed-event-store.js");
|
|
6105
|
+
shardTools = await createFrontendPlanLedgerTools({
|
|
6106
|
+
attemptId: `${meta.runId}:${input.task.id}:parallel:${index + 1}`,
|
|
6107
|
+
store: createTypedEventStore(),
|
|
6108
|
+
runDir: meta.runDir,
|
|
6109
|
+
nodeId: `${input.task.id}/parallel/coverage-${index + 1}`,
|
|
6110
|
+
skeleton: input.task.structuredContractOutput?.skeleton,
|
|
6111
|
+
sourceBinding: meta.spec.sourceBinding,
|
|
6112
|
+
requirementIds: slice,
|
|
6113
|
+
writeSetPatterns: input.task.writeSet,
|
|
6114
|
+
canonicalVerificationTargetIds: await resolveFrontendCanonicalVerificationTargetIds({
|
|
5506
6115
|
runDir: meta.runDir,
|
|
5507
|
-
|
|
5508
|
-
|
|
6116
|
+
}),
|
|
6117
|
+
componentNewSourceReferences: await resolveFrontendPlanNewComponentSourceReferences({
|
|
6118
|
+
cwd: input.cwd,
|
|
5509
6119
|
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"),
|
|
6120
|
+
}),
|
|
6121
|
+
});
|
|
6122
|
+
const shardSessionOptions = {
|
|
6123
|
+
...piSessionOptions,
|
|
6124
|
+
sessionEventsPath: path.join(meta.runDir, input.task.id, "parallel", `coverage-${index + 1}`, "session-events.jsonl"),
|
|
5595
6125
|
};
|
|
6126
|
+
shardResult = await runPlanSessions({
|
|
6127
|
+
ledgerTools: shardTools,
|
|
6128
|
+
sessionOptions: shardSessionOptions,
|
|
6129
|
+
requirementIds: slice,
|
|
6130
|
+
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.`,
|
|
6131
|
+
parallelCoverageOnly: true,
|
|
6132
|
+
observation: {
|
|
6133
|
+
artifactPath: path.join(meta.runDir, input.task.id, "parallel", `coverage-${index + 1}`, "session-budget", `attempt-${input.attempt ?? 1}`),
|
|
6134
|
+
runId: meta.runId,
|
|
6135
|
+
nodeId: `${input.task.id}/parallel/coverage-${index + 1}`,
|
|
6136
|
+
taskId: meta.spec?.sourceBinding?.taskId,
|
|
6137
|
+
attempt: input.attempt ?? 1,
|
|
6138
|
+
model: input.model,
|
|
6139
|
+
sourceDigest: meta.spec?.sourceBinding?.schemaVersion === 2 ? meta.spec.sourceBinding.inputDigest : undefined,
|
|
6140
|
+
contractDigest: meta.spec?.taskContractBinding?.canonicalHash,
|
|
6141
|
+
},
|
|
6142
|
+
});
|
|
6143
|
+
await shardTools.flush();
|
|
6144
|
+
return { index, result: shardResult, tools: shardTools };
|
|
5596
6145
|
}
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
6146
|
+
catch (error) {
|
|
6147
|
+
const crashMessage = `frontend plan coverage shard ${index + 1} crashed: ${error instanceof Error ? error.message : String(error)}`;
|
|
6148
|
+
return {
|
|
6149
|
+
index,
|
|
6150
|
+
tools: shardTools,
|
|
6151
|
+
result: shardResult
|
|
6152
|
+
? {
|
|
6153
|
+
...shardResult,
|
|
6154
|
+
ok: false,
|
|
6155
|
+
failureCategory: shardResult.ok
|
|
6156
|
+
? "invalid-output"
|
|
6157
|
+
: shardResult.failureCategory,
|
|
6158
|
+
stderr: [shardResult.stderr, crashMessage]
|
|
6159
|
+
.filter(Boolean)
|
|
6160
|
+
.join("\n"),
|
|
6161
|
+
}
|
|
6162
|
+
: {
|
|
6163
|
+
ok: false,
|
|
6164
|
+
assistantText: "",
|
|
6165
|
+
command: [],
|
|
6166
|
+
durationMs: 0,
|
|
6167
|
+
exitCode: null,
|
|
6168
|
+
failureCategory: "tool-policy",
|
|
6169
|
+
modelDisplay: "unknown",
|
|
6170
|
+
parsedEvents: 0,
|
|
6171
|
+
stderr: crashMessage,
|
|
6172
|
+
stdout: "",
|
|
6173
|
+
timedOut: false,
|
|
6174
|
+
attemptedModels: [],
|
|
6175
|
+
fallbackUsed: false,
|
|
6176
|
+
tokensUsed: 0,
|
|
6177
|
+
},
|
|
5602
6178
|
};
|
|
5603
6179
|
}
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
6180
|
+
}, shard => shard.result.failureCategory === "rate-limit");
|
|
6181
|
+
const coverageResult = aggregateParallelPiResults(shardResults.map((shard) => shard.result));
|
|
6182
|
+
let mergeFailure;
|
|
6183
|
+
try {
|
|
6184
|
+
// Flatten every shard's facts FIRST, then pre-reduce: shards
|
|
6185
|
+
// partition requirements but a frozen verification target can
|
|
6186
|
+
// span shards, so same-id targets must merge across shards
|
|
6187
|
+
// before adoption, not per shard.
|
|
6188
|
+
await planLedgerTools.adoptCommittedFacts(reduceParallelCoverageShardRecords(shardResults
|
|
6189
|
+
.filter((item) => item.result.ok && item.tools)
|
|
6190
|
+
.flatMap((shard) => normalizeParallelCoverageShardRecords(shard.tools.committedFacts(), shard.index + 1))));
|
|
6191
|
+
}
|
|
6192
|
+
catch (error) {
|
|
6193
|
+
mergeFailure = error;
|
|
6194
|
+
}
|
|
6195
|
+
const failedShard = shardResults.find((shard) => !shard.result.ok);
|
|
6196
|
+
if (mergeFailure) {
|
|
6197
|
+
result = {
|
|
6198
|
+
...coverageResult,
|
|
6199
|
+
ok: false,
|
|
6200
|
+
failureCategory: "invalid-output",
|
|
6201
|
+
stderr: [
|
|
6202
|
+
coverageResult.stderr,
|
|
6203
|
+
`frontend plan coverage shard merge failed: ${mergeFailure instanceof Error ? mergeFailure.message : String(mergeFailure)}`,
|
|
6204
|
+
]
|
|
6205
|
+
.filter(Boolean)
|
|
6206
|
+
.join("\n"),
|
|
6207
|
+
};
|
|
6208
|
+
}
|
|
6209
|
+
else if (failedShard) {
|
|
6210
|
+
result = {
|
|
6211
|
+
...coverageResult,
|
|
6212
|
+
ok: false,
|
|
6213
|
+
stderr: `${coverageResult.stderr}\nfrontend plan coverage shard ${failedShard.index + 1} failed before reduce`.trim(),
|
|
6214
|
+
};
|
|
5611
6215
|
}
|
|
5612
6216
|
else {
|
|
5613
|
-
|
|
6217
|
+
const reducerResult = await runPlanSessions({
|
|
5614
6218
|
ledgerTools: planLedgerTools,
|
|
5615
6219
|
sessionOptions: piSessionOptions,
|
|
5616
|
-
compactSmallPlan: true,
|
|
5617
6220
|
});
|
|
6221
|
+
result = combineSequentialPiResults(coverageResult, reducerResult);
|
|
5618
6222
|
}
|
|
5619
6223
|
}
|
|
6224
|
+
else {
|
|
6225
|
+
result = await runPlanSessions({
|
|
6226
|
+
ledgerTools: planLedgerTools,
|
|
6227
|
+
sessionOptions: piSessionOptions,
|
|
6228
|
+
compactSmallPlan: true,
|
|
6229
|
+
});
|
|
6230
|
+
}
|
|
5620
6231
|
}
|
|
5621
6232
|
else {
|
|
5622
6233
|
result = await runPlanSessions({
|
|
@@ -5626,12 +6237,31 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
5626
6237
|
});
|
|
5627
6238
|
}
|
|
5628
6239
|
}
|
|
6240
|
+
else if (contractTools) {
|
|
6241
|
+
result = await runFrontendContractSegmentedSessions({
|
|
6242
|
+
piStepFn, sessionOptions: piSessionOptions, basePrompt: input.prompt, tools: contractTools,
|
|
6243
|
+
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 },
|
|
6244
|
+
});
|
|
6245
|
+
}
|
|
6246
|
+
else if ((reviewInventory && reviewTerminalTools) || (designInventory && designTerminalTools)) {
|
|
6247
|
+
result = await runFrontendReviewSegmentedSessions({ piStepFn, sessionOptions: piSessionOptions, basePrompt: input.prompt, inventory: (reviewInventory ?? designInventory), tools: (reviewTerminalTools ?? designTerminalTools), customTools: writerToolPolicy.customTools, phase: reviewInventory ? "review" : "design",
|
|
6248
|
+
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 } });
|
|
6249
|
+
}
|
|
6250
|
+
else if (scoutEvidenceTools && parseFrontendInputBlock(input.prompt, "scout")) {
|
|
6251
|
+
result = await runFrontendScoutSegmentedSessions({ piStepFn, sessionOptions: piSessionOptions, basePrompt: input.prompt, tools: scoutEvidenceTools, customTools: writerToolPolicy.customTools,
|
|
6252
|
+
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 } });
|
|
6253
|
+
}
|
|
5629
6254
|
else {
|
|
5630
|
-
result = await
|
|
6255
|
+
result = await observeFrontendSession({
|
|
6256
|
+
artifactPath: input.task.id.startsWith("frontend-") ? path.join(meta.runDir, input.task.id, "session-budget", `attempt-${input.attempt ?? 1}.json`) : undefined,
|
|
6257
|
+
phase: input.task.id, prompt: input.prompt, userMessage: piSessionOptions.userMessage, customTools: writerToolPolicy?.customTools,
|
|
6258
|
+
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,
|
|
6259
|
+
}, observer => piStepFn({
|
|
5631
6260
|
...piSessionOptions,
|
|
6261
|
+
onAttemptObservation: observer,
|
|
5632
6262
|
prompt: input.prompt,
|
|
5633
6263
|
...(writerToolPolicy ? { writerToolPolicy } : {}),
|
|
5634
|
-
});
|
|
6264
|
+
}));
|
|
5635
6265
|
}
|
|
5636
6266
|
try {
|
|
5637
6267
|
const verificationCommandFiles = collectFrontendVerificationCommandFiles(input.prompt);
|
|
@@ -5796,8 +6426,8 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
5796
6426
|
try {
|
|
5797
6427
|
await scoutEvidenceTools?.flush?.();
|
|
5798
6428
|
}
|
|
5799
|
-
catch {
|
|
5800
|
-
|
|
6429
|
+
catch (error) {
|
|
6430
|
+
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
6431
|
}
|
|
5802
6432
|
if (mapped.ok) {
|
|
5803
6433
|
const { checkCommittedOriginFacts, readCommittedOriginFacts } = await import("../workflows/dag/frontend-shadow-dual-write.js");
|