@tea-agent/loop-agent 0.44.0-next.6 → 0.44.0-next.8

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 (105) hide show
  1. package/CHANGELOG.md +288 -454
  2. package/dist/build-stamp.json +3 -3
  3. package/dist/executors/dag-pi/guards.js +4 -0
  4. package/dist/executors/dag-pi/model-config.js +30 -0
  5. package/dist/{workflows/dag/frontend-plan-completeness.js → executors/dag-pi/plan/facts.js} +25 -41
  6. package/dist/executors/dag-pi/plan/ledger-contract.js +55 -0
  7. package/dist/executors/dag-pi/tools/contract-tools.js +515 -0
  8. package/dist/executors/dag-pi/tools/design-terminal-tools.js +156 -0
  9. package/dist/executors/dag-pi/tools/plan-decision-tools.js +519 -0
  10. package/dist/executors/dag-pi/tools/review-terminal-tools.js +156 -0
  11. package/dist/executors/dag-pi/tools/scout-evidence-tools.js +270 -0
  12. package/dist/executors/dag-pi-executor.js +64 -1907
  13. package/dist/shared/git-workspace-status.js +2 -0
  14. package/dist/task/frontend-preflight.js +1 -1
  15. package/dist/task/source-prepare/semantic-intake.js +1 -1
  16. package/dist/task/source-prepare/source-fidelity-pi.js +1 -1
  17. package/dist/worker/console/chat/assistant-content.js +5 -34
  18. package/dist/worker/console/chat/chat-event-store.js +27 -10
  19. package/dist/worker/console/chat/db-connections.js +72 -8
  20. package/dist/worker/console/chat/pi-runtime/custom-tools/sql-tools.js +52 -24
  21. package/dist/worker/console/chat/pi-runtime.js +34 -0
  22. package/dist/worker/console/chat/routes.js +138 -9
  23. package/dist/worker/console/chat/session-store.js +147 -19
  24. package/dist/worker/console/chat/turn-process.js +69 -23
  25. package/dist/worker/console/chat/user-turn-outline.js +19 -0
  26. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Brn2sPzL.js → abnfDiagram-N423BO3Z-r1NAPU_5.js} +1 -1
  27. package/dist/worker/console/static/assets/{arc-BIMNTG0W.js → arc-dT5T4sVL.js} +1 -1
  28. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G--WtcgR_A.js → architectureDiagram-T3A2C74G-CP_cOq46.js} +1 -1
  29. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-BT_Ztog1.js → blockDiagram-VBNYF7ZC-sToEZxEf.js} +1 -1
  30. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-C5hbVr-l.js → c4Diagram-5PPSVZJV-DSxwwi2m.js} +1 -1
  31. package/dist/worker/console/static/assets/channel-CyBY_yGk.js +1 -0
  32. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BKKnyR9Z.js → chunk-2GRJ4B5K-B9fh0IkF.js} +1 -1
  33. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-BHs6biqA.js → chunk-2Q5K7J3B-CAUNl-A6.js} +1 -1
  34. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BdSFy_vP.js → chunk-5RXB4S5H-BomeqbNX.js} +1 -1
  35. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BpJIdTKV.js → chunk-5VM5RSS4-BNAuTOLE.js} +1 -1
  36. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-C_iWJhkT.js → chunk-6Q2QTUOP-DfzjRD5y.js} +1 -1
  37. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-BTbmx3-k.js → chunk-GF5L2VYU-C3uE-yc2.js} +1 -1
  38. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-D4Ee6DmP.js → chunk-JWPE2WC7-C3sQMMYr.js} +1 -1
  39. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-B487SORR.js → chunk-KBJHAD2P-D_AtEYQL.js} +1 -1
  40. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BKYHjNrr.js → chunk-RYQCIY6F-DW4TwhKv.js} +1 -1
  41. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DrTdoYZG.js → chunk-XXDRQBXY-CTQDmJcp.js} +1 -1
  42. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D5lN4E_E.js +1 -0
  43. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D5lN4E_E.js +1 -0
  44. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CttUY_Jo.js → cose-bilkent-JH36ORCC-iMqPuBsL.js} +1 -1
  45. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-B4Rm5gnR.js → cynefin-VYW2F7L2-DktIuDWs.js} +1 -1
  46. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-ByxHBq_S.js → cynefinDiagram-MW4NZA55-BgVnjyEs.js} +1 -1
  47. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-DGrfNFA0.js → dagre-VZM6K2ZE-CR25E4bu.js} +1 -1
  48. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-KjrUPquv.js → diagram-7IWD3JNH-CPM2h5HY.js} +1 -1
  49. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-VS4mlm75.js → diagram-B4RE2ZJO-DqF0x2g1.js} +1 -1
  50. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-BLXYmFTM.js → diagram-LBJQPF4R-G8BYp2eb.js} +1 -1
  51. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-C6xoFAqH.js → diagram-Q27KOJAE-BbC4kT_D.js} +1 -1
  52. package/dist/worker/console/static/assets/{diagram-UB23O5K3-C478dCe7.js → diagram-UB23O5K3-CKZwZ-G5.js} +1 -1
  53. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DHEpW5zN.js → ebnfDiagram-BXEA7PRR-kJDmheia.js} +1 -1
  54. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-CA5GKfo9.js → erDiagram-JOGREHBK-DFTVHyxB.js} +1 -1
  55. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-zysi0Fl-.js → flowDiagram-UKHOOZJN-BqdN5_os.js} +1 -1
  56. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DGzcKA8Y.js → ganttDiagram-PKOTCBZU-SYDjVakh.js} +1 -1
  57. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CHsuIG1w.js → gitGraphDiagram-DS77QQ5N-Bf8bNpGA.js} +1 -1
  58. package/dist/worker/console/static/assets/index-B28Onmfy.css +1 -0
  59. package/dist/worker/console/static/assets/index-CTMZsDmh.js +486 -0
  60. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BexZHPCp.js → infoDiagram-6WML65LV-DIeAmJ45.js} +1 -1
  61. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-Gkx8q-DB.js → ishikawaDiagram-WSZJBQD7-oikqUAlo.js} +1 -1
  62. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-XDW3qrJ7.js → journeyDiagram-NVQOT4AX-mKEF_xYT.js} +1 -1
  63. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-DJ07eEaN.js → kanban-definition-27J2QSJJ-BdWJHPnN.js} +1 -1
  64. package/dist/worker/console/static/assets/{linear-DL-zeqOn.js → linear-DGRlieMH.js} +1 -1
  65. package/dist/worker/console/static/assets/{mermaid.core-C0HdXtOf.js → mermaid.core-4P9yPC9c.js} +5 -5
  66. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CqRX0lrt.js → mindmap-definition-FAOFIHXS-5PTSkos6.js} +1 -1
  67. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVM9JZ2H.js → pegDiagram-VL7TDLO6-DONIFpG5.js} +1 -1
  68. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BA3ozdKk.js → pieDiagram-7S7Q4E2Y-D_Z4S70M.js} +1 -1
  69. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B-0_p9_4.js → quadrantDiagram-CIZ2JOQS-CAsjh8iY.js} +1 -1
  70. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-DHRzPWFc.js → railroadDiagram-AXF67PYL-Cx4Dd6ac.js} +1 -1
  71. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-9BEC_PcW.js → requirementDiagram-LRYGKXZP-cQb7mwyH.js} +1 -1
  72. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-DMxeW4rN.js → sankeyDiagram-W5VNT64P-Cd-DhUdf.js} +1 -1
  73. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DixP6LIU.js → sequenceDiagram-SI44F4Z6-AlKYRgIk.js} +1 -1
  74. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B_EifJBo.js → sizeCapture-X5ZJPWSS-CUy_ET4e.js} +1 -1
  75. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BwikwWbX.js → stateDiagram-OKZ733FA-CTuVe6Sa.js} +1 -1
  76. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BsWhs8O7.js +1 -0
  77. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-C162l9o1.js → swimlanes-SLNWSIFB-ChFLJb-v.js} +2 -2
  78. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CJcioYGH.js +8 -0
  79. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-mpe_u1i4.js → timeline-definition-Z64GVDOM-DZZeiAPr.js} +1 -1
  80. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DyUk54HX.js → vennDiagram-T6HMQDX7-t4O91cZU.js} +1 -1
  81. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-HYyMkzfm.js → wardleyDiagram-T6FBY63Y-CV5qGHZu.js} +1 -1
  82. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-DRoh7skR.js → xychartDiagram-ELKLHX3M-DyXbytUI.js} +1 -1
  83. package/dist/worker/console/static/index.html +2 -2
  84. package/dist/worker/console/static-src/operator-chat/chat-link.js +20 -0
  85. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +58 -3
  86. package/dist/worker/console/static-src/operator-chat/process-label.js +19 -6
  87. package/dist/worker/console/static-src/operator-chat/transcript-mode.js +21 -0
  88. package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +2 -0
  89. package/dist/worker/console/static-src/operator-chat/turn-process-disclosure.js +13 -5
  90. package/dist/worker/console/static-src/operator-chat/turn-state-projection.js +166 -0
  91. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +56 -2
  92. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +202 -43
  93. package/dist/worker/console/static-src/operator-chat/useChatStream.js +78 -23
  94. package/dist/worker/console/static-src/operator-chat/useChatThread.js +55 -18
  95. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +5 -2
  96. package/dist/worker/scheduler/scheduled-goal-dispatch.js +3 -2
  97. package/dist/worker/scheduler/scheduled-goal-evidence.js +6 -1
  98. package/package.json +1 -1
  99. package/dist/worker/console/static/assets/channel-DNWB4hpR.js +0 -1
  100. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-COhFgJji.js +0 -1
  101. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-COhFgJji.js +0 -1
  102. package/dist/worker/console/static/assets/index-CJh6pVLm.js +0 -482
  103. package/dist/worker/console/static/assets/index-C_kvKmFM.css +0 -1
  104. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-S2ZHMo1c.js +0 -1
  105. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BZm4ByfV.js +0 -8
@@ -0,0 +1,519 @@
1
+ /** DAG Pi plan decision 工具工厂:decision 通道 record_* 工具与 finalize 编译,禁止反向依赖执行器入口。 */
2
+ import path from "node:path";
3
+ import { createHash, randomUUID } from "node:crypto";
4
+ /** Translate derived-patch validation findings into decision-channel
5
+ * vocabulary. Each zod issue path names a COMPILED patch array, but the model
6
+ * authored record_* facts — so quote the offending entry's identity and name
7
+ * the tool that owns it (r14: "uiComponentChoices.0.specReference.section:
8
+ * Required" is unactionable when the model has never heard of specReference). */
9
+ export function translateDecisionPatchFindings(message, decision) {
10
+ const body = message.replace(/^invalid-output:\s*/, "");
11
+ return body
12
+ .split(/;\s*/)
13
+ .map(issue => {
14
+ const choice = issue.match(/uiComponentChoices\.(\d+)\.(.+)/);
15
+ if (choice) {
16
+ const row = decision.reuseDecisions[Number(choice[1])];
17
+ const base = row
18
+ ? `record_reuse_decision purpose "${row.purpose}" (component ${row.symbol}, decision ${row.decision}): ${issue}`
19
+ : `record_reuse_decision row ${choice[1]}: ${issue}`;
20
+ const field = choice[2];
21
+ return field?.startsWith("specReference")
22
+ ? `${base} — pass specSection (the AC id / task-source section it implements) on that entry, or use decision "reuse-existing"`
23
+ : base;
24
+ }
25
+ const focus = issue.match(/verificationTargets\.(\d+)\.(.+)/);
26
+ if (focus) {
27
+ const row = decision.verificationFocus[Number(focus[1])];
28
+ if (!row)
29
+ return `record_verification_focus row ${focus[1]}: ${issue}`;
30
+ return `record_verification_focus ${row.id}: ${issue} — behavior targets derive uiStates from record_state_ownership rows whose owner equals behaviorGroupId "${row.behaviorGroupId}"; record one for this group if missing`;
31
+ }
32
+ const state = issue.match(/uiStates\.(\d+)\.(.+)/);
33
+ if (state) {
34
+ const row = decision.stateOwnership[Number(state[1])];
35
+ return row
36
+ ? `record_state_ownership "${row.state}" (owner ${row.owner}): ${issue}`
37
+ : `record_state_ownership row ${state[1]}: ${issue}`;
38
+ }
39
+ const interaction = issue.match(/interactions\.(\d+)\.(.+)/);
40
+ if (interaction) {
41
+ const row = decision.dataFlows[Number(interaction[1])];
42
+ return row
43
+ ? `record_data_flow "${row.interaction}": ${issue}`
44
+ : `record_data_flow row ${interaction[1]}: ${issue}`;
45
+ }
46
+ const requirement = issue.match(/requirements\.(\d+)\.(.+)/);
47
+ if (requirement) {
48
+ return `record_module_placement row ${requirement[1]}: ${issue}`;
49
+ }
50
+ return issue;
51
+ })
52
+ .join("; ");
53
+ }
54
+ export async function createFrontendPlanDecisionTools(input) {
55
+ const [{ Type }, { defineTool }, { assembleDecisionContractFromFacts, assembleExperimentPlanContract, buildFrontendPlanRelationshipPatch }] = await Promise.all([
56
+ import("typebox"),
57
+ import("@earendil-works/pi-coding-agent"),
58
+ import("../../../workflows/dag/frontend-plan-decision-contract.js"),
59
+ ]);
60
+ const { readCommittedEvents } = await import("../../../workflows/dag/frontend-typed-event-store.js");
61
+ const { adoptTypedEventFact, stageTypedEventFact } = await import("../../../workflows/dag/frontend-typed-event-transaction.js");
62
+ const { createDurableFrontendTools } = await import("../../../workflows/dag/frontend-durable-tools.js");
63
+ let store = input.store;
64
+ const { attemptId, authority } = input;
65
+ const optionalStringArray = Type.Optional(Type.Array(Type.String({ minLength: 1 })));
66
+ const optionalString = Type.Optional(Type.String({ minLength: 1 }));
67
+ // In-session pre-validation of the derived relationship patch. The
68
+ // post-session bridge runs the identical analysis; running it first inside
69
+ // finalize_decision turns findings into a bounded in-session correction
70
+ // (fix facts, finalize again) instead of an attempt burn. Skipped when the
71
+ // caller does not supply the runtime skeleton/sourceBinding.
72
+ let requiredDeliverablesCache;
73
+ const prevalidateDerivedPatch = async () => {
74
+ if (!input.skeleton || !input.sourceBinding)
75
+ return { ok: true, canonicalSha256: "", canonical: {} };
76
+ const committed = readCommittedEvents(store, attemptId);
77
+ const decision = assembleDecisionContractFromFacts(committed.map(record => record.fact));
78
+ requiredDeliverablesCache ??= (async () => {
79
+ const map = new Map();
80
+ try {
81
+ const { readTypedEventStoreFromJsonl } = await import("../../../workflows/dag/frontend-typed-event-store.js");
82
+ const contractRecords = await readTypedEventStoreFromJsonl(path.join(input.runDir, "frontend-contract-pi", "contract-typed-facts.jsonl"));
83
+ for (const record of [...contractRecords].reverse()) {
84
+ const fact = record.fact;
85
+ if (!fact || fact.origin !== "contract" || fact.kind !== "required-deliverables")
86
+ continue;
87
+ for (const item of Array.isArray(fact.items) ? fact.items : []) {
88
+ if (!item || typeof item !== "object" || Array.isArray(item))
89
+ continue;
90
+ const requirementId = item.requirementId;
91
+ const file = item.path;
92
+ if (typeof requirementId !== "string" || typeof file !== "string")
93
+ continue;
94
+ const paths = map.get(requirementId) ?? [];
95
+ paths.push(file);
96
+ map.set(requirementId, paths);
97
+ }
98
+ break;
99
+ }
100
+ }
101
+ catch {
102
+ // Missing/unreadable contract ledger → no deliverable obligations.
103
+ }
104
+ return map;
105
+ })();
106
+ const concreteWriteSet = [
107
+ ...new Set([
108
+ ...decision.modulePlacements.flatMap(item => item.paths),
109
+ ...decision.verificationFocus.map(item => item.file),
110
+ ]),
111
+ ];
112
+ const { applyFrontendContractMergePatch, analyzeFrontendPlanPatchCandidate, FrontendContractFailure, PlanPolicyPrecheckFailure, serializeDeterministicJson, } = await import("../../../workflows/dag/frontend-implementation-contract.js");
113
+ try {
114
+ const patch = buildFrontendPlanRelationshipPatch({
115
+ decision,
116
+ authority,
117
+ concreteWriteSet,
118
+ requiredDeliverables: await requiredDeliverablesCache,
119
+ });
120
+ const merged = applyFrontendContractMergePatch(input.skeleton, patch);
121
+ const analysis = await analyzeFrontendPlanPatchCandidate({
122
+ runDir: input.runDir,
123
+ rawContractText: serializeDeterministicJson(merged),
124
+ sourceBinding: input.sourceBinding,
125
+ });
126
+ return {
127
+ ok: true,
128
+ canonicalSha256: createHash("sha256")
129
+ .update(serializeDeterministicJson(analysis.canonical))
130
+ .digest("hex"),
131
+ canonical: analysis.canonical,
132
+ };
133
+ }
134
+ catch (error) {
135
+ if (error instanceof PlanPolicyPrecheckFailure) {
136
+ // Template the fix: every uncovered interaction/state maps to a
137
+ // fill-in record_reuse_decision row (the decision channel's
138
+ // component-choice tool).
139
+ const suggestions = error.findings
140
+ .filter(finding => finding.code === "ui-design-coverage-missing" && finding.path)
141
+ .map(finding => ({
142
+ tool: "record_reuse_decision",
143
+ args: {
144
+ entry: {
145
+ symbol: "<name the existing or new component>",
146
+ decision: "reuse-existing",
147
+ evidence: ["<existing repo file that proves this reuse>"],
148
+ purpose: finding.path,
149
+ },
150
+ },
151
+ }));
152
+ const suggestionBlock = suggestions.length > 0
153
+ ? ` Suggested record_* calls (copy, fill component, submit): ${JSON.stringify(suggestions)}`
154
+ : "";
155
+ return {
156
+ ok: false,
157
+ error: `finalize_decision pre-validation failed (fix the listed plan facts, then call finalize_decision again): ${error.message}${suggestionBlock}`,
158
+ };
159
+ }
160
+ // FrontendContractFailure messages carry zod issue paths and are
161
+ // translated into record_* vocabulary; the bridge compiler throws
162
+ // decision-state-unbindable / decision-styling-strategy-conflict
163
+ // diagnostics that are already model-fixable as written.
164
+ const message = error instanceof Error ? error.message : String(error);
165
+ const translated = error instanceof FrontendContractFailure
166
+ ? translateDecisionPatchFindings(message, decision)
167
+ : message;
168
+ return {
169
+ ok: false,
170
+ error: `finalize_decision pre-validation failed (fix the listed facts with the named record_* tools — resubmit a corrected row with the same identity and replace:true to replace it — then call finalize_decision again): ${translated}`,
171
+ };
172
+ }
173
+ };
174
+ const planToolReceipt = (details) => ({
175
+ content: [{ type: "text", text: JSON.stringify(details) }],
176
+ details,
177
+ });
178
+ async function adoptDecisionFact(kind, requestId, fact) {
179
+ try {
180
+ const staged = stageTypedEventFact({
181
+ store,
182
+ requestId,
183
+ attemptId,
184
+ fact: fact,
185
+ });
186
+ const committed = await adoptTypedEventFact({
187
+ store,
188
+ requestId,
189
+ attemptId,
190
+ fact: fact,
191
+ eventId: staged.eventId,
192
+ expectedRevision: store.revision,
193
+ });
194
+ return { ok: true, kind, eventId: committed.eventId, revision: committed.revision, error: "" };
195
+ }
196
+ catch (error) {
197
+ return {
198
+ ok: false,
199
+ kind,
200
+ code: error?.code,
201
+ error: error instanceof Error ? error.message : String(error),
202
+ };
203
+ }
204
+ }
205
+ const record = (name, label, description, entrySchema, gate) => defineTool({
206
+ name,
207
+ label,
208
+ description,
209
+ promptSnippet: `Record ${label}.`,
210
+ parameters: Type.Object({ entry: entrySchema }, { additionalProperties: false }),
211
+ async execute(_toolCallId, params) {
212
+ const entry = params?.entry;
213
+ const kind = name.replace("record_", "").replace(/_/g, "-");
214
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
215
+ return planToolReceipt({ ok: false, kind, error: `${name} requires a non-empty entry object` });
216
+ }
217
+ if (gate) {
218
+ const rejected = await gate(entry);
219
+ if (rejected)
220
+ return rejected;
221
+ }
222
+ const result = await adoptDecisionFact(kind, `${attemptId}:${name}:${randomUUID()}`, { kind, origin: "plan", entry });
223
+ return planToolReceipt(result);
224
+ },
225
+ });
226
+ const recordModulePlacementTool = record("record_module_placement", "module placement", "Record one module placement: which behavior-group/module id maps to which concrete files. Example: {\"entry\": {\"id\": \"page\", \"paths\": [\"src/page.tsx\"]}}", Type.Object({ id: Type.String({ minLength: 1 }), paths: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }) }, { additionalProperties: false }));
227
+ const recordReuseDecisionTool = record("record_reuse_decision", "reuse decision", "Record one component decision: which component serves which UI state/interaction (purpose). decision \"reuse-existing\" = an existing repo component (evidence[0] = the repo file that proves it); \"specified\" = the frontend spec mandates it; \"new\" = the task source mandates a bounded addition — specified/new require specSection naming the spec/task-source declaration they implement (e.g. an AC id). Set stylingStrategy once (on the first call) when the task mandates a style contract. Resubmit the same purpose with replace:true to correct a row. Example: {\"entry\": {\"symbol\": \"Spinner\", \"decision\": \"reuse-existing\", \"evidence\": [\"src/ui/Spinner.tsx\"], \"purpose\": \"loading\"}} or {\"entry\": {\"symbol\": \"SmokeCounter\", \"decision\": \"new\", \"evidence\": [\"需求.md\"], \"purpose\": \"increment\", \"specSection\": \"AC-FE-002\", \"stylingStrategy\": \"compact, stable, animation-free single card\"}}", Type.Object({
228
+ symbol: Type.String({ minLength: 1 }),
229
+ decision: Type.Union([Type.Literal("specified"), Type.Literal("reuse-existing"), Type.Literal("new")]),
230
+ evidence: Type.Array(Type.String()),
231
+ specSection: Type.Optional(Type.String({ minLength: 1 })),
232
+ purpose: Type.String({ minLength: 1 }),
233
+ covers: optionalStringArray,
234
+ rationale: optionalString,
235
+ stylingStrategy: optionalString,
236
+ }, { additionalProperties: false }), async (entry) => {
237
+ // specified/new compile into a canonical specReference whose .section
238
+ // the contract schema mandates; without the section the row can only
239
+ // fail at finalize, so demand it here where the model can still act.
240
+ if (entry.decision === "specified") {
241
+ const evidenceList = Array.isArray(entry.evidence) ? entry.evidence : [];
242
+ const evidencePath = typeof evidenceList[0] === "string" ? evidenceList[0] : "";
243
+ const candidates = input.componentSpecCandidatePaths ?? [];
244
+ if (candidates.length === 0) {
245
+ return planToolReceipt({
246
+ ok: false,
247
+ kind: "reuse-decision",
248
+ error: `record_reuse_decision specified-requires-spec-candidates: decision "specified" means a declared component spec mandates this choice, but this task declares no component spec candidates — use decision "new" (task-source-mandated addition, evidence[0] = the task-source file) or "reuse-existing"`,
249
+ });
250
+ }
251
+ if (evidencePath && !candidates.includes(evidencePath)) {
252
+ return planToolReceipt({
253
+ ok: false,
254
+ kind: "reuse-decision",
255
+ error: `record_reuse_decision specified-spec-reference-outside-candidates: evidence[0] "${evidencePath}" is not a declared component spec candidate; declared candidates: [${candidates.join(", ")}]`,
256
+ });
257
+ }
258
+ }
259
+ if ((entry.decision === "specified" || entry.decision === "new") &&
260
+ !(typeof entry.specSection === "string" && entry.specSection.trim())) {
261
+ return planToolReceipt({
262
+ ok: false,
263
+ kind: "reuse-decision",
264
+ error: `record_reuse_decision spec-section-required: decision "${entry.decision}" must name the spec/task-source declaration it implements via specSection (e.g. an AC id); use decision "reuse-existing" for existing repo conventions. Example: {"entry": {"symbol": "SmokeCounter", "decision": "new", "evidence": ["src/components/SmokeCounter.tsx"], "purpose": "render the counter card", "specSection": "AC-FE-001"}}`,
265
+ });
266
+ }
267
+ return undefined;
268
+ });
269
+ const recordStateOwnershipTool = record("record_state_ownership", "state ownership", "Record one UI state and its owner, applicability and expected behavior. owner must equal the behaviorGroupId of the record_verification_focus entries it supports; every behavior group referenced by a verification focus needs at least one state row or finalize fails. Resubmit the same state name with replace:true to correct a row. Example: {\"entry\": {\"state\": \"loading\", \"owner\": \"page\", \"applicable\": true, \"expectedBehavior\": \"render loading\"}}", Type.Object({
270
+ state: Type.String({ minLength: 1 }),
271
+ owner: Type.String({ minLength: 1 }),
272
+ applicable: Type.Boolean(),
273
+ expectedBehavior: optionalString,
274
+ notApplicableReason: optionalString,
275
+ }, { additionalProperties: false }));
276
+ const frozenInteractionIds = authority.interactionIds ?? [];
277
+ const recordDataFlowTool = record("record_data_flow", "data flow", "Record one interaction data flow: source behavior group, trigger and expected behavior. One row per interaction name; resubmit the same interaction name to replace it. When a frozen interaction id vocabulary is declared, the interaction name MUST be one of the declared ids — wrong-named records are rejected at submission and stale ones are retracted via retract_data_flow. Example: {\"entry\": {\"interaction\": \"load\", \"source\": \"page\", \"trigger\": \"submit query\", \"expectedBehavior\": \"show loading then results\"}}", Type.Object({
278
+ interaction: Type.String({ minLength: 1 }),
279
+ source: Type.String({ minLength: 1 }),
280
+ trigger: Type.String({ minLength: 1 }),
281
+ expectedBehavior: Type.String({ minLength: 1 }),
282
+ }, { additionalProperties: false }), frozenInteractionIds.length > 0
283
+ ? async (entry) => {
284
+ const name = typeof entry.interaction === "string" ? entry.interaction : "";
285
+ if (name && !frozenInteractionIds.includes(name)) {
286
+ return planToolReceipt({
287
+ ok: false,
288
+ kind: "data-flow",
289
+ code: "INTERACTION_ID_NOT_DECLARED",
290
+ error: `interaction "${name}" is not in the frozen vocabulary; allowed: [${frozenInteractionIds.join(", ")}]. Submit the declared id. To remove an already-recorded wrong interaction, call retract_data_flow with {"interaction": "${name}"}.`,
291
+ });
292
+ }
293
+ return undefined;
294
+ }
295
+ : undefined);
296
+ const retractDataFlowTool = defineTool({
297
+ name: "retract_data_flow",
298
+ label: "retract data flow",
299
+ description: "Retract a previously recorded interaction data flow (e.g. one recorded under a wrong interaction id). The retracted record and every fact keyed to that interaction name stop participating in the compiled plan. Declared vocabulary ids cannot be retracted. Example: {\"interaction\": \"increment-counter\", \"reason\": \"recorded under a wrong id\"}",
300
+ promptSnippet: "Retract one interaction data flow by interaction name.",
301
+ parameters: Type.Object({
302
+ interaction: Type.String({ minLength: 1 }),
303
+ reason: Type.Optional(Type.String({ minLength: 1 })),
304
+ }, { additionalProperties: false }),
305
+ async execute(_toolCallId, params) {
306
+ const interaction = typeof params?.interaction === "string" ? params.interaction : "";
307
+ if (!interaction) {
308
+ return planToolReceipt({ ok: false, kind: "data-flow", error: "retract_data_flow requires a non-empty interaction" });
309
+ }
310
+ if (frozenInteractionIds.includes(interaction)) {
311
+ return planToolReceipt({ ok: false, kind: "data-flow", error: `interaction "${interaction}" is a declared vocabulary id and cannot be retracted` });
312
+ }
313
+ const result = await adoptDecisionFact("data-flow", `${attemptId}:retract_data_flow:${randomUUID()}`, { kind: "data-flow", origin: "plan", entry: { interaction, retracted: true, ...(typeof params?.reason === "string" ? { reason: params.reason } : {}) } });
314
+ return planToolReceipt(result);
315
+ },
316
+ });
317
+ const recordApiMockBoundaryTool = record("record_api_mock_boundary", "API/Mock boundary", "Record one API boundary and its mode (real/mock/not-needed) with evidence. The boundary is the stable identity: resubmit the same boundary with replace:true to change its mode (e.g. mock→real); the old decision is replaced, not appended. Real/mock endpoints require fixture (the test fixture path) and consumer (the implementation file that calls the API) — design policy rejects endpoints without them. Example: {\"entry\": {\"boundary\": \"GET /items\", \"mode\": \"mock\", \"evidence\": \"fixture only\", \"fixture\": \"test/fixtures/items.ts\", \"consumer\": \"src/page.tsx\"}}", Type.Object({
318
+ boundary: Type.String({ minLength: 1 }),
319
+ mode: Type.Union([Type.Literal("real"), Type.Literal("mock"), Type.Literal("not-needed")]),
320
+ evidence: Type.String({ minLength: 1 }),
321
+ fixture: optionalString,
322
+ consumer: optionalString,
323
+ }, { additionalProperties: false }), async (entry) => {
324
+ // Design policy hard-requires fixture + consumer on every derived
325
+ // endpoint (non-not-needed strategies); demand them here where the
326
+ // model can still act instead of failing the compile post-session.
327
+ if ((entry.mode === "real" || entry.mode === "mock") &&
328
+ (!(typeof entry.fixture === "string" && entry.fixture.trim()) ||
329
+ !(typeof entry.consumer === "string" && entry.consumer.trim()))) {
330
+ return planToolReceipt({
331
+ ok: false,
332
+ kind: "api-mock-boundary",
333
+ error: `record_api_mock_boundary fixture-and-consumer-required: ${entry.mode} boundary "${entry.boundary}" needs fixture (the test fixture path) and consumer (the implementation file that calls the API). Example: {"entry": {"boundary": "${entry.boundary}", "mode": "${entry.mode}", "evidence": "<why>", "fixture": "test/fixtures/items.ts", "consumer": "src/page.tsx"}}`,
334
+ });
335
+ }
336
+ return undefined;
337
+ });
338
+ const recordVerificationFocusTool = record("record_verification_focus", "verification focus", "Record one behavior-group verification focus: which test file/command proves which behavior group at what evidence level. Every behaviorGroupId must have at least one record_state_ownership row whose owner equals it — declare one UI state per behavior group before finalizing. Example: {\"entry\": {\"id\": \"VT-1\", \"behaviorGroupId\": \"page\", \"file\": \"test/page.test.tsx\", \"commandId\": \"test\", \"evidenceLevel\": \"mounted\"}}", Type.Object({
339
+ id: Type.String({ minLength: 1 }),
340
+ behaviorGroupId: Type.String({ minLength: 1 }),
341
+ file: Type.String({ minLength: 1 }),
342
+ commandId: Type.String({ minLength: 1 }),
343
+ evidenceLevel: Type.Union([Type.Literal("unit"), Type.Literal("mounted"), Type.Literal("real-integration")]),
344
+ boundary: optionalString,
345
+ }, { additionalProperties: false }), async (entry) => {
346
+ // Mirror the relationship record_plan_verification_target boundary so
347
+ // protocol errors surface in-node (bounded correction) instead of
348
+ // burning attempts on an immutable committed fact that finalize rejects.
349
+ // Duplicate/identity enforcement lives in the durable wrapper (FACT_IDENTITY_CONFLICT
350
+ // unless replace:true) and the compile collapses same-id records to the
351
+ // latest replacement, so no duplicate gate is needed here.
352
+ const verificationCommandId = typeof entry.commandId === "string" ? entry.commandId.trim() : "";
353
+ if (!verificationCommandId) {
354
+ return planToolReceipt({
355
+ ok: false,
356
+ kind: "verification-focus",
357
+ error: `record_verification_focus entry.commandId is required (received ${JSON.stringify(entry.commandId ?? null)}); pick one id from the frozen command directory in your prompt`,
358
+ });
359
+ }
360
+ const { deriveFrontendVerifyCommandDirectoryFromRun, isFrontendTestFilePath } = await import("../../../workflows/dag/frontend-implementation-contract.js");
361
+ const verifyDirectory = await deriveFrontendVerifyCommandDirectoryFromRun(input.runDir);
362
+ if (verifyDirectory.length > 0) {
363
+ const directoryEntry = verifyDirectory.find((candidate) => candidate.commandId === verificationCommandId);
364
+ if (!directoryEntry) {
365
+ return planToolReceipt({
366
+ ok: false,
367
+ kind: "verification-focus",
368
+ error: `record_verification_focus verification-target-unknown-command: unknown commandId "${verificationCommandId}"; available frozen commands: [${verifyDirectory.map((candidate) => `${candidate.commandId} (${candidate.mode}: ${candidate.label})`).join(", ")}]`,
369
+ });
370
+ }
371
+ if (directoryEntry.mode === "behavior" &&
372
+ typeof entry.file === "string" &&
373
+ !isFrontendTestFilePath(entry.file)) {
374
+ return planToolReceipt({
375
+ ok: false,
376
+ kind: "verification-focus",
377
+ error: `record_verification_focus verification-target-phase-mismatch: behavior command "${directoryEntry.label}" (${directoryEntry.commandId}) must bind a test file (__tests__/, tests?/, e2e/, cypress/, *.test.*, *.spec.*, *.cy.*); received file "${entry.file}"`,
378
+ });
379
+ }
380
+ }
381
+ // A behavior verification target derives its contract uiStates solely
382
+ // from the committed state-ownership rows whose owner equals its
383
+ // behaviorGroupId, and the relationship patch rejects a behavior
384
+ // target with empty uiStates whenever the plan declares any state or
385
+ // interaction. That rejection currently surfaces only in the
386
+ // post-session bridge and burns the whole attempt, so enforce the
387
+ // pairing here as a bounded in-session correction. A ledger with no
388
+ // ownership and no data-flow facts stays exempt (pure-logic plan),
389
+ // mirroring the contract-level exemption.
390
+ const committedLedger = readCommittedEvents(store, attemptId);
391
+ const ownershipRows = committedLedger.filter((event) => event.fact.kind === "state-ownership");
392
+ const hasInteractionFacts = committedLedger.some((event) => event.fact.kind === "data-flow");
393
+ if ((ownershipRows.length > 0 || hasInteractionFacts) &&
394
+ !ownershipRows.some((event) => event.fact.entry?.owner ===
395
+ entry.behaviorGroupId)) {
396
+ return planToolReceipt({
397
+ ok: false,
398
+ kind: "verification-focus",
399
+ error: `record_verification_focus undeclared-behavior-group-state: behavior group "${entry.behaviorGroupId}" (verification target ${entry.id}) has no record_state_ownership row; finalize derives each behavior target's UI states from ownership rows whose owner equals the behaviorGroupId, so record at least one for this group: {"state": "<name>", "owner": "${entry.behaviorGroupId}", "applicable": true, "expectedBehavior": "<what the state does>"}`,
400
+ });
401
+ }
402
+ return undefined;
403
+ });
404
+ const recordDependencyTool = record("record_dependency", "dependency", "Record one dependency name. Example: {\"entry\": {\"name\": \"none\"}}", Type.Object({ name: Type.String({ minLength: 1 }) }, { additionalProperties: false }));
405
+ const finalizeDecisionTool = defineTool({
406
+ name: "finalize_decision",
407
+ label: "finalize_decision",
408
+ description: "Compile the committed decision facts into a decision contract, pre-validate the derived canonical contract, and commit the terminal. Fails closed on any safety finding or validation finding; correct the reported record_* facts and call finalize_decision again.",
409
+ promptSnippet: "Compile and finalize the committed decision facts.",
410
+ parameters: Type.Object({}, { additionalProperties: false }),
411
+ async execute(_toolCallId) {
412
+ const committed = readCommittedEvents(store, attemptId);
413
+ const decision = assembleDecisionContractFromFacts(committed.map(record => record.fact));
414
+ const expectedWriteSet = [
415
+ ...decision.modulePlacements.flatMap(item => item.paths),
416
+ ...decision.verificationFocus.map(item => item.file),
417
+ ];
418
+ const derived = assembleExperimentPlanContract({
419
+ decision,
420
+ authority,
421
+ concreteWriteSet: expectedWriteSet,
422
+ });
423
+ if (derived.findings.length > 0) {
424
+ return planToolReceipt({
425
+ ok: false,
426
+ kind: "finalize_decision",
427
+ code: "DECISION_SAFETY_FINDINGS",
428
+ error: `decision safety projection failed: ${derived.findings.join("; ")}`,
429
+ });
430
+ }
431
+ const prevalidation = await prevalidateDerivedPatch();
432
+ if (!prevalidation.ok) {
433
+ return planToolReceipt({
434
+ ok: false,
435
+ kind: "finalize_decision",
436
+ code: "DECISION_PREVALIDATION_FAILED",
437
+ error: prevalidation.error,
438
+ });
439
+ }
440
+ // Recovery-mode guard: a repair child that finalizes a plan identical
441
+ // to its parent's has not performed the repair the findings demand —
442
+ // the identical plan is exactly what admission rejected. Both hashes
443
+ // use the same deterministic serializer over the canonical contract,
444
+ // so only a real semantic change passes.
445
+ // Recovery-mode guard: a repair child must demonstrably close the
446
+ // parent findings before the run re-enters design review.
447
+ if (input.parentDecisionSnapshot) {
448
+ const { readParentCanonical, parentCanonicalContentSha256 } = await import("../../../workflows/dag/frontend-implementation-contract.js");
449
+ const { extractRepairAssertions, evaluateRepairClosure, decisionIdentitySha256 } = await import("../../../workflows/dag/frontend-plan-decision-contract.js");
450
+ const parentRunDir = path.join(input.runDir, "..", input.parentDecisionSnapshot.parentRunId);
451
+ const parentCanonical = await readParentCanonical(parentRunDir);
452
+ // 1. identical-plan guard. Decisions, not prose: a rationale-only
453
+ // rewrite is not a repair (smoke r27 cleared a raw byte-hash guard
454
+ // by editing two rationale strings and changing nothing else). The
455
+ // byte hash stays as the fallback when the parent canonical cannot
456
+ // be read.
457
+ const parentSha = await parentCanonicalContentSha256(parentRunDir);
458
+ const parentDecisions = parentCanonical
459
+ ? decisionIdentitySha256(parentCanonical)
460
+ : undefined;
461
+ const childDecisions = decisionIdentitySha256(prevalidation.canonical);
462
+ if (parentDecisions
463
+ ? parentDecisions === childDecisions
464
+ : parentSha !== undefined && parentSha === prevalidation.canonicalSha256) {
465
+ return planToolReceipt({
466
+ ok: false,
467
+ kind: "finalize_decision",
468
+ code: "DECISION_REPAIR_NO_CHANGE",
469
+ error: "recovery finalize blocked: every structured decision row is identical to the parent run's (rationale-only edits do not count as a repair), but the review findings require changes. Correct the flagged rows (same identity, replace:true), then call finalize_decision again.",
470
+ });
471
+ }
472
+ // 2. per-finding closure: every deterministic assertion extracted
473
+ // from the parent findings must hold on the child canonical.
474
+ const findings = input.parentDecisionSnapshot.designFindings ?? [];
475
+ if (parentCanonical && findings.length > 0) {
476
+ const assertions = extractRepairAssertions(findings, parentCanonical);
477
+ const closure = evaluateRepairClosure(assertions, prevalidation.canonical, parentCanonical);
478
+ if (!closure.closed) {
479
+ return planToolReceipt({
480
+ ok: false,
481
+ kind: "finalize_decision",
482
+ code: "DECISION_REPAIR_NOT_CLOSED",
483
+ error: `recovery finalize blocked: ${closure.unmet.length} finding(s) are still not closed — ${closure.unmet.join("; ")}. Correct the flagged rows (same identity, replace:true), then call finalize_decision again.`,
484
+ });
485
+ }
486
+ }
487
+ }
488
+ const result = await adoptDecisionFact("finalize_decision", `${attemptId}:finalize_decision:${randomUUID()}`, { kind: "finalize_decision", origin: "plan", findings: [] });
489
+ return planToolReceipt(result);
490
+ },
491
+ });
492
+ const durable = await createDurableFrontendTools({
493
+ file: path.join(input.runDir, input.nodeId, "plan-decision-facts.jsonl"),
494
+ attemptId,
495
+ store: input.store,
496
+ setWorkingStore: next => { store = next; },
497
+ binding: { authority },
498
+ tools: [
499
+ recordModulePlacementTool,
500
+ recordReuseDecisionTool,
501
+ recordStateOwnershipTool,
502
+ recordDataFlowTool,
503
+ retractDataFlowTool,
504
+ recordApiMockBoundaryTool,
505
+ recordVerificationFocusTool,
506
+ recordDependencyTool,
507
+ finalizeDecisionTool,
508
+ ],
509
+ });
510
+ const durableFinalizeDecisionTool = durable.customTools.find((tool) => typeof tool === "object" && tool !== null && tool.name === "finalize_decision");
511
+ if (!durableFinalizeDecisionTool)
512
+ throw new Error("frontend plan decision durable finalize tool unavailable");
513
+ return {
514
+ customTools: durable.customTools,
515
+ flush: durable.flush,
516
+ committedFacts: () => readCommittedEvents(store, attemptId),
517
+ finalizeDecision: () => durableFinalizeDecisionTool.execute(`${attemptId}:auto-finalize-decision`, {}, undefined, undefined, {}),
518
+ };
519
+ }