@tea-agent/loop-agent 0.39.0-beta.12 → 0.39.0-beta.13
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 +26 -0
- package/dist/application/dag/generate-task-dag.js +6 -2
- package/dist/build-stamp.json +3 -3
- package/dist/commands/client-recovery.js +8 -36
- package/dist/executors/dag-pi-executor.js +343 -40
- package/dist/executors/pi-executor.js +8 -4
- package/dist/executors/pi-sdk-executor.js +33 -5
- package/dist/executors/shell-executor.js +102 -32
- package/dist/governance/checks.js +1 -0
- package/dist/shared/pi-context-pressure/checkpoint.js +116 -0
- package/dist/shared/pi-context-pressure/compaction-policy.js +151 -0
- package/dist/shared/pi-context-pressure/env.js +58 -0
- package/dist/shared/pi-context-pressure/extension.js +100 -0
- package/dist/shared/pi-context-pressure/index.js +7 -0
- package/dist/shared/pi-context-pressure/overflow.js +252 -0
- package/dist/shared/pi-context-pressure/sift-bridge.js +386 -0
- package/dist/shared/pi-context-pressure/telemetry.js +51 -0
- package/dist/task/frontend-project-capability.js +3 -1
- package/dist/task/source-prepare/fragment-inventory.js +4 -1
- package/dist/worker/console/chat/pi-runtime.js +146 -5
- package/dist/worker/console/chat/provider-error.js +2 -1
- package/dist/worker/console/chat/routes.js +3 -0
- package/dist/worker/console/chat/sift-bridge.js +1 -0
- package/dist/worker/console/dag-execution-receipt.js +20 -2
- package/dist/worker/console/operator-actions.js +4 -3
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-CXj_GnSb.js → abnfDiagram-N423BO3Z-DC863mud.js} +1 -1
- package/dist/worker/console/static/assets/{arc-BZp6JAp7.js → arc-CftC38G9.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DfpcEuYU.js → architectureDiagram-T3A2C74G-B3PAPQCw.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-_Gf0xadb.js → blockDiagram-VBNYF7ZC-C9UDJNRv.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Ct2QPCmv.js → c4Diagram-5PPSVZJV--BJ76fv_.js} +1 -1
- package/dist/worker/console/static/assets/channel-CUz-Bg86.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BfklkzKl.js → chunk-2GRJ4B5K--hiIqoGp.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-Dy25vZJV.js → chunk-2Q5K7J3B-DgTzpIa3.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BFlCRZep.js → chunk-5RXB4S5H-DIwpJziP.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-op3oVxIE.js → chunk-5VM5RSS4-BJzbdUi0.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-C0R7rzF2.js → chunk-6Q2QTUOP-BbGouI1Z.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Bt44TCGy.js → chunk-GF5L2VYU-B9247w69.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-_uEE_XFx.js → chunk-JWPE2WC7-CXob_wYy.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-C3TOYGZ9.js → chunk-KBJHAD2P-C6FUel1g.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-Cz60oBPV.js → chunk-RYQCIY6F-DGKRYKHu.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-8Bik0qis.js → chunk-XXDRQBXY-kNBqNPfx.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BhlUacmD.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BhlUacmD.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-C2QIOA4a.js → cose-bilkent-JH36ORCC--xmkDwfD.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CSH_yUUd.js → cynefin-VYW2F7L2-Cw5FMMuG.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BDLw2XFM.js → cynefinDiagram-MW4NZA55-CuLslt2t.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CcD9ZtF4.js → dagre-VZM6K2ZE-D9ngqrs2.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DqwtkBsI.js → diagram-7IWD3JNH-RDRSbHQp.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BWVEcqsC.js → diagram-B4RE2ZJO-BgQ1P9aV.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-M-WAbtQi.js → diagram-LBJQPF4R-D3WWyPtn.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DKW7SixP.js → diagram-Q27KOJAE-L2k28wTR.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-PHHPPtrj.js → diagram-UB23O5K3-DDNrA5Qw.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BQD-B4RX.js → ebnfDiagram-BXEA7PRR-BaRFCOdM.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BFvULb51.js → erDiagram-JOGREHBK-CM33DVVM.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-Bw-aXTCb.js → flowDiagram-UKHOOZJN-CVBSjOxe.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BGKw04Qx.js → ganttDiagram-PKOTCBZU-BdscewE_.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-RqKaPR-I.js → gitGraphDiagram-DS77QQ5N-HRuSZb7g.js} +1 -1
- package/dist/worker/console/static/assets/{index-B_D8rbWc.js → index-B9JJQsVK.js} +102 -72
- package/dist/worker/console/static/assets/index-rWaGv4jz.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-YO5dnzrJ.js → infoDiagram-6WML65LV-cYfHvfAR.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-Bi1VZHMq.js → ishikawaDiagram-WSZJBQD7-CZmaoOuy.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Bszls1DD.js → journeyDiagram-NVQOT4AX-ntYijh7g.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-PhzeaZ09.js → kanban-definition-27J2QSJJ-Cz3b0DeD.js} +1 -1
- package/dist/worker/console/static/assets/{linear-BsjbDoXi.js → linear-DvGonpsP.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-0B7NnWKk.js → mermaid.core-B-3vjfyW.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BJr4Fj-q.js → mindmap-definition-FAOFIHXS-5iKxlsX3.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-moC4fpGB.js → pegDiagram-VL7TDLO6-C8BUwapW.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BEw37-2c.js → pieDiagram-7S7Q4E2Y-B2rPoQQG.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Cq6LyasU.js → quadrantDiagram-CIZ2JOQS-jDeVUAy4.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-DUCMcK0D.js → railroadDiagram-AXF67PYL-DV140Dwm.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-C3upTZm7.js → requirementDiagram-LRYGKXZP-CnYgHtYC.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BI_gMsCW.js → sankeyDiagram-W5VNT64P-SIK3CdWw.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-YFOIRzfN.js → sequenceDiagram-SI44F4Z6-DW_vZix7.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-dOnB7UDD.js → sizeCapture-X5ZJPWSS-NBAtkg0C.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BXUniaIh.js → stateDiagram-OKZ733FA-Bi1bQxpi.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DjWKYjQ1.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-2tA4wTNu.js → swimlanes-SLNWSIFB-8PT_uP_i.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B4cdm7bH.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DO0HkJXC.js → timeline-definition-Z64GVDOM-CD0ZZPk0.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DvIOzixv.js → vennDiagram-T6HMQDX7-BbEgWdhK.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DXDZ0cTj.js → wardleyDiagram-T6FBY63Y-DCwPKdLl.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-B32Ark0D.js → xychartDiagram-ELKLHX3M-sfC5PcNg.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/observe/static/styles.css +9 -0
- package/dist/worker/observe/static/views/dag-inspector.js +40 -0
- package/dist/worker/observe/static/views/session-timeline.js +135 -0
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +157 -7
- package/dist/workflows/dag/backend-test-pytest-collection.js +70 -2
- package/dist/workflows/dag/backend-test-result-contract.js +4 -0
- package/dist/workflows/dag/backend-test-scenario-param.js +339 -53
- package/dist/workflows/dag/backend-test-writer-completeness.js +11 -0
- package/dist/workflows/dag/dag-retry-schema.js +138 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +118 -22
- package/dist/workflows/dag/frontend-shadow-dual-write.js +1 -1
- package/dist/workflows/dag/frontend-writer-admission.js +13 -0
- package/dist/workflows/dag/init-hybrid.js +7 -3
- package/dist/workflows/dag/node-execution.js +277 -3
- package/dist/workflows/dag/rerun-feedback.js +135 -3
- package/dist/workflows/dag/retry-policy.js +13 -122
- package/dist/workflows/dag/types.js +6 -1
- package/docs/architecture/runtime-boundaries.md +2 -1
- package/docs/templates/backend-test-dag.json +4 -3
- package/package.json +4 -3
- package/dist/worker/console/static/assets/channel-3TxJgYaH.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BHIkXpp3.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BHIkXpp3.js +0 -1
- package/dist/worker/console/static/assets/index-BdNx6fj0.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cdi6UhLa.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D-RJBbb0.js +0 -8
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { readFile, stat } from "node:fs/promises";
|
|
4
4
|
import { writeDagNodeJsonArtifact, writeTextArtifactFile, } from "../infrastructure/harness/artifact-store.js";
|
|
5
5
|
import { writeJsonAtomic } from "../infrastructure/harness/atomic-write.js";
|
|
6
6
|
import { mapContractBlockedOwner } from "../workflows/dag/frontend-human-decision.js";
|
|
@@ -15,6 +15,7 @@ import { parseLedgerJson } from "../task/source-prepare/ledger.js";
|
|
|
15
15
|
import { redactPromptForLog, truncateOutput, } from "../shared/output-truncation.js";
|
|
16
16
|
import { GitStatusUnavailableError, pathsChangedDuringRun, readGitStatusPorcelain, recoverRootNulArtifact, snapshotGitStatusPathFingerprints, snapshotGitStatusPorcelain, validateShellWriteGuard, } from "./shell-write-guard.js";
|
|
17
17
|
import { captureWorkspaceWriteSnapshot, diffWorkspaceWriteSnapshots, } from "./workspace-write-snapshot.js";
|
|
18
|
+
import { pathMatchesPattern } from "../shared/git-progress.js";
|
|
18
19
|
import { isTargetTemplateTransientRetryNode, isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, INCOMPLETE_WRITE_SET_RETRY_CATEGORY, WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY, WRITER_EMPTY_DIFF_RETRY_CATEGORY, } from "../workflows/dag/retry-policy.js";
|
|
19
20
|
import { assessBackendTestMdPlanCompleteness, assessBackendTestMdWriterCompleteness, assessBackendTestPytestPlanCompleteness, assessBackendTestPytestWriterCompleteness, assessBackendTestShardChildCompleteness, backendTestWriterProgressRoleForTask, classifyBackendTestWriterCompletenessFailure, isBackendTestCompletenessRetryCandidate, isBackendTestMdPlanTask, isBackendTestPytestPlanTask, isBackendTestShardChildTask, writeBackendTestWriterProgressArtifacts, } from "../workflows/dag/backend-test-writer-completeness.js";
|
|
20
21
|
import { resolveBackendTestLayout } from "../workflows/dag/backend-test-layout.js";
|
|
@@ -235,6 +236,16 @@ export const FRONTEND_PLAN_RECORD_TOOL_NAMES = [
|
|
|
235
236
|
];
|
|
236
237
|
export const FRONTEND_PLAN_TERMINAL_TOOL_NAMES = ["finalize_plan"];
|
|
237
238
|
export const FRONTEND_PLAN_ADOPT_TOOL_NAMES = ["adopt_staged_fact"];
|
|
239
|
+
/**
|
|
240
|
+
* Inter-call delay for high-frequency frontend record_* tools. Incremental
|
|
241
|
+
* submission (one model response per 1-5 entries) means dozens of sequential
|
|
242
|
+
* API round trips in a few minutes; third-party gateways (huu.dqy.ink) trip
|
|
243
|
+
* rate limits whose windows exceed normal API RPM even at ~13 calls/min. A
|
|
244
|
+
* short fixed delay before each tool body keeps the sustained rate under
|
|
245
|
+
* typical thresholds while batching cuts the total call count.
|
|
246
|
+
*/
|
|
247
|
+
const FRONTEND_RECORD_TOOL_THROTTLE_MS = 1000;
|
|
248
|
+
const sleepRecordThrottle = () => new Promise((resolve) => setTimeout(resolve, FRONTEND_RECORD_TOOL_THROTTLE_MS));
|
|
238
249
|
/** M5: `frontend-review-pi` emits its authoritative terminal verdict through
|
|
239
250
|
* committed typed tools instead of the legacy JSON verdict parse. */
|
|
240
251
|
export function isFrontendReviewTypedTerminalNode(task) {
|
|
@@ -261,6 +272,45 @@ export function isFrontendScoutEvidenceNode(task) {
|
|
|
261
272
|
export function isFrontendPlanLedgerNode(task) {
|
|
262
273
|
return task.id === "frontend-plan-pi";
|
|
263
274
|
}
|
|
275
|
+
/** Facts-first nodes whose authoritative output is a committed typed terminal
|
|
276
|
+
* fact (not the assistant text). Downstream compilation reads the flushed
|
|
277
|
+
* `<nodeId>/<file>` store and never the node narrative, so a committed
|
|
278
|
+
* terminal means the work is done. */
|
|
279
|
+
const TYPED_TERMINAL_FACT_NODES = {
|
|
280
|
+
"frontend-contract-pi": {
|
|
281
|
+
file: "contract-typed-facts.jsonl",
|
|
282
|
+
kind: "contract-finalized",
|
|
283
|
+
},
|
|
284
|
+
"frontend-plan-pi": {
|
|
285
|
+
file: "plan-typed-facts.jsonl",
|
|
286
|
+
kind: "finalize_plan",
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
/**
|
|
290
|
+
* Accept a facts-terminal node result whose final assistant text is blank
|
|
291
|
+
* when the typed terminal fact was committed successfully. Small-output
|
|
292
|
+
* models legitimately end after the terminal tool call; without this the
|
|
293
|
+
* empty assistantText fails the node as empty-output, the failure classifier
|
|
294
|
+
* phrase-scans the whole session stream and can mislabel the committed run
|
|
295
|
+
* as rate-limit/network, and the finished ledger is thrown away for a
|
|
296
|
+
* deterministic retry that burns the full prompt budget again. Fail-closed:
|
|
297
|
+
* acceptance requires a committed terminal record from the flushed typed
|
|
298
|
+
* facts store; provider-error attempts (non-empty stderr) are never accepted
|
|
299
|
+
* by the caller.
|
|
300
|
+
*/
|
|
301
|
+
export async function acceptCommittedTypedTerminalFact(runDir, nodeId) {
|
|
302
|
+
const binding = TYPED_TERMINAL_FACT_NODES[nodeId];
|
|
303
|
+
if (!binding)
|
|
304
|
+
return false;
|
|
305
|
+
try {
|
|
306
|
+
const { readCommittedOriginFacts } = await import("../workflows/dag/frontend-shadow-dual-write.js");
|
|
307
|
+
const records = await readCommittedOriginFacts(runDir, nodeId, binding.file);
|
|
308
|
+
return records.some((record) => record.fact.kind === binding.kind);
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
264
314
|
export function resolveDagPiToolNames(task) {
|
|
265
315
|
if (isFrontendReviewTypedTerminalNode(task)) {
|
|
266
316
|
return [
|
|
@@ -277,8 +327,11 @@ export function resolveDagPiToolNames(task) {
|
|
|
277
327
|
];
|
|
278
328
|
}
|
|
279
329
|
if (isFrontendContractTypedNode(task)) {
|
|
330
|
+
// Contract is an incremental-commit node: the source-fidelity ledger is
|
|
331
|
+
// compiled into the <frontend_contract_input> block (node-execution), so
|
|
332
|
+
// no read tools — mirrors the plan node. Omitting read tools prevents a
|
|
333
|
+
// contract from spending its output budget re-reading the raw source.
|
|
280
334
|
return [
|
|
281
|
-
...DAG_PI_READONLY_TOOLS,
|
|
282
335
|
...FRONTEND_CONTRACT_RECORD_TOOL_NAMES,
|
|
283
336
|
...FRONTEND_CONTRACT_TERMINAL_TOOL_NAMES,
|
|
284
337
|
];
|
|
@@ -829,8 +882,9 @@ async function loadContractRequirementInheritance(runDir) {
|
|
|
829
882
|
}
|
|
830
883
|
/**
|
|
831
884
|
* Resolve task-source citations from the source-fidelity ledger before the
|
|
832
|
-
* planner starts. The planner names a frozen requirement id
|
|
833
|
-
* to re-read a PRD merely to recover a
|
|
885
|
+
* planner starts. The planner names a frozen requirement id and one of its
|
|
886
|
+
* fragment ids; it never needs to re-read a PRD merely to recover a
|
|
887
|
+
* path/section/line triple.
|
|
834
888
|
*/
|
|
835
889
|
async function resolveFrontendPlanNewComponentSourceReferences(input) {
|
|
836
890
|
const binding = input.sourceBinding;
|
|
@@ -848,16 +902,17 @@ async function resolveFrontendPlanNewComponentSourceReferences(input) {
|
|
|
848
902
|
const fragmentsById = new Map(ledger.fragments.map((fragment) => [fragment.id, fragment]));
|
|
849
903
|
const references = new Map();
|
|
850
904
|
for (const requirement of ledger.canonicalRequirements) {
|
|
851
|
-
const
|
|
905
|
+
const citations = requirement.sourceFragmentIds
|
|
852
906
|
.map((fragmentId) => fragmentsById.get(fragmentId))
|
|
853
|
-
.
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
references.set(requirement.id, {
|
|
907
|
+
.filter((fragment) => fragment !== undefined)
|
|
908
|
+
.map((fragment) => ({
|
|
909
|
+
fragmentId: fragment.id,
|
|
857
910
|
path: fragment.path,
|
|
858
911
|
section: fragment.headingPath,
|
|
859
912
|
line: fragment.lineRange.start,
|
|
860
|
-
});
|
|
913
|
+
}));
|
|
914
|
+
if (citations.length > 0)
|
|
915
|
+
references.set(requirement.id, citations);
|
|
861
916
|
}
|
|
862
917
|
return references;
|
|
863
918
|
}
|
|
@@ -924,7 +979,12 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
924
979
|
consumer: optionalString,
|
|
925
980
|
}, { additionalProperties: false });
|
|
926
981
|
const mockApiSchema = Type.Object({
|
|
927
|
-
strategy: Type.
|
|
982
|
+
strategy: Type.Union([
|
|
983
|
+
Type.Literal("native"),
|
|
984
|
+
Type.Literal("browser-intercept"),
|
|
985
|
+
Type.Literal("request-adapter"),
|
|
986
|
+
Type.Literal("not-needed"),
|
|
987
|
+
], {
|
|
928
988
|
description: "native | browser-intercept | request-adapter | not-needed",
|
|
929
989
|
}),
|
|
930
990
|
activation: Type.String({}),
|
|
@@ -935,11 +995,20 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
935
995
|
paths: stringArray,
|
|
936
996
|
conflicts: stringArray,
|
|
937
997
|
}, { additionalProperties: false });
|
|
998
|
+
// Enum fields use literal unions, not advisory strings: a soft Type.String
|
|
999
|
+
// lets the model commit values like type="behavior" that pass the tool
|
|
1000
|
+
// boundary, flush into the ledger, and only fail the compile-time zod enum
|
|
1001
|
+
// — a deterministic attempt failure the model could have fixed in-node.
|
|
1002
|
+
const verificationTargetTypeSchema = Type.Union([
|
|
1003
|
+
Type.Literal("static"),
|
|
1004
|
+
Type.Literal("unit"),
|
|
1005
|
+
Type.Literal("component"),
|
|
1006
|
+
Type.Literal("integration"),
|
|
1007
|
+
Type.Literal("mock"),
|
|
1008
|
+
]);
|
|
938
1009
|
const verificationTargetSchema = Type.Object({
|
|
939
1010
|
id: Type.String({}),
|
|
940
|
-
type:
|
|
941
|
-
description: "static | unit | component | integration | mock",
|
|
942
|
-
}),
|
|
1011
|
+
type: verificationTargetTypeSchema,
|
|
943
1012
|
commandLabel: Type.String({}),
|
|
944
1013
|
file: Type.String({}),
|
|
945
1014
|
symbol: Type.Optional(Type.String({
|
|
@@ -958,7 +1027,11 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
958
1027
|
const uiComponentChoiceSchema = Type.Object({
|
|
959
1028
|
purpose: Type.String({}),
|
|
960
1029
|
component: Type.String({}),
|
|
961
|
-
decision: Type.
|
|
1030
|
+
decision: Type.Union([
|
|
1031
|
+
Type.Literal("specified"),
|
|
1032
|
+
Type.Literal("reuse-existing"),
|
|
1033
|
+
Type.Literal("new"),
|
|
1034
|
+
], {
|
|
962
1035
|
description: "specified | reuse-existing | new",
|
|
963
1036
|
}),
|
|
964
1037
|
specReference: Type.Optional(Type.Object({
|
|
@@ -1033,7 +1106,7 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1033
1106
|
const recordRouteSelectionTool = defineTool({
|
|
1034
1107
|
name: "record_route_selection",
|
|
1035
1108
|
label: "record_route_selection",
|
|
1036
|
-
description: "Record the route selection needed by this plan. Repository target surface and file ownership belong to Scout/runtime.",
|
|
1109
|
+
description: "Record the route selection needed by this plan. Repository target surface and file ownership belong to Scout/runtime. Example: {\"routes\": [\"/<route>\"]}",
|
|
1037
1110
|
promptSnippet: "Record the selected routes.",
|
|
1038
1111
|
parameters: Type.Object({ routes: stringArray }, { additionalProperties: false }),
|
|
1039
1112
|
async execute(_toolCallId, params) {
|
|
@@ -1045,14 +1118,16 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1045
1118
|
const recordComponentChoiceTool = defineTool({
|
|
1046
1119
|
name: "record_component_choice",
|
|
1047
1120
|
label: "record_component_choice",
|
|
1048
|
-
description: "Record ONE component choice (origin=plan component-choice fact). Declare every UI purpose's component selection. For decision=new, pass sourceRequirementIds containing the frozen requirement ID(s) that mandate the component; the runtime derives
|
|
1049
|
-
promptSnippet: "Record
|
|
1121
|
+
description: "Record ONE component choice (origin=plan component-choice fact). Declare every UI purpose's component selection. For decision=new, pass sourceRequirementIds containing the frozen requirement ID(s) that mandate the component and sourceFragmentId selecting one frozen citation listed in the plan checklist; the runtime validates the relation and derives the exact PRD specReference. Do not read the PRD or invent a path/line. decision=reuse-existing is only for components that already exist in the repo (e.g. reusing ActiveRunBadge's styling convention). Omit rationale for reuse-existing; it is optional. Call up to 5 component choices per assistant message (batching reduces API round trips and rate-limit risk); never more than 5 per message. Optionally include stylingStrategy (set it once, on the first call). Example: {\"choice\": {\"purpose\": \"<interaction or UI state name>\", \"component\": \"<component name>\", \"decision\": \"new\"}, \"sourceRequirementIds\": [\"<AC-XXX mandating this component>\"], \"sourceFragmentId\": \"<REQ-SRC-...>\"}",
|
|
1122
|
+
promptSnippet: "Record 1-5 component choices (up to 5 per message).",
|
|
1050
1123
|
parameters: Type.Object({
|
|
1051
1124
|
choice: uiComponentChoiceSchema,
|
|
1052
1125
|
sourceRequirementIds: Type.Optional(stringArray),
|
|
1126
|
+
sourceFragmentId: optionalString,
|
|
1053
1127
|
stylingStrategy: optionalString,
|
|
1054
1128
|
}, { additionalProperties: false }),
|
|
1055
1129
|
async execute(_toolCallId, params) {
|
|
1130
|
+
await sleepRecordThrottle();
|
|
1056
1131
|
const rawChoice = params?.choice;
|
|
1057
1132
|
if (!isRecordObject(rawChoice)) {
|
|
1058
1133
|
return planToolReceipt({
|
|
@@ -1062,6 +1137,7 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1062
1137
|
});
|
|
1063
1138
|
}
|
|
1064
1139
|
const sourceRequirementIds = stringList(params?.sourceRequirementIds);
|
|
1140
|
+
const sourceFragmentId = nonEmptyString(params?.sourceFragmentId);
|
|
1065
1141
|
const choice = { ...rawChoice };
|
|
1066
1142
|
if (choice.decision === "new") {
|
|
1067
1143
|
if (sourceRequirementIds.length === 0) {
|
|
@@ -1071,17 +1147,28 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1071
1147
|
error: "decision=new requires sourceRequirementIds so runtime can materialize the task-source specReference",
|
|
1072
1148
|
});
|
|
1073
1149
|
}
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1150
|
+
if (!sourceFragmentId) {
|
|
1151
|
+
return planToolReceipt({
|
|
1152
|
+
ok: false,
|
|
1153
|
+
kind: "component-choice",
|
|
1154
|
+
error: "decision=new requires sourceFragmentId selecting a frozen task-source citation",
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
const citation = sourceRequirementIds
|
|
1158
|
+
.flatMap((id) => input.componentNewSourceReferences?.get(id) ?? [])
|
|
1159
|
+
.find((candidate) => candidate.fragmentId === sourceFragmentId);
|
|
1160
|
+
if (!citation) {
|
|
1078
1161
|
return planToolReceipt({
|
|
1079
1162
|
ok: false,
|
|
1080
1163
|
kind: "component-choice",
|
|
1081
|
-
error: `decision=new
|
|
1164
|
+
error: `decision=new sourceFragmentId ${sourceFragmentId} is not bound to sourceRequirementIds ${sourceRequirementIds.join(", ")}`,
|
|
1082
1165
|
});
|
|
1083
1166
|
}
|
|
1084
|
-
choice.specReference =
|
|
1167
|
+
choice.specReference = {
|
|
1168
|
+
path: citation.path,
|
|
1169
|
+
section: citation.section,
|
|
1170
|
+
...(citation.line !== undefined ? { line: citation.line } : {}),
|
|
1171
|
+
};
|
|
1085
1172
|
}
|
|
1086
1173
|
const components = typeof choice.component === "string" ? [choice.component] : [];
|
|
1087
1174
|
const result = await adoptPlanFact("component-choice", `${attemptId}:record_component_choice:${randomUUID()}`, {
|
|
@@ -1099,7 +1186,7 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1099
1186
|
const recordStateFlowTool = defineTool({
|
|
1100
1187
|
name: "record_state_flow",
|
|
1101
1188
|
label: "record_state_flow",
|
|
1102
|
-
description: "Record UI states and interactions as an origin=plan state-flow fact.",
|
|
1189
|
+
description: "Record UI states and interactions as an origin=plan state-flow fact. Example: {\"uiStates\": [{\"name\": \"<state>\", \"applicable\": true, \"expectedBehavior\": \"<behavior>\", \"implementationTargets\": [\"<file>\"], \"verificationTargetIds\": [\"<VT-XXX>\"]}], \"interactions\": [{\"name\": \"<interaction>\", \"trigger\": \"<user event>\", \"expectedBehavior\": \"<behavior>\", \"implementationTargets\": [\"<file>\"], \"verificationTargetIds\": [\"<VT-XXX>\"]}]}",
|
|
1103
1190
|
promptSnippet: "Record the plan state-flow fact.",
|
|
1104
1191
|
parameters: Type.Object({
|
|
1105
1192
|
uiStates: Type.Array(uiStateSchema),
|
|
@@ -1195,7 +1282,7 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1195
1282
|
const recordDataFlowTool = defineTool({
|
|
1196
1283
|
name: "record_data_flow",
|
|
1197
1284
|
label: "record_data_flow",
|
|
1198
|
-
description: "Record interaction/endpoint data flow as an origin=plan data-flow fact.",
|
|
1285
|
+
description: "Record interaction/endpoint data flow as an origin=plan data-flow fact. Example: {\"interactions\": [\"<interaction name>\"], \"endpoints\": [\"GET <path>\"]}",
|
|
1199
1286
|
promptSnippet: "Record the plan data-flow fact.",
|
|
1200
1287
|
parameters: Type.Object({ interactions: stringArray, endpoints: stringArray }, { additionalProperties: false }),
|
|
1201
1288
|
async execute(_toolCallId, params) {
|
|
@@ -1211,7 +1298,7 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1211
1298
|
const recordMockApiTool = defineTool({
|
|
1212
1299
|
name: "record_mock_api",
|
|
1213
1300
|
label: "record_mock_api",
|
|
1214
|
-
description: "Record the Mock/API strategy as an origin=plan mock-api fact.",
|
|
1301
|
+
description: "Record the Mock/API strategy as an origin=plan mock-api fact. Example: {\"mockApi\": {\"strategy\": \"not-needed\", \"activation\": \"n/a\", \"endpoints\": []}}",
|
|
1215
1302
|
promptSnippet: "Record the plan mock-api fact.",
|
|
1216
1303
|
parameters: Type.Object({ mockApi: mockApiSchema }, { additionalProperties: false }),
|
|
1217
1304
|
async execute(_toolCallId, params) {
|
|
@@ -1234,7 +1321,7 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1234
1321
|
const recordDesignDeviationTool = defineTool({
|
|
1235
1322
|
name: "record_design_deviation",
|
|
1236
1323
|
label: "record_design_deviation",
|
|
1237
|
-
description: "Record design evidence conflicts as an origin=plan design-deviation fact.",
|
|
1324
|
+
description: "Record design evidence conflicts as an origin=plan design-deviation fact. Example: {\"designEvidence\": {\"source\": \"<source>\", \"paths\": [\"<file>\"], \"conflicts\": [\"<conflicting requirement id>\"]}}",
|
|
1238
1325
|
promptSnippet: "Record the plan design-deviation fact.",
|
|
1239
1326
|
parameters: Type.Object({ designEvidence: designEvidenceSchema }, { additionalProperties: false }),
|
|
1240
1327
|
async execute(_toolCallId, params) {
|
|
@@ -1246,7 +1333,7 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1246
1333
|
const recordDependencyTool = defineTool({
|
|
1247
1334
|
name: "record_dependency",
|
|
1248
1335
|
label: "record_dependency",
|
|
1249
|
-
description: "Record the dependency policy as an origin=plan dependency fact.",
|
|
1336
|
+
description: "Record the dependency policy as an origin=plan dependency fact. Example: {\"policy\": \"<dependency policy statement>\"}",
|
|
1250
1337
|
promptSnippet: "Record the plan dependency fact.",
|
|
1251
1338
|
parameters: Type.Object({ policy: Type.String({}) }, { additionalProperties: false }),
|
|
1252
1339
|
async execute(_toolCallId, params) {
|
|
@@ -1266,12 +1353,13 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1266
1353
|
const recordPlanRequirementTool = defineTool({
|
|
1267
1354
|
name: "record_plan_requirement",
|
|
1268
1355
|
label: "record_plan_requirement",
|
|
1269
|
-
description: "Commit one plan requirement entry (origin=plan plan-requirement fact). Call once per requirement; entry carries id, implementationTargets, verificationTargetIds, and optional expectedOutcome (omit it — the runtime derives the outcome text from the contract requirement). Each requirement id must be recorded EXACTLY once — re-recording the same id is rejected as a duplicate and would compile a duplicated requirements[] entry. IMPORTANT:
|
|
1270
|
-
promptSnippet: "Commit
|
|
1356
|
+
description: "Commit one plan requirement entry (origin=plan plan-requirement fact). Call once per requirement; entry carries id, implementationTargets, verificationTargetIds, and optional expectedOutcome (omit it — the runtime derives the outcome text from the contract requirement). Each requirement id must be recorded EXACTLY once — re-recording the same id is rejected as a duplicate and would compile a duplicated requirements[] entry. IMPORTANT: batch up to 5 record_* calls per assistant message (4-5 entries per message minimizes API round trips and rate-limit risk); never batch more than 5 — a larger single message risks output truncation under a small output window. Example: {\"entry\": {\"id\": \"<AC-XXX>\", \"implementationTargets\": [\"<deliverable file>\"], \"verificationTargetIds\": [\"<VT-XXX>\"]}}",
|
|
1357
|
+
promptSnippet: "Commit 1-5 plan requirement entries (up to 5 per message).",
|
|
1271
1358
|
parameters: Type.Object({
|
|
1272
1359
|
entry: requirementSchema,
|
|
1273
1360
|
}, { additionalProperties: false }),
|
|
1274
1361
|
async execute(_toolCallId, params) {
|
|
1362
|
+
await sleepRecordThrottle();
|
|
1275
1363
|
const rawEntry = params?.entry;
|
|
1276
1364
|
if (!isRecordObject(rawEntry)) {
|
|
1277
1365
|
return planToolReceipt({
|
|
@@ -1280,7 +1368,25 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1280
1368
|
error: "record_plan_requirement requires a non-empty entry object",
|
|
1281
1369
|
});
|
|
1282
1370
|
}
|
|
1283
|
-
|
|
1371
|
+
let entry = rawEntry;
|
|
1372
|
+
// An embedded evidenceGap with a blank description means "no gap":
|
|
1373
|
+
// small-output models emit the slot defensively with description ""
|
|
1374
|
+
// on every requirement. The canonical contract schema requires a
|
|
1375
|
+
// non-empty gap description (min 1 char), so passing the empty slot
|
|
1376
|
+
// through would deterministically fail the plan compile with
|
|
1377
|
+
// invalid-output and burn every retry. Drop the empty slot — the
|
|
1378
|
+
// field is optional and the runtime derives real blocking gaps when
|
|
1379
|
+
// a requirement has no proof.
|
|
1380
|
+
const rawGap = isRecordObject(entry.evidenceGap)
|
|
1381
|
+
? entry.evidenceGap
|
|
1382
|
+
: undefined;
|
|
1383
|
+
if (rawGap &&
|
|
1384
|
+
typeof rawGap.description === "string" &&
|
|
1385
|
+
rawGap.description.trim() === "") {
|
|
1386
|
+
const { evidenceGap: _omittedGap, ...rest } = entry;
|
|
1387
|
+
void _omittedGap;
|
|
1388
|
+
entry = rest;
|
|
1389
|
+
}
|
|
1284
1390
|
// A requirement id is a canonical identity: recording it twice would
|
|
1285
1391
|
// compile a duplicate requirements[] entry and fail design review.
|
|
1286
1392
|
// Reject duplicates at the tool boundary so the model can fix them
|
|
@@ -1310,12 +1416,13 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1310
1416
|
const recordPlanVerificationTargetTool = defineTool({
|
|
1311
1417
|
name: "record_plan_verification_target",
|
|
1312
1418
|
label: "record_plan_verification_target",
|
|
1313
|
-
description: "Commit one plan verification target entry (origin=plan plan-verification-target fact). Call once per target; entry carries id, type, commandLabel, file, requirementIds, uiStates, and optional symbol (omit it — the trace gate verifies the file and command, not a symbol). IMPORTANT:
|
|
1314
|
-
promptSnippet: "Commit
|
|
1419
|
+
description: "Commit one plan verification target entry (origin=plan plan-verification-target fact). Call once per target; entry carries id, type, commandLabel, file, requirementIds, uiStates, and optional symbol (omit it — the trace gate verifies the file and command, not a symbol). IMPORTANT: batch up to 5 record_* calls per assistant message (4-5 entries per message minimizes API round trips and rate-limit risk); never batch more than 5 — a larger single message risks output truncation under a small output window. Example: {\"entry\": {\"id\": \"<VT-XXX>\", \"type\": \"unit\", \"commandLabel\": \"<frozen command label>\", \"file\": \"<test file>\", \"requirementIds\": [\"<AC-XXX>\"], \"uiStates\": []}}",
|
|
1420
|
+
promptSnippet: "Commit 1-5 plan verification target entries (up to 5 per message).",
|
|
1315
1421
|
parameters: Type.Object({
|
|
1316
1422
|
entry: verificationTargetSchema,
|
|
1317
1423
|
}, { additionalProperties: false }),
|
|
1318
1424
|
async execute(_toolCallId, params) {
|
|
1425
|
+
await sleepRecordThrottle();
|
|
1319
1426
|
const rawEntry = params?.entry;
|
|
1320
1427
|
if (!isRecordObject(rawEntry)) {
|
|
1321
1428
|
return planToolReceipt({
|
|
@@ -1324,6 +1431,52 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1324
1431
|
error: "record_plan_verification_target requires a non-empty entry object",
|
|
1325
1432
|
});
|
|
1326
1433
|
}
|
|
1434
|
+
// Belt-and-braces for providers that do not strictly enforce the
|
|
1435
|
+
// tool-schema enum: reject an invalid type here with the allowed
|
|
1436
|
+
// values so the model can re-record in-node instead of the whole
|
|
1437
|
+
// attempt dying at compile time on the strict zod enum.
|
|
1438
|
+
const verificationTargetType = rawEntry.type;
|
|
1439
|
+
if (typeof verificationTargetType !== "string" ||
|
|
1440
|
+
!["static", "unit", "component", "integration", "mock"].includes(verificationTargetType)) {
|
|
1441
|
+
return planToolReceipt({
|
|
1442
|
+
ok: false,
|
|
1443
|
+
kind: "plan-verification-target",
|
|
1444
|
+
error: `record_plan_verification_target entry.type must be one of static | unit | component | integration | mock (received ${JSON.stringify(verificationTargetType ?? null)}); for a runtime behavior check use type=mock or type=integration`,
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
// Duplicate-id rejection: committed typed facts are immutable, so
|
|
1448
|
+
// re-recording the same VT id would deadlock the compile with a
|
|
1449
|
+
// duplicate-id error the model cannot fix in-node. Reject here so
|
|
1450
|
+
// the model submits the correction under a fresh id.
|
|
1451
|
+
const vtId = typeof rawEntry.id === "string" ? rawEntry.id : "";
|
|
1452
|
+
if (vtId &&
|
|
1453
|
+
readCommittedEvents(store, attemptId).some((event) => {
|
|
1454
|
+
const fact = event.fact;
|
|
1455
|
+
if (!fact || fact.kind !== "plan-verification-target")
|
|
1456
|
+
return false;
|
|
1457
|
+
const entryFact = fact.entry;
|
|
1458
|
+
return entryFact?.id === vtId;
|
|
1459
|
+
})) {
|
|
1460
|
+
return planToolReceipt({
|
|
1461
|
+
ok: false,
|
|
1462
|
+
kind: "plan-verification-target",
|
|
1463
|
+
error: `record_plan_verification_target duplicate: verification target ${vtId} is already recorded; submit the corrected target under a new id instead`,
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
// WriteSet containment at the boundary: a committed VT fact whose
|
|
1467
|
+
// file is outside the task writeSet is immutable, and the finalize
|
|
1468
|
+
// pre-validation would then fail the whole attempt with no in-node
|
|
1469
|
+
// cure (r17 post-merge). Reject here with the allowed patterns.
|
|
1470
|
+
if (typeof rawEntry.file === "string" &&
|
|
1471
|
+
input.writeSetPatterns &&
|
|
1472
|
+
input.writeSetPatterns.length > 0 &&
|
|
1473
|
+
!input.writeSetPatterns.some((pattern) => pathMatchesPattern(rawEntry.file, pattern))) {
|
|
1474
|
+
return planToolReceipt({
|
|
1475
|
+
ok: false,
|
|
1476
|
+
kind: "plan-verification-target",
|
|
1477
|
+
error: `record_plan_verification_target file is outside the writeSet patterns [${input.writeSetPatterns.join(", ")}]: ${rawEntry.file}; verification targets must point inside the task writeSet`,
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1327
1480
|
// uiStates: [] means this verification target is intentionally not
|
|
1328
1481
|
// bound to a named UI state. Keep that canonical representation even
|
|
1329
1482
|
// when a model omits the optional tool-boundary field.
|
|
@@ -1331,6 +1484,48 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1331
1484
|
...rawEntry,
|
|
1332
1485
|
uiStates: stringList(rawEntry.uiStates),
|
|
1333
1486
|
};
|
|
1487
|
+
// Cross-reference integrity at the boundary: the compile gate
|
|
1488
|
+
// rejects verification targets referencing UI states or
|
|
1489
|
+
// requirements that were never declared. Validate against the
|
|
1490
|
+
// facts already committed in this attempt so the model fixes the
|
|
1491
|
+
// reference in-node instead of burning the attempt at compile time
|
|
1492
|
+
// (r7: one full attempt lost to a single unknown UI state name).
|
|
1493
|
+
const committedEvents = readCommittedEvents(store, attemptId);
|
|
1494
|
+
const declaredUiStateNames = new Set(committedEvents.flatMap((event) => {
|
|
1495
|
+
const fact = event.fact;
|
|
1496
|
+
if (!fact || fact.kind !== "state-flow")
|
|
1497
|
+
return [];
|
|
1498
|
+
return (Array.isArray(fact.uiStates) ? fact.uiStates : [])
|
|
1499
|
+
.map((state) => isRecordObject(state) && typeof state.name === "string"
|
|
1500
|
+
? state.name
|
|
1501
|
+
: "")
|
|
1502
|
+
.filter(Boolean);
|
|
1503
|
+
}));
|
|
1504
|
+
const unknownUiStates = entry.uiStates.filter((name) => !declaredUiStateNames.has(name));
|
|
1505
|
+
if (unknownUiStates.length > 0) {
|
|
1506
|
+
return planToolReceipt({
|
|
1507
|
+
ok: false,
|
|
1508
|
+
kind: "plan-verification-target",
|
|
1509
|
+
error: `record_plan_verification_target references UI states that were never declared: ${unknownUiStates.join(", ")}; declare every referenced UI state with record_state_flow first, or pass uiStates: [] for intentionally unbound targets`,
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
const declaredRequirementIds = new Set(committedEvents.flatMap((event) => {
|
|
1513
|
+
const fact = event.fact;
|
|
1514
|
+
if (!fact || fact.kind !== "plan-requirement")
|
|
1515
|
+
return [];
|
|
1516
|
+
const requirementEntry = fact.entry;
|
|
1517
|
+
return typeof requirementEntry?.id === "string"
|
|
1518
|
+
? [requirementEntry.id]
|
|
1519
|
+
: [];
|
|
1520
|
+
}));
|
|
1521
|
+
const unknownRequirementIds = stringList(rawEntry.requirementIds).filter((id) => !declaredRequirementIds.has(id));
|
|
1522
|
+
if (unknownRequirementIds.length > 0) {
|
|
1523
|
+
return planToolReceipt({
|
|
1524
|
+
ok: false,
|
|
1525
|
+
kind: "plan-verification-target",
|
|
1526
|
+
error: `record_plan_verification_target references unknown requirement ids: ${unknownRequirementIds.join(", ")}; record every referenced requirement with record_plan_requirement first`,
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1334
1529
|
const result = await adoptPlanFact("plan-verification-target", `${attemptId}:record_plan_verification_target:${randomUUID()}`, { kind: "plan-verification-target", origin: "plan", entry });
|
|
1335
1530
|
return planToolReceipt(result);
|
|
1336
1531
|
},
|
|
@@ -1338,12 +1533,13 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1338
1533
|
const recordPlanEvidenceGapTool = defineTool({
|
|
1339
1534
|
name: "record_plan_evidence_gap",
|
|
1340
1535
|
label: "record_plan_evidence_gap",
|
|
1341
|
-
description: "Commit one plan evidence gap entry (origin=plan plan-evidence-gap fact). Call once per gap; entry carries requirementId, description, blocking. IMPORTANT:
|
|
1342
|
-
promptSnippet: "Commit
|
|
1536
|
+
description: "Commit one plan evidence gap entry (origin=plan plan-evidence-gap fact). Call once per gap; entry carries requirementId, description, blocking. IMPORTANT: batch up to 5 record_* calls per assistant message (4-5 entries per message minimizes API round trips and rate-limit risk); never batch more than 5 — a larger single message risks output truncation under a small output window. Example: {\"entry\": {\"requirementId\": \"<AC-XXX>\", \"description\": \"<what evidence is missing and why>\", \"blocking\": false}}",
|
|
1537
|
+
promptSnippet: "Commit 1-5 plan evidence gap entries (up to 5 per message).",
|
|
1343
1538
|
parameters: Type.Object({
|
|
1344
1539
|
entry: evidenceGapSchema,
|
|
1345
1540
|
}, { additionalProperties: false }),
|
|
1346
1541
|
async execute(_toolCallId, params) {
|
|
1542
|
+
await sleepRecordThrottle();
|
|
1347
1543
|
const rawEntry = params?.entry;
|
|
1348
1544
|
if (!isRecordObject(rawEntry)) {
|
|
1349
1545
|
return planToolReceipt({
|
|
@@ -1353,6 +1549,18 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1353
1549
|
});
|
|
1354
1550
|
}
|
|
1355
1551
|
const entry = rawEntry;
|
|
1552
|
+
// A standalone evidence gap IS the gap statement: a blank description
|
|
1553
|
+
// would fail the canonical contract schema (min 1 char) after the
|
|
1554
|
+
// whole attempt finished. Reject at the boundary so the model writes
|
|
1555
|
+
// a real description in-node instead of burning the attempt.
|
|
1556
|
+
if (typeof entry.description === "string" &&
|
|
1557
|
+
entry.description.trim() === "") {
|
|
1558
|
+
return planToolReceipt({
|
|
1559
|
+
ok: false,
|
|
1560
|
+
kind: "plan-evidence-gap",
|
|
1561
|
+
error: "record_plan_evidence_gap requires a non-empty description describing the gap",
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1356
1564
|
const result = await adoptPlanFact("plan-evidence-gap", `${attemptId}:record_plan_evidence_gap:${randomUUID()}`, { kind: "plan-evidence-gap", origin: "plan", entry });
|
|
1357
1565
|
return planToolReceipt(result);
|
|
1358
1566
|
},
|
|
@@ -1385,6 +1593,66 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1385
1593
|
? { realIntegrationGap: params.realIntegrationGap }
|
|
1386
1594
|
: {}),
|
|
1387
1595
|
};
|
|
1596
|
+
// Front-load the node's compile + policy gates into the finalize
|
|
1597
|
+
// receipt (same pipeline the design-policy shell and the node
|
|
1598
|
+
// self-check run: merge the patch onto the runtime skeleton,
|
|
1599
|
+
// then the full analyze). A failing gate used to burn an entire
|
|
1600
|
+
// attempt per finding (r8/r9: ui-design-coverage, verification
|
|
1601
|
+
// targets, UI-state shape, one attempt each); surfaced here the
|
|
1602
|
+
// model fixes the facts and re-calls finalize_plan in-node.
|
|
1603
|
+
if (input.skeleton && input.sourceBinding) {
|
|
1604
|
+
// Front-load the exact pipeline the design-policy shell and
|
|
1605
|
+
// the node self-check run (patch ⊕ skeleton -> analyze ->
|
|
1606
|
+
// policy pre-checks) into the finalize receipt. Findings
|
|
1607
|
+
// come back as fixable receipt errors instead of burning
|
|
1608
|
+
// an attempt per gate (r8/r9: coverage, verification
|
|
1609
|
+
// targets, UI-state shape each cost a full attempt).
|
|
1610
|
+
const { analyzeFrontendPlanPatchCandidate, applyFrontendContractMergePatch, FrontendContractFailure, PlanPolicyPrecheckFailure, serializeDeterministicJson, } = await import("../workflows/dag/frontend-implementation-contract.js");
|
|
1611
|
+
try {
|
|
1612
|
+
const merged = applyFrontendContractMergePatch(input.skeleton, patch);
|
|
1613
|
+
await analyzeFrontendPlanPatchCandidate({
|
|
1614
|
+
runDir: input.runDir,
|
|
1615
|
+
rawContractText: serializeDeterministicJson(merged),
|
|
1616
|
+
sourceBinding: input.sourceBinding,
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1619
|
+
catch (error) {
|
|
1620
|
+
if (error instanceof PlanPolicyPrecheckFailure) {
|
|
1621
|
+
// Template the fix: every uncovered interaction / state
|
|
1622
|
+
// maps to a ready-to-submit record_component_choice
|
|
1623
|
+
// call. One reuse-existing choice covers all
|
|
1624
|
+
// behavioural interactions.
|
|
1625
|
+
const suggestions = error.findings
|
|
1626
|
+
.filter((finding) => finding.code === "ui-design-coverage-missing" &&
|
|
1627
|
+
finding.path)
|
|
1628
|
+
.map((finding) => ({
|
|
1629
|
+
tool: "record_component_choice",
|
|
1630
|
+
args: {
|
|
1631
|
+
choice: {
|
|
1632
|
+
purpose: finding.path,
|
|
1633
|
+
component: "<name the existing or new component>",
|
|
1634
|
+
decision: "reuse-existing",
|
|
1635
|
+
},
|
|
1636
|
+
},
|
|
1637
|
+
}));
|
|
1638
|
+
const suggestionBlock = suggestions.length > 0
|
|
1639
|
+
? ` Suggested record_* calls (copy, fill component, submit): ${JSON.stringify(suggestions)}`
|
|
1640
|
+
: "";
|
|
1641
|
+
return planToolReceipt({
|
|
1642
|
+
ok: false,
|
|
1643
|
+
kind: "finalize_plan",
|
|
1644
|
+
error: `finalize_plan pre-validation failed (fix the listed plan facts with record_* tools, then call finalize_plan again): ${error.message}${suggestionBlock}`,
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1647
|
+
if (!(error instanceof FrontendContractFailure))
|
|
1648
|
+
throw error;
|
|
1649
|
+
return planToolReceipt({
|
|
1650
|
+
ok: false,
|
|
1651
|
+
kind: "finalize_plan",
|
|
1652
|
+
error: `finalize_plan pre-validation failed (fix the listed plan facts with record_* tools, then call finalize_plan again): ${error.message}`,
|
|
1653
|
+
});
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1388
1656
|
const patchResult = await adoptPlanFact("target-surface", `${attemptId}:finalize_plan:patch:${randomUUID()}`, { kind: "target-surface", origin: "plan", patch });
|
|
1389
1657
|
if (!patchResult.ok) {
|
|
1390
1658
|
return planToolReceipt({
|
|
@@ -1597,10 +1865,11 @@ export async function createFrontendContractTools(input) {
|
|
|
1597
1865
|
const recordTools = Object.entries(recordKinds).map(([name, kind]) => defineTool({
|
|
1598
1866
|
name,
|
|
1599
1867
|
label: name,
|
|
1600
|
-
description: `Commit an origin=contract ${kind} fact.`,
|
|
1601
|
-
promptSnippet: `Commit
|
|
1868
|
+
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.`,
|
|
1869
|
+
promptSnippet: `Commit 1-5 origin=contract ${kind} facts (up to 5 per message).`,
|
|
1602
1870
|
parameters: Type.Object({}, { additionalProperties: true }),
|
|
1603
1871
|
async execute(_toolCallId, params) {
|
|
1872
|
+
await sleepRecordThrottle();
|
|
1604
1873
|
const result = await adoptContractFact(kind, {
|
|
1605
1874
|
kind,
|
|
1606
1875
|
origin: "contract",
|
|
@@ -1753,6 +2022,22 @@ export async function createFrontendScoutEvidenceTools(input) {
|
|
|
1753
2022
|
continue;
|
|
1754
2023
|
}
|
|
1755
2024
|
try {
|
|
2025
|
+
// Directories are legitimate named targets (greenfield smoke: the
|
|
2026
|
+
// page directory exists while the files inside it are to be
|
|
2027
|
+
// created). readFile on a directory throws EISDIR, which used to
|
|
2028
|
+
// mark every directory path fresh=false and structurally fail the
|
|
2029
|
+
// freshness gate for create-new surfaces. stat() first: a directory
|
|
2030
|
+
// counts as fresh existence evidence; its content hash is a stable
|
|
2031
|
+
// directory marker since there is no single file content to hash.
|
|
2032
|
+
const info = await stat(absolute);
|
|
2033
|
+
if (info.isDirectory()) {
|
|
2034
|
+
evidence.push({
|
|
2035
|
+
path: relative,
|
|
2036
|
+
sha256: createHash("sha256").update(`directory:${relative}`).digest("hex"),
|
|
2037
|
+
fresh: true,
|
|
2038
|
+
});
|
|
2039
|
+
continue;
|
|
2040
|
+
}
|
|
1756
2041
|
const bytes = await readFile(absolute);
|
|
1757
2042
|
evidence.push({
|
|
1758
2043
|
path: relative,
|
|
@@ -1803,7 +2088,7 @@ export async function createFrontendScoutEvidenceTools(input) {
|
|
|
1803
2088
|
const recordTargetSurfaceTool = defineTool({
|
|
1804
2089
|
name: "record_target_surface",
|
|
1805
2090
|
label: "record_target_surface",
|
|
1806
|
-
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.",
|
|
2091
|
+
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\": []}",
|
|
1807
2092
|
promptSnippet: "Commit an origin=scout target-surface fact.",
|
|
1808
2093
|
parameters: Type.Object({
|
|
1809
2094
|
completeness: scoutCompleteness,
|
|
@@ -1842,7 +2127,7 @@ export async function createFrontendScoutEvidenceTools(input) {
|
|
|
1842
2127
|
const recordDesignEvidenceTool = defineTool({
|
|
1843
2128
|
name: "record_design_evidence",
|
|
1844
2129
|
label: "record_design_evidence",
|
|
1845
|
-
description: "Commit an origin=scout design-evidence fact (source, paths, conflicts).",
|
|
2130
|
+
description: "Commit an origin=scout design-evidence fact (source, paths, conflicts). Example: {\"source\": \"<source>\", \"paths\": [\"<file>\"], \"conflicts\": []}",
|
|
1846
2131
|
promptSnippet: "Commit an origin=scout design-evidence fact.",
|
|
1847
2132
|
parameters: Type.Object({ source: Type.String({}), paths: stringArray, conflicts: stringArray }, { additionalProperties: false }),
|
|
1848
2133
|
async execute(_toolCallId, params) {
|
|
@@ -2524,6 +2809,8 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
2524
2809
|
runDir: meta.runDir,
|
|
2525
2810
|
nodeId: input.task.id,
|
|
2526
2811
|
skeleton: input.task.structuredContractOutput?.skeleton,
|
|
2812
|
+
sourceBinding: meta.spec.sourceBinding,
|
|
2813
|
+
writeSetPatterns: input.task.writeSet,
|
|
2527
2814
|
componentNewSourceReferences: await resolveFrontendPlanNewComponentSourceReferences({
|
|
2528
2815
|
cwd: input.cwd,
|
|
2529
2816
|
sourceBinding: meta.spec.sourceBinding,
|
|
@@ -2806,6 +3093,22 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
2806
3093
|
}
|
|
2807
3094
|
}
|
|
2808
3095
|
if (!isWriteTask) {
|
|
3096
|
+
if (!mapped.ok &&
|
|
3097
|
+
!(mapped.assistantText ?? "").trim() &&
|
|
3098
|
+
!mapped.stderr.trim()) {
|
|
3099
|
+
// Terminal-fact acceptance: the run finished with every fact committed
|
|
3100
|
+
// (including the terminal) but no final narrative text. A timeout or
|
|
3101
|
+
// provider error always leaves supervision/provider stderr, so a
|
|
3102
|
+
// blank stderr here means the only "failure" is the empty text.
|
|
3103
|
+
const terminalAccepted = await acceptCommittedTypedTerminalFact(meta.runDir, input.task.id);
|
|
3104
|
+
if (terminalAccepted) {
|
|
3105
|
+
return {
|
|
3106
|
+
...mapped,
|
|
3107
|
+
ok: true,
|
|
3108
|
+
failureCategory: undefined,
|
|
3109
|
+
};
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
2809
3112
|
return mapped;
|
|
2810
3113
|
}
|
|
2811
3114
|
let writeGuardOk = true;
|