@tea-agent/loop-agent 0.43.0-next.3 → 0.43.0-next.5

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 +81 -9
  2. package/dist/application/task-lifecycle/advance.js +17 -4
  3. package/dist/application/task-lifecycle/observe.js +43 -29
  4. package/dist/application/task-lifecycle/plan-transitions.js +5 -4
  5. package/dist/build-stamp.json +3 -3
  6. package/dist/executors/dag-pi-executor.js +244 -33
  7. package/dist/executors/pi-executor.js +2 -0
  8. package/dist/executors/pi-sdk-executor.js +75 -39
  9. package/dist/task/source-prepare/source-fidelity-pi.js +2 -2
  10. package/dist/task/source-references.js +48 -23
  11. package/dist/worker/console/chat/assistant-content.js +23 -2
  12. package/dist/worker/console/chat/explore-tools.js +13 -0
  13. package/dist/worker/console/chat/pi-runtime.js +131 -95
  14. package/dist/worker/console/chat/routes.js +175 -54
  15. package/dist/worker/console/chat/scm-routes.js +193 -0
  16. package/dist/worker/console/chat/scm-service.js +253 -0
  17. package/dist/worker/console/chat/scm-tools.js +111 -0
  18. package/dist/worker/console/chat/sdd-data-alignment.js +65 -9
  19. package/dist/worker/console/chat/session-catalog.js +1 -0
  20. package/dist/worker/console/chat/session-mode-view.js +48 -0
  21. package/dist/worker/console/chat/session-mode.js +218 -0
  22. package/dist/worker/console/chat/session-store.js +22 -6
  23. package/dist/worker/console/chat/shortcuts.js +6 -0
  24. package/dist/worker/console/chat/terminal-routes.js +216 -0
  25. package/dist/worker/console/chat/terminal-sessions.js +284 -0
  26. package/dist/worker/console/chat/terminal-tools.js +199 -0
  27. package/dist/worker/console/chat/tool-preview.js +11 -0
  28. package/dist/worker/console/chat/tools.js +2 -0
  29. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-hUmM_GRf.js → abnfDiagram-N423BO3Z-BuyrlONK.js} +1 -1
  30. package/dist/worker/console/static/assets/{arc-Bt-xuieW.js → arc-BvCl_wpi.js} +1 -1
  31. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DTqkL4DJ.js → architectureDiagram-T3A2C74G-BkBpBaRq.js} +1 -1
  32. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-Cy0MNnzV.js → blockDiagram-VBNYF7ZC-yve4Mcb_.js} +1 -1
  33. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DDdlT0Pd.js → c4Diagram-5PPSVZJV-9yLGboBx.js} +1 -1
  34. package/dist/worker/console/static/assets/channel-BRahIhjw.js +1 -0
  35. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-3JawwNZ6.js → chunk-2GRJ4B5K-BKP69b8r.js} +1 -1
  36. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DoTp7Cri.js → chunk-2Q5K7J3B-NWFRnXju.js} +1 -1
  37. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-CcirhZaS.js → chunk-5RXB4S5H-BiweqLEc.js} +1 -1
  38. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-VrG21gnP.js → chunk-5VM5RSS4-CP9GHmRY.js} +1 -1
  39. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DDOgIS70.js → chunk-6Q2QTUOP-BiGDkBtj.js} +1 -1
  40. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CBjmgvJ_.js → chunk-GF5L2VYU-DkJ4ZjQS.js} +1 -1
  41. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CCQSht8M.js → chunk-JWPE2WC7-B5jjn7zq.js} +1 -1
  42. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BpGMkUUV.js → chunk-KBJHAD2P-C6KoK9O0.js} +1 -1
  43. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DNDT41QL.js → chunk-RYQCIY6F-BvNIfyn-.js} +1 -1
  44. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-euXnDFbn.js → chunk-XXDRQBXY-BZ33iX4W.js} +1 -1
  45. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-GOC80mhq.js +1 -0
  46. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-GOC80mhq.js +1 -0
  47. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BJ6NAeql.js → cose-bilkent-JH36ORCC-CP8eIuZd.js} +1 -1
  48. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-F5JKIvIq.js → cynefin-VYW2F7L2-DZECjpOe.js} +1 -1
  49. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Cwmkl_Gi.js → cynefinDiagram-MW4NZA55-CvVJoFqf.js} +1 -1
  50. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BmCZ8815.js → dagre-VZM6K2ZE-D_JTqdAn.js} +1 -1
  51. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-zWA3hUnX.js → diagram-7IWD3JNH-Db7AQ0z0.js} +1 -1
  52. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BzA-888F.js → diagram-B4RE2ZJO-AZPVo5CZ.js} +1 -1
  53. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-tPNheoch.js → diagram-LBJQPF4R-BgXqI3E6.js} +1 -1
  54. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-Cdi7Fyqp.js → diagram-Q27KOJAE-BYjFn3Ba.js} +1 -1
  55. package/dist/worker/console/static/assets/{diagram-UB23O5K3-DFK9VBf1.js → diagram-UB23O5K3-B4rJgGGO.js} +1 -1
  56. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-xCUr1kpg.js → ebnfDiagram-BXEA7PRR-C6XWZhpw.js} +1 -1
  57. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BANQHY_A.js → erDiagram-JOGREHBK-1YdQRIQW.js} +1 -1
  58. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-VHJYEMkP.js → flowDiagram-UKHOOZJN-l3a0AwN0.js} +1 -1
  59. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DiyZQ0C6.js → ganttDiagram-PKOTCBZU-CQL96F2Z.js} +1 -1
  60. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CZtIeBBa.js → gitGraphDiagram-DS77QQ5N-BxCvafRb.js} +1 -1
  61. package/dist/worker/console/static/assets/index-4wCGsXuU.js +465 -0
  62. package/dist/worker/console/static/assets/index-CiF9WBoR.css +1 -0
  63. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BuSspC0X.js → infoDiagram-6WML65LV-BUipIeDO.js} +1 -1
  64. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DRJZyPFy.js → ishikawaDiagram-WSZJBQD7-CRfomVMX.js} +1 -1
  65. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DIfXIVuE.js → journeyDiagram-NVQOT4AX-_UFvyviX.js} +1 -1
  66. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CT10_7Lt.js → kanban-definition-27J2QSJJ-CKIRoqKv.js} +1 -1
  67. package/dist/worker/console/static/assets/{linear-ufemj74H.js → linear-CjttJDCM.js} +1 -1
  68. package/dist/worker/console/static/assets/{mermaid.core-DEQe8-_m.js → mermaid.core-DPvHX155.js} +5 -5
  69. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BRLUrbrC.js → mindmap-definition-FAOFIHXS-Qmj-qwBe.js} +1 -1
  70. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BgibKtiF.js → pegDiagram-VL7TDLO6-Auw6DRgq.js} +1 -1
  71. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-D3eyfdV9.js → pieDiagram-7S7Q4E2Y-CqMrfD4c.js} +1 -1
  72. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DXeTk5Px.js → quadrantDiagram-CIZ2JOQS-BAddeggH.js} +1 -1
  73. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BgXdFEe7.js → railroadDiagram-AXF67PYL-DG1JuOu2.js} +1 -1
  74. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CseC2A7R.js → requirementDiagram-LRYGKXZP-DLYGMKlC.js} +1 -1
  75. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-D80PFzfJ.js → sankeyDiagram-W5VNT64P-g68SbLM9.js} +1 -1
  76. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BWzHW7LS.js → sequenceDiagram-SI44F4Z6-CmszLKv8.js} +1 -1
  77. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-xWQYTLAn.js → sizeCapture-X5ZJPWSS-D2tEKUCj.js} +1 -1
  78. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-CLxtpZeL.js → stateDiagram-OKZ733FA-Syc5E_Jf.js} +1 -1
  79. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-CwWwuPI-.js +1 -0
  80. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DwugtmWV.js → swimlanes-SLNWSIFB-Ibn0IRCa.js} +2 -2
  81. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-A7fqemr6.js +8 -0
  82. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DHEOwHIe.js → timeline-definition-Z64GVDOM-2ZW9W3iS.js} +1 -1
  83. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-pmbZKlBK.js → vennDiagram-T6HMQDX7-v50HsKUh.js} +1 -1
  84. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CivHoM5q.js → wardleyDiagram-T6FBY63Y-DqCXEoDH.js} +1 -1
  85. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-BfHIE7o6.js → xychartDiagram-ELKLHX3M-vIP8Vo9d.js} +1 -1
  86. package/dist/worker/console/static/index.html +2 -2
  87. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +122 -10
  88. package/dist/worker/console/static-src/operator-chat/pending-user-message.js +27 -15
  89. package/dist/worker/console/static-src/operator-chat/process-label.js +41 -0
  90. package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +13 -0
  91. package/dist/worker/console/static-src/operator-chat/use-searchable-hidden.js +51 -0
  92. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +20 -4
  93. package/dist/worker/console/static-src/operator-chat/useChatStream.js +18 -2
  94. package/dist/worker/console/static-src/operator-chat/useChatThread.js +39 -4
  95. package/dist/worker/console/static-src/operator-chat/useComposer.js +13 -7
  96. package/dist/worker/console/static-src/operator-chat/user-turn-anchor-equality.js +21 -0
  97. package/dist/worker/console/workspace-context.js +15 -1
  98. package/dist/workflows/dag/frontend-capacity.js +9 -0
  99. package/dist/workflows/dag/frontend-design-policy.js +2 -1
  100. package/dist/workflows/dag/frontend-shape-facts.js +12 -2
  101. package/package.json +6 -3
  102. package/skills/frontend-plan/SKILL.md +7 -0
  103. package/dist/worker/console/static/assets/channel-BpzRnZmU.js +0 -1
  104. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BhlMVqCc.js +0 -1
  105. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BhlMVqCc.js +0 -1
  106. package/dist/worker/console/static/assets/index-BlN6ytLr.css +0 -1
  107. package/dist/worker/console/static/assets/index-CA-jfWlE.js +0 -449
  108. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-CiVoq4G-.js +0 -1
  109. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BkdgdpfR.js +0 -8
@@ -268,6 +268,16 @@ export function isPlannerThinkingExhausted(result, committedAnyFacts) {
268
268
  return false;
269
269
  return true;
270
270
  }
271
+ /** Capacity recovery must not depend on whether a gateway emitted prose or
272
+ * diagnostics. Precise provider/governance failures retain their ownership. */
273
+ function isFrontendPlanCapacityStop(result) {
274
+ if (result.ok || result.timedOut)
275
+ return false;
276
+ if (result.failureCategory === OUTPUT_LIMIT_RETRY_CATEGORY)
277
+ return true;
278
+ return readWriterThinkingExhaustionEvidence(result).stopReason === "length" &&
279
+ (!result.failureCategory || ["empty-output", "network", "nonzero-exit", "unknown"].includes(result.failureCategory));
280
+ }
271
281
  /**
272
282
  * The writer session burned an excessive token budget (a read-edit-test loop
273
283
  * that never converged) and still failed. Distinct from empty-output so the
@@ -592,6 +602,7 @@ export function resolveDagPiToolNames(task) {
592
602
  ...FRONTEND_PLAN_RECORD_TOOL_NAMES,
593
603
  ...FRONTEND_PLAN_TERMINAL_TOOL_NAMES,
594
604
  ...FRONTEND_PLAN_ADOPT_TOOL_NAMES,
605
+ "read_plan_facts",
595
606
  ];
596
607
  }
597
608
  if ((task.readSet?.length ?? 0) > 0) {
@@ -1811,6 +1822,44 @@ export async function createFrontendPlanLedgerTools(input) {
1811
1822
  });
1812
1823
  }
1813
1824
  }
1825
+ // Named entries are last-wins at assembly. A scoped correction may
1826
+ // replace its own bindings, but must retain other scopes' coverage.
1827
+ const scope = new Set(scopedRequirementIds());
1828
+ const committed = readCommittedEvents(store, attemptId).map(event => event.fact);
1829
+ const targetOwners = new Map();
1830
+ for (const fact of committed) {
1831
+ if (fact.kind === "plan-verification-target" && isRecordObject(fact.entry) && typeof fact.entry.id === "string") {
1832
+ targetOwners.set(fact.entry.id, stringList(fact.entry.requirementIds));
1833
+ }
1834
+ }
1835
+ for (const [field, removals] of [["uiStates", removeUiStateNames], ["interactions", removeInteractionNames]]) {
1836
+ const live = new Map();
1837
+ for (const fact of committed) {
1838
+ if (fact.kind !== "state-flow")
1839
+ continue;
1840
+ for (const name of stringList(fact[field === "uiStates" ? "removeUiStateNames" : "removeInteractionNames"]))
1841
+ live.delete(name);
1842
+ for (const entry of Array.isArray(fact[field]) ? fact[field] : []) {
1843
+ if (isRecordObject(entry) && typeof entry.name === "string")
1844
+ live.set(entry.name, entry);
1845
+ }
1846
+ }
1847
+ const foreignBindings = (entry) => scope.size === 0 ? [] : stringList(entry.verificationTargetIds).filter(id => {
1848
+ const owners = targetOwners.get(id);
1849
+ return !owners?.length || owners.some(owner => !scope.has(owner));
1850
+ });
1851
+ for (const name of removals) {
1852
+ const previous = live.get(name);
1853
+ if (previous && foreignBindings(previous).length)
1854
+ return planToolReceipt({ ok: false, kind: "state-flow",
1855
+ error: `Cannot remove shared ${name} from this requirement scope; retain it and update its scoped bindings instead` });
1856
+ }
1857
+ for (const entry of field === "uiStates" ? uiStates : interactions) {
1858
+ const previous = live.get(String(entry.name));
1859
+ if (previous)
1860
+ entry.verificationTargetIds = [...new Set([...foreignBindings(previous), ...stringList(entry.verificationTargetIds)])];
1861
+ }
1862
+ }
1814
1863
  const result = await adoptPlanFact("state-flow", `${attemptId}:record_state_flow:${randomUUID()}`, {
1815
1864
  kind: "state-flow",
1816
1865
  origin: "plan",
@@ -2415,6 +2464,74 @@ export async function createFrontendPlanLedgerTools(input) {
2415
2464
  }
2416
2465
  },
2417
2466
  });
2467
+ const readPlanFactsTool = defineTool({
2468
+ name: "read_plan_facts",
2469
+ label: "read_plan_facts",
2470
+ description: "Read the LIVE committed plan ledger without repository access. Filter by kind and optional entryId or eventId. Use field (dot path, e.g. entry.requirementIds or uiStates) to page large arrays/strings. offset/limit paginate results; follow nextOffset until null before replacing any full-array fact. expectedRevision pins all pages to one ledger revision; restart if it changes. No writes.",
2471
+ parameters: Type.Object({
2472
+ kind: Type.String(), entryId: Type.Optional(Type.String()), eventId: Type.Optional(Type.String()),
2473
+ field: Type.Optional(Type.String()), offset: Type.Optional(Type.Integer({ minimum: 0 })),
2474
+ limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 50 })),
2475
+ stringOffset: Type.Optional(Type.Integer({ minimum: 0 })),
2476
+ expectedRevision: Type.Optional(Type.Integer({ minimum: 0 })),
2477
+ }, { additionalProperties: false }),
2478
+ async execute(_id, params) {
2479
+ const reply = (details) => ({ content: [{ type: "text", text: JSON.stringify(details) }], details });
2480
+ if (params.expectedRevision !== undefined && params.expectedRevision !== store.revision) {
2481
+ return reply({ ok: false, revision: store.revision, error: "ledger revision changed; restart pagination" });
2482
+ }
2483
+ let records = readCommittedEvents(store, attemptId).filter(record => {
2484
+ const fact = record.fact;
2485
+ return fact.kind === params.kind && (!params.eventId || record.eventId === params.eventId) &&
2486
+ (!params.entryId || fact.entry?.id === params.entryId);
2487
+ });
2488
+ // Full-entry corrections and registries are last-wins. Other kinds
2489
+ // remain an ordered event log (state removals must remain visible).
2490
+ if (params.entryId || params.kind === "state-registry")
2491
+ records = records.slice(-1);
2492
+ let values = records.map(record => ({ eventId: record.eventId, ...record.fact }));
2493
+ if (params.field) {
2494
+ values = records.flatMap(record => {
2495
+ let value = record.fact;
2496
+ for (const key of params.field.split(".")) {
2497
+ if (!value || typeof value !== "object" || !Object.hasOwn(value, key))
2498
+ return [];
2499
+ value = value[key];
2500
+ }
2501
+ return Array.isArray(value) ? value : [value];
2502
+ });
2503
+ }
2504
+ const offset = params.offset ?? 0;
2505
+ if (params.stringOffset !== undefined) {
2506
+ const value = values[offset];
2507
+ if (typeof value !== "string")
2508
+ return reply({ ok: false, error: "stringOffset requires a string field item" });
2509
+ // Unicode code points prevent a page boundary splitting a surrogate pair.
2510
+ const characters = Array.from(value);
2511
+ const chunk = characters.slice(params.stringOffset, params.stringOffset + 1000).join("");
2512
+ const next = params.stringOffset + Array.from(chunk).length;
2513
+ return reply({ ok: true, revision: store.revision, offset, stringOffset: params.stringOffset,
2514
+ chunk, totalCharacters: characters.length, nextStringOffset: next < characters.length ? next : null });
2515
+ }
2516
+ const items = [];
2517
+ let bytes = 0;
2518
+ for (const value of values.slice(offset, offset + (params.limit ?? 10))) {
2519
+ const size = Buffer.byteLength(JSON.stringify(value));
2520
+ if (bytes + size > 12_000)
2521
+ break;
2522
+ bytes += size;
2523
+ items.push(value);
2524
+ }
2525
+ if (!items.length && offset < values.length)
2526
+ return reply({
2527
+ ok: false, revision: store.revision, total: values.length, offset,
2528
+ error: "item exceeds the page budget; select an eventId/entryId and a narrower field path; for a string item set stringOffset: 0 and follow nextStringOffset",
2529
+ fields: typeof values[offset] === "object" && values[offset] !== null ? Object.keys(values[offset]) : [],
2530
+ });
2531
+ return reply({ ok: true, revision: store.revision, total: values.length, offset, items,
2532
+ nextOffset: offset + items.length < values.length ? offset + items.length : null });
2533
+ },
2534
+ });
2418
2535
  const durable = await createDurableFrontendTools({
2419
2536
  file: path.join(input.runDir, input.nodeId, "plan-typed-facts.jsonl"), attemptId, store: input.store,
2420
2537
  setWorkingStore: next => { store = next; },
@@ -2438,7 +2555,7 @@ export async function createFrontendPlanLedgerTools(input) {
2438
2555
  ],
2439
2556
  });
2440
2557
  return {
2441
- customTools: durable.customTools,
2558
+ customTools: [...durable.customTools, { ...readPlanFactsTool, execute: async (...args) => { await durable.flush(); return readPlanFactsTool.execute(...args); } }],
2442
2559
  adoptCommittedFacts: async (records) => durable.commitExternal(async () => {
2443
2560
  for (const record of records) {
2444
2561
  if (record.phase !== "committed")
@@ -3808,6 +3925,11 @@ export function collectFrontendPlanPhaseMissingFacts(input) {
3808
3925
  ];
3809
3926
  }
3810
3927
  if (input.phase === "ux-local") {
3928
+ if (input.requirementIds.length > 1) {
3929
+ return input.requirementIds.flatMap(id => collectFrontendPlanPhaseMissingFacts({
3930
+ ...input, requirementIds: [id],
3931
+ }));
3932
+ }
3811
3933
  const needsUx = input.requirementIds.some((id) => input.behaviorRequiredRequirementIds?.includes(id));
3812
3934
  if (!needsUx)
3813
3935
  return [];
@@ -3817,8 +3939,9 @@ export function collectFrontendPlanPhaseMissingFacts(input) {
3817
3939
  Array.isArray(fact.uiComponentChoices) &&
3818
3940
  fact.uiComponentChoices.length > 0);
3819
3941
  const canonicalStateFlow = collectCanonicalStateFlowNames(scopedFacts);
3820
- const hasStateFlow = canonicalStateFlow.uiStateNames.size > 0 ||
3821
- canonicalStateFlow.interactionNames.size > 0;
3942
+ const liveStateFlow = collectCanonicalStateFlowNames(facts);
3943
+ const hasStateFlow = [...canonicalStateFlow.uiStateNames].some(name => liveStateFlow.uiStateNames.has(name)) ||
3944
+ [...canonicalStateFlow.interactionNames].some(name => liveStateFlow.interactionNames.has(name));
3822
3945
  const missing = [];
3823
3946
  if (!hasChoice) {
3824
3947
  missing.push({
@@ -3903,9 +4026,8 @@ export function batchFrontendPlanRequirements(input) {
3903
4026
  }
3904
4027
  const FRONTEND_PLAN_COVERAGE_MAX_RECORD_CALLS = 10;
3905
4028
  const FRONTEND_PLAN_COVERAGE_MAX_CONCURRENCY = 4;
3906
- // A large requirement set creates parallel coverage shards; UX remains one
3907
- // global decision session so behavior names and component/state facts are not
3908
- // reinvented per AC batch. Keep a safety bound for adaptive coverage retries
4029
+ // A single registry owns global names; large UX work uses bounded serial
4030
+ // scopes against that registry. Keep a safety bound for adaptive retries
3909
4031
  // without letting the old 32-session ceiling skip finalize.
3910
4032
  const FRONTEND_PLAN_BATCH_MAX_SESSIONS = 128;
3911
4033
  const FRONTEND_PLAN_SMALL_MAX_REQUIREMENTS = 8;
@@ -4046,26 +4168,28 @@ export function compactFrontendPlanPromptForRequirementSlice(basePrompt, require
4046
4168
  if (!value || typeof value !== "object" || Array.isArray(value))
4047
4169
  return [];
4048
4170
  const target = value;
4049
- const targetRequirementIds = compactPromptStringArray(target.requirementIds, 20, 80);
4171
+ if (options.verificationTargetIds && !options.verificationTargetIds.includes(String(target.id)))
4172
+ return [];
4173
+ const targetRequirementIds = planFactStringList(target.requirementIds);
4050
4174
  const relatedRequirementIds = targetRequirementIds.filter((id) => sliceSet.has(id));
4051
4175
  if (relatedRequirementIds.length === 0)
4052
4176
  return [];
4053
4177
  return [
4054
4178
  {
4055
- ...(compactPromptString(target.id, 80)
4056
- ? { id: compactPromptString(target.id, 80) }
4179
+ ...(typeof target.id === "string"
4180
+ ? { id: target.id }
4057
4181
  : {}),
4058
- ...(compactPromptString(target.commandId, 80)
4059
- ? { commandId: compactPromptString(target.commandId, 80) }
4182
+ ...(typeof target.commandId === "string"
4183
+ ? { commandId: target.commandId }
4060
4184
  : {}),
4061
4185
  ...(compactPromptString(target.commandLabel, 180)
4062
4186
  ? { commandLabel: compactPromptString(target.commandLabel, 180) }
4063
4187
  : {}),
4064
- ...(compactPromptString(target.file, 180)
4065
- ? { file: compactPromptString(target.file, 180) }
4188
+ ...(typeof target.file === "string"
4189
+ ? { file: target.file }
4066
4190
  : {}),
4067
4191
  requirementIds: relatedRequirementIds,
4068
- uiStates: compactPromptStringArray(target.uiStates, 12, 100),
4192
+ uiStates: planFactStringList(target.uiStates),
4069
4193
  },
4070
4194
  ];
4071
4195
  })
@@ -4117,7 +4241,7 @@ export function compactFrontendPlanPromptForRequirementSlice(basePrompt, require
4117
4241
  if (!value || typeof value !== "object" || Array.isArray(value))
4118
4242
  return [];
4119
4243
  const state = value;
4120
- const id = compactPromptString(state.id, 80);
4244
+ const id = typeof state.id === "string" ? state.id : undefined;
4121
4245
  if (!id)
4122
4246
  return [];
4123
4247
  return [{
@@ -4217,9 +4341,9 @@ function compactFrontendPlanLedgerContext(input) {
4217
4341
  compactFacts.push({
4218
4342
  kind: fact.kind,
4219
4343
  entry: {
4220
- id: compactPromptString(entry.id, 80),
4221
- implementationTargets: compactPromptStringArray(entry.implementationTargets, 12, 180),
4222
- verificationTargetIds: compactPromptStringArray(entry.verificationTargetIds, 12, 80),
4344
+ id: entry.id,
4345
+ implementationTargets: planFactStringList(entry.implementationTargets).slice(0, 12),
4346
+ verificationTargetIds: planFactStringList(entry.verificationTargetIds).slice(0, 12),
4223
4347
  ...(compactPromptString(entry.expectedOutcome, 240)
4224
4348
  ? { expectedOutcome: compactPromptString(entry.expectedOutcome, 240) }
4225
4349
  : {}),
@@ -4233,17 +4357,19 @@ function compactFrontendPlanLedgerContext(input) {
4233
4357
  if (fact.kind === "plan-verification-target") {
4234
4358
  if (!entry)
4235
4359
  continue;
4236
- const requirementIds = compactPromptStringArray(entry.requirementIds, 20, 80);
4237
- if (!requirementIds.some((id) => slice.has(id)))
4360
+ // Scope against the complete canonical binding before compacting.
4361
+ // A shared target may bind a late requirement beyond the preview cap.
4362
+ const requirementIds = planFactStringList(entry.requirementIds).filter((id) => slice.has(id));
4363
+ if (requirementIds.length === 0)
4238
4364
  continue;
4239
4365
  compactFacts.push({
4240
4366
  kind: fact.kind,
4241
4367
  entry: {
4242
- id: compactPromptString(entry.id, 80),
4243
- commandId: compactPromptString(entry.commandId, 80),
4244
- file: compactPromptString(entry.file, 180),
4368
+ id: entry.id,
4369
+ commandId: entry.commandId,
4370
+ file: entry.file,
4245
4371
  requirementIds,
4246
- uiStates: compactPromptStringArray(entry.uiStates, 12, 100),
4372
+ uiStates: planFactStringList(entry.uiStates).slice(0, 12),
4247
4373
  },
4248
4374
  });
4249
4375
  continue;
@@ -4270,8 +4396,8 @@ function compactFrontendPlanLedgerContext(input) {
4270
4396
  if (fact.kind === "state-registry") {
4271
4397
  compactFacts.push({
4272
4398
  kind: fact.kind,
4273
- uiStateNames: compactPromptStringArray(fact.uiStateNames, 40, 100),
4274
- interactionNames: compactPromptStringArray(fact.interactionNames, 60, 120),
4399
+ uiStateNames: planFactStringList(fact.uiStateNames).slice(0, 40),
4400
+ interactionNames: planFactStringList(fact.interactionNames).slice(0, 60),
4275
4401
  });
4276
4402
  continue;
4277
4403
  }
@@ -4323,13 +4449,21 @@ function compactFrontendPlanLedgerContext(input) {
4323
4449
  return "";
4324
4450
  const priorityFacts = compactFacts.filter((fact) => fact.kind === "plan-requirement" || fact.kind === "plan-verification-target");
4325
4451
  const otherFacts = compactFacts.filter((fact) => fact.kind !== "plan-requirement" && fact.kind !== "plan-verification-target");
4452
+ let previewBytes = 0;
4326
4453
  const boundedFacts = [
4327
- ...priorityFacts,
4328
- ...otherFacts,
4329
- ];
4454
+ ...priorityFacts.slice(0, 64),
4455
+ ...otherFacts.slice(-32),
4456
+ ].slice(0, 96).filter(fact => {
4457
+ const bytes = Buffer.byteLength(JSON.stringify(fact));
4458
+ if (previewBytes + bytes > 24_000)
4459
+ return false;
4460
+ previewBytes += bytes;
4461
+ return true;
4462
+ });
4330
4463
  return [
4331
4464
  "<frontend_plan_ledger>",
4332
- "Committed plan facts from earlier sessions. Treat these as authoritative; correct them only with the allowed replacement/removal fields.",
4465
+ "Preview of committed plan facts; not a replacement payload. Fields and lists may be omitted or shortened. Use read_plan_facts with kind, entryId/eventId and optional field to retrieve complete values in bounded pages before corrections; preserve all other bindings. Absence from this preview is not absence from the ledger.",
4466
+ JSON.stringify({ complete: false, matchingFacts: compactFacts.length, omittedFacts: compactFacts.length - boundedFacts.length }),
4333
4467
  JSON.stringify(boundedFacts),
4334
4468
  "</frontend_plan_ledger>",
4335
4469
  ].join("\n");
@@ -4707,7 +4841,7 @@ export async function runFrontendPlanSegmentedSessions(input) {
4707
4841
  "component-choice",
4708
4842
  "state-flow",
4709
4843
  ],
4710
- scopedKinds: ["state-flow"],
4844
+ // Include shared state bindings from other scopes; page full values before correcting.
4711
4845
  })
4712
4846
  : "";
4713
4847
  return [
@@ -4926,6 +5060,27 @@ export async function runFrontendPlanSegmentedSessions(input) {
4926
5060
  let lastDurableCount = input.committedFactCount();
4927
5061
  while (index < queue.length) {
4928
5062
  const session = queue[index];
5063
+ if (input.attempt > 1 && input.committedFacts && session.id === "ux-registry" &&
5064
+ collectFrontendPlanPhaseMissingFacts({ phase: "ux-registry", requirementIds: allRequirementIds, committedFacts: input.committedFacts() }).length === 0) {
5065
+ index += 1;
5066
+ continue;
5067
+ }
5068
+ // Reuse complete UX scopes on node retry. Require explicit per-requirement
5069
+ // ownership; an unrelated/global fact must not prove a slice complete.
5070
+ if (input.attempt > 1 && input.committedFacts && session.id.startsWith("ux-local-") && session.requirementSlice?.length) {
5071
+ const facts = input.committedFacts();
5072
+ const complete = session.requirementSlice.every(id => {
5073
+ const owned = facts.filter(value => {
5074
+ const fact = committedFactFromPlanRecord(value);
5075
+ return fact && planFactStringList(fact.scopeRequirementIds).includes(id);
5076
+ });
5077
+ return owned.length > 0 && collectFrontendPlanPhaseMissingFacts({ phase: "ux-local", requirementIds: [id], committedFacts: facts, behaviorRequiredRequirementIds: input.behaviorRequiredRequirementIds }).length === 0;
5078
+ });
5079
+ if (complete) {
5080
+ index += 1;
5081
+ continue;
5082
+ }
5083
+ }
4929
5084
  const remaining = (session.coverageOnly ? session.coverageSlice ?? [] : []).filter((id) => !input.committedRequirementIds?.().has(id));
4930
5085
  const preexistingMissing = session.coverageOnly && session.coverageSlice && input.committedFacts
4931
5086
  ? collectFrontendPlanMissingFacts({
@@ -4971,12 +5126,30 @@ export async function runFrontendPlanSegmentedSessions(input) {
4971
5126
  : buildPhasePrompt(segment, session.missingFacts, session.requirementSlice ?? allRequirementIds);
4972
5127
  }
4973
5128
  }
5129
+ const atomicFocus = session.atomicRecovery ? session.missingFacts?.[0] : undefined;
5130
+ if (atomicFocus) {
5131
+ prompt = [
5132
+ compactFrontendPlanPromptForRequirementSlice(input.basePrompt, atomicFocus.requirementIds, {
5133
+ includeChecklist: false,
5134
+ ...(atomicFocus.kind === "plan-verification-target" && atomicFocus.id
5135
+ ? { verificationTargetIds: [atomicFocus.id] } : {}),
5136
+ }),
5137
+ `ATOMIC FACT: ${atomicFocus.kind}${atomicFocus.id ? ` ${atomicFocus.id}` : ""}`,
5138
+ atomicFocus.reason,
5139
+ "Commit ONLY this missing fact with one record_* call, then end this session. Other facts are queued separately. Preserve canonical IDs and shared bindings; use read_plan_facts for existing values. Do not summarize or plan the entire requirement. Capacity exhaustion is not evidence of a requirement gap.",
5140
+ ].join("\n\n");
5141
+ }
4974
5142
  const committedBefore = input.committedFactCount();
4975
5143
  input.setActiveRequirementScope?.(session.coverageOnly ? session.coverageSlice ?? [] : session.requirementSlice ?? []);
4976
5144
  if (invocationCount >= FRONTEND_PLAN_BATCH_MAX_SESSIONS)
4977
5145
  break;
4978
5146
  invocationCount += 1;
4979
- const customTools = input.segmentCustomTools(session.toolNames);
5147
+ const atomicTools = {
5148
+ "plan-requirement": "record_plan_requirement", "plan-verification-target": "record_plan_verification_target",
5149
+ "state-registry": "record_state_registry", "component-choice": "record_component_choice",
5150
+ "state-flow": "record_state_flow", "data-flow": "record_data_flow", "mock-api": "record_mock_api",
5151
+ };
5152
+ const customTools = input.segmentCustomTools(atomicFocus ? new Set([atomicTools[atomicFocus.kind]]) : session.toolNames);
4980
5153
  const scopeForPacking = session.coverageSlice ?? session.requirementSlice;
4981
5154
  const envelopeBytes = Buffer.byteLength(prompt) + Buffer.byteLength(input.sessionOptions.userMessage ?? "") + Buffer.byteLength(JSON.stringify(customTools));
4982
5155
  if (envelopeBytes > (input.sessionOptions.frontendExecutionPolicy?.scopeTargetBytes ?? FRONTEND_SCOPE_TARGET_BYTES) && scopeForPacking && session.id !== "compact-local") {
@@ -5107,6 +5280,43 @@ export async function runFrontendPlanSegmentedSessions(input) {
5107
5280
  : buildPhasePrompt(globalMockDataSegment, missing, session.requirementSlice ?? allRequirementIds);
5108
5281
  if (result.timedOut || /interrupt|termination-unconfirmed|budget_breach/.test(result.failureCategory))
5109
5282
  return { ...result, ok: false };
5283
+ if ((isFrontendPlanCapacityStop(result) || (session.atomicRecovery && result.ok)) && input.committedFacts &&
5284
+ !(missingPhaseFacts.length === 1 && missingPhaseFacts[0].kind === "plan-requirement") &&
5285
+ (session.coverageOnly || isUxRegistrySession || isUxLocalSession || session.id.startsWith("global-mock-data"))) {
5286
+ if (missingPhaseFacts.length === 0) {
5287
+ // A complete validated slice does not need a successful prose turn.
5288
+ // This never accepts finalize or bypasses the final contract validator.
5289
+ accumulated = { ...accumulated, ok: true, failureCategory: "success" };
5290
+ index += 1;
5291
+ continue;
5292
+ }
5293
+ const missingIds = [...new Set(missingPhaseFacts.flatMap(item => item.requirementIds))];
5294
+ const scope = session.coverageSlice ?? session.requirementSlice;
5295
+ if (scope && missingIds.length < scope.length) {
5296
+ queue[index] = { ...session, missingFacts: missingPhaseFacts,
5297
+ ...(session.coverageOnly ? { coverageSlice: missingIds } : { requirementSlice: missingIds }),
5298
+ retryCount: 0 };
5299
+ continue;
5300
+ }
5301
+ if (scope && missingIds.length > 1 && (session.coverageOnly || isUxLocalSession)) {
5302
+ const half = Math.ceil(missingIds.length / 2);
5303
+ queue.splice(index, 1, ...[missingIds.slice(0, half), missingIds.slice(half)].map(ids => ({
5304
+ ...session, retryCount: 0,
5305
+ ...(session.coverageOnly ? { coverageSlice: ids } : { requirementSlice: ids }),
5306
+ missingFacts: missingPhaseFacts.filter(item => item.requirementIds.some(id => ids.includes(id))),
5307
+ })));
5308
+ continue;
5309
+ }
5310
+ const focusStillMissing = atomicFocus && missingPhaseFacts.some(item => item.kind === atomicFocus.kind && item.id === atomicFocus.id &&
5311
+ item.requirementIds.join("\0") === atomicFocus.requirementIds.join("\0"));
5312
+ const retries = focusStillMissing ? (session.retryCount ?? 0) + 1 : 0;
5313
+ if (retries < 2) {
5314
+ queue[index] = { ...session, atomicRecovery: true, missingFacts: missingPhaseFacts, retryCount: retries };
5315
+ continue;
5316
+ }
5317
+ return { ...accumulated, ok: false, failureCategory: OUTPUT_LIMIT_RETRY_CATEGORY,
5318
+ stderr: `${accumulated.stderr}\nfrontend plan capacity recovery exhausted: preserve committed facts; phase=${session.id}; scope=${missingIds.join(",")}; strategy=atomic-fact; missing=${JSON.stringify(missingPhaseFacts)}`.trim() };
5319
+ }
5110
5320
  const capacityExhausted = readWriterThinkingExhaustionEvidence(result).stopReason === "length" || ["context-overflow", "context-budget-exhausted"].includes(result.failureCategory);
5111
5321
  if (capacityExhausted && !result.timedOut) {
5112
5322
  const failure = () => mapPlannerExhaustion({ ...result, ok: false, stderr: `${result.stderr}\nFRONTEND_INPUT_UNIT_TOO_LARGE: ${session.id}; no smaller complete scope can finish; unchanged retries are disabled` }, committedAfter > committedBefore);
@@ -5933,6 +6143,7 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
5933
6143
  frontendExecutionPolicy: meta.spec?.frontendExecutionPolicy,
5934
6144
  attachedFiles: [],
5935
6145
  modelConfig,
6146
+ outputLimitRecovery: { terminalToolNames: ["finalize_plan", "finalize_contract", "approve_design", "request_design_changes", "approve_review", "request_review_changes"] },
5936
6147
  repoRoot: input.cwd,
5937
6148
  step,
5938
6149
  toolNames,
@@ -6051,7 +6262,7 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
6051
6262
  ? options.ledgerTools.customTools
6052
6263
  : options.ledgerTools.customTools.filter((tool) => typeof tool === "object" &&
6053
6264
  tool !== null &&
6054
- toolNames.has(tool.name)),
6265
+ (toolNames.has(tool.name) || tool.name === "read_plan_facts")),
6055
6266
  flushLedger: () => options.ledgerTools.flush(),
6056
6267
  });
6057
6268
  if (planRequirementIds.length > 1) {
@@ -707,6 +707,8 @@ function shouldFallbackSdkToCli(sdkResult, validationIssues, options) {
707
707
  return true;
708
708
  if (!sdkResult.ok &&
709
709
  (sdkResult.failureCategory === "auth" ||
710
+ sdkResult.failureCategory === "output-limit" ||
711
+ sdkResult.failureCategory === "cancelled" ||
710
712
  sdkResult.failureCategory === "timeout" ||
711
713
  sdkResult.failureCategory === "termination-unconfirmed")) {
712
714
  return false;