@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.
Files changed (109) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/application/dag/generate-task-dag.js +6 -2
  3. package/dist/build-stamp.json +3 -3
  4. package/dist/commands/client-recovery.js +8 -36
  5. package/dist/executors/dag-pi-executor.js +343 -40
  6. package/dist/executors/pi-executor.js +8 -4
  7. package/dist/executors/pi-sdk-executor.js +33 -5
  8. package/dist/executors/shell-executor.js +102 -32
  9. package/dist/governance/checks.js +1 -0
  10. package/dist/shared/pi-context-pressure/checkpoint.js +116 -0
  11. package/dist/shared/pi-context-pressure/compaction-policy.js +151 -0
  12. package/dist/shared/pi-context-pressure/env.js +58 -0
  13. package/dist/shared/pi-context-pressure/extension.js +100 -0
  14. package/dist/shared/pi-context-pressure/index.js +7 -0
  15. package/dist/shared/pi-context-pressure/overflow.js +252 -0
  16. package/dist/shared/pi-context-pressure/sift-bridge.js +386 -0
  17. package/dist/shared/pi-context-pressure/telemetry.js +51 -0
  18. package/dist/task/frontend-project-capability.js +3 -1
  19. package/dist/task/source-prepare/fragment-inventory.js +4 -1
  20. package/dist/worker/console/chat/pi-runtime.js +146 -5
  21. package/dist/worker/console/chat/provider-error.js +2 -1
  22. package/dist/worker/console/chat/routes.js +3 -0
  23. package/dist/worker/console/chat/sift-bridge.js +1 -0
  24. package/dist/worker/console/dag-execution-receipt.js +20 -2
  25. package/dist/worker/console/operator-actions.js +4 -3
  26. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-CXj_GnSb.js → abnfDiagram-N423BO3Z-DC863mud.js} +1 -1
  27. package/dist/worker/console/static/assets/{arc-BZp6JAp7.js → arc-CftC38G9.js} +1 -1
  28. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DfpcEuYU.js → architectureDiagram-T3A2C74G-B3PAPQCw.js} +1 -1
  29. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-_Gf0xadb.js → blockDiagram-VBNYF7ZC-C9UDJNRv.js} +1 -1
  30. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Ct2QPCmv.js → c4Diagram-5PPSVZJV--BJ76fv_.js} +1 -1
  31. package/dist/worker/console/static/assets/channel-CUz-Bg86.js +1 -0
  32. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BfklkzKl.js → chunk-2GRJ4B5K--hiIqoGp.js} +1 -1
  33. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-Dy25vZJV.js → chunk-2Q5K7J3B-DgTzpIa3.js} +1 -1
  34. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BFlCRZep.js → chunk-5RXB4S5H-DIwpJziP.js} +1 -1
  35. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-op3oVxIE.js → chunk-5VM5RSS4-BJzbdUi0.js} +1 -1
  36. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-C0R7rzF2.js → chunk-6Q2QTUOP-BbGouI1Z.js} +1 -1
  37. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Bt44TCGy.js → chunk-GF5L2VYU-B9247w69.js} +1 -1
  38. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-_uEE_XFx.js → chunk-JWPE2WC7-CXob_wYy.js} +1 -1
  39. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-C3TOYGZ9.js → chunk-KBJHAD2P-C6FUel1g.js} +1 -1
  40. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-Cz60oBPV.js → chunk-RYQCIY6F-DGKRYKHu.js} +1 -1
  41. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-8Bik0qis.js → chunk-XXDRQBXY-kNBqNPfx.js} +1 -1
  42. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BhlUacmD.js +1 -0
  43. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BhlUacmD.js +1 -0
  44. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-C2QIOA4a.js → cose-bilkent-JH36ORCC--xmkDwfD.js} +1 -1
  45. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CSH_yUUd.js → cynefin-VYW2F7L2-Cw5FMMuG.js} +1 -1
  46. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BDLw2XFM.js → cynefinDiagram-MW4NZA55-CuLslt2t.js} +1 -1
  47. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CcD9ZtF4.js → dagre-VZM6K2ZE-D9ngqrs2.js} +1 -1
  48. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DqwtkBsI.js → diagram-7IWD3JNH-RDRSbHQp.js} +1 -1
  49. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BWVEcqsC.js → diagram-B4RE2ZJO-BgQ1P9aV.js} +1 -1
  50. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-M-WAbtQi.js → diagram-LBJQPF4R-D3WWyPtn.js} +1 -1
  51. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DKW7SixP.js → diagram-Q27KOJAE-L2k28wTR.js} +1 -1
  52. package/dist/worker/console/static/assets/{diagram-UB23O5K3-PHHPPtrj.js → diagram-UB23O5K3-DDNrA5Qw.js} +1 -1
  53. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BQD-B4RX.js → ebnfDiagram-BXEA7PRR-BaRFCOdM.js} +1 -1
  54. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BFvULb51.js → erDiagram-JOGREHBK-CM33DVVM.js} +1 -1
  55. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-Bw-aXTCb.js → flowDiagram-UKHOOZJN-CVBSjOxe.js} +1 -1
  56. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BGKw04Qx.js → ganttDiagram-PKOTCBZU-BdscewE_.js} +1 -1
  57. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-RqKaPR-I.js → gitGraphDiagram-DS77QQ5N-HRuSZb7g.js} +1 -1
  58. package/dist/worker/console/static/assets/{index-B_D8rbWc.js → index-B9JJQsVK.js} +102 -72
  59. package/dist/worker/console/static/assets/index-rWaGv4jz.css +1 -0
  60. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-YO5dnzrJ.js → infoDiagram-6WML65LV-cYfHvfAR.js} +1 -1
  61. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-Bi1VZHMq.js → ishikawaDiagram-WSZJBQD7-CZmaoOuy.js} +1 -1
  62. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Bszls1DD.js → journeyDiagram-NVQOT4AX-ntYijh7g.js} +1 -1
  63. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-PhzeaZ09.js → kanban-definition-27J2QSJJ-Cz3b0DeD.js} +1 -1
  64. package/dist/worker/console/static/assets/{linear-BsjbDoXi.js → linear-DvGonpsP.js} +1 -1
  65. package/dist/worker/console/static/assets/{mermaid.core-0B7NnWKk.js → mermaid.core-B-3vjfyW.js} +5 -5
  66. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BJr4Fj-q.js → mindmap-definition-FAOFIHXS-5iKxlsX3.js} +1 -1
  67. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-moC4fpGB.js → pegDiagram-VL7TDLO6-C8BUwapW.js} +1 -1
  68. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BEw37-2c.js → pieDiagram-7S7Q4E2Y-B2rPoQQG.js} +1 -1
  69. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Cq6LyasU.js → quadrantDiagram-CIZ2JOQS-jDeVUAy4.js} +1 -1
  70. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-DUCMcK0D.js → railroadDiagram-AXF67PYL-DV140Dwm.js} +1 -1
  71. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-C3upTZm7.js → requirementDiagram-LRYGKXZP-CnYgHtYC.js} +1 -1
  72. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BI_gMsCW.js → sankeyDiagram-W5VNT64P-SIK3CdWw.js} +1 -1
  73. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-YFOIRzfN.js → sequenceDiagram-SI44F4Z6-DW_vZix7.js} +1 -1
  74. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-dOnB7UDD.js → sizeCapture-X5ZJPWSS-NBAtkg0C.js} +1 -1
  75. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BXUniaIh.js → stateDiagram-OKZ733FA-Bi1bQxpi.js} +1 -1
  76. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DjWKYjQ1.js +1 -0
  77. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-2tA4wTNu.js → swimlanes-SLNWSIFB-8PT_uP_i.js} +2 -2
  78. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B4cdm7bH.js +8 -0
  79. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DO0HkJXC.js → timeline-definition-Z64GVDOM-CD0ZZPk0.js} +1 -1
  80. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DvIOzixv.js → vennDiagram-T6HMQDX7-BbEgWdhK.js} +1 -1
  81. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DXDZ0cTj.js → wardleyDiagram-T6FBY63Y-DCwPKdLl.js} +1 -1
  82. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-B32Ark0D.js → xychartDiagram-ELKLHX3M-sfC5PcNg.js} +1 -1
  83. package/dist/worker/console/static/index.html +2 -2
  84. package/dist/worker/observe/static/styles.css +9 -0
  85. package/dist/worker/observe/static/views/dag-inspector.js +40 -0
  86. package/dist/worker/observe/static/views/session-timeline.js +135 -0
  87. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +157 -7
  88. package/dist/workflows/dag/backend-test-pytest-collection.js +70 -2
  89. package/dist/workflows/dag/backend-test-result-contract.js +4 -0
  90. package/dist/workflows/dag/backend-test-scenario-param.js +339 -53
  91. package/dist/workflows/dag/backend-test-writer-completeness.js +11 -0
  92. package/dist/workflows/dag/dag-retry-schema.js +138 -0
  93. package/dist/workflows/dag/frontend-implementation-contract.js +118 -22
  94. package/dist/workflows/dag/frontend-shadow-dual-write.js +1 -1
  95. package/dist/workflows/dag/frontend-writer-admission.js +13 -0
  96. package/dist/workflows/dag/init-hybrid.js +7 -3
  97. package/dist/workflows/dag/node-execution.js +277 -3
  98. package/dist/workflows/dag/rerun-feedback.js +135 -3
  99. package/dist/workflows/dag/retry-policy.js +13 -122
  100. package/dist/workflows/dag/types.js +6 -1
  101. package/docs/architecture/runtime-boundaries.md +2 -1
  102. package/docs/templates/backend-test-dag.json +4 -3
  103. package/package.json +4 -3
  104. package/dist/worker/console/static/assets/channel-3TxJgYaH.js +0 -1
  105. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BHIkXpp3.js +0 -1
  106. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BHIkXpp3.js +0 -1
  107. package/dist/worker/console/static/assets/index-BdNx6fj0.css +0 -1
  108. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cdi6UhLa.js +0 -1
  109. 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; it never needs
833
- * to re-read a PRD merely to recover a path/section/line triple.
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 fragment = requirement.sourceFragmentIds
905
+ const citations = requirement.sourceFragmentIds
852
906
  .map((fragmentId) => fragmentsById.get(fragmentId))
853
- .find((candidate) => candidate !== undefined);
854
- if (!fragment)
855
- continue;
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.String({
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: Type.String({
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.String({
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 its exact PRD specReference from the source-fidelity ledger. 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 once per component — one tool call per message. Optionally include stylingStrategy (set it once, on the first call).",
1049
- promptSnippet: "Record one component choice (one tool call per message).",
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
- const specReference = sourceRequirementIds
1075
- .map((id) => input.componentNewSourceReferences?.get(id))
1076
- .find((reference) => reference !== undefined);
1077
- if (!specReference) {
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 sourceRequirementIds have no frozen task-source citation: ${sourceRequirementIds.join(", ")}`,
1164
+ error: `decision=new sourceFragmentId ${sourceFragmentId} is not bound to sourceRequirementIds ${sourceRequirementIds.join(", ")}`,
1082
1165
  });
1083
1166
  }
1084
- choice.specReference = 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: call this tool exactly ONE time per assistant message — never batch multiple record_* calls together in one message; emit one call, wait for its result, then call the next.",
1270
- promptSnippet: "Commit one plan requirement entry (one tool call per message).",
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
- const entry = rawEntry;
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: call this tool exactly ONE time per assistant message — never batch multiple record_* calls together in one message; emit one call, wait for its result, then call the next.",
1314
- promptSnippet: "Commit one plan verification target entry (one tool call per message).",
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: call this tool exactly ONE time per assistant message — never batch multiple record_* calls together in one message; emit one call, wait for its result, then call the next.",
1342
- promptSnippet: "Commit one plan evidence gap entry (one tool call per message).",
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 an origin=contract ${kind} fact.`,
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;