@tea-agent/loop-agent 0.42.0-next.8 → 0.42.0
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 +56 -50
- package/dist/application/dag/run-dag.js +41 -0
- package/dist/application/task-lifecycle/advance.js +9 -4
- package/dist/build-stamp.json +3 -3
- package/dist/cli/program.js +1 -1
- package/dist/commands/dag-rerun-task.js +2 -0
- package/dist/commands/task-source-prepare.js +3 -1
- package/dist/executors/dag-pi-executor.js +1809 -213
- package/dist/executors/pi-extension-resolver.js +14 -2
- package/dist/executors/shell-executor.js +178 -73
- package/dist/shared/dag-failure-category.js +6 -0
- package/dist/task/contract/apply.js +36 -2
- package/dist/task/source-prepare/parse-intent.js +7 -0
- package/dist/worker/console/chat/chat-event-store.js +4 -2
- package/dist/worker/console/chat/pi-runtime.js +45 -2
- package/dist/worker/console/chat/resource-loader.js +4 -1
- package/dist/worker/console/chat/routes.js +28 -12
- package/dist/worker/console/chat/sdd-data-alignment.js +13 -2
- package/dist/worker/console/chat/session-store.js +5 -1
- package/dist/worker/console/chat/subagents/agent-tool.js +62 -0
- package/dist/worker/console/chat/subagents/explore-agent.js +169 -0
- package/dist/worker/console/chat/subagents/index.js +5 -0
- package/dist/worker/console/chat/subagents/orchestrator.js +273 -0
- package/dist/worker/console/chat/subagents/tool-policy.js +53 -0
- package/dist/worker/console/chat/subagents/types.js +13 -0
- package/dist/worker/console/chat/tool-preview.js +10 -0
- package/dist/worker/console/chat/tools.js +11 -1
- package/dist/worker/console/chat/turn-process.js +1 -0
- package/dist/worker/console/interview/tools.js +1 -0
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C6n9_m0P.js → abnfDiagram-N423BO3Z-B0Q0nClf.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DQh-IfZ1.js → arc-DCPjC19G.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-54NnrwUC.js → architectureDiagram-T3A2C74G-CP5n9jmG.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-pivRALGK.js → blockDiagram-VBNYF7ZC-CmaWi_Wg.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BR7OV2NJ.js → c4Diagram-5PPSVZJV-Ct5tcMfZ.js} +1 -1
- package/dist/worker/console/static/assets/channel-DAS07MdS.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-B1Aq6BcK.js → chunk-2GRJ4B5K-C0osm1Zf.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CbdU0rjo.js → chunk-2Q5K7J3B-CDWviED7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-Dqi2GJeD.js → chunk-5RXB4S5H-mtSk1-j7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BYn1Hu4R.js → chunk-5VM5RSS4-D5J9PHC7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DRLjDL0k.js → chunk-6Q2QTUOP-Begg4WAa.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CY9Xx2jV.js → chunk-GF5L2VYU-sF1AEy7T.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BNCZs7_z.js → chunk-JWPE2WC7-BqbsWXZb.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DZ8AStLL.js → chunk-KBJHAD2P-BKJrPcJ6.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DsxjYrzz.js → chunk-RYQCIY6F-CcwNMRho.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Ddx1KC1l.js → chunk-XXDRQBXY-BEJi3iIs.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Cwk-5jiW.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Cwk-5jiW.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-W9TveCnK.js → cose-bilkent-JH36ORCC-DRLgTvVu.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-l9j_ztZH.js → cynefin-VYW2F7L2-BoAVG3cs.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BMJMKi4G.js → cynefinDiagram-MW4NZA55-Bzol06hR.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-Bb4mG9pH.js → dagre-VZM6K2ZE-UmliJM77.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BMgL_Qv0.js → diagram-7IWD3JNH-BWLcJkFo.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Bvo2T4OQ.js → diagram-B4RE2ZJO-CgApOm-O.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-_5kWRN9c.js → diagram-LBJQPF4R-Bh3RgTLs.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DqdMrltM.js → diagram-Q27KOJAE-Dsh-D5nE.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-CDDYsNkp.js → diagram-UB23O5K3-Dkbbpcpb.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-c4nfnZUV.js → ebnfDiagram-BXEA7PRR-3EBaA3t3.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DnkUcNMs.js → erDiagram-JOGREHBK-Bq2Dc5ok.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-Dz0KGLZE.js → flowDiagram-UKHOOZJN-De7y55ha.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Cj1t1uka.js → ganttDiagram-PKOTCBZU-fLiDbQRh.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-tp2FrBHd.js → gitGraphDiagram-DS77QQ5N-Du617mp1.js} +1 -1
- package/dist/worker/console/static/assets/index-C0O48S_P.js +449 -0
- package/dist/worker/console/static/assets/index-CzKf4U8P.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DQwJS8DH.js → infoDiagram-6WML65LV-BwmlF-XP.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-eimCQWD4.js → ishikawaDiagram-WSZJBQD7-zsGtS59u.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Dd9Gbwgv.js → journeyDiagram-NVQOT4AX-DTTPTe3f.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Qve3cyft.js → kanban-definition-27J2QSJJ-BkXg9aP-.js} +1 -1
- package/dist/worker/console/static/assets/{linear-UXKSe36Z.js → linear-7U2ue5IE.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-CWhj4JXN.js → mermaid.core-BUuGHmWO.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-yTcwf4SJ.js → mindmap-definition-FAOFIHXS-7q6fX0Sv.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-_7qToaZP.js → pegDiagram-VL7TDLO6-CkQPJN55.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CXmrKmDm.js → pieDiagram-7S7Q4E2Y-CCal_tfX.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-hMmrJyaS.js → quadrantDiagram-CIZ2JOQS-B7bpxyAZ.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BXw8tCBe.js → railroadDiagram-AXF67PYL-CJmww7D-.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BmjgnLZl.js → requirementDiagram-LRYGKXZP-D6ldWJEv.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Bj77SPpN.js → sankeyDiagram-W5VNT64P-VGh33I9e.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-B2FDVysL.js → sequenceDiagram-SI44F4Z6-CCkDrIjU.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-UChNY9ZZ.js → sizeCapture-X5ZJPWSS-NC8f5Otb.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-wbAEkbd7.js → stateDiagram-OKZ733FA-_92ezZdF.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DyJg5gzW.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DCEkU8HR.js → swimlanes-SLNWSIFB-BdUCGtbP.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B1cw_0Uy.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-BGp7H06U.js → timeline-definition-Z64GVDOM-CdD1H8ia.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DsCIQuWR.js → vennDiagram-T6HMQDX7-Dkdk3oFo.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-cRZLP4Xe.js → wardleyDiagram-T6FBY63Y-CKu2uPPY.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CP0xLR9Y.js → xychartDiagram-ELKLHX3M-ClqAnuG8.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/chat-view-types.js +1 -1
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +23 -0
- package/dist/worker/console/static-src/operator-chat/pending-user-message.js +32 -0
- package/dist/worker/console/static-src/operator-chat/tools-catalog.js +21 -2
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +2 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +2 -0
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +30 -14
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +133 -5
- package/dist/worker/console/static-src/shell/workspace-route.js +4 -0
- package/dist/worker/observe/routes.js +4 -0
- package/dist/worker/observe/static/constants.js +22 -22
- package/dist/worker/observe/static/dag-context-reason-labels.js +19 -0
- package/dist/worker/observe/static/dag-history-labels.js +1 -0
- package/dist/worker/observe/static/dag-inspector-humanize.d.ts +16 -0
- package/dist/worker/observe/static/dag-inspector-humanize.js +339 -0
- package/dist/worker/observe/static/dag-node-purpose.js +10 -10
- package/dist/worker/observe/static/index.html +4 -4
- package/dist/worker/observe/static/prompt-restart-candidates.js +4 -2
- package/dist/worker/observe/static/relations.js +5 -5
- package/dist/worker/observe/static/styles.css +171 -0
- package/dist/worker/observe/static/views/dag-graph.js +1 -1
- package/dist/worker/observe/static/views/dag-inspector.js +329 -174
- package/dist/worker/observe/static/views/dag.js +16 -12
- package/dist/worker/observe/static/views/session-timeline.js +5 -3
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +18 -0
- package/dist/workflows/dag/backend-test-plan-protocol.js +104 -0
- package/dist/workflows/dag/backend-test-scenario-param.js +30 -1
- package/dist/workflows/dag/dag-retry-schema.js +3 -0
- package/dist/workflows/dag/frontend-contract-facts.js +130 -0
- package/dist/workflows/dag/frontend-design-policy.js +101 -16
- package/dist/workflows/dag/frontend-implementation-contract.js +410 -41
- package/dist/workflows/dag/frontend-plan-render.js +13 -2
- package/dist/workflows/dag/frontend-prewrite-gate.js +1 -1
- package/dist/workflows/dag/frontend-recovery-controller.js +25 -1
- package/dist/workflows/dag/frontend-recovery-plan.js +6 -2
- package/dist/workflows/dag/frontend-recovery-run.js +169 -15
- package/dist/workflows/dag/frontend-risk.js +2 -0
- package/dist/workflows/dag/frontend-shadow-dual-write.js +17 -1
- package/dist/workflows/dag/frontend-shape.js +16 -6
- package/dist/workflows/dag/frontend-test-execution-evidence.js +48 -0
- package/dist/workflows/dag/frontend-typed-event-store.js +3 -0
- package/dist/workflows/dag/frontend-verification-trace.js +38 -4
- package/dist/workflows/dag/frontend-writer-admission.js +46 -12
- package/dist/workflows/dag/init-hybrid.js +59 -29
- package/dist/workflows/dag/node-execution.js +277 -85
- package/dist/workflows/dag/recovery-lease.js +106 -16
- package/dist/workflows/dag/rerun-feedback.js +60 -0
- package/dist/workflows/dag/rerun-plan.js +90 -4
- package/dist/workflows/dag/rerun-run.js +28 -6
- package/dist/workflows/dag/rerun-task.js +224 -15
- package/dist/workflows/dag/retry-policy.js +27 -10
- package/dist/workflows/dag/runner-exit-diagnostics.js +125 -0
- package/dist/workflows/dag/runner.js +238 -29
- package/dist/workflows/dag/scheduler.js +21 -6
- package/dist/workflows/dag/structured-output-repair.js +4 -1
- package/dist/workflows/dag/types.js +4 -3
- package/dist/workflows/dag/validate.js +10 -8
- package/dist/workflows/dag/workspace-checkpoint.js +66 -0
- package/docs/operations/README.md +1 -0
- package/docs/templates/agent-dag.schema.json +2 -2
- package/docs/templates/backend-test-dag.json +6 -4
- package/docs/templates/frontend-implementation-contract.schema.json +34 -2
- package/harness.json +1 -1
- package/package.json +4 -5
- package/skills/frontend-plan/SKILL.md +14 -1
- package/skills/frontend-plan/references/decision-contract.md +101 -5
- package/skills/frontend-plan/references/design-decisions.md +32 -0
- package/dist/worker/console/static/assets/channel-DsZxrgqe.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CuToPeGV.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CuToPeGV.js +0 -1
- package/dist/worker/console/static/assets/index-D9Sc0f0y.js +0 -449
- package/dist/worker/console/static/assets/index-DDQc5a50.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-yPoT19ft.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-VWdarchG.js +0 -8
|
@@ -6,13 +6,14 @@ import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
|
6
6
|
import { redactSecrets, truncateUtf8Preview } from "../../shared/preview.js";
|
|
7
7
|
import { recordDecisionEnvelopeForNode, shouldPauseOnHumanEscalation, writeHumanEscalationArtifacts, } from "./decision-envelope.js";
|
|
8
8
|
import { FRONTEND_PREWRITE_RESULT_SOURCE_ARTIFACT, FRONTEND_WRITER_NODE_IDS, isFrontendWriterAuthorized, readFrontendPrewriteResult, } from "./scheduler.js";
|
|
9
|
+
import { collectVerificationCommandFiles, } from "./frontend-writer-admission.js";
|
|
9
10
|
import { writeNodeRecord, writeNodeSkillArtifacts } from "./run-store.js";
|
|
10
11
|
import { resolveContextPolicy } from "./context-policy.js";
|
|
11
12
|
import { buildDagNodePromptEnvelope, formatConvergenceFeedbackBlock, } from "./prompt.js";
|
|
12
13
|
import { persistLongNodeOutputArtifacts } from "./upstream-artifacts.js";
|
|
13
14
|
import { materializeDeclaredArtifactFacts } from "./artifact-bindings.js";
|
|
14
15
|
import { buildOutputLimitRecoverySection, loadBackendTestWriterProgressForRetry, } from "./backend-test-writer-completeness.js";
|
|
15
|
-
import { computeBackoffDelayMs, isCanonicalFinalVerifyShellRetryCandidate, isRetryablePiFailureCategory, isSafeReadOnlyPiRetryCandidate, isTargetTemplateTransientRetryNode, isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, projectFrontendNodeProtocolFailureReason, resolveFrontendPlanBackupRoute, resolveFrontendPlanRetryStep, VERIFY_SHELL_RETRY_POLICY, } from "./retry-policy.js";
|
|
16
|
+
import { computeBackoffDelayMs, isCanonicalFinalVerifyShellRetryCandidate, isRetryablePiFailureCategory, isSafeReadOnlyPiRetryCandidate, isTargetTemplateTransientRetryNode, isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, OUTPUT_LIMIT_RETRY_CATEGORY, projectFrontendNodeProtocolFailureReason, resolveFrontendPlanBackupRoute, resolveFrontendPlanRetryStep, VERIFY_SHELL_RETRY_POLICY, } from "./retry-policy.js";
|
|
16
17
|
import { applyNodeActivity, evaluateNodeLiveness, resolveLivenessPolicy, } from "./liveness-policy.js";
|
|
17
18
|
import { buildProtocolRetryInstruction, normalizeReviewVerdictAfterRetries, parseJsonReviewVerdict, validateOutputProtocol, } from "./output-protocol.js";
|
|
18
19
|
import { getStructuredContractValidator } from "./contract-output-registry.js";
|
|
@@ -20,6 +21,7 @@ import "./contract-validator-registrations.js";
|
|
|
20
21
|
import { computeNormalizedFailureFingerprint } from "./frontend-recovery-lineage.js";
|
|
21
22
|
import { parseLedgerJson } from "../../task/source-prepare/ledger.js";
|
|
22
23
|
import { readTypedEventStoreFromJsonl } from "./frontend-typed-event-store.js";
|
|
24
|
+
import { collectCanonicalStateFlowNames, extractRequiredDeliverablePaths, resolveFrontendContractRequirements, } from "./frontend-contract-facts.js";
|
|
23
25
|
import { allowedRepairReadPaths, auditRepairAttemptToolUse, buildStructuredOutputRepairPrompt, freezeStructuredOutputRepairContext, hasNonEmptyStructuredCandidate, isFrontendStructuredRepairSchemaId, isStructuredRepairableFailureCategory, persistStructuredAttemptRaw, sessionEventsByteLength, GOVERNANCE_BLOCKED_CATEGORY, STRUCTURED_REPAIR_EXHAUSTED_CATEGORY, } from "./structured-output-repair.js";
|
|
24
26
|
import { readFrontendCanonicalCandidate } from "./frontend-implementation-contract.js";
|
|
25
27
|
import { writeDagNodeJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
|
|
@@ -255,6 +257,9 @@ function frontendPlanValidationRetryGuidance(reason) {
|
|
|
255
257
|
if (/verificationTargets.*uiStates|uiStates.*verificationTargets/i.test(reason)) {
|
|
256
258
|
guidance.push("For this retry, provide verificationTargets[].uiStates as an array; use [] when the target has no named UI state.");
|
|
257
259
|
}
|
|
260
|
+
if (/duplicate|already recorded/i.test(reason)) {
|
|
261
|
+
guidance.push("For this retry, re-commit the corrected entry with the same id and replace=true; the ledger compiles the latest submission as the full replacement. A duplicate receipt is a correction invitation, not a prohibition, and backfilling a cited fact's reverse reference in-node does not re-derive committed facts.");
|
|
262
|
+
}
|
|
258
263
|
return guidance;
|
|
259
264
|
}
|
|
260
265
|
function compactRetryText(text, maxChars) {
|
|
@@ -324,6 +329,8 @@ const FRONTEND_CONTRACT_RECORD_TOOL_NAMES_LOCAL = new Set([
|
|
|
324
329
|
"record_handoff_intent",
|
|
325
330
|
"record_open_question",
|
|
326
331
|
"record_split_proposal",
|
|
332
|
+
"record_ui_state",
|
|
333
|
+
"record_required_deliverables",
|
|
327
334
|
]);
|
|
328
335
|
async function countContractRecordSubmissions(runDir, nodeId) {
|
|
329
336
|
const eventsPath = path.join(runDir, nodeId, "session-events.jsonl");
|
|
@@ -552,8 +559,9 @@ export function renderFrontendPlanInputContext(input) {
|
|
|
552
559
|
: []);
|
|
553
560
|
const contractFacts = committedFacts(input.contractRecords);
|
|
554
561
|
const scoutFacts = committedFacts(input.scoutRecords);
|
|
555
|
-
const
|
|
556
|
-
|
|
562
|
+
const requirementFacts = resolveFrontendContractRequirements(contractFacts);
|
|
563
|
+
const requiredDeliverables = extractRequiredDeliverablePaths(contractFacts);
|
|
564
|
+
const requirements = requirementFacts
|
|
557
565
|
.map((fact) => ({
|
|
558
566
|
id: planInputText(fact.id, 80),
|
|
559
567
|
text: planInputText(fact.text),
|
|
@@ -579,19 +587,28 @@ export function renderFrontendPlanInputContext(input) {
|
|
|
579
587
|
paths: fact.paths,
|
|
580
588
|
conflicts: fact.conflicts,
|
|
581
589
|
}));
|
|
590
|
+
// Authoritative UI states (contract-declared): the source's UI-state table
|
|
591
|
+
// extracted by the contract node. The planner binds these ids instead of
|
|
592
|
+
// inventing list-visibility variants.
|
|
593
|
+
const declaredUiStates = contractFacts
|
|
594
|
+
.filter((fact) => fact.kind === "ui-state-declaration" && fact.origin === "contract")
|
|
595
|
+
.map((fact) => ({
|
|
596
|
+
id: planInputText(fact.id, 80),
|
|
597
|
+
trigger: planInputText(fact.trigger),
|
|
598
|
+
observableOutcome: planInputText(fact.observableOutcome),
|
|
599
|
+
}))
|
|
600
|
+
.filter((state) => state.id !== undefined);
|
|
601
|
+
// Replay registry edits and state-flow removals/additions in commit order.
|
|
602
|
+
const committedUxNames = collectCanonicalStateFlowNames(input.planRecords ?? [], true);
|
|
603
|
+
const committedUiStateNames = [...committedUxNames.uiStateNames];
|
|
604
|
+
const committedInteractionNames = [...committedUxNames.interactionNames];
|
|
582
605
|
// Reviewer-rubric scaffold: the design reviewer re-runs the design-policy
|
|
583
606
|
// checks on the committed facts, so publish the checklist to the producer.
|
|
584
607
|
// Requirements whose contract evidence expects behavioural verification are
|
|
585
608
|
// enumerated explicitly — those are the slots the reviewer finds missing
|
|
586
609
|
// when the plan models interactions ad hoc (r8/r9 findings).
|
|
587
|
-
const behaviorRequiredIds =
|
|
588
|
-
.filter((requirement) =>
|
|
589
|
-
const fact = contractFacts.find((candidate) => candidate.kind === "requirement" &&
|
|
590
|
-
candidate.origin === "contract" &&
|
|
591
|
-
candidate.id === requirement.id);
|
|
592
|
-
const evidence = fact?.evidence;
|
|
593
|
-
return evidence?.behavior === "required";
|
|
594
|
-
})
|
|
610
|
+
const behaviorRequiredIds = requirementFacts
|
|
611
|
+
.filter((requirement) => requirement.evidence.behavior === "required")
|
|
595
612
|
.map((requirement) => requirement.id);
|
|
596
613
|
const serializeAtCap = (cap) => JSON.stringify({
|
|
597
614
|
requirements: requirements.map((requirement) => ({
|
|
@@ -599,6 +616,7 @@ export function renderFrontendPlanInputContext(input) {
|
|
|
599
616
|
text: planInputText(requirement.text, cap.text),
|
|
600
617
|
sourceFragmentIds: planInputStrings(requirement.sourceFragmentIds).slice(0, cap.array),
|
|
601
618
|
})),
|
|
619
|
+
requiredDeliverables,
|
|
602
620
|
targetSurface: targetSurface.map((surface) => ({
|
|
603
621
|
completeness: planInputText(surface.completeness, 32),
|
|
604
622
|
entrypoint: planInputText(surface.entrypoint, cap.text),
|
|
@@ -614,6 +632,18 @@ export function renderFrontendPlanInputContext(input) {
|
|
|
614
632
|
paths: planInputStrings(evidence.paths).slice(0, cap.array),
|
|
615
633
|
conflicts: planInputStrings(evidence.conflicts).slice(0, cap.array),
|
|
616
634
|
})),
|
|
635
|
+
declaredUiStates: declaredUiStates.map((state) => ({
|
|
636
|
+
id: state.id,
|
|
637
|
+
trigger: planInputText(state.trigger, cap.text),
|
|
638
|
+
observableOutcome: planInputText(state.observableOutcome, cap.text),
|
|
639
|
+
})),
|
|
640
|
+
committedUx: committedUiStateNames.length > 0 ||
|
|
641
|
+
committedInteractionNames.length > 0
|
|
642
|
+
? {
|
|
643
|
+
uiStateNames: committedUiStateNames,
|
|
644
|
+
interactionNames: committedInteractionNames,
|
|
645
|
+
}
|
|
646
|
+
: undefined,
|
|
617
647
|
});
|
|
618
648
|
let serialized = serializeAtCap(FRONTEND_PLAN_INPUT_CAP_LADDER[0]);
|
|
619
649
|
for (const cap of FRONTEND_PLAN_INPUT_CAP_LADDER.slice(1)) {
|
|
@@ -627,6 +657,7 @@ export function renderFrontendPlanInputContext(input) {
|
|
|
627
657
|
// to the minimum, and declare the degradation instead of corrupting JSON.
|
|
628
658
|
let fallback = {
|
|
629
659
|
degraded: "requirement-texts-truncated",
|
|
660
|
+
requiredDeliverables,
|
|
630
661
|
requirements: requirements.map((requirement) => ({
|
|
631
662
|
id: requirement.id,
|
|
632
663
|
text: "(truncated)",
|
|
@@ -651,20 +682,20 @@ export function renderFrontendPlanInputContext(input) {
|
|
|
651
682
|
serialized = bounded;
|
|
652
683
|
}
|
|
653
684
|
const checklistLines = [
|
|
654
|
-
"1.
|
|
655
|
-
"2. Every
|
|
656
|
-
"3. Every
|
|
657
|
-
"4.
|
|
685
|
+
"1. Record the GLOBAL UX vocabulary with record_state_registry BEFORE any record_state_flow: one stable behavior-domain name per state/interaction (e.g. planner-task-edit), never one name per AC number, and never a rename of an already-recorded concept. Coverage slices by AC; UX does not.",
|
|
686
|
+
"2. Every recorded interaction/uiState name must be in that registry, and uiState names must use the contract's declared authoritative ids (declaredUiStates below) when present.",
|
|
687
|
+
"3. Every interaction and every applicable UI state must be covered by a uiComponentChoices entry: its purpose equals the name, or the choice lists the name in covers (one choice may cover many ids). A reuse-existing decision without evidencePath is rejected; stylingStrategy alone covers nothing.",
|
|
688
|
+
"4. Every requirement marked (behavior) below needs modelled interactions plus at least one verification target that references it.",
|
|
658
689
|
"5. Verification targets may only reference UI states and requirements you actually recorded (the record_* tools reject unknown references).",
|
|
659
690
|
`Requirements requiring behavioural coverage: ${behaviorRequiredIds.length > 0 ? behaviorRequiredIds.join(", ") : "(none)"}`,
|
|
660
|
-
"6. A decision=new component must declare sourceRequirementIds and pass sourceFragmentId for the frozen PRD fragment whose section matches the component's purpose — the runtime validates that binding and derives specReference (path/section/line); the reviewer checks purpose↔citation consistency.",
|
|
691
|
+
"6. A decision=new component must declare sourceRequirementIds and pass sourceFragmentId for the frozen PRD fragment whose section matches the component's purpose — the runtime validates that binding and derives specReference (path/section/line); the reviewer checks purpose↔citation consistency. A decision=reuse-existing component must pass evidencePath pointing at the existing repo file that proves the reuse.",
|
|
661
692
|
...[...input.componentSourceCitations ?? []]
|
|
662
693
|
.filter(([id]) => behaviorRequiredIds.includes(id))
|
|
663
694
|
.flatMap(([id, citations]) => citations.map((citation) => ` ${id} + ${citation.fragmentId} → ${citation.section}${citation.line ? ` (line ${citation.line})` : ""}`)),
|
|
664
695
|
];
|
|
665
696
|
return [
|
|
666
697
|
"<frontend_plan_input>",
|
|
667
|
-
"Committed Contract/Scout facts, compiled by the runner. Treat them as the complete planning evidence.",
|
|
698
|
+
"Committed Contract/Scout facts, compiled by the runner. Treat them as the complete planning evidence. declaredUiStates are the authoritative UI states from the task source; committedUx (retry attempts) is the UX vocabulary already recorded — reuse those names, never re-invent them.",
|
|
668
699
|
serialized,
|
|
669
700
|
"Do not read upstream artifacts, task sources, or repository files. If this input cannot support a decision, record a genuine evidence gap.",
|
|
670
701
|
"</frontend_plan_input>",
|
|
@@ -691,6 +722,16 @@ export async function buildFrontendPlanInputContext(runDir, componentSourceCitat
|
|
|
691
722
|
// forbids reading anything.
|
|
692
723
|
throw new Error(`frontend-plan-input-unavailable: cannot read committed typed facts (${error instanceof Error ? error.message : String(error)})`);
|
|
693
724
|
}
|
|
725
|
+
// Retry-attempt continuity (UX slice visibility): the plan node's own
|
|
726
|
+
// committed facts are absent on the first attempt and present on retries;
|
|
727
|
+
// a missing file is normal there, not a broken pipeline.
|
|
728
|
+
let planRecords = [];
|
|
729
|
+
try {
|
|
730
|
+
planRecords = await readTypedEventStoreFromJsonl(path.join(runDir, "frontend-plan-pi", "plan-typed-facts.jsonl"));
|
|
731
|
+
}
|
|
732
|
+
catch {
|
|
733
|
+
planRecords = [];
|
|
734
|
+
}
|
|
694
735
|
const committedCount = [...contractRecords, ...scoutRecords].filter((record) => record.phase === "committed").length;
|
|
695
736
|
if (committedCount === 0) {
|
|
696
737
|
throw new Error(`frontend-plan-input-unavailable: no committed Contract/Scout facts in ${contractFactsPath} / ${scoutFactsPath}`);
|
|
@@ -698,6 +739,7 @@ export async function buildFrontendPlanInputContext(runDir, componentSourceCitat
|
|
|
698
739
|
return renderFrontendPlanInputContext({
|
|
699
740
|
contractRecords,
|
|
700
741
|
scoutRecords,
|
|
742
|
+
planRecords,
|
|
701
743
|
componentSourceCitations,
|
|
702
744
|
});
|
|
703
745
|
}
|
|
@@ -720,77 +762,27 @@ export function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFail
|
|
|
720
762
|
"</retry_instruction>",
|
|
721
763
|
].join("\n");
|
|
722
764
|
}
|
|
723
|
-
if (
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
"",
|
|
727
|
-
"<retry_instruction>",
|
|
728
|
-
"Frontend plan retry ladder step: compact-terminal-first.",
|
|
729
|
-
"Do NOT rewrite the narrative. Only adopt already staged/quarantined fresh facts, fill the missing required facts, then call finalize_plan exactly once.",
|
|
730
|
-
"Keep the existing committed ledger intact; do not re-derive already committed facts.",
|
|
731
|
-
"</retry_instruction>",
|
|
732
|
-
].join("\n");
|
|
733
|
-
}
|
|
734
|
-
if (frontendPlanRetryStep === "bounded-tool-only") {
|
|
735
|
-
return [
|
|
736
|
-
basePrompt,
|
|
737
|
-
"",
|
|
738
|
-
"<retry_instruction>",
|
|
739
|
-
"Frontend plan retry ladder step: bounded-tool-only.",
|
|
740
|
-
"Reduce reasoning and wall-clock time. Use only read / fact / terminal tools and only for the necessary missing facts, then call finalize_plan exactly once.",
|
|
741
|
-
"Do not expand scope or re-derive already committed facts.",
|
|
742
|
-
"</retry_instruction>",
|
|
743
|
-
].join("\n");
|
|
744
|
-
}
|
|
745
|
-
if (frontendPlanRetryStep === "backup-model") {
|
|
746
|
-
return [
|
|
747
|
-
basePrompt,
|
|
748
|
-
"",
|
|
749
|
-
"<retry_instruction>",
|
|
750
|
-
"Frontend plan retry ladder step: backup-model.",
|
|
751
|
-
"A backup provider/model route was selected after repeated non-converging failures. Re-derive only the missing committed facts, then call finalize_plan exactly once; do not repeat the failed strategy.",
|
|
752
|
-
"</retry_instruction>",
|
|
753
|
-
].join("\n");
|
|
754
|
-
}
|
|
755
|
-
if (previousFailureCategory === "protocol-invalid" &&
|
|
756
|
-
task.outputProtocol &&
|
|
757
|
-
previousProtocolReason) {
|
|
758
|
-
return [
|
|
759
|
-
basePrompt,
|
|
760
|
-
"",
|
|
761
|
-
buildProtocolRetryInstruction(task.outputProtocol, previousProtocolReason),
|
|
762
|
-
].join("\n");
|
|
763
|
-
}
|
|
764
|
-
if (previousFailureCategory === "review-terminal-missing") {
|
|
765
|
-
return [
|
|
766
|
-
basePrompt,
|
|
767
|
-
"",
|
|
768
|
-
"<retry_instruction>",
|
|
769
|
-
"The review emitted a verdict in response text but never committed the authoritative typed terminal tool call (approve_review / request_review_changes). The response text is NOT the authority: no branch or gate reads it.",
|
|
770
|
-
"Call exactly one typed terminal tool to finish: approve_review (implementation passes, no Critical/Important findings) or request_review_changes (with typed issueCategory, at least one evidenceRef, and non-empty findings). Do not repeat the review analysis; commit the terminal tool once and stop.",
|
|
771
|
-
"</retry_instruction>",
|
|
772
|
-
].join("\n");
|
|
773
|
-
}
|
|
774
|
-
if (previousFailureCategory === "read-burst") {
|
|
775
|
-
if (task.id !== FRONTEND_PLAN_NODE_ID) {
|
|
776
|
-
return [
|
|
777
|
-
basePrompt,
|
|
778
|
-
"",
|
|
779
|
-
"<retry_instruction>",
|
|
780
|
-
"The previous attempt exceeded its read budget. Start a fresh, evidence-minimal pass: do not re-read task sources, upstream stdout, skills, or full diff artifacts already represented by a canonical context artifact.",
|
|
781
|
-
"Read the smallest relevant summary first, then only the specific source file or diff fragment needed for the required decision. Finish the required terminal/output protocol as soon as evidence is sufficient.",
|
|
782
|
-
"</retry_instruction>",
|
|
783
|
-
].join("\n");
|
|
784
|
-
}
|
|
765
|
+
if (task.id === "generate-backend-md-plan-pi" &&
|
|
766
|
+
(previousFailureCategory === "output-too-large" ||
|
|
767
|
+
previousFailureCategory === "invalid-output")) {
|
|
785
768
|
return [
|
|
786
769
|
basePrompt,
|
|
787
770
|
"",
|
|
788
771
|
"<retry_instruction>",
|
|
789
|
-
"
|
|
790
|
-
|
|
772
|
+
"The previous backend-test plan attempt was truncated or failed its mandatory Markdown protocol.",
|
|
773
|
+
previousProtocolReason ?? "The previous plan artifact was incomplete.",
|
|
774
|
+
"Return the final Markdown artifact immediately. Do not output analysis, reasoning, source summaries, or planning narration.",
|
|
775
|
+
"Emit the complete section skeleton first, including exactly one ## Coverage Scope, exactly one ## Coverage Matrix, optional ## Scenario Partitions only when applicable, and exactly one ## Module Index with its required 8-column table and at least one module row.",
|
|
776
|
+
"After the complete skeleton exists, fill only concise table rows within the remaining output budget. Do not use code fences.",
|
|
791
777
|
"</retry_instruction>",
|
|
792
778
|
].join("\n");
|
|
793
779
|
}
|
|
780
|
+
// Repair-category guidance must outrank the retry ladder position: the
|
|
781
|
+
// ladder advances monotonically on transport failures (e.g. length →
|
|
782
|
+
// compact-terminal-first), and its "keep the committed ledger intact"
|
|
783
|
+
// instruction directly contradicts the repair action for invalid-output /
|
|
784
|
+
// truncated ledger facts (re-commit corrected record_* facts). When both
|
|
785
|
+
// apply, the model receives the repair instruction, not the rung script.
|
|
794
786
|
if (previousFailureCategory === "invalid-output" &&
|
|
795
787
|
task.structuredContractOutput &&
|
|
796
788
|
previousProtocolReason) {
|
|
@@ -804,7 +796,7 @@ export function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFail
|
|
|
804
796
|
const splitGuidance = /write-set-too-large|split the task/.test(previousProtocolReason ?? "")
|
|
805
797
|
? [
|
|
806
798
|
"",
|
|
807
|
-
"The writeSet is too large for one implement node.
|
|
799
|
+
"The writeSet is too large for one implement node. Preserve the complete requirement and file coverage. This needs a Contract-level task split, not a Plan formatting repair. Report the write-set-too-large finding and the affected paths for the controller to resume Contract/split orchestration. Plan cannot change protected targets.files or call Contract-only tools; do not invent a split tool or discard required files.",
|
|
808
800
|
]
|
|
809
801
|
: [];
|
|
810
802
|
return [
|
|
@@ -866,8 +858,96 @@ export function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFail
|
|
|
866
858
|
"</retry_instruction>",
|
|
867
859
|
].join("\n");
|
|
868
860
|
}
|
|
861
|
+
if (frontendPlanRetryStep === "compact-terminal-first") {
|
|
862
|
+
return [
|
|
863
|
+
basePrompt,
|
|
864
|
+
"",
|
|
865
|
+
"<retry_instruction>",
|
|
866
|
+
"Frontend plan retry ladder step: compact-terminal-first.",
|
|
867
|
+
"Do NOT rewrite the narrative. Only adopt already staged/quarantined fresh facts, fill the missing required facts, then call finalize_plan exactly once.",
|
|
868
|
+
"Keep the existing committed ledger intact; do not re-derive already committed facts.",
|
|
869
|
+
"</retry_instruction>",
|
|
870
|
+
].join("\n");
|
|
871
|
+
}
|
|
872
|
+
if (frontendPlanRetryStep === "bounded-tool-only") {
|
|
873
|
+
return [
|
|
874
|
+
basePrompt,
|
|
875
|
+
"",
|
|
876
|
+
"<retry_instruction>",
|
|
877
|
+
"Frontend plan retry ladder step: bounded-tool-only.",
|
|
878
|
+
"Reduce reasoning and wall-clock time. Use only read / fact / terminal tools and only for the necessary missing facts, then call finalize_plan exactly once.",
|
|
879
|
+
"Do not expand scope or re-derive already committed facts.",
|
|
880
|
+
"</retry_instruction>",
|
|
881
|
+
].join("\n");
|
|
882
|
+
}
|
|
883
|
+
if (frontendPlanRetryStep === "backup-model") {
|
|
884
|
+
return [
|
|
885
|
+
basePrompt,
|
|
886
|
+
"",
|
|
887
|
+
"<retry_instruction>",
|
|
888
|
+
"Frontend plan retry ladder step: backup-model.",
|
|
889
|
+
"A backup provider/model route was selected after repeated non-converging failures. Re-derive only the missing committed facts, then call finalize_plan exactly once; do not repeat the failed strategy.",
|
|
890
|
+
"</retry_instruction>",
|
|
891
|
+
].join("\n");
|
|
892
|
+
}
|
|
893
|
+
if (previousFailureCategory === "protocol-invalid" &&
|
|
894
|
+
task.outputProtocol &&
|
|
895
|
+
previousProtocolReason) {
|
|
896
|
+
return [
|
|
897
|
+
basePrompt,
|
|
898
|
+
"",
|
|
899
|
+
buildProtocolRetryInstruction(task.outputProtocol, previousProtocolReason),
|
|
900
|
+
].join("\n");
|
|
901
|
+
}
|
|
902
|
+
if (previousFailureCategory === "review-terminal-missing") {
|
|
903
|
+
return [
|
|
904
|
+
basePrompt,
|
|
905
|
+
"",
|
|
906
|
+
"<retry_instruction>",
|
|
907
|
+
"The review emitted a verdict in response text but never committed the authoritative typed terminal tool call (approve_review / request_review_changes). The response text is NOT the authority: no branch or gate reads it.",
|
|
908
|
+
"Call exactly one typed terminal tool to finish: approve_review (implementation passes, no Critical/Important findings) or request_review_changes (with typed issueCategory, at least one evidenceRef, and non-empty findings). Do not repeat the review analysis; commit the terminal tool once and stop.",
|
|
909
|
+
"</retry_instruction>",
|
|
910
|
+
].join("\n");
|
|
911
|
+
}
|
|
912
|
+
if (previousFailureCategory === "read-burst") {
|
|
913
|
+
if (task.id !== FRONTEND_PLAN_NODE_ID) {
|
|
914
|
+
return [
|
|
915
|
+
basePrompt,
|
|
916
|
+
"",
|
|
917
|
+
"<retry_instruction>",
|
|
918
|
+
"The previous attempt exceeded its read budget. Start a fresh, evidence-minimal pass: do not re-read task sources, upstream stdout, skills, or full diff artifacts already represented by a canonical context artifact.",
|
|
919
|
+
"Read the smallest relevant summary first, then only the specific source file or diff fragment needed for the required decision. Finish the required terminal/output protocol as soon as evidence is sufficient.",
|
|
920
|
+
"</retry_instruction>",
|
|
921
|
+
].join("\n");
|
|
922
|
+
}
|
|
923
|
+
return [
|
|
924
|
+
basePrompt,
|
|
925
|
+
"",
|
|
926
|
+
"<retry_instruction>",
|
|
927
|
+
"Previous plan attempt issued too many read-only tool calls (read/grep/ls/find) and blew up the context window. Trust the upstream frontend-contract-pi typed requirement facts and frontend-scout-pi target surface already provided — do NOT re-read contract/scout stdout, PRD/source files, or component sources you already inspected.",
|
|
928
|
+
"Minimize discovery reads: only read what you genuinely need, once. Commit record_plan_requirement / record_plan_verification_target / record_* facts directly from the facts already in context (one tool call per message), then call finalize_plan exactly once.",
|
|
929
|
+
"</retry_instruction>",
|
|
930
|
+
].join("\n");
|
|
931
|
+
}
|
|
932
|
+
// Generic output-limit fallback. Every branch above this one carries a
|
|
933
|
+
// more precise instruction for the same capacity signal (contract repair
|
|
934
|
+
// reasons, ladder rungs — compact-terminal-first is the reason-mandated
|
|
935
|
+
// rung for length-before-terminal —, protocol/review/read-burst repair),
|
|
936
|
+
// so output-limit must not shadow them.
|
|
937
|
+
if (previousFailureCategory === OUTPUT_LIMIT_RETRY_CATEGORY) {
|
|
938
|
+
return [
|
|
939
|
+
basePrompt,
|
|
940
|
+
"",
|
|
941
|
+
"<retry_instruction>",
|
|
942
|
+
"The previous turn ended with stopReason=length before the required output was complete. This is output-capacity truncation, not empty output.",
|
|
943
|
+
"Continue incrementally from already committed typed facts and current in-scope workspace files. Do not repeat completed discovery, decisions, facts, or writes.",
|
|
944
|
+
"Generate the smallest unfinished unit next, validate its required format immediately, repair any format error in this node, and only then continue to the next unfinished unit or terminal tool.",
|
|
945
|
+
"Keep prose minimal and finish the required terminal/output protocol as soon as the remaining work is valid.",
|
|
946
|
+
"</retry_instruction>",
|
|
947
|
+
].join("\n");
|
|
948
|
+
}
|
|
869
949
|
if (previousFailureCategory === "writer-empty-diff") {
|
|
870
|
-
const maxAttempts = task.retryPolicy?.maxAttempts ??
|
|
950
|
+
const maxAttempts = task.retryPolicy?.maxAttempts ?? 5;
|
|
871
951
|
// When a completeness progress exists for this writer, fold the concrete
|
|
872
952
|
// target paths into the empty-diff retry so the model does not guess and
|
|
873
953
|
// does not need to read a forbidden `.harness/**` evidence file.
|
|
@@ -895,7 +975,7 @@ export function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFail
|
|
|
895
975
|
].join("\n");
|
|
896
976
|
}
|
|
897
977
|
if (previousFailureCategory === "incomplete-write-set") {
|
|
898
|
-
const maxAttempts = task.retryPolicy?.maxAttempts ??
|
|
978
|
+
const maxAttempts = task.retryPolicy?.maxAttempts ?? 5;
|
|
899
979
|
const bindingOnly = task.id?.startsWith("generate-backend-md-case-") === true &&
|
|
900
980
|
(recoveryTargetPaths?.length ?? 0) === 1 &&
|
|
901
981
|
Object.values(recoveryDiagnostics ?? {}).flat().some((detail) => /(?:unclassified Test Points|duplicate Test Point bindings)/i.test(detail));
|
|
@@ -1282,6 +1362,52 @@ export async function executeDagNode(input) {
|
|
|
1282
1362
|
await skipFrontendWriter(record);
|
|
1283
1363
|
return;
|
|
1284
1364
|
}
|
|
1365
|
+
// The admission artifact is the effective authorization boundary. Never
|
|
1366
|
+
// leave the writer using the broad task glob after the shell has frozen a
|
|
1367
|
+
// concrete set: doing so makes the receipt auditable but unenforceable.
|
|
1368
|
+
// Files referenced by frozen verification commands are unioned in: the
|
|
1369
|
+
// plan's verification targets do not always name verification
|
|
1370
|
+
// infrastructure, yet verify-shell cannot run without it and the writer
|
|
1371
|
+
// must be authorized to create it.
|
|
1372
|
+
const admissionWriteSetEntries = admission.result.writeSet.map((entry) => entry.trim().replace(/\\/g, "/").replace(/^\.\//, ""));
|
|
1373
|
+
const frozenVerificationBundle = spec.tasks.find((specTask) => specTask.shell?.frontendVerificationBundle)?.shell?.frontendVerificationBundle;
|
|
1374
|
+
const verificationCommandFiles = frozenVerificationBundle
|
|
1375
|
+
? collectVerificationCommandFiles([
|
|
1376
|
+
...(frozenVerificationBundle.staticCommands ?? []),
|
|
1377
|
+
...(frozenVerificationBundle.behaviorCommands ?? []),
|
|
1378
|
+
...(frozenVerificationBundle.mockCommands ?? []),
|
|
1379
|
+
...(frozenVerificationBundle.lintCommands ?? []),
|
|
1380
|
+
])
|
|
1381
|
+
: [];
|
|
1382
|
+
const extraVerificationFiles = verificationCommandFiles.filter((file) => !admissionWriteSetEntries.includes(file) &&
|
|
1383
|
+
file &&
|
|
1384
|
+
!file.includes("*") &&
|
|
1385
|
+
!file.includes("?") &&
|
|
1386
|
+
!file.split("/").some((segment) => segment === "..") &&
|
|
1387
|
+
task.allowedPaths.some((allowed) => pathMatchesPattern(file, allowed)) &&
|
|
1388
|
+
!task.forbiddenPaths.some((forbidden) => pathMatchesPattern(file, forbidden)));
|
|
1389
|
+
const admittedWriteSet = [
|
|
1390
|
+
...new Set([...admissionWriteSetEntries, ...extraVerificationFiles]),
|
|
1391
|
+
];
|
|
1392
|
+
if (admittedWriteSet.length === 0 ||
|
|
1393
|
+
new Set(admittedWriteSet).size !== admittedWriteSet.length ||
|
|
1394
|
+
admittedWriteSet.some((entry) => !entry ||
|
|
1395
|
+
entry.includes("*") ||
|
|
1396
|
+
entry.includes("?") ||
|
|
1397
|
+
entry.split("/").some((segment) => segment === "..") ||
|
|
1398
|
+
!task.allowedPaths.some((allowed) => pathMatchesPattern(entry, allowed)) ||
|
|
1399
|
+
task.forbiddenPaths.some((forbidden) => pathMatchesPattern(entry, forbidden)))) {
|
|
1400
|
+
await failBeforePrompt(new Error("frontend writer admission contains an invalid effective writeSet"), "final-write-set-approval-invalid");
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
task = { ...task, writeSet: admittedWriteSet };
|
|
1404
|
+
node.runtimeWriteAuthorization = {
|
|
1405
|
+
schemaVersion: 1,
|
|
1406
|
+
status: "validated",
|
|
1407
|
+
approvalSourceNodeId: FRONTEND_PREWRITE_RESULT_SOURCE_ARTIFACT,
|
|
1408
|
+
approvalDigest: admission.result.admissionDigest,
|
|
1409
|
+
effectiveWriteSet: [...admittedWriteSet],
|
|
1410
|
+
};
|
|
1285
1411
|
node.frontendWriterAdmission = record;
|
|
1286
1412
|
}
|
|
1287
1413
|
let projectGovernanceContext;
|
|
@@ -1457,6 +1583,26 @@ export async function executeDagNode(input) {
|
|
|
1457
1583
|
return;
|
|
1458
1584
|
}
|
|
1459
1585
|
}
|
|
1586
|
+
if (FRONTEND_WRITER_NODE_IDS.includes(nodeId)) {
|
|
1587
|
+
// Design-review findings are not reliable in the provider's prose output
|
|
1588
|
+
// (typed terminal nodes commonly return an empty assistant message). Inject
|
|
1589
|
+
// the bounded admission capsule explicitly so an authorized retry has the
|
|
1590
|
+
// reviewer's concrete issue/evidence context.
|
|
1591
|
+
try {
|
|
1592
|
+
const admission = await readFrontendPrewriteResult(runDir);
|
|
1593
|
+
const advisory = admission.ok ? admission.result.designReviewAdvisory : undefined;
|
|
1594
|
+
if (advisory?.findings?.length || advisory?.evidenceRefs?.length) {
|
|
1595
|
+
prompt = `${prompt}\n\n<design_review_findings>\n${JSON.stringify({
|
|
1596
|
+
verdict: advisory.verdict,
|
|
1597
|
+
findings: advisory.findings.slice(0, 16),
|
|
1598
|
+
evidenceRefs: advisory.evidenceRefs.slice(0, 16),
|
|
1599
|
+
})}\n</design_review_findings>\nAddress every Critical/Important finding before writing.`;
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
catch {
|
|
1603
|
+
// Admission is already enforced above; prompt enrichment is best effort.
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1460
1606
|
node.resolvedSkills = resolvedSkills;
|
|
1461
1607
|
await writeNodeSkillArtifacts(runDir, nodeId, resolvedSkills);
|
|
1462
1608
|
let model = resolveModelForTask(task, spec.executorModels);
|
|
@@ -1568,6 +1714,9 @@ export async function executeDagNode(input) {
|
|
|
1568
1714
|
return acc;
|
|
1569
1715
|
}, {});
|
|
1570
1716
|
let attemptPrompt = buildAttemptPrompt(task, prompt, attemptNumber, previousFailureCategory, previousProtocolReason, recoveryTargetPaths, recoveryDiagnostics, frontendPlanRetryStep);
|
|
1717
|
+
if (task.id === "frontend-scout-pi" && attemptNumber > 1) {
|
|
1718
|
+
attemptPrompt = `${attemptPrompt}\n\nSCOUT RETRY (reuse existing evidence): preserve all committed target-surface/design-evidence facts and do not re-read files already covered by the prior attempt. Inspect only unresolvedPaths or missing target-surface fields, then commit the minimal correction. If the existing facts are complete, commit the same canonical facts without broad rediscovery.`;
|
|
1719
|
+
}
|
|
1571
1720
|
if (attemptNumber > 1 &&
|
|
1572
1721
|
// The plan node (frontend-plan-pi) is a typed-facts ladder task:
|
|
1573
1722
|
// its retry is driven by the §5.1 ladder (compact-terminal-first
|
|
@@ -1642,6 +1791,43 @@ export async function executeDagNode(input) {
|
|
|
1642
1791
|
durationMs: 0,
|
|
1643
1792
|
};
|
|
1644
1793
|
}
|
|
1794
|
+
// stopReason=length on top of a bare empty-output verdict is a provider
|
|
1795
|
+
// capacity signal, not a true empty response: reroute it through the
|
|
1796
|
+
// dedicated output-limit retry path while keeping the raw category for
|
|
1797
|
+
// diagnostics. Bare empty-output and transport aliases (network,
|
|
1798
|
+
// nonzero-exit, unknown) are rerouted — categories that already carry a
|
|
1799
|
+
// precise repair instruction (output-too-large, invalid-output,
|
|
1800
|
+
// protocol-invalid, structured-output-truncated via the validators below,
|
|
1801
|
+
// writer categories) keep their classification so their exact retry
|
|
1802
|
+
// guidance still reaches the model.
|
|
1803
|
+
if (task.executor === "pi" &&
|
|
1804
|
+
!result.ok &&
|
|
1805
|
+
result.stopReason === "length" &&
|
|
1806
|
+
(result.failureCategory === undefined ||
|
|
1807
|
+
["empty-output", "network", "nonzero-exit", "unknown"].includes(result.failureCategory))) {
|
|
1808
|
+
result = {
|
|
1809
|
+
...result,
|
|
1810
|
+
rawFailureCategory: result.rawFailureCategory ?? result.failureCategory,
|
|
1811
|
+
failureCategory: OUTPUT_LIMIT_RETRY_CATEGORY,
|
|
1812
|
+
stderr: [
|
|
1813
|
+
result.stderr,
|
|
1814
|
+
"output-limit: stopReason=length; preserve completed work and retry only the unfinished output",
|
|
1815
|
+
]
|
|
1816
|
+
.filter(Boolean)
|
|
1817
|
+
.join("\n"),
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
if (task.id === "generate-backend-md-plan-pi" &&
|
|
1821
|
+
!result.ok &&
|
|
1822
|
+
(result.failureCategory === "output-too-large" ||
|
|
1823
|
+
result.failureCategory === "invalid-output")) {
|
|
1824
|
+
const marker = "backend-test Markdown plan";
|
|
1825
|
+
const markerIndex = result.stderr?.lastIndexOf(marker) ?? -1;
|
|
1826
|
+
previousProtocolReason =
|
|
1827
|
+
markerIndex >= 0
|
|
1828
|
+
? result.stderr.slice(markerIndex, markerIndex + 4_000).trim()
|
|
1829
|
+
: `backend-test Markdown plan attempt failed with ${result.failureCategory}`;
|
|
1830
|
+
}
|
|
1645
1831
|
if (isFrontendStructuredRepairSchemaId(task.structuredContractOutput?.schemaId)) {
|
|
1646
1832
|
const rawText = canonicalNodeOutput(result);
|
|
1647
1833
|
if (rawText.trim().length > 0) {
|
|
@@ -1764,7 +1950,7 @@ export async function executeDagNode(input) {
|
|
|
1764
1950
|
!result.ok &&
|
|
1765
1951
|
retryPolicy !== undefined) {
|
|
1766
1952
|
const submissions = await countContractRecordSubmissions(runDir, nodeId);
|
|
1767
|
-
if (submissions === 0) {
|
|
1953
|
+
if (submissions === 0 && result.stopReason !== "length") {
|
|
1768
1954
|
result = {
|
|
1769
1955
|
...result,
|
|
1770
1956
|
failureCategory: "empty-output",
|
|
@@ -1793,6 +1979,9 @@ export async function executeDagNode(input) {
|
|
|
1793
1979
|
sdkAttempted: result.sdkAttempted,
|
|
1794
1980
|
tokensUsed: result.tokensUsed,
|
|
1795
1981
|
parsedEvents: result.parsedEvents,
|
|
1982
|
+
stopReason: result.stopReason,
|
|
1983
|
+
thinkingObserved: result.thinkingObserved,
|
|
1984
|
+
writeToolCallCount: result.writeToolCallCount,
|
|
1796
1985
|
artifactPath: `${nodeId}/attempt-${attemptNumber}.json`,
|
|
1797
1986
|
};
|
|
1798
1987
|
if (retryPolicy !== undefined) {
|
|
@@ -1874,6 +2063,9 @@ export async function executeDagNode(input) {
|
|
|
1874
2063
|
retryPolicy === undefined
|
|
1875
2064
|
? result.parsedEvents
|
|
1876
2065
|
: sumAttemptMetric(attempts, (attempt) => attempt.parsedEvents);
|
|
2066
|
+
node.stopReason = result.stopReason;
|
|
2067
|
+
node.thinkingObserved = result.thinkingObserved;
|
|
2068
|
+
node.writeToolCallCount = result.writeToolCallCount;
|
|
1877
2069
|
node.lastActivityAt = attemptFinishedAt;
|
|
1878
2070
|
if (result.failureCategory === "termination-unconfirmed") {
|
|
1879
2071
|
node.needsAttentionReason = "attempt-termination-unconfirmed";
|