@tea-agent/loop-agent 0.43.0-next.4 → 0.43.0-next.6

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 (96) hide show
  1. package/CHANGELOG.md +82 -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 +233 -202
  7. package/dist/executors/pi-executor.js +2 -0
  8. package/dist/executors/pi-sdk-executor.js +77 -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/pi-runtime.js +113 -95
  12. package/dist/worker/console/chat/routes.js +164 -54
  13. package/dist/worker/console/chat/scm-routes.js +193 -0
  14. package/dist/worker/console/chat/scm-service.js +253 -0
  15. package/dist/worker/console/chat/scm-tools.js +111 -0
  16. package/dist/worker/console/chat/session-catalog.js +1 -0
  17. package/dist/worker/console/chat/session-mode-view.js +48 -0
  18. package/dist/worker/console/chat/session-mode.js +218 -0
  19. package/dist/worker/console/chat/session-store.js +22 -6
  20. package/dist/worker/console/chat/shortcuts.js +6 -0
  21. package/dist/worker/console/chat/tools.js +1 -0
  22. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-CCj36aaC.js → abnfDiagram-N423BO3Z-BuyrlONK.js} +1 -1
  23. package/dist/worker/console/static/assets/{arc-Cl3MIVFh.js → arc-BvCl_wpi.js} +1 -1
  24. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-C_n4kMJS.js → architectureDiagram-T3A2C74G-BkBpBaRq.js} +1 -1
  25. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-DlWYJ7cK.js → blockDiagram-VBNYF7ZC-yve4Mcb_.js} +1 -1
  26. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Ud5pS7vS.js → c4Diagram-5PPSVZJV-9yLGboBx.js} +1 -1
  27. package/dist/worker/console/static/assets/channel-BRahIhjw.js +1 -0
  28. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-CQPwOv_j.js → chunk-2GRJ4B5K-BKP69b8r.js} +1 -1
  29. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CMzjxrWI.js → chunk-2Q5K7J3B-NWFRnXju.js} +1 -1
  30. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-pcnXnD6t.js → chunk-5RXB4S5H-BiweqLEc.js} +1 -1
  31. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CEB3AvU2.js → chunk-5VM5RSS4-CP9GHmRY.js} +1 -1
  32. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DSKnbXbp.js → chunk-6Q2QTUOP-BiGDkBtj.js} +1 -1
  33. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CS6-PjCZ.js → chunk-GF5L2VYU-DkJ4ZjQS.js} +1 -1
  34. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-f6q8Tnzz.js → chunk-JWPE2WC7-B5jjn7zq.js} +1 -1
  35. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DkB0QX3q.js → chunk-KBJHAD2P-C6KoK9O0.js} +1 -1
  36. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BdB4jAv6.js → chunk-RYQCIY6F-BvNIfyn-.js} +1 -1
  37. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BVm1bnX6.js → chunk-XXDRQBXY-BZ33iX4W.js} +1 -1
  38. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-GOC80mhq.js +1 -0
  39. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-GOC80mhq.js +1 -0
  40. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-FV_P-IEp.js → cose-bilkent-JH36ORCC-CP8eIuZd.js} +1 -1
  41. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BM7RtkA2.js → cynefin-VYW2F7L2-DZECjpOe.js} +1 -1
  42. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-C1BZwTG5.js → cynefinDiagram-MW4NZA55-CvVJoFqf.js} +1 -1
  43. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-C6McNKmY.js → dagre-VZM6K2ZE-D_JTqdAn.js} +1 -1
  44. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-C0aWRUaE.js → diagram-7IWD3JNH-Db7AQ0z0.js} +1 -1
  45. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Bfk_SKE8.js → diagram-B4RE2ZJO-AZPVo5CZ.js} +1 -1
  46. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-prx1T4qf.js → diagram-LBJQPF4R-BgXqI3E6.js} +1 -1
  47. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DBvty0eU.js → diagram-Q27KOJAE-BYjFn3Ba.js} +1 -1
  48. package/dist/worker/console/static/assets/{diagram-UB23O5K3-CF0em8e9.js → diagram-UB23O5K3-B4rJgGGO.js} +1 -1
  49. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-CF90b4Ws.js → ebnfDiagram-BXEA7PRR-C6XWZhpw.js} +1 -1
  50. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-hfunxzof.js → erDiagram-JOGREHBK-1YdQRIQW.js} +1 -1
  51. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-D-QSy4Nl.js → flowDiagram-UKHOOZJN-l3a0AwN0.js} +1 -1
  52. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DaBGNcvT.js → ganttDiagram-PKOTCBZU-CQL96F2Z.js} +1 -1
  53. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-BChcubGL.js → gitGraphDiagram-DS77QQ5N-BxCvafRb.js} +1 -1
  54. package/dist/worker/console/static/assets/index-4wCGsXuU.js +465 -0
  55. package/dist/worker/console/static/assets/index-CiF9WBoR.css +1 -0
  56. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BkC67fIa.js → infoDiagram-6WML65LV-BUipIeDO.js} +1 -1
  57. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-ibhLczCD.js → ishikawaDiagram-WSZJBQD7-CRfomVMX.js} +1 -1
  58. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DCRMitay.js → journeyDiagram-NVQOT4AX-_UFvyviX.js} +1 -1
  59. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-DJ_hKaYI.js → kanban-definition-27J2QSJJ-CKIRoqKv.js} +1 -1
  60. package/dist/worker/console/static/assets/{linear-CIlLsYsA.js → linear-CjttJDCM.js} +1 -1
  61. package/dist/worker/console/static/assets/{mermaid.core-C-5IQMP_.js → mermaid.core-DPvHX155.js} +5 -5
  62. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-TCzV5TQT.js → mindmap-definition-FAOFIHXS-Qmj-qwBe.js} +1 -1
  63. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-bfhd2BQH.js → pegDiagram-VL7TDLO6-Auw6DRgq.js} +1 -1
  64. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BxFRAm5N.js → pieDiagram-7S7Q4E2Y-CqMrfD4c.js} +1 -1
  65. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-C-m12lxv.js → quadrantDiagram-CIZ2JOQS-BAddeggH.js} +1 -1
  66. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-CwmLiGgx.js → railroadDiagram-AXF67PYL-DG1JuOu2.js} +1 -1
  67. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-oc0Q1jiD.js → requirementDiagram-LRYGKXZP-DLYGMKlC.js} +1 -1
  68. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-9Ght6Yur.js → sankeyDiagram-W5VNT64P-g68SbLM9.js} +1 -1
  69. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-ndm3lyz9.js → sequenceDiagram-SI44F4Z6-CmszLKv8.js} +1 -1
  70. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-7sPlKVcw.js → sizeCapture-X5ZJPWSS-D2tEKUCj.js} +1 -1
  71. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Izl21-oS.js → stateDiagram-OKZ733FA-Syc5E_Jf.js} +1 -1
  72. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-CwWwuPI-.js +1 -0
  73. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CtO3D65o.js → swimlanes-SLNWSIFB-Ibn0IRCa.js} +2 -2
  74. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-A7fqemr6.js +8 -0
  75. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Cyg5cY7z.js → timeline-definition-Z64GVDOM-2ZW9W3iS.js} +1 -1
  76. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-CwtE_TFK.js → vennDiagram-T6HMQDX7-v50HsKUh.js} +1 -1
  77. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-Dtt9v8OO.js → wardleyDiagram-T6FBY63Y-DqCXEoDH.js} +1 -1
  78. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-B2Y3ryb6.js → xychartDiagram-ELKLHX3M-vIP8Vo9d.js} +1 -1
  79. package/dist/worker/console/static/index.html +2 -2
  80. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +20 -4
  81. package/dist/worker/console/static-src/operator-chat/useComposer.js +13 -7
  82. package/dist/worker/console/workspace-context.js +6 -0
  83. package/dist/workflows/dag/frontend-capacity.js +9 -0
  84. package/dist/workflows/dag/frontend-design-policy.js +2 -1
  85. package/dist/workflows/dag/frontend-plan-completeness.js +186 -0
  86. package/dist/workflows/dag/frontend-plan-recovery-policy.js +18 -0
  87. package/dist/workflows/dag/frontend-shape-facts.js +12 -2
  88. package/package.json +1 -1
  89. package/skills/frontend-plan/SKILL.md +7 -0
  90. package/dist/worker/console/static/assets/channel-CEMRpNm-.js +0 -1
  91. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C9lhaZkR.js +0 -1
  92. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C9lhaZkR.js +0 -1
  93. package/dist/worker/console/static/assets/index-CJkCY1XR.js +0 -464
  94. package/dist/worker/console/static/assets/index-DUBCbiqv.css +0 -1
  95. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-CmElLFpP.js +0 -1
  96. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DKRbGG8V.js +0 -8
@@ -1,3 +1,6 @@
1
+ import { collectFrontendPlanMissingFacts, collectFrontendPlanPhaseMissingFacts, committedFactFromPlanRecord, planFactStringList, planFactScopeIntersects } from "../workflows/dag/frontend-plan-completeness.js";
2
+ export { collectFrontendPlanMissingFacts, collectFrontendPlanPhaseMissingFacts } from "../workflows/dag/frontend-plan-completeness.js";
3
+ import { classifyFrontendPlanRecovery } from "../workflows/dag/frontend-plan-recovery-policy.js";
1
4
  import { collectFrontendExecutionGroups, frontendExecutionSchema } from "../workflows/dag/frontend-execution-groups.js";
2
5
  import { FRONTEND_SCOPE_TARGET_BYTES, packFrontendInputUnits, parseFrontendInputBlock, projectFrontendContractPrompt, projectFrontendInputScope } from "../workflows/dag/frontend-input-projection.js";
3
6
  import { createDurableFrontendTools } from "../workflows/dag/frontend-durable-tools.js";
@@ -592,6 +595,7 @@ export function resolveDagPiToolNames(task) {
592
595
  ...FRONTEND_PLAN_RECORD_TOOL_NAMES,
593
596
  ...FRONTEND_PLAN_TERMINAL_TOOL_NAMES,
594
597
  ...FRONTEND_PLAN_ADOPT_TOOL_NAMES,
598
+ "read_plan_facts",
595
599
  ];
596
600
  }
597
601
  if ((task.readSet?.length ?? 0) > 0) {
@@ -1811,6 +1815,44 @@ export async function createFrontendPlanLedgerTools(input) {
1811
1815
  });
1812
1816
  }
1813
1817
  }
1818
+ // Named entries are last-wins at assembly. A scoped correction may
1819
+ // replace its own bindings, but must retain other scopes' coverage.
1820
+ const scope = new Set(scopedRequirementIds());
1821
+ const committed = readCommittedEvents(store, attemptId).map(event => event.fact);
1822
+ const targetOwners = new Map();
1823
+ for (const fact of committed) {
1824
+ if (fact.kind === "plan-verification-target" && isRecordObject(fact.entry) && typeof fact.entry.id === "string") {
1825
+ targetOwners.set(fact.entry.id, stringList(fact.entry.requirementIds));
1826
+ }
1827
+ }
1828
+ for (const [field, removals] of [["uiStates", removeUiStateNames], ["interactions", removeInteractionNames]]) {
1829
+ const live = new Map();
1830
+ for (const fact of committed) {
1831
+ if (fact.kind !== "state-flow")
1832
+ continue;
1833
+ for (const name of stringList(fact[field === "uiStates" ? "removeUiStateNames" : "removeInteractionNames"]))
1834
+ live.delete(name);
1835
+ for (const entry of Array.isArray(fact[field]) ? fact[field] : []) {
1836
+ if (isRecordObject(entry) && typeof entry.name === "string")
1837
+ live.set(entry.name, entry);
1838
+ }
1839
+ }
1840
+ const foreignBindings = (entry) => scope.size === 0 ? [] : stringList(entry.verificationTargetIds).filter(id => {
1841
+ const owners = targetOwners.get(id);
1842
+ return !owners?.length || owners.some(owner => !scope.has(owner));
1843
+ });
1844
+ for (const name of removals) {
1845
+ const previous = live.get(name);
1846
+ if (previous && foreignBindings(previous).length)
1847
+ return planToolReceipt({ ok: false, kind: "state-flow",
1848
+ error: `Cannot remove shared ${name} from this requirement scope; retain it and update its scoped bindings instead` });
1849
+ }
1850
+ for (const entry of field === "uiStates" ? uiStates : interactions) {
1851
+ const previous = live.get(String(entry.name));
1852
+ if (previous)
1853
+ entry.verificationTargetIds = [...new Set([...foreignBindings(previous), ...stringList(entry.verificationTargetIds)])];
1854
+ }
1855
+ }
1814
1856
  const result = await adoptPlanFact("state-flow", `${attemptId}:record_state_flow:${randomUUID()}`, {
1815
1857
  kind: "state-flow",
1816
1858
  origin: "plan",
@@ -2415,6 +2457,74 @@ export async function createFrontendPlanLedgerTools(input) {
2415
2457
  }
2416
2458
  },
2417
2459
  });
2460
+ const readPlanFactsTool = defineTool({
2461
+ name: "read_plan_facts",
2462
+ label: "read_plan_facts",
2463
+ 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.",
2464
+ parameters: Type.Object({
2465
+ kind: Type.String(), entryId: Type.Optional(Type.String()), eventId: Type.Optional(Type.String()),
2466
+ field: Type.Optional(Type.String()), offset: Type.Optional(Type.Integer({ minimum: 0 })),
2467
+ limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 50 })),
2468
+ stringOffset: Type.Optional(Type.Integer({ minimum: 0 })),
2469
+ expectedRevision: Type.Optional(Type.Integer({ minimum: 0 })),
2470
+ }, { additionalProperties: false }),
2471
+ async execute(_id, params) {
2472
+ const reply = (details) => ({ content: [{ type: "text", text: JSON.stringify(details) }], details });
2473
+ if (params.expectedRevision !== undefined && params.expectedRevision !== store.revision) {
2474
+ return reply({ ok: false, revision: store.revision, error: "ledger revision changed; restart pagination" });
2475
+ }
2476
+ let records = readCommittedEvents(store, attemptId).filter(record => {
2477
+ const fact = record.fact;
2478
+ return fact.kind === params.kind && (!params.eventId || record.eventId === params.eventId) &&
2479
+ (!params.entryId || fact.entry?.id === params.entryId);
2480
+ });
2481
+ // Full-entry corrections and registries are last-wins. Other kinds
2482
+ // remain an ordered event log (state removals must remain visible).
2483
+ if (params.entryId || params.kind === "state-registry")
2484
+ records = records.slice(-1);
2485
+ let values = records.map(record => ({ eventId: record.eventId, ...record.fact }));
2486
+ if (params.field) {
2487
+ values = records.flatMap(record => {
2488
+ let value = record.fact;
2489
+ for (const key of params.field.split(".")) {
2490
+ if (!value || typeof value !== "object" || !Object.hasOwn(value, key))
2491
+ return [];
2492
+ value = value[key];
2493
+ }
2494
+ return Array.isArray(value) ? value : [value];
2495
+ });
2496
+ }
2497
+ const offset = params.offset ?? 0;
2498
+ if (params.stringOffset !== undefined) {
2499
+ const value = values[offset];
2500
+ if (typeof value !== "string")
2501
+ return reply({ ok: false, error: "stringOffset requires a string field item" });
2502
+ // Unicode code points prevent a page boundary splitting a surrogate pair.
2503
+ const characters = Array.from(value);
2504
+ const chunk = characters.slice(params.stringOffset, params.stringOffset + 1000).join("");
2505
+ const next = params.stringOffset + Array.from(chunk).length;
2506
+ return reply({ ok: true, revision: store.revision, offset, stringOffset: params.stringOffset,
2507
+ chunk, totalCharacters: characters.length, nextStringOffset: next < characters.length ? next : null });
2508
+ }
2509
+ const items = [];
2510
+ let bytes = 0;
2511
+ for (const value of values.slice(offset, offset + (params.limit ?? 10))) {
2512
+ const size = Buffer.byteLength(JSON.stringify(value));
2513
+ if (bytes + size > 12_000)
2514
+ break;
2515
+ bytes += size;
2516
+ items.push(value);
2517
+ }
2518
+ if (!items.length && offset < values.length)
2519
+ return reply({
2520
+ ok: false, revision: store.revision, total: values.length, offset,
2521
+ 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",
2522
+ fields: typeof values[offset] === "object" && values[offset] !== null ? Object.keys(values[offset]) : [],
2523
+ });
2524
+ return reply({ ok: true, revision: store.revision, total: values.length, offset, items,
2525
+ nextOffset: offset + items.length < values.length ? offset + items.length : null });
2526
+ },
2527
+ });
2418
2528
  const durable = await createDurableFrontendTools({
2419
2529
  file: path.join(input.runDir, input.nodeId, "plan-typed-facts.jsonl"), attemptId, store: input.store,
2420
2530
  setWorkingStore: next => { store = next; },
@@ -2438,7 +2548,7 @@ export async function createFrontendPlanLedgerTools(input) {
2438
2548
  ],
2439
2549
  });
2440
2550
  return {
2441
- customTools: durable.customTools,
2551
+ customTools: [...durable.customTools, { ...readPlanFactsTool, execute: async (...args) => { await durable.flush(); return readPlanFactsTool.execute(...args); } }],
2442
2552
  adoptCommittedFacts: async (records) => durable.commitExternal(async () => {
2443
2553
  for (const record of records) {
2444
2554
  if (record.phase !== "committed")
@@ -3694,174 +3804,6 @@ const FRONTEND_PLAN_SEGMENTS = [
3694
3804
  ].join(" "),
3695
3805
  },
3696
3806
  ];
3697
- function committedFactFromPlanRecord(value) {
3698
- if (!value || typeof value !== "object" || Array.isArray(value))
3699
- return undefined;
3700
- const record = value;
3701
- if (record.phase !== undefined && record.phase !== "committed")
3702
- return undefined;
3703
- const fact = record.fact;
3704
- return fact && typeof fact === "object" && !Array.isArray(fact)
3705
- ? fact
3706
- : typeof record.kind === "string"
3707
- ? record
3708
- : undefined;
3709
- }
3710
- function planFactStringList(value) {
3711
- if (!Array.isArray(value))
3712
- return [];
3713
- return value.filter((item) => typeof item === "string" && item.trim().length > 0);
3714
- }
3715
- function planFactScopeIntersects(fact, requirementIds) {
3716
- return planFactStringList(fact.scopeRequirementIds).some((id) => requirementIds.has(id));
3717
- }
3718
- /** Compute the authoritative coverage queue from the committed plan ledger. */
3719
- export function collectFrontendPlanMissingFacts(input) {
3720
- const requirements = new Map();
3721
- const standaloneEvidenceGaps = new Set();
3722
- const verificationTargetIds = new Set();
3723
- const verificationTargetRequirements = new Map();
3724
- for (const value of input.committedFacts) {
3725
- const fact = committedFactFromPlanRecord(value);
3726
- if (!fact || fact.origin !== "plan")
3727
- continue;
3728
- if (fact.kind === "plan-requirement" && fact.entry && typeof fact.entry === "object") {
3729
- const entry = fact.entry;
3730
- if (typeof entry.id === "string" && entry.id.trim())
3731
- requirements.set(entry.id, entry);
3732
- }
3733
- if (fact.kind === "plan-verification-target" && fact.entry && typeof fact.entry === "object") {
3734
- const entry = fact.entry;
3735
- const id = entry.id;
3736
- if (typeof id === "string" && id.trim()) {
3737
- verificationTargetIds.add(id);
3738
- verificationTargetRequirements.set(id, new Set(Array.isArray(entry.requirementIds)
3739
- ? entry.requirementIds.filter((value) => typeof value === "string")
3740
- : []));
3741
- }
3742
- }
3743
- if (fact.kind === "plan-evidence-gap" && fact.entry && typeof fact.entry === "object") {
3744
- const entry = fact.entry;
3745
- const requirementId = entry.requirementId;
3746
- const description = entry.description;
3747
- if (typeof requirementId === "string" && requirementId.trim() && typeof description === "string" && description.trim()) {
3748
- standaloneEvidenceGaps.add(requirementId);
3749
- }
3750
- }
3751
- }
3752
- const missing = [];
3753
- for (const id of input.requirementIds) {
3754
- const entry = requirements.get(id);
3755
- if (!entry) {
3756
- missing.push({
3757
- kind: "plan-requirement",
3758
- id,
3759
- requirementIds: [id],
3760
- reason: `requirement ${id} has no committed plan-requirement fact`,
3761
- });
3762
- continue;
3763
- }
3764
- const targetIds = Array.isArray(entry.verificationTargetIds)
3765
- ? entry.verificationTargetIds.filter((value) => typeof value === "string" && value.trim().length > 0)
3766
- : [];
3767
- const gap = entry.evidenceGap && typeof entry.evidenceGap === "object"
3768
- ? entry.evidenceGap
3769
- : undefined;
3770
- const hasEvidenceGap = (typeof gap?.description === "string" && gap.description.trim().length > 0) ||
3771
- standaloneEvidenceGaps.has(id);
3772
- if (targetIds.length === 0 && !hasEvidenceGap) {
3773
- missing.push({
3774
- kind: "plan-verification-target",
3775
- requirementIds: [id],
3776
- reason: `requirement ${id} declares neither a verification target nor a non-empty evidenceGap`,
3777
- });
3778
- continue;
3779
- }
3780
- for (const targetId of targetIds) {
3781
- if (!verificationTargetIds.has(targetId) ||
3782
- !verificationTargetRequirements.get(targetId)?.has(id)) {
3783
- missing.push({
3784
- kind: "plan-verification-target",
3785
- id: targetId,
3786
- requirementIds: [id],
3787
- reason: `requirement ${id} references verification target ${targetId}, but that target is not committed`,
3788
- });
3789
- }
3790
- }
3791
- }
3792
- return missing;
3793
- }
3794
- /** Completeness checks for phases whose facts are committed incrementally. */
3795
- export function collectFrontendPlanPhaseMissingFacts(input) {
3796
- const facts = input.committedFacts
3797
- .map(committedFactFromPlanRecord)
3798
- .filter((fact) => Boolean(fact && fact.origin === "plan"));
3799
- if (input.phase === "ux-registry") {
3800
- return facts.some((fact) => fact.kind === "state-registry")
3801
- ? []
3802
- : [
3803
- {
3804
- kind: "state-registry",
3805
- requirementIds: [...input.requirementIds],
3806
- reason: "global UX vocabulary phase has no committed state-registry fact",
3807
- },
3808
- ];
3809
- }
3810
- if (input.phase === "ux-local") {
3811
- const needsUx = input.requirementIds.some((id) => input.behaviorRequiredRequirementIds?.includes(id));
3812
- if (!needsUx)
3813
- return [];
3814
- const requirementSlice = new Set(input.requirementIds);
3815
- const scopedFacts = facts.filter((fact) => planFactScopeIntersects(fact, requirementSlice));
3816
- const hasChoice = scopedFacts.some((fact) => fact.kind === "component-choice" &&
3817
- Array.isArray(fact.uiComponentChoices) &&
3818
- fact.uiComponentChoices.length > 0);
3819
- const canonicalStateFlow = collectCanonicalStateFlowNames(scopedFacts);
3820
- const hasStateFlow = canonicalStateFlow.uiStateNames.size > 0 ||
3821
- canonicalStateFlow.interactionNames.size > 0;
3822
- const missing = [];
3823
- if (!hasChoice) {
3824
- missing.push({
3825
- kind: "component-choice",
3826
- requirementIds: [...input.requirementIds],
3827
- reason: "behaviour-required UX slice has no committed component-choice fact",
3828
- });
3829
- }
3830
- if (!hasStateFlow) {
3831
- missing.push({
3832
- kind: "state-flow",
3833
- requirementIds: [...input.requirementIds],
3834
- reason: "behaviour-required UX slice has no committed state-flow fact",
3835
- });
3836
- }
3837
- return missing;
3838
- }
3839
- const hasMockApi = facts.some((fact) => fact.kind === "mock-api");
3840
- const allInteractions = collectCanonicalStateFlowNames(input.committedFacts).interactionNames;
3841
- const liveInteractions = new Set([...collectCanonicalStateFlowNames(facts.filter(f => !planFactStringList(f.scopeRequirementIds).length || planFactScopeIntersects(f, new Set(input.requirementIds)))).interactionNames].filter(name => allInteractions.has(name)));
3842
- const coveredInteractions = new Set(facts
3843
- .filter((fact) => fact.kind === "data-flow")
3844
- .flatMap((fact) => planFactStringList(fact.interactions)));
3845
- const missing = [];
3846
- if (!hasMockApi) {
3847
- missing.push({
3848
- kind: "mock-api",
3849
- requirementIds: [...input.requirementIds],
3850
- reason: "global Mock/data phase has no committed mock-api fact",
3851
- });
3852
- }
3853
- for (const interaction of liveInteractions) {
3854
- if (coveredInteractions.has(interaction))
3855
- continue;
3856
- missing.push({
3857
- kind: "data-flow",
3858
- id: interaction,
3859
- requirementIds: [...input.requirementIds],
3860
- reason: `interaction ${interaction} has no committed data-flow fact`,
3861
- });
3862
- }
3863
- return missing;
3864
- }
3865
3807
  /** Estimate calls conservatively: requirement + one VT, with a second VT
3866
3808
  * reserved for behaviour-required requirements. Explicit declarations win. */
3867
3809
  export function estimateFrontendPlanRequirementRecordCalls(fact) {
@@ -3903,9 +3845,8 @@ export function batchFrontendPlanRequirements(input) {
3903
3845
  }
3904
3846
  const FRONTEND_PLAN_COVERAGE_MAX_RECORD_CALLS = 10;
3905
3847
  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
3848
+ // A single registry owns global names; large UX work uses bounded serial
3849
+ // scopes against that registry. Keep a safety bound for adaptive retries
3909
3850
  // without letting the old 32-session ceiling skip finalize.
3910
3851
  const FRONTEND_PLAN_BATCH_MAX_SESSIONS = 128;
3911
3852
  const FRONTEND_PLAN_SMALL_MAX_REQUIREMENTS = 8;
@@ -4046,26 +3987,28 @@ export function compactFrontendPlanPromptForRequirementSlice(basePrompt, require
4046
3987
  if (!value || typeof value !== "object" || Array.isArray(value))
4047
3988
  return [];
4048
3989
  const target = value;
4049
- const targetRequirementIds = compactPromptStringArray(target.requirementIds, 20, 80);
3990
+ if (options.verificationTargetIds && !options.verificationTargetIds.includes(String(target.id)))
3991
+ return [];
3992
+ const targetRequirementIds = planFactStringList(target.requirementIds);
4050
3993
  const relatedRequirementIds = targetRequirementIds.filter((id) => sliceSet.has(id));
4051
3994
  if (relatedRequirementIds.length === 0)
4052
3995
  return [];
4053
3996
  return [
4054
3997
  {
4055
- ...(compactPromptString(target.id, 80)
4056
- ? { id: compactPromptString(target.id, 80) }
3998
+ ...(typeof target.id === "string"
3999
+ ? { id: target.id }
4057
4000
  : {}),
4058
- ...(compactPromptString(target.commandId, 80)
4059
- ? { commandId: compactPromptString(target.commandId, 80) }
4001
+ ...(typeof target.commandId === "string"
4002
+ ? { commandId: target.commandId }
4060
4003
  : {}),
4061
4004
  ...(compactPromptString(target.commandLabel, 180)
4062
4005
  ? { commandLabel: compactPromptString(target.commandLabel, 180) }
4063
4006
  : {}),
4064
- ...(compactPromptString(target.file, 180)
4065
- ? { file: compactPromptString(target.file, 180) }
4007
+ ...(typeof target.file === "string"
4008
+ ? { file: target.file }
4066
4009
  : {}),
4067
4010
  requirementIds: relatedRequirementIds,
4068
- uiStates: compactPromptStringArray(target.uiStates, 12, 100),
4011
+ uiStates: planFactStringList(target.uiStates),
4069
4012
  },
4070
4013
  ];
4071
4014
  })
@@ -4117,7 +4060,7 @@ export function compactFrontendPlanPromptForRequirementSlice(basePrompt, require
4117
4060
  if (!value || typeof value !== "object" || Array.isArray(value))
4118
4061
  return [];
4119
4062
  const state = value;
4120
- const id = compactPromptString(state.id, 80);
4063
+ const id = typeof state.id === "string" ? state.id : undefined;
4121
4064
  if (!id)
4122
4065
  return [];
4123
4066
  return [{
@@ -4217,9 +4160,9 @@ function compactFrontendPlanLedgerContext(input) {
4217
4160
  compactFacts.push({
4218
4161
  kind: fact.kind,
4219
4162
  entry: {
4220
- id: compactPromptString(entry.id, 80),
4221
- implementationTargets: compactPromptStringArray(entry.implementationTargets, 12, 180),
4222
- verificationTargetIds: compactPromptStringArray(entry.verificationTargetIds, 12, 80),
4163
+ id: entry.id,
4164
+ implementationTargets: planFactStringList(entry.implementationTargets).slice(0, 12),
4165
+ verificationTargetIds: planFactStringList(entry.verificationTargetIds).slice(0, 12),
4223
4166
  ...(compactPromptString(entry.expectedOutcome, 240)
4224
4167
  ? { expectedOutcome: compactPromptString(entry.expectedOutcome, 240) }
4225
4168
  : {}),
@@ -4233,17 +4176,19 @@ function compactFrontendPlanLedgerContext(input) {
4233
4176
  if (fact.kind === "plan-verification-target") {
4234
4177
  if (!entry)
4235
4178
  continue;
4236
- const requirementIds = compactPromptStringArray(entry.requirementIds, 20, 80);
4237
- if (!requirementIds.some((id) => slice.has(id)))
4179
+ // Scope against the complete canonical binding before compacting.
4180
+ // A shared target may bind a late requirement beyond the preview cap.
4181
+ const requirementIds = planFactStringList(entry.requirementIds).filter((id) => slice.has(id));
4182
+ if (requirementIds.length === 0)
4238
4183
  continue;
4239
4184
  compactFacts.push({
4240
4185
  kind: fact.kind,
4241
4186
  entry: {
4242
- id: compactPromptString(entry.id, 80),
4243
- commandId: compactPromptString(entry.commandId, 80),
4244
- file: compactPromptString(entry.file, 180),
4187
+ id: entry.id,
4188
+ commandId: entry.commandId,
4189
+ file: entry.file,
4245
4190
  requirementIds,
4246
- uiStates: compactPromptStringArray(entry.uiStates, 12, 100),
4191
+ uiStates: planFactStringList(entry.uiStates).slice(0, 12),
4247
4192
  },
4248
4193
  });
4249
4194
  continue;
@@ -4270,8 +4215,8 @@ function compactFrontendPlanLedgerContext(input) {
4270
4215
  if (fact.kind === "state-registry") {
4271
4216
  compactFacts.push({
4272
4217
  kind: fact.kind,
4273
- uiStateNames: compactPromptStringArray(fact.uiStateNames, 40, 100),
4274
- interactionNames: compactPromptStringArray(fact.interactionNames, 60, 120),
4218
+ uiStateNames: planFactStringList(fact.uiStateNames).slice(0, 40),
4219
+ interactionNames: planFactStringList(fact.interactionNames).slice(0, 60),
4275
4220
  });
4276
4221
  continue;
4277
4222
  }
@@ -4323,13 +4268,21 @@ function compactFrontendPlanLedgerContext(input) {
4323
4268
  return "";
4324
4269
  const priorityFacts = compactFacts.filter((fact) => fact.kind === "plan-requirement" || fact.kind === "plan-verification-target");
4325
4270
  const otherFacts = compactFacts.filter((fact) => fact.kind !== "plan-requirement" && fact.kind !== "plan-verification-target");
4271
+ let previewBytes = 0;
4326
4272
  const boundedFacts = [
4327
- ...priorityFacts,
4328
- ...otherFacts,
4329
- ];
4273
+ ...priorityFacts.slice(0, 64),
4274
+ ...otherFacts.slice(-32),
4275
+ ].slice(0, 96).filter(fact => {
4276
+ const bytes = Buffer.byteLength(JSON.stringify(fact));
4277
+ if (previewBytes + bytes > 24_000)
4278
+ return false;
4279
+ previewBytes += bytes;
4280
+ return true;
4281
+ });
4330
4282
  return [
4331
4283
  "<frontend_plan_ledger>",
4332
- "Committed plan facts from earlier sessions. Treat these as authoritative; correct them only with the allowed replacement/removal fields.",
4284
+ "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.",
4285
+ JSON.stringify({ complete: false, matchingFacts: compactFacts.length, omittedFacts: compactFacts.length - boundedFacts.length }),
4333
4286
  JSON.stringify(boundedFacts),
4334
4287
  "</frontend_plan_ledger>",
4335
4288
  ].join("\n");
@@ -4707,7 +4660,7 @@ export async function runFrontendPlanSegmentedSessions(input) {
4707
4660
  "component-choice",
4708
4661
  "state-flow",
4709
4662
  ],
4710
- scopedKinds: ["state-flow"],
4663
+ // Include shared state bindings from other scopes; page full values before correcting.
4711
4664
  })
4712
4665
  : "";
4713
4666
  return [
@@ -4926,6 +4879,27 @@ export async function runFrontendPlanSegmentedSessions(input) {
4926
4879
  let lastDurableCount = input.committedFactCount();
4927
4880
  while (index < queue.length) {
4928
4881
  const session = queue[index];
4882
+ if (input.attempt > 1 && input.committedFacts && session.id === "ux-registry" &&
4883
+ collectFrontendPlanPhaseMissingFacts({ phase: "ux-registry", requirementIds: allRequirementIds, committedFacts: input.committedFacts() }).length === 0) {
4884
+ index += 1;
4885
+ continue;
4886
+ }
4887
+ // Reuse complete UX scopes on node retry. Require explicit per-requirement
4888
+ // ownership; an unrelated/global fact must not prove a slice complete.
4889
+ if (input.attempt > 1 && input.committedFacts && session.id.startsWith("ux-local-") && session.requirementSlice?.length) {
4890
+ const facts = input.committedFacts();
4891
+ const ownedIds = new Set(facts.flatMap(value => {
4892
+ const fact = committedFactFromPlanRecord(value);
4893
+ return fact ? planFactStringList(fact.scopeRequirementIds) : [];
4894
+ }));
4895
+ const complete = session.requirementSlice.every(id => ownedIds.has(id)) &&
4896
+ collectFrontendPlanPhaseMissingFacts({ phase: "ux-local", requirementIds: session.requirementSlice,
4897
+ committedFacts: facts, behaviorRequiredRequirementIds: input.behaviorRequiredRequirementIds }).length === 0;
4898
+ if (complete) {
4899
+ index += 1;
4900
+ continue;
4901
+ }
4902
+ }
4929
4903
  const remaining = (session.coverageOnly ? session.coverageSlice ?? [] : []).filter((id) => !input.committedRequirementIds?.().has(id));
4930
4904
  const preexistingMissing = session.coverageOnly && session.coverageSlice && input.committedFacts
4931
4905
  ? collectFrontendPlanMissingFacts({
@@ -4971,12 +4945,30 @@ export async function runFrontendPlanSegmentedSessions(input) {
4971
4945
  : buildPhasePrompt(segment, session.missingFacts, session.requirementSlice ?? allRequirementIds);
4972
4946
  }
4973
4947
  }
4948
+ const atomicFocus = session.atomicRecovery ? session.missingFacts?.[0] : undefined;
4949
+ if (atomicFocus) {
4950
+ prompt = [
4951
+ compactFrontendPlanPromptForRequirementSlice(input.basePrompt, atomicFocus.requirementIds, {
4952
+ includeChecklist: false,
4953
+ ...(atomicFocus.kind === "plan-verification-target" && atomicFocus.id
4954
+ ? { verificationTargetIds: [atomicFocus.id] } : {}),
4955
+ }),
4956
+ `ATOMIC FACT: ${atomicFocus.kind}${atomicFocus.id ? ` ${atomicFocus.id}` : ""}`,
4957
+ atomicFocus.reason,
4958
+ "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.",
4959
+ ].join("\n\n");
4960
+ }
4974
4961
  const committedBefore = input.committedFactCount();
4975
4962
  input.setActiveRequirementScope?.(session.coverageOnly ? session.coverageSlice ?? [] : session.requirementSlice ?? []);
4976
4963
  if (invocationCount >= FRONTEND_PLAN_BATCH_MAX_SESSIONS)
4977
4964
  break;
4978
4965
  invocationCount += 1;
4979
- const customTools = input.segmentCustomTools(session.toolNames);
4966
+ const atomicTools = {
4967
+ "plan-requirement": "record_plan_requirement", "plan-verification-target": "record_plan_verification_target",
4968
+ "state-registry": "record_state_registry", "component-choice": "record_component_choice",
4969
+ "state-flow": "record_state_flow", "data-flow": "record_data_flow", "mock-api": "record_mock_api",
4970
+ };
4971
+ const customTools = input.segmentCustomTools(atomicFocus ? new Set([atomicTools[atomicFocus.kind]]) : session.toolNames);
4980
4972
  const scopeForPacking = session.coverageSlice ?? session.requirementSlice;
4981
4973
  const envelopeBytes = Buffer.byteLength(prompt) + Buffer.byteLength(input.sessionOptions.userMessage ?? "") + Buffer.byteLength(JSON.stringify(customTools));
4982
4974
  if (envelopeBytes > (input.sessionOptions.frontendExecutionPolicy?.scopeTargetBytes ?? FRONTEND_SCOPE_TARGET_BYTES) && scopeForPacking && session.id !== "compact-local") {
@@ -5105,9 +5097,47 @@ export async function runFrontendPlanSegmentedSessions(input) {
5105
5097
  : isUxLocalSession
5106
5098
  ? buildUxPrompt(session.requirementSlice ?? [], missing)
5107
5099
  : buildPhasePrompt(globalMockDataSegment, missing, session.requirementSlice ?? allRequirementIds);
5108
- if (result.timedOut || /interrupt|termination-unconfirmed|budget_breach/.test(result.failureCategory))
5100
+ const recovery = classifyFrontendPlanRecovery({ ...result, stopReason: readWriterThinkingExhaustionEvidence(result).stopReason });
5101
+ if (recovery === "stop")
5109
5102
  return { ...result, ok: false };
5110
- const capacityExhausted = readWriterThinkingExhaustionEvidence(result).stopReason === "length" || ["context-overflow", "context-budget-exhausted"].includes(result.failureCategory);
5103
+ if ((recovery === "output" || (session.atomicRecovery && result.ok)) && input.committedFacts &&
5104
+ !(missingPhaseFacts.length === 1 && missingPhaseFacts[0].kind === "plan-requirement") &&
5105
+ (session.coverageOnly || isUxRegistrySession || isUxLocalSession || session.id.startsWith("global-mock-data"))) {
5106
+ if (missingPhaseFacts.length === 0) {
5107
+ // A complete validated slice does not need a successful prose turn.
5108
+ // This never accepts finalize or bypasses the final contract validator.
5109
+ accumulated = { ...accumulated, ok: true, failureCategory: "success" };
5110
+ index += 1;
5111
+ continue;
5112
+ }
5113
+ const missingIds = [...new Set(missingPhaseFacts.flatMap(item => item.requirementIds))];
5114
+ const scope = session.coverageSlice ?? session.requirementSlice;
5115
+ if (scope && missingIds.length < scope.length) {
5116
+ queue[index] = { ...session, missingFacts: missingPhaseFacts,
5117
+ ...(session.coverageOnly ? { coverageSlice: missingIds } : { requirementSlice: missingIds }),
5118
+ retryCount: 0 };
5119
+ continue;
5120
+ }
5121
+ if (scope && missingIds.length > 1 && (session.coverageOnly || isUxLocalSession)) {
5122
+ const half = Math.ceil(missingIds.length / 2);
5123
+ queue.splice(index, 1, ...[missingIds.slice(0, half), missingIds.slice(half)].map(ids => ({
5124
+ ...session, retryCount: 0,
5125
+ ...(session.coverageOnly ? { coverageSlice: ids } : { requirementSlice: ids }),
5126
+ missingFacts: missingPhaseFacts.filter(item => item.requirementIds.some(id => ids.includes(id))),
5127
+ })));
5128
+ continue;
5129
+ }
5130
+ const focusStillMissing = atomicFocus && missingPhaseFacts.some(item => item.kind === atomicFocus.kind && item.id === atomicFocus.id &&
5131
+ item.requirementIds.join("\0") === atomicFocus.requirementIds.join("\0"));
5132
+ const retries = focusStillMissing ? (session.retryCount ?? 0) + 1 : 0;
5133
+ if (retries < 2) {
5134
+ queue[index] = { ...session, atomicRecovery: true, missingFacts: missingPhaseFacts, retryCount: retries };
5135
+ continue;
5136
+ }
5137
+ return { ...accumulated, ok: false, failureCategory: OUTPUT_LIMIT_RETRY_CATEGORY,
5138
+ 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() };
5139
+ }
5140
+ const capacityExhausted = recovery === "output" || recovery === "context";
5111
5141
  if (capacityExhausted && !result.timedOut) {
5112
5142
  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);
5113
5143
  const missingCoverageIds = input.committedFacts ? [...new Set(collectFrontendPlanMissingFacts({ requirementIds: session.coverageSlice ?? session.requirementSlice ?? allRequirementIds, committedFacts: input.committedFacts() }).flatMap(f => f.requirementIds))] : [...(session.coverageSlice ?? session.requirementSlice ?? allRequirementIds)];
@@ -5213,7 +5243,7 @@ export async function runFrontendPlanSegmentedSessions(input) {
5213
5243
  : undefined;
5214
5244
  const zeroProgressBurn = coverageSlice !== undefined &&
5215
5245
  coverageSlice.length > 1 &&
5216
- (readWriterThinkingExhaustionEvidence(result).stopReason === "length" || ["context-overflow", "context-budget-exhausted"].includes(result.failureCategory) || (committedAfter === committedBefore && !(result.assistantText ?? "").trim() && !result.stderr.trim())) &&
5246
+ (capacityExhausted || (["empty-output", "unknown"].includes(result.failureCategory) && committedAfter === committedBefore && !(result.assistantText ?? "").trim() && !result.stderr.trim())) &&
5217
5247
  !result.timedOut;
5218
5248
  if (zeroProgressBurn && coverageSlice) {
5219
5249
  const missingIds = input.committedFacts ? new Set(collectFrontendPlanMissingFacts({ requirementIds: coverageSlice, committedFacts: input.committedFacts() }).flatMap(f => f.requirementIds)) : undefined;
@@ -5933,6 +5963,7 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
5933
5963
  frontendExecutionPolicy: meta.spec?.frontendExecutionPolicy,
5934
5964
  attachedFiles: [],
5935
5965
  modelConfig,
5966
+ outputLimitRecovery: { terminalToolNames: ["finalize_plan", "finalize_contract", "approve_design", "request_design_changes", "approve_review", "request_review_changes"] },
5936
5967
  repoRoot: input.cwd,
5937
5968
  step,
5938
5969
  toolNames,
@@ -6051,7 +6082,7 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
6051
6082
  ? options.ledgerTools.customTools
6052
6083
  : options.ledgerTools.customTools.filter((tool) => typeof tool === "object" &&
6053
6084
  tool !== null &&
6054
- toolNames.has(tool.name)),
6085
+ (toolNames.has(tool.name) || tool.name === "read_plan_facts")),
6055
6086
  flushLedger: () => options.ledgerTools.flush(),
6056
6087
  });
6057
6088
  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;