@tea-agent/loop-agent 0.44.0-next.7 → 0.44.0-next.9

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 (147) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/dist/application/evaluation/budget.js +21 -0
  3. package/dist/application/evaluation/corpus-hash.js +10 -15
  4. package/dist/application/evaluation/corpus.js +2 -1
  5. package/dist/application/evaluation/frontend-browser-acceptance.js +77 -0
  6. package/dist/application/evaluation/frontend-corpus-browser-acceptance.js +175 -0
  7. package/dist/application/evaluation/frontend-gateway-evidence.js +226 -0
  8. package/dist/application/evaluation/frontend-pair-registration.js +143 -0
  9. package/dist/application/evaluation/frontend-paired-summary.js +150 -0
  10. package/dist/application/evaluation/frontend-run-observation.js +144 -0
  11. package/dist/application/evaluation/frontend-shared-evidence.js +105 -0
  12. package/dist/application/evaluation/types.js +45 -12
  13. package/dist/application/task-lifecycle/observe.js +26 -1
  14. package/dist/build-stamp.json +3 -3
  15. package/dist/cli/command-definitions.js +11 -0
  16. package/dist/cli/program.js +4 -0
  17. package/dist/commands/task-advance.js +3 -5
  18. package/dist/commands/task-source-prepare.js +13 -1
  19. package/dist/executors/dag-pi/guards.js +4 -0
  20. package/dist/executors/dag-pi/model-config.js +30 -0
  21. package/dist/{workflows/dag/frontend-plan-completeness.js → executors/dag-pi/plan/facts.js} +25 -41
  22. package/dist/executors/dag-pi/plan/ledger-contract.js +55 -0
  23. package/dist/executors/dag-pi/tools/contract-tools.js +515 -0
  24. package/dist/executors/dag-pi/tools/design-terminal-tools.js +166 -0
  25. package/dist/executors/dag-pi/tools/plan-decision-tools.js +519 -0
  26. package/dist/executors/dag-pi/tools/review-terminal-tools.js +166 -0
  27. package/dist/executors/dag-pi/tools/scout-evidence-tools.js +270 -0
  28. package/dist/executors/dag-pi-executor.js +198 -118
  29. package/dist/executors/pi-executor.js +6 -1
  30. package/dist/executors/pi-sdk-executor.js +76 -11
  31. package/dist/executors/shell-executor.js +88 -12
  32. package/dist/infrastructure/console/operation-store.js +2 -0
  33. package/dist/infrastructure/evaluation/corpus-store.js +37 -12
  34. package/dist/shared/operator/capabilities.js +10 -5
  35. package/dist/task/config-types.js +22 -9
  36. package/dist/task/contract/project.js +13 -0
  37. package/dist/task/contract/schema.js +2 -8
  38. package/dist/task/frontend-preflight.js +1 -1
  39. package/dist/task/source-prepare/parse-intent.js +124 -5
  40. package/dist/task/source-prepare/prepare.js +6 -1
  41. package/dist/task/source-prepare/semantic-intake.js +18 -3
  42. package/dist/task/source-prepare/source-execution.js +65 -0
  43. package/dist/task/source-prepare/source-fidelity-pi.js +22 -9
  44. package/dist/task/source-prepare/source-provider-budget.js +290 -0
  45. package/dist/worker/console/operator-actions.js +0 -1
  46. package/dist/worker/console/operator-user-error.js +1 -1
  47. package/dist/worker/console/prd-intake-bridge.js +5 -15
  48. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DTuku_-7.js → abnfDiagram-N423BO3Z-r1NAPU_5.js} +1 -1
  49. package/dist/worker/console/static/assets/{arc-CQbCsSqb.js → arc-dT5T4sVL.js} +1 -1
  50. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-C-xUntTR.js → architectureDiagram-T3A2C74G-CP_cOq46.js} +1 -1
  51. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-DhNlwGAl.js → blockDiagram-VBNYF7ZC-sToEZxEf.js} +1 -1
  52. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B7j3nx8o.js → c4Diagram-5PPSVZJV-DSxwwi2m.js} +1 -1
  53. package/dist/worker/console/static/assets/channel-CyBY_yGk.js +1 -0
  54. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BCrcxEh2.js → chunk-2GRJ4B5K-B9fh0IkF.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CsaePDb3.js → chunk-2Q5K7J3B-CAUNl-A6.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-5RXB4S5H--SRQc1rN.js → chunk-5RXB4S5H-BomeqbNX.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-C5BFY2Po.js → chunk-5VM5RSS4-BNAuTOLE.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Caso6uN7.js → chunk-6Q2QTUOP-DfzjRD5y.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-AY2ZSAx2.js → chunk-GF5L2VYU-C3uE-yc2.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-DiMqZQ_8.js → chunk-JWPE2WC7-C3sQMMYr.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-gpvvriOp.js → chunk-KBJHAD2P-D_AtEYQL.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CUdyoJhI.js → chunk-RYQCIY6F-DW4TwhKv.js} +1 -1
  63. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Bp1xW3T4.js → chunk-XXDRQBXY-CTQDmJcp.js} +1 -1
  64. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D5lN4E_E.js +1 -0
  65. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D5lN4E_E.js +1 -0
  66. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-D-JIcOIT.js → cose-bilkent-JH36ORCC-iMqPuBsL.js} +1 -1
  67. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-C-NKFflx.js → cynefin-VYW2F7L2-DktIuDWs.js} +1 -1
  68. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-CSOcvOAA.js → cynefinDiagram-MW4NZA55-BgVnjyEs.js} +1 -1
  69. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BQSBltMy.js → dagre-VZM6K2ZE-CR25E4bu.js} +1 -1
  70. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B0LVC45G.js → diagram-7IWD3JNH-CPM2h5HY.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Cypg3nqy.js → diagram-B4RE2ZJO-DqF0x2g1.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-BBDU88XJ.js → diagram-LBJQPF4R-G8BYp2eb.js} +1 -1
  73. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BZusLqSb.js → diagram-Q27KOJAE-BbC4kT_D.js} +1 -1
  74. package/dist/worker/console/static/assets/{diagram-UB23O5K3-Cfolotcr.js → diagram-UB23O5K3-CKZwZ-G5.js} +1 -1
  75. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-CjfyUy0m.js → ebnfDiagram-BXEA7PRR-kJDmheia.js} +1 -1
  76. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DJdz7txE.js → erDiagram-JOGREHBK-DFTVHyxB.js} +1 -1
  77. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CQqgPO6y.js → flowDiagram-UKHOOZJN-BqdN5_os.js} +1 -1
  78. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BOpGfS2m.js → ganttDiagram-PKOTCBZU-SYDjVakh.js} +1 -1
  79. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-C6UCu0qx.js → gitGraphDiagram-DS77QQ5N-Bf8bNpGA.js} +1 -1
  80. package/dist/worker/console/static/assets/index-B28Onmfy.css +1 -0
  81. package/dist/worker/console/static/assets/{index-DWKejNVJ.js → index-CTMZsDmh.js} +98 -98
  82. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BWeaVEUB.js → infoDiagram-6WML65LV-DIeAmJ45.js} +1 -1
  83. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-C8gmlBbk.js → ishikawaDiagram-WSZJBQD7-oikqUAlo.js} +1 -1
  84. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DF3bm8Bp.js → journeyDiagram-NVQOT4AX-mKEF_xYT.js} +1 -1
  85. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-D2fG2Vxi.js → kanban-definition-27J2QSJJ-BdWJHPnN.js} +1 -1
  86. package/dist/worker/console/static/assets/{linear-BGKgMc2T.js → linear-DGRlieMH.js} +1 -1
  87. package/dist/worker/console/static/assets/{mermaid.core-DF6mMPJ9.js → mermaid.core-4P9yPC9c.js} +5 -5
  88. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-AMhePheC.js → mindmap-definition-FAOFIHXS-5PTSkos6.js} +1 -1
  89. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DN1Cl0W0.js → pegDiagram-VL7TDLO6-DONIFpG5.js} +1 -1
  90. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-Cx9ngUeN.js → pieDiagram-7S7Q4E2Y-D_Z4S70M.js} +1 -1
  91. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-BcYPTQJb.js → quadrantDiagram-CIZ2JOQS-CAsjh8iY.js} +1 -1
  92. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BqHUZWnH.js → railroadDiagram-AXF67PYL-Cx4Dd6ac.js} +1 -1
  93. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CmqWZ0-B.js → requirementDiagram-LRYGKXZP-cQb7mwyH.js} +1 -1
  94. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P--4YetH1y.js → sankeyDiagram-W5VNT64P-Cd-DhUdf.js} +1 -1
  95. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DGD4VMfD.js → sequenceDiagram-SI44F4Z6-AlKYRgIk.js} +1 -1
  96. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-QVw9q7rh.js → sizeCapture-X5ZJPWSS-CUy_ET4e.js} +1 -1
  97. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-CuemV9pe.js → stateDiagram-OKZ733FA-CTuVe6Sa.js} +1 -1
  98. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BsWhs8O7.js +1 -0
  99. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-C7oKzZKs.js → swimlanes-SLNWSIFB-ChFLJb-v.js} +2 -2
  100. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CJcioYGH.js +8 -0
  101. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-C2Of60nT.js → timeline-definition-Z64GVDOM-DZZeiAPr.js} +1 -1
  102. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DHtpQHyL.js → vennDiagram-T6HMQDX7-t4O91cZU.js} +1 -1
  103. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-COi9OhSy.js → wardleyDiagram-T6FBY63Y-CV5qGHZu.js} +1 -1
  104. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-DPHQfRrt.js → xychartDiagram-ELKLHX3M-DyXbytUI.js} +1 -1
  105. package/dist/worker/console/static/index.html +2 -2
  106. package/dist/workflows/dag/budget-enforcement.js +31 -2
  107. package/dist/workflows/dag/frontend-closeout.js +3 -1
  108. package/dist/workflows/dag/frontend-contract-facts.js +11 -8
  109. package/dist/workflows/dag/frontend-design-policy.js +6 -6
  110. package/dist/workflows/dag/frontend-durable-tools.js +90 -70
  111. package/dist/workflows/dag/frontend-implementation-contract.js +146 -59
  112. package/dist/workflows/dag/frontend-plan-canary.js +66 -16
  113. package/dist/workflows/dag/frontend-plan-decision-contract.js +13 -21
  114. package/dist/workflows/dag/frontend-plan-progress.js +92 -0
  115. package/dist/workflows/dag/frontend-recovery-plan.js +11 -10
  116. package/dist/workflows/dag/frontend-recovery-run.js +51 -46
  117. package/dist/workflows/dag/frontend-repair-assertions.js +36 -0
  118. package/dist/workflows/dag/frontend-review-context.js +29 -1
  119. package/dist/workflows/dag/frontend-review-scopes.js +92 -2
  120. package/dist/workflows/dag/frontend-risk.js +8 -3
  121. package/dist/workflows/dag/frontend-root-observation.js +261 -0
  122. package/dist/workflows/dag/frontend-session-budget.js +52 -39
  123. package/dist/workflows/dag/frontend-session-context.js +10 -0
  124. package/dist/workflows/dag/frontend-test-execution-evidence.js +206 -48
  125. package/dist/workflows/dag/frontend-typed-event-store.js +11 -6
  126. package/dist/workflows/dag/frontend-verification-trace.js +6 -0
  127. package/dist/workflows/dag/frontend-writer-admission.js +2 -1
  128. package/dist/workflows/dag/init-hybrid.js +108 -37
  129. package/dist/workflows/dag/node-execution.js +11 -11
  130. package/dist/workflows/dag/rerun-feedback.js +35 -4
  131. package/dist/workflows/dag/rerun-task.js +62 -67
  132. package/dist/workflows/dag/runner.js +58 -12
  133. package/dist/workflows/dag/types.js +15 -8
  134. package/docs/architecture/runtime-boundaries.md +1 -1
  135. package/docs/templates/backend-test-dag.json +4 -4
  136. package/docs/templates/frontend-implementation-contract.schema.json +31 -11
  137. package/package.json +1 -1
  138. package/skills/frontend-design-review/SKILL.md +8 -11
  139. package/skills/frontend-design-review/references/review-checklist.md +3 -4
  140. package/skills/frontend-review/SKILL.md +5 -1
  141. package/skills/loop-agent/references/command-reference.md +8 -0
  142. package/dist/worker/console/static/assets/channel-5_os7t_x.js +0 -1
  143. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-loz0n18W.js +0 -1
  144. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-loz0n18W.js +0 -1
  145. package/dist/worker/console/static/assets/index-CUT440A8.css +0 -1
  146. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BdlGdIJw.js +0 -1
  147. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DjjbHbde.js +0 -8
@@ -0,0 +1,166 @@
1
+ /** DAG Pi review terminal 工具工厂:M5 committed typed review 终审工具,禁止反向依赖执行器入口。 */
2
+ import { createFrontendReviewScopeProtocol } from "../../../workflows/dag/frontend-review-scopes.js";
3
+ import { createDurableFrontendTools } from "../../../workflows/dag/frontend-durable-tools.js";
4
+ import { randomUUID } from "node:crypto";
5
+ import path from "node:path";
6
+ /**
7
+ * M5: build the two committed typed review terminal tools (approve_review /
8
+ * request_review_changes). Each tool validates its parameters with the review
9
+ * fact zod schemas, stages + adopts a terminal fact into the typed event
10
+ * store, and returns a structured receipt. Terminal conflicts (a second
11
+ * terminal commit) are caught inside execute and returned as an error receipt
12
+ * rather than crashing the node.
13
+ */
14
+ export async function createFrontendReviewTerminalTools(input) {
15
+ const [{ Type }, { defineTool }] = await Promise.all([
16
+ import("typebox"),
17
+ import("@earendil-works/pi-coding-agent"),
18
+ ]);
19
+ const { approveReviewFactSchema, readCommittedEvents, requestReviewChangesFactSchema, reviewFindingSchema, writeTypedEventStoreJsonl, } = await import("../../../workflows/dag/frontend-typed-event-store.js");
20
+ const { adoptTypedEventFact, stageTypedEventFact } = await import("../../../workflows/dag/frontend-typed-event-transaction.js");
21
+ let store = input.store;
22
+ const attemptId = input.attemptId;
23
+ const findingSchema = Type.Object({
24
+ severity: Type.Enum({ Critical: "Critical", Important: "Important", Minor: "Minor", Info: "Info" }),
25
+ file: Type.Optional(Type.String({ minLength: 1 })),
26
+ line: Type.Optional(Type.Integer({ minimum: 1 })),
27
+ issue: Type.String({ minLength: 1 }),
28
+ requiredChange: Type.Optional(Type.String({ minLength: 1 })),
29
+ repairAssertions: Type.Optional(Type.Unknown()),
30
+ }, { additionalProperties: false });
31
+ const scopeProtocol = createFrontendReviewScopeProtocol({ phase: "review", inventory: input.inventory, getStore: () => store, attemptId });
32
+ const savedFindings = () => [...new Map(readCommittedEvents(store, attemptId).filter(r => r.fact.kind === "review-finding").map(r => [r.fact.id, r.fact.finding])).values()];
33
+ const allFindings = (direct) => [...new Map([...savedFindings(), ...(Array.isArray(direct) ? direct : [])].map(finding => [JSON.stringify(finding), finding])).values()];
34
+ const recordFindingTool = defineTool({
35
+ name: "record_review_finding", label: "record_review_finding",
36
+ description: "Save one finding with a stable id. Submit findings incrementally, then finalize without repeating the findings array. Saved blocking findings cannot be omitted from approval; correct a finding explicitly with replace:true.",
37
+ parameters: Type.Object({ id: Type.String({ minLength: 1 }), finding: findingSchema }, { additionalProperties: false }),
38
+ async execute(_callId, params) {
39
+ try {
40
+ const previous = readCommittedEvents(store, attemptId).filter(r => r.fact.kind === "review-finding" && r.fact.id === params.id).at(-1);
41
+ const scopeIds = scopeProtocol.findingScopeIds(previous?.fact.scopeIds);
42
+ const fact = { kind: "review-finding", id: params.id, finding: reviewFindingSchema.parse(params.finding), ...(scopeIds ? { scopeIds } : {}) };
43
+ const requestId = `${attemptId}:finding:${randomUUID()}`;
44
+ const staged = stageTypedEventFact({ store, requestId, attemptId, fact });
45
+ const adopted = await adoptTypedEventFact({ store, requestId, attemptId, fact, eventId: staged.eventId, expectedRevision: store.revision });
46
+ const details = { ok: true, eventId: adopted.eventId, revision: adopted.revision };
47
+ return { content: [{ type: "text", text: JSON.stringify(details) }], details };
48
+ }
49
+ catch (error) {
50
+ const code = error.code ?? "FRONTEND_INPUT_SCOPE_VIOLATION";
51
+ const details = { ok: false, code, error: error instanceof Error ? error.message : String(error) };
52
+ return { content: [{ type: "text", text: JSON.stringify(details) }], details };
53
+ }
54
+ },
55
+ });
56
+ const approveParameters = Type.Object({
57
+ findings: Type.Optional(Type.Array(findingSchema, {
58
+ description: "Optional informational findings (Minor/Info only; no Critical/Important on approval)",
59
+ })),
60
+ }, { additionalProperties: false });
61
+ const requestParameters = Type.Object({
62
+ issueCategory: Type.Enum({
63
+ "implementation-mismatch": "implementation-mismatch",
64
+ "approved-design-defect": "approved-design-defect",
65
+ "target-surface-defect": "target-surface-defect",
66
+ "contract-requirement-gap": "contract-requirement-gap",
67
+ "unknown": "unknown",
68
+ }, { description: "Typed issue category (five-value enum)" }),
69
+ evidenceRefs: Type.Array(Type.String({ minLength: 1 }), {
70
+ description: "Evidence refs (paths or artifact ids); at least one", minItems: 1,
71
+ }),
72
+ findings: Type.Optional(Type.Array(findingSchema, {
73
+ description: "At least one finding", minItems: 1,
74
+ })),
75
+ }, { additionalProperties: false });
76
+ async function adoptReviewFact(kind, fact) {
77
+ const requestId = randomUUID();
78
+ try {
79
+ scopeProtocol.assertComplete();
80
+ const parsed = kind === "approve_review"
81
+ ? approveReviewFactSchema.parse(fact)
82
+ : requestReviewChangesFactSchema.parse(fact);
83
+ const staged = stageTypedEventFact({
84
+ store,
85
+ requestId,
86
+ attemptId,
87
+ fact: parsed,
88
+ });
89
+ const adopted = await adoptTypedEventFact({
90
+ store,
91
+ requestId,
92
+ attemptId,
93
+ fact: parsed,
94
+ eventId: staged.eventId,
95
+ expectedRevision: store.revision,
96
+ });
97
+ return {
98
+ content: [
99
+ {
100
+ type: "text",
101
+ text: JSON.stringify({
102
+ ok: true,
103
+ kind,
104
+ eventId: adopted.eventId,
105
+ revision: adopted.revision,
106
+ }),
107
+ },
108
+ ],
109
+ details: {
110
+ ok: true,
111
+ kind,
112
+ eventId: adopted.eventId,
113
+ revision: adopted.revision,
114
+ },
115
+ };
116
+ }
117
+ catch (error) {
118
+ const code = error?.code;
119
+ const message = error instanceof Error ? error.message : String(error);
120
+ return {
121
+ content: [
122
+ {
123
+ type: "text",
124
+ text: JSON.stringify({ ok: false, kind, code, error: message }),
125
+ },
126
+ ],
127
+ details: { ok: false, kind, code, error: message },
128
+ };
129
+ }
130
+ }
131
+ const approveReviewTool = defineTool({
132
+ name: "approve_review",
133
+ label: "approve_review",
134
+ description: "Commit the authoritative approve_review terminal fact. Use only when the implementation passes review with no Critical/Important findings.",
135
+ promptSnippet: "Commit the authoritative approve_review terminal verdict (no Critical/Important findings).",
136
+ parameters: approveParameters,
137
+ async execute(_toolCallId, params) {
138
+ return adoptReviewFact("approve_review", {
139
+ kind: "approve_review",
140
+ verdict: "approve_review",
141
+ findings: allFindings(params?.findings),
142
+ });
143
+ },
144
+ });
145
+ const requestReviewChangesTool = defineTool({
146
+ name: "request_review_changes",
147
+ label: "request_review_changes",
148
+ description: "Commit the authoritative request_review_changes terminal fact. Requires a typed issueCategory, at least one evidenceRef, and non-empty findings.",
149
+ promptSnippet: "Commit the authoritative request_review_changes terminal verdict (issueCategory + evidenceRefs + findings required).",
150
+ parameters: requestParameters,
151
+ async execute(_toolCallId, params) {
152
+ return adoptReviewFact("request_review_changes", {
153
+ kind: "request_review_changes",
154
+ verdict: "request_review_changes",
155
+ issueCategory: params?.issueCategory,
156
+ evidenceRefs: params?.evidenceRefs,
157
+ findings: allFindings(params?.findings),
158
+ });
159
+ },
160
+ });
161
+ const durable = await createDurableFrontendTools({
162
+ file: path.join(input.runDir, input.nodeId, "review-typed-facts.jsonl"), attemptId, store: input.store,
163
+ binding: { inputDigest: input.inputDigest, scopeDigest: input.inventory?.digest }, setWorkingStore: next => { store = next; }, tools: [...scopeProtocol.customTools, recordFindingTool, approveReviewTool, requestReviewChangesTool], validateRestored: async () => { await input.inventory?.validate(); },
164
+ });
165
+ return { ...durable, scopeProtocol };
166
+ }
@@ -0,0 +1,270 @@
1
+ /** DAG Pi scout evidence 工具工厂:origin=scout 增量证据工具,禁止反向依赖执行器入口。 */
2
+ import { createDurableFrontendTools } from "../../../workflows/dag/frontend-durable-tools.js";
3
+ import { createHash, randomUUID } from "node:crypto";
4
+ import { readFile, stat } from "node:fs/promises";
5
+ import path from "node:path";
6
+ import { isRecordObject } from "../guards.js";
7
+ /**
8
+ * A+B: `frontend-scout-pi` incremental evidence tools (origin=scout). Runtime
9
+ * enriches committed target-surface / design-evidence facts with hash/section/
10
+ * freshness from real read events; the tool itself never trusts model self-report.
11
+ */
12
+ export async function createFrontendScoutEvidenceTools(input) {
13
+ const [{ Type }, { defineTool }] = await Promise.all([
14
+ import("typebox"),
15
+ import("@earendil-works/pi-coding-agent"),
16
+ ]);
17
+ const { readCommittedEvents, writeTypedEventStoreJsonl } = await import("../../../workflows/dag/frontend-typed-event-store.js");
18
+ const { adoptTypedEventFact, stageTypedEventFact } = await import("../../../workflows/dag/frontend-typed-event-transaction.js");
19
+ let store = input.store;
20
+ const attemptId = input.attemptId;
21
+ const stringArray = Type.Array(Type.String({}));
22
+ const optionalString = Type.Optional(Type.String({}));
23
+ const scoutCompleteness = Type.Union([
24
+ Type.Literal("complete"),
25
+ Type.Literal("blocked"),
26
+ ]);
27
+ const receipt = (details) => ({
28
+ content: [{ type: "text", text: JSON.stringify(details) }],
29
+ details,
30
+ });
31
+ const sourceDeclaredPaths = (input.sourceDeclaredPaths ?? []).map((value) => value.replaceAll("\\", "/").replace(/^\.\//, "").replace(/\/$/, ""));
32
+ const hasSourceDeclarations = input.sourceDeclaredPaths !== undefined;
33
+ let activeScope;
34
+ const scopeIdentity = (ids) => createHash("sha256").update(JSON.stringify([...ids].sort())).digest("hex");
35
+ const latestScopes = () => {
36
+ const byRequirement = new Map();
37
+ for (const record of readCommittedEvents(store, attemptId))
38
+ if (record.fact.kind === "scout-scope" && Array.isArray(record.fact.requirementIds)) {
39
+ for (const id of record.fact.requirementIds)
40
+ if (typeof id === "string")
41
+ byRequirement.set(id, record.fact);
42
+ }
43
+ return byRequirement;
44
+ };
45
+ const isSourceDeclared = (candidate) => {
46
+ const normalized = candidate.replaceAll("\\", "/").replace(/^\.\//, "").replace(/\/$/, "");
47
+ return sourceDeclaredPaths.some((declared) => declared === normalized || declared.startsWith(`${normalized}/`));
48
+ };
49
+ // A+B (AC-003): runtime enriches declared scout paths with hash/freshness
50
+ // from the real filesystem. The model's self-reported path list is never
51
+ // trusted for content identity; a missing file fails closed to fresh=false
52
+ // with a zero hash instead of inventing content.
53
+ const enrichScoutPathEvidence = async (paths) => {
54
+ if (!input.workspaceRoot)
55
+ return [];
56
+ const workspaceRoot = path.resolve(input.workspaceRoot);
57
+ const evidence = [];
58
+ for (const relative of new Set(paths)) {
59
+ const absolute = path.resolve(workspaceRoot, relative);
60
+ if (absolute !== workspaceRoot &&
61
+ !absolute.startsWith(`${workspaceRoot}${path.sep}`)) {
62
+ evidence.push({ path: relative, sha256: "0".repeat(64), fresh: false, sourceDeclared: isSourceDeclared(relative) });
63
+ continue;
64
+ }
65
+ try {
66
+ // Directories are legitimate named targets (greenfield smoke: the
67
+ // page directory exists while the files inside it are to be
68
+ // created). readFile on a directory throws EISDIR, which used to
69
+ // mark every directory path fresh=false and structurally fail the
70
+ // freshness gate for create-new surfaces. stat() first: a directory
71
+ // counts as fresh existence evidence; its content hash is a stable
72
+ // directory marker since there is no single file content to hash.
73
+ const info = await stat(absolute);
74
+ if (info.isDirectory()) {
75
+ evidence.push({
76
+ path: relative,
77
+ sha256: createHash("sha256").update(`directory:${relative}`).digest("hex"),
78
+ fresh: true,
79
+ sourceDeclared: isSourceDeclared(relative),
80
+ });
81
+ continue;
82
+ }
83
+ const bytes = await readFile(absolute);
84
+ evidence.push({
85
+ path: relative,
86
+ sha256: createHash("sha256").update(bytes).digest("hex"),
87
+ fresh: true,
88
+ sourceDeclared: isSourceDeclared(relative),
89
+ });
90
+ }
91
+ catch {
92
+ evidence.push({ path: relative, sha256: "0".repeat(64), fresh: false, sourceDeclared: isSourceDeclared(relative) });
93
+ }
94
+ }
95
+ return evidence;
96
+ };
97
+ async function adoptScoutFact(kind, fact) {
98
+ try {
99
+ const requestId = `${attemptId}:${kind}:${randomUUID()}`;
100
+ const staged = stageTypedEventFact({
101
+ store,
102
+ requestId,
103
+ attemptId,
104
+ fact: fact,
105
+ });
106
+ const committed = await adoptTypedEventFact({
107
+ store,
108
+ requestId,
109
+ attemptId,
110
+ fact: fact,
111
+ eventId: staged.eventId,
112
+ expectedRevision: store.revision,
113
+ });
114
+ return {
115
+ ok: true,
116
+ kind,
117
+ eventId: committed.eventId,
118
+ revision: committed.revision,
119
+ error: "",
120
+ };
121
+ }
122
+ catch (error) {
123
+ return {
124
+ ok: false,
125
+ kind,
126
+ code: error?.code,
127
+ error: error instanceof Error ? error.message : String(error),
128
+ };
129
+ }
130
+ }
131
+ const recordTargetSurfaceTool = defineTool({
132
+ name: "record_target_surface",
133
+ label: "record_target_surface",
134
+ description: "Commit an origin=scout target-surface fact with complete/blocked discovery status. A complete surface needs a proven target path and no unresolved paths; blocked surfaces name the unresolved paths instead of guessing. Example: {\"completeness\": \"complete\", \"entrypoint\": \"<file>\", \"implementationPaths\": [\"<dir or file>\"], \"testPaths\": [\"<file>\"], \"allowedPathConflicts\": [], \"unresolvedPaths\": []}",
135
+ promptSnippet: "Commit an origin=scout target-surface fact.",
136
+ parameters: Type.Object({
137
+ scopeId: Type.Optional(Type.String({ minLength: 1 })),
138
+ completeness: scoutCompleteness,
139
+ entrypoint: optionalString,
140
+ routeOrMount: optionalString,
141
+ implementationPaths: stringArray,
142
+ proposedPaths: Type.Optional(stringArray),
143
+ testPaths: stringArray,
144
+ dataSource: optionalString,
145
+ allowedPathConflicts: stringArray,
146
+ unresolvedPaths: stringArray,
147
+ }, { additionalProperties: false }),
148
+ async execute(_toolCallId, params) {
149
+ if (input.requirementIds && (!activeScope?.length || params.scopeId !== scopeIdentity(activeScope)))
150
+ return receipt({ ok: false, code: "FRONTEND_INPUT_SCOPE_VIOLATION", error: "Use exactly the runtime Scout scopeId; discovery may complete only the supplied obligations" });
151
+ const implementationPaths = params?.implementationPaths ?? [];
152
+ const proposedPaths = params?.proposedPaths ?? [];
153
+ const testPaths = params?.testPaths ?? [];
154
+ const pathEvidence = await enrichScoutPathEvidence([
155
+ ...(params?.entrypoint ? [params.entrypoint] : []),
156
+ ...implementationPaths,
157
+ ...testPaths,
158
+ ]);
159
+ for (const proposedPath of proposedPaths) {
160
+ if (!pathEvidence.some((item) => item.path === proposedPath)) {
161
+ pathEvidence.push({ path: proposedPath, sha256: "0".repeat(64), fresh: false, sourceDeclared: false, proposed: true });
162
+ }
163
+ }
164
+ const surface = {
165
+ kind: "target-surface",
166
+ origin: "scout",
167
+ completeness: params?.completeness ?? "blocked",
168
+ entrypoint: params?.entrypoint ?? "",
169
+ routeOrMount: params?.routeOrMount ?? "",
170
+ implementationPaths,
171
+ ...(proposedPaths.length > 0 ? { proposedPaths } : {}),
172
+ testPaths,
173
+ dataSource: params?.dataSource ?? "",
174
+ allowedPathConflicts: params?.allowedPathConflicts ?? [],
175
+ unresolvedPaths: params?.unresolvedPaths ?? [],
176
+ ...(hasSourceDeclarations ? { sourceDeclaredPaths } : {}),
177
+ ...(pathEvidence.length > 0 ? { pathEvidence } : {}),
178
+ };
179
+ if (activeScope) {
180
+ const { readCompleteScoutTargetSurface } = await import("../../../workflows/dag/frontend-committed-facts.js");
181
+ if (surface.completeness === "complete") {
182
+ const check = readCompleteScoutTargetSurface([{ phase: "committed", fact: surface }]);
183
+ if (!check.ok)
184
+ return receipt({ ok: false, code: "SCOUT_SCOPE_INCOMPLETE", error: check.reason });
185
+ }
186
+ const saved = await adoptScoutFact("scout-scope", { kind: "scout-scope", origin: "scout", id: params.scopeId, requirementIds: activeScope, surface });
187
+ if (!saved.ok)
188
+ return receipt(saved);
189
+ const current = latestScopes();
190
+ if (input.requirementIds?.every(id => current.get(id)?.surface?.completeness === "complete")) {
191
+ const surfaces = [...new Set(input.requirementIds.map(id => current.get(id)))].map(f => f.surface);
192
+ const union = (key) => [...new Set(surfaces.flatMap(s => Array.isArray(s[key]) ? s[key] : []))];
193
+ const entries = [...new Set(surfaces.map(s => String(s.entrypoint ?? "")).filter(Boolean))];
194
+ return receipt(await adoptScoutFact("target-surface", { kind: "target-surface", origin: "scout", completeness: "complete", entrypoint: entries[0] ?? "", implementationPaths: [...new Set([...entries, ...union("implementationPaths")])], proposedPaths: union("proposedPaths"), testPaths: union("testPaths"), allowedPathConflicts: union("allowedPathConflicts"), unresolvedPaths: union("unresolvedPaths"), routeOrMount: [...new Set(surfaces.map(s => s.routeOrMount).filter(Boolean))].join("\n"), dataSource: [...new Set(surfaces.map(s => s.dataSource).filter(Boolean))].join("\n"), pathEvidence: surfaces.flatMap(s => s.pathEvidence ?? []), ...(hasSourceDeclarations ? { sourceDeclaredPaths } : {}) }));
195
+ }
196
+ return receipt(saved);
197
+ }
198
+ const result = await adoptScoutFact("target-surface", surface);
199
+ return receipt(result);
200
+ },
201
+ });
202
+ const recordDesignEvidenceTool = defineTool({
203
+ name: "record_design_evidence",
204
+ label: "record_design_evidence",
205
+ description: "Commit an origin=scout design-evidence fact (source, paths, conflicts). Example: {\"source\": \"<source>\", \"paths\": [\"<file>\"], \"conflicts\": []}",
206
+ promptSnippet: "Commit an origin=scout design-evidence fact.",
207
+ parameters: Type.Object({ source: Type.String({}), paths: stringArray, conflicts: stringArray }, { additionalProperties: false }),
208
+ async execute(_toolCallId, params) {
209
+ const paths = params?.paths ?? [];
210
+ const pathEvidence = await enrichScoutPathEvidence(paths);
211
+ const result = await adoptScoutFact("design-evidence", {
212
+ kind: "design-evidence",
213
+ origin: "scout",
214
+ source: params?.source ?? "",
215
+ paths,
216
+ conflicts: params?.conflicts ?? [],
217
+ ...(pathEvidence.length > 0 ? { pathEvidence } : {}),
218
+ });
219
+ return receipt(result);
220
+ },
221
+ });
222
+ const durable = await createDurableFrontendTools({
223
+ file: path.join(input.runDir, input.nodeId, "scout-typed-facts.jsonl"), attemptId, store: input.store,
224
+ setWorkingStore: next => { store = next; }, binding: { requirementIds: input.requirementIds, sourceDeclaredPaths: input.sourceDeclaredPaths, sourceDigest: input.sourceDigest, workspaceRoot: input.workspaceRoot },
225
+ tools: [recordTargetSurfaceTool, recordDesignEvidenceTool],
226
+ validateRestored: async (records) => {
227
+ if (!input.workspaceRoot)
228
+ return;
229
+ for (const record of records) {
230
+ const evidence = record.fact.kind === "scout-scope" ? record.fact.surface?.pathEvidence : record.fact.pathEvidence;
231
+ if (!Array.isArray(evidence))
232
+ continue;
233
+ for (const previous of evidence) {
234
+ if (!isRecordObject(previous) || typeof previous.path !== "string")
235
+ throw Error("scout path evidence is malformed");
236
+ const current = (await enrichScoutPathEvidence([previous.path]))[0];
237
+ if (!current || current.sha256 !== previous.sha256 || current.fresh !== previous.fresh)
238
+ throw Error(`scout evidence drift: ${previous.path}; refresh Scout before reusing facts`);
239
+ }
240
+ }
241
+ },
242
+ });
243
+ return {
244
+ ...durable,
245
+ adoptCommittedFacts: async (records) => durable.commitExternal(async () => {
246
+ for (const record of records) {
247
+ if (record.phase !== "committed")
248
+ continue;
249
+ const fact = record.fact;
250
+ if (!fact || typeof fact !== "object" || Array.isArray(fact))
251
+ continue;
252
+ const result = await adoptScoutFact(typeof fact.kind === "string"
253
+ ? fact.kind
254
+ : "scout-fact", fact);
255
+ if (!result.ok) {
256
+ throw new Error(`frontend scout shard fact merge failed: ${result.error}`);
257
+ }
258
+ }
259
+ }),
260
+ setActiveScope: ids => {
261
+ if (!ids.length || ids.some(id => !input.requirementIds?.includes(id)))
262
+ throw Error("FRONTEND_INPUT_SCOPE_VIOLATION");
263
+ activeScope = [...new Set(ids)];
264
+ return scopeIdentity(activeScope);
265
+ },
266
+ completedRequirementIds: () => new Set([...latestScopes()].filter(([, fact]) => fact.surface?.completeness === "complete").map(([id]) => id)),
267
+ completedScopeFacts: () => [...new Set(latestScopes().values())].filter(f => f.surface?.completeness === "complete"),
268
+ committedFacts: () => readCommittedEvents(store, attemptId),
269
+ };
270
+ }