@tea-agent/loop-agent 0.13.0-alpha.0 → 0.13.0-beta.0

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 (262) hide show
  1. package/AGENTS.md +155 -153
  2. package/CHANGELOG.md +326 -301
  3. package/README.md +345 -326
  4. package/bin/agent-worker.js +22 -22
  5. package/bin/loop-agent.js +21 -21
  6. package/dist/application/dag/generate-task-dag.js +28 -58
  7. package/dist/application/evaluation/candidate-hash.js +75 -0
  8. package/dist/application/evaluation/candidate.js +52 -0
  9. package/dist/application/evaluation/replay.js +289 -0
  10. package/dist/application/evaluation/types.js +130 -0
  11. package/dist/cli/command-definitions.js +17 -4
  12. package/dist/cli/program.js +8 -4
  13. package/dist/commands/cursor-prompt.js +6 -6
  14. package/dist/commands/eval.js +235 -0
  15. package/dist/commands/init.js +544 -506
  16. package/dist/commands/loop-benchmark.js +11 -11
  17. package/dist/commands/pi-reuse-benchmark.js +16 -16
  18. package/dist/executors/pi-sdk-executor.js +38 -24
  19. package/dist/executors/shell-executor.js +34 -2
  20. package/dist/executors/shell-presets.js +20 -0
  21. package/dist/executors/shell-verification.js +7 -0
  22. package/dist/governance/manifest-types.js +1 -0
  23. package/dist/infrastructure/evaluation/candidate-store.js +435 -0
  24. package/dist/infrastructure/evaluation/store.js +40 -0
  25. package/dist/sidecars/cursor-prompt/executor.js +1 -1
  26. package/dist/task/config-types.js +23 -0
  27. package/dist/task/runtime.js +27 -27
  28. package/dist/worker/observe/routes.js +18 -3
  29. package/dist/worker/observe/spec-evidence.js +1 -1
  30. package/dist/worker/observe/static/api.js +46 -46
  31. package/dist/worker/observe/static/app.js +150 -150
  32. package/dist/worker/observe/static/constants.js +148 -148
  33. package/dist/worker/observe/static/copy.js +67 -67
  34. package/dist/worker/observe/static/dag-helpers.js +172 -172
  35. package/dist/worker/observe/static/dag-layout.d.ts +31 -31
  36. package/dist/worker/observe/static/dag-layout.js +83 -83
  37. package/dist/worker/observe/static/dag-model.js +72 -72
  38. package/dist/worker/observe/static/dom.js +61 -61
  39. package/dist/worker/observe/static/format-pool.js +67 -67
  40. package/dist/worker/observe/static/format.js +292 -292
  41. package/dist/worker/observe/static/index.html +308 -308
  42. package/dist/worker/observe/static/kpi.js +94 -94
  43. package/dist/worker/observe/static/relations.js +133 -133
  44. package/dist/worker/observe/static/router.js +93 -93
  45. package/dist/worker/observe/static/run-processing.js +148 -148
  46. package/dist/worker/observe/static/shell-chrome.js +68 -68
  47. package/dist/worker/observe/static/state.js +253 -253
  48. package/dist/worker/observe/static/styles.css +1902 -1902
  49. package/dist/worker/observe/static/views/batch.js +227 -227
  50. package/dist/worker/observe/static/views/dag-graph.js +172 -172
  51. package/dist/worker/observe/static/views/dag-inspector.js +607 -596
  52. package/dist/worker/observe/static/views/dag.js +362 -362
  53. package/dist/worker/observe/static/views/dashboard.js +445 -445
  54. package/dist/worker/observe/static/views/failures.js +143 -143
  55. package/dist/worker/observe/static/views/feature.js +492 -492
  56. package/dist/worker/observe/static/views/pool.js +350 -350
  57. package/dist/worker/observe/static/views/run.js +453 -453
  58. package/dist/worker/observe/static/views/session-timeline.js +205 -205
  59. package/dist/worker/observe/static/views/shell.js +7 -7
  60. package/dist/worker/observe/static/views/task.js +314 -314
  61. package/dist/worker/observe/static/views/timeline.js +163 -163
  62. package/dist/workflows/dag/backend-test-analysis-contract.js +120 -0
  63. package/dist/workflows/dag/canvas-observer.js +275 -275
  64. package/dist/workflows/dag/dynamic-runtime/map.js +90 -2
  65. package/dist/workflows/dag/init-hybrid.js +1415 -200
  66. package/dist/workflows/dag/node-execution.js +9 -0
  67. package/dist/workflows/dag/prompt.js +9 -0
  68. package/dist/workflows/dag/report.js +35 -1
  69. package/dist/workflows/dag/runner.js +28 -2
  70. package/dist/workflows/dag/task-demand-routing.js +383 -0
  71. package/dist/workflows/dag/types.js +50 -13
  72. package/dist/workflows/dag/upstream-artifacts.js +1 -0
  73. package/dist/workflows/dag/validate.js +59 -1
  74. package/docs/README.md +106 -104
  75. package/docs/agent-dag-recovery-playbook.md +195 -193
  76. package/docs/agent-dag-runner.md +67 -67
  77. package/docs/architecture/README.md +26 -26
  78. package/docs/architecture/dag-execution.md +140 -140
  79. package/docs/architecture/evolution.md +54 -54
  80. package/docs/architecture/facts-and-state.md +71 -71
  81. package/docs/architecture/runtime-boundaries.md +191 -191
  82. package/docs/architecture/system-overview.md +93 -93
  83. package/docs/architecture/worker-and-feature.md +85 -85
  84. package/docs/cursor-prompt-sidecar.md +36 -36
  85. package/docs/decisions/README.md +18 -18
  86. package/docs/design/README.md +167 -85
  87. package/docs/development-principles.md +73 -73
  88. package/docs/exec-plans/README.md +6 -6
  89. package/docs/exec-plans/active/README.md +15 -11
  90. package/docs/exec-plans/completed/README.md +85 -74
  91. package/docs/feature-workflow.md +389 -339
  92. package/docs/harness-methodology-debugging.md +153 -153
  93. package/docs/harness-methodology-tdd.md +130 -130
  94. package/docs/harness-methodology-verification.md +27 -27
  95. package/docs/init-surface.manifest.json +289 -280
  96. package/docs/loop-agent-harness.md +142 -141
  97. package/docs/production-readiness.md +96 -96
  98. package/docs/progress/README.md +64 -58
  99. package/docs/reports/README.md +117 -100
  100. package/docs/skills/README.md +7 -7
  101. package/docs/skills/vetted-skill-registry.md +29 -27
  102. package/docs/templates/adr.md +60 -60
  103. package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
  104. package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
  105. package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -117
  106. package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
  107. package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
  108. package/docs/templates/agent-dag-report.schema.json +473 -473
  109. package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
  110. package/docs/templates/agent-dag.base.json +190 -190
  111. package/docs/templates/agent-dag.final-verification.json +185 -185
  112. package/docs/templates/agent-dag.schema.json +411 -383
  113. package/docs/templates/agent-dag.supervised-implementation.json +501 -501
  114. package/docs/templates/backend-test-analysis.schema.json +44 -0
  115. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +202 -139
  116. package/docs/templates/backend-test-dag.json +311 -288
  117. package/docs/templates/backend-test-dag.retrospect.prompt.md +125 -125
  118. package/docs/templates/backend-test-dag.review-cases.prompt.md +81 -81
  119. package/docs/templates/exec-plan.md +64 -64
  120. package/docs/templates/feature-spec.md +53 -53
  121. package/docs/templates/frontend-design-contract.md +42 -33
  122. package/docs/templates/frontend-task-constraints.md +35 -25
  123. package/docs/templates/frontend-task-requirement.md +70 -61
  124. package/docs/templates/frontend-test-dag.generate-cases.prompt.md +5 -0
  125. package/docs/templates/frontend-test-dag.json +23 -0
  126. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +3 -0
  127. package/docs/templates/frontend-test-dag.retrospect.prompt.md +3 -0
  128. package/docs/templates/frontend-test-dag.review-cases.prompt.md +3 -0
  129. package/docs/templates/frontend-test-dag.review-execution.prompt.md +3 -0
  130. package/docs/templates/harness.schema.json +221 -221
  131. package/docs/templates/hybrid-dag.json +188 -188
  132. package/docs/templates/init-evolution-review.md +35 -35
  133. package/docs/templates/interactive-ui-round2-experiment.md +66 -66
  134. package/docs/templates/knowledge-graph-bootstrap-dag.json +118 -118
  135. package/docs/templates/knowledge-sync-dag.json +178 -177
  136. package/docs/templates/knowledge-sync-draft.schema.json +71 -71
  137. package/docs/templates/product-line/AGENTS.md +8 -8
  138. package/docs/templates/product-line/README.md +9 -9
  139. package/docs/templates/product-line/acceptance.yaml +14 -14
  140. package/docs/templates/product-line/closeout.yaml +9 -9
  141. package/docs/templates/product-line/design.md +13 -13
  142. package/docs/templates/product-line/links.md +10 -10
  143. package/docs/templates/product-line/requirement.md +17 -17
  144. package/docs/templates/product-line/task-graph.yaml +15 -15
  145. package/docs/templates/product-line/task.yaml +64 -64
  146. package/docs/templates/product-line/test-plan.md +7 -7
  147. package/docs/templates/production-readiness-checklist.md +57 -57
  148. package/docs/templates/progress-log.md +17 -17
  149. package/docs/templates/project-start-checklist.md +9 -9
  150. package/docs/templates/qa-report.md +48 -48
  151. package/docs/templates/sprint-contract.md +29 -29
  152. package/docs/templates/worker-dogfood-evidence.md +80 -80
  153. package/docs/templates/worker-dogfood-setup.md +68 -68
  154. package/docs/verification-matrix.md +70 -67
  155. package/examples/decision-gate-agent-dag.json +177 -177
  156. package/examples/example-dag.json +46 -46
  157. package/examples/hybrid-loop-agent-dag.json +189 -189
  158. package/harness.json +66 -66
  159. package/package.json +88 -52
  160. package/scripts/check-product-line-docs.sh +29 -29
  161. package/scripts/check-task-pool-root.sh +32 -32
  162. package/scripts/kb-bootstrap-init-skeleton.sh +240 -239
  163. package/scripts/kb-graph-incremental-prepare.mjs +386 -372
  164. package/scripts/kb-graph-incremental-prepare.sh +5 -5
  165. package/scripts/kb-graph-materialize.mjs +105 -105
  166. package/scripts/kb-graph-materialize.sh +4 -4
  167. package/scripts/kb-graph-promote.mjs +164 -153
  168. package/scripts/kb-graph-promote.sh +4 -4
  169. package/scripts/kb-query.mjs +554 -554
  170. package/scripts/kb-query.sh +5 -5
  171. package/skills/agent-worker/SKILL.md +39 -39
  172. package/skills/agent-worker/references/agent-worker-operator.md +60 -60
  173. package/skills/ai-engineering-context/SKILL.md +48 -48
  174. package/skills/analyze-product-dependencies/SKILL.md +67 -0
  175. package/skills/analyze-product-dependencies/agents/openai.yaml +4 -0
  176. package/skills/analyze-product-dependencies/references/api-documentation-schema.md +30 -0
  177. package/skills/analyze-product-dependencies/references/dependency-analysis-schema.md +28 -0
  178. package/skills/analyze-product-dependencies/references/example.md +76 -0
  179. package/skills/analyze-product-dependencies/references/forward-test-cases.md +35 -0
  180. package/skills/analyze-product-dependencies/references/input-contract.md +11 -0
  181. package/skills/analyze-product-dependencies/references/scouting-rules.md +61 -0
  182. package/skills/analyze-product-dependencies/scripts/test-validators.mjs +267 -0
  183. package/skills/analyze-product-dependencies/scripts/validate-api-documentation.mjs +101 -0
  184. package/skills/analyze-product-dependencies/scripts/validate-dependency-analysis.mjs +142 -0
  185. package/skills/analyze-product-dependencies/scripts/validate-product-requirement-input.mjs +76 -0
  186. package/skills/analyze-product-dependencies/scripts/validation-helpers.mjs +146 -0
  187. package/skills/analyze-product-requirements/SKILL.md +90 -0
  188. package/skills/analyze-product-requirements/agents/openai.yaml +4 -0
  189. package/skills/analyze-product-requirements/references/acceptance-criteria.md +91 -0
  190. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +56 -0
  191. package/skills/analyze-product-requirements/references/example.md +86 -0
  192. package/skills/analyze-product-requirements/references/forward-test-cases.md +66 -0
  193. package/skills/analyze-product-requirements/references/product-analysis-schema.md +32 -0
  194. package/skills/analyze-product-requirements/references/product-requirement-schema.md +33 -0
  195. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +35 -0
  196. package/skills/analyze-product-requirements/scripts/test-validators.mjs +193 -0
  197. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +69 -0
  198. package/skills/analyze-product-requirements/scripts/validate-product-requirement.mjs +97 -0
  199. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +98 -0
  200. package/skills/analyze-product-requirements/scripts/validation-helpers.mjs +156 -0
  201. package/skills/code-review-core/SKILL.md +20 -20
  202. package/skills/codebase-scout/SKILL.md +19 -19
  203. package/skills/frontend-design-review/SKILL.md +66 -61
  204. package/skills/frontend-design-review/references/review-checklist.md +58 -37
  205. package/skills/frontend-implementation/SKILL.md +45 -52
  206. package/skills/frontend-implementation/references/code-standards.md +32 -34
  207. package/skills/frontend-implementation/references/design-spec.md +46 -46
  208. package/skills/frontend-implementation/references/node-contracts.md +76 -63
  209. package/skills/frontend-review/SKILL.md +59 -53
  210. package/skills/frontend-review/references/review-findings.md +47 -42
  211. package/skills/frontend-verification/SKILL.md +53 -40
  212. package/skills/frontend-verification/references/verification-checklist.md +68 -56
  213. package/skills/grill-me/SKILL.md +10 -10
  214. package/skills/grill-with-docs/SKILL.md +88 -88
  215. package/skills/grill-with-docs/adr-format.md +47 -47
  216. package/skills/grill-with-docs/context-format.md +60 -60
  217. package/skills/init-capability-evolution/SKILL.md +70 -70
  218. package/skills/loop-agent/SKILL.md +151 -151
  219. package/skills/loop-agent/references/README.md +67 -67
  220. package/skills/loop-agent/references/command-reference.md +505 -453
  221. package/skills/loop-agent/references/docs-converge.md +126 -126
  222. package/skills/loop-agent/references/harness-policy.md +263 -263
  223. package/skills/loop-agent/references/hybrid-dag.md +238 -233
  224. package/skills/loop-agent/references/learned/README.md +21 -21
  225. package/skills/loop-agent/references/long-running-loop.md +57 -57
  226. package/skills/loop-agent/references/model-routing.md +36 -36
  227. package/skills/loop-agent/references/multi-worktree.md +54 -54
  228. package/skills/loop-agent/references/one-shot-runs.md +85 -85
  229. package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -169
  230. package/skills/loop-agent/references/pi-prompt.md +23 -23
  231. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +84 -84
  232. package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -44
  233. package/skills/loop-agent/references/task-workflow.md +89 -89
  234. package/skills/loop-agent/references/verification-and-failure-handling.md +139 -139
  235. package/skills/playwright-cli/SKILL.md +420 -0
  236. package/skills/playwright-cli/references/element-attributes.md +23 -0
  237. package/skills/playwright-cli/references/playwright-tests.md +39 -0
  238. package/skills/playwright-cli/references/request-mocking.md +87 -0
  239. package/skills/playwright-cli/references/running-code.md +241 -0
  240. package/skills/playwright-cli/references/session-management.md +225 -0
  241. package/skills/playwright-cli/references/storage-state.md +275 -0
  242. package/skills/playwright-cli/references/test-generation.md +433 -0
  243. package/skills/playwright-cli/references/tracing.md +139 -0
  244. package/skills/playwright-cli/references/video-recording.md +143 -0
  245. package/skills/playwright-cli-case-generator/SKILL.md +74 -0
  246. package/skills/requesting-code-review/SKILL.md +101 -101
  247. package/skills/requesting-code-review/code-reviewer.md +168 -168
  248. package/skills/systematic-debugging/CREATION-LOG.md +119 -119
  249. package/skills/systematic-debugging/SKILL.md +296 -296
  250. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
  251. package/skills/systematic-debugging/condition-based-waiting.md +115 -115
  252. package/skills/systematic-debugging/defense-in-depth.md +122 -122
  253. package/skills/systematic-debugging/find-polluter.sh +63 -63
  254. package/skills/systematic-debugging/root-cause-tracing.md +169 -169
  255. package/skills/systematic-debugging/test-academic.md +14 -14
  256. package/skills/systematic-debugging/test-pressure-1.md +58 -58
  257. package/skills/systematic-debugging/test-pressure-2.md +68 -68
  258. package/skills/systematic-debugging/test-pressure-3.md +69 -69
  259. package/skills/test-driven-development/SKILL.md +20 -20
  260. package/skills/using-git-worktrees/SKILL.md +215 -215
  261. package/skills/verification-before-completion/SKILL.md +154 -154
  262. package/skills/webapp-testing/SKILL.md +19 -19
@@ -1,4 +1,5 @@
1
- import { access, readdir, readFile, writeFile } from "node:fs/promises";
1
+ import { createHash } from "node:crypto";
2
+ import { access, readdir, readFile, realpath, writeFile } from "node:fs/promises";
2
3
  import os from "node:os";
3
4
  import path from "node:path";
4
5
  import { assertValidDagSpec } from "./validate.js";
@@ -14,6 +15,7 @@ import { getTaskPaths, loadTaskConfig } from "../../task/runtime.js";
14
15
  import { materializeTaskReferenceDocs } from "../../task/source-references.js";
15
16
  import { resolveVerifyPreset } from "../../executors/shell-verification.js";
16
17
  import { resolveExecutorModelMatrices } from "../../executors/model-routing.js";
18
+ import { normalizeTaskRequirementText, resolveTaskDagTemplateSelection, } from "./task-demand-routing.js";
17
19
  const REQUIREMENT_FILE = "需求.md";
18
20
  const CONSTRAINT_FILE = "执行约束.md";
19
21
  const REFERENCE_DIRECTORY = "references";
@@ -148,6 +150,425 @@ const STANDARD_GLOBAL_CONSTRAINTS = [
148
150
  "exclusive implementer nodes must use narrow, concrete writeSet paths; never keep ** or repo root",
149
151
  `Replace ${IMPLEMENT_WRITESET_PLACEHOLDER} with concrete paths before executing the implementation writer`,
150
152
  ];
153
+ // ---------------------------------------------------------------------------
154
+ // Frontend Mock capability discovery & mode resolution
155
+ // ---------------------------------------------------------------------------
156
+ /** Evidence-based check: does package.json contain a mock-related script? */
157
+ async function packageJsonHasMockScript(repoRoot) {
158
+ try {
159
+ const raw = await readFile(path.join(repoRoot, "package.json"), "utf-8");
160
+ const pkg = JSON.parse(raw);
161
+ const scripts = pkg.scripts ?? {};
162
+ const mockScripts = Object.keys(scripts).filter((name) => name === "mock" ||
163
+ name.startsWith("mock:") ||
164
+ name.startsWith("dev:mock") ||
165
+ /mock/i.test(name));
166
+ const verifyCommands = mockScripts
167
+ .filter((name) => /(?:test|check|verify|contract)/i.test(name))
168
+ .map((name) => ({
169
+ label: `npm run ${name}`,
170
+ args: ["npm", "run", name],
171
+ cwd: repoRoot,
172
+ }));
173
+ return {
174
+ hasScript: mockScripts.length > 0,
175
+ scriptNames: mockScripts,
176
+ verifyCommands,
177
+ };
178
+ }
179
+ catch {
180
+ return { hasScript: false, scriptNames: [], verifyCommands: [] };
181
+ }
182
+ }
183
+ /** Check whether a direct (non-transitive) dependency exists in package.json. */
184
+ async function hasDirectDependency(repoRoot, depName) {
185
+ try {
186
+ const raw = await readFile(path.join(repoRoot, "package.json"), "utf-8");
187
+ const pkg = JSON.parse(raw);
188
+ const deps = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
189
+ return depName in deps;
190
+ }
191
+ catch {
192
+ return false;
193
+ }
194
+ }
195
+ /** Check whether handler/fixture/bootstrap files exist for known mock frameworks. */
196
+ async function discoverMockHandlerFiles(repoRoot, serviceRoot) {
197
+ const exactCandidates = [
198
+ ...[
199
+ "src/mocks/handlers.ts",
200
+ "src/mocks/handlers.js",
201
+ "src/mocks/browser.ts",
202
+ "src/mocks/browser.js",
203
+ "src/mocks/server.ts",
204
+ "src/mocks/server.js",
205
+ "mocks/handlers.ts",
206
+ "mocks/handlers.js",
207
+ "mocks/browser.ts",
208
+ "mocks/browser.js",
209
+ ].map((candidatePath) => ({ framework: "msw", path: candidatePath })),
210
+ ...["db.json", "mock/db.json", "src/mock/db.json"].map((candidatePath) => ({ framework: "json-server", path: candidatePath })),
211
+ ];
212
+ const directoryCandidates = [
213
+ ...(serviceRoot ? [{ path: serviceRoot }] : []),
214
+ { framework: "mockjs", path: "src/mock" },
215
+ { framework: "mockjs", path: "mock" },
216
+ { framework: "msw", path: "src/mocks" },
217
+ { framework: "msw", path: "mocks" },
218
+ { framework: "mirage", path: "src/mirage" },
219
+ { framework: "mirage", path: "mirage" },
220
+ ];
221
+ const foundPaths = new Set();
222
+ let foundFramework;
223
+ for (const candidate of exactCandidates) {
224
+ try {
225
+ await access(path.join(repoRoot, candidate.path));
226
+ foundPaths.add(candidate.path);
227
+ foundFramework ??= candidate.framework;
228
+ }
229
+ catch {
230
+ // Exact candidate does not exist.
231
+ }
232
+ }
233
+ async function collectFiles(directory, relativeDirectory) {
234
+ let entries;
235
+ try {
236
+ entries = await readdir(directory, { withFileTypes: true });
237
+ }
238
+ catch {
239
+ return;
240
+ }
241
+ for (const entry of entries) {
242
+ if (foundPaths.size >= 24)
243
+ return;
244
+ const absoluteEntry = path.join(directory, entry.name);
245
+ const relativeEntry = path.posix.join(relativeDirectory.replace(/\\/g, "/"), entry.name);
246
+ if (entry.isDirectory()) {
247
+ await collectFiles(absoluteEntry, relativeEntry);
248
+ }
249
+ else if (entry.isFile()) {
250
+ foundPaths.add(relativeEntry);
251
+ }
252
+ }
253
+ }
254
+ for (const candidate of directoryCandidates) {
255
+ const before = foundPaths.size;
256
+ await collectFiles(path.join(repoRoot, candidate.path), candidate.path);
257
+ if (foundPaths.size > before && candidate.framework) {
258
+ foundFramework ??= candidate.framework;
259
+ }
260
+ }
261
+ return { framework: foundFramework, paths: [...foundPaths].sort() };
262
+ }
263
+ async function discoverMockBootstrapImports(repoRoot) {
264
+ const entryCandidates = [
265
+ "src/main.ts",
266
+ "src/main.tsx",
267
+ "src/main.js",
268
+ "src/main.jsx",
269
+ "src/index.ts",
270
+ "src/index.tsx",
271
+ "src/index.js",
272
+ "src/index.jsx",
273
+ "src/setupTests.ts",
274
+ "src/setupTests.js",
275
+ "test/setup.ts",
276
+ "test/setup.js",
277
+ ];
278
+ const imports = [];
279
+ for (const candidate of entryCandidates) {
280
+ try {
281
+ const content = await readFile(path.join(repoRoot, candidate), "utf-8");
282
+ if (/(?:from\s*|import\s*)["'][^"']*(?:mock|msw|mirage)[^"']*["']/i.test(content)) {
283
+ imports.push(candidate);
284
+ }
285
+ }
286
+ catch {
287
+ // Candidate entry does not exist or is unreadable.
288
+ }
289
+ }
290
+ return imports;
291
+ }
292
+ /**
293
+ * Deterministic frontend Mock capability discovery.
294
+ *
295
+ * Strong evidence (at least one must be hit to judge "present"):
296
+ * 1. package.json mock script + corresponding config/entry
297
+ * 2. Direct dependency (MSW, Mock.js, Mirage, json-server, Vite Mock plugin) + handler files
298
+ * 3. Application bootstrap imports project mock files
299
+ * 4. Project specs explicitly define mock service root, handler dir, and startup method
300
+ *
301
+ * Anti-evidence (cannot alone judge "present"):
302
+ * - lockfile-only or transitive dependency
303
+ * - test variable named "mock"
304
+ * - fixtures without service registration
305
+ * - neighboring project mock services
306
+ * - model-directory-name guessing
307
+ */
308
+ export async function discoverFrontendMockCapability(repoRoot, taskConfig) {
309
+ const safetyViolation = await frontendMockServiceRootSafetyViolation(repoRoot, taskConfig);
310
+ if (safetyViolation) {
311
+ return {
312
+ status: "ambiguous",
313
+ serviceRoot: taskConfig.frontendMock?.serviceRoot,
314
+ safetyViolation,
315
+ evidencePaths: [],
316
+ verifyCommands: [],
317
+ reasons: [safetyViolation],
318
+ };
319
+ }
320
+ const evidencePaths = [];
321
+ const reasons = [];
322
+ let framework;
323
+ let serviceRoot;
324
+ let strongEvidenceCount = 0;
325
+ let ambiguousSignals = 0;
326
+ // 1. Check package.json mock scripts
327
+ const scriptResult = await packageJsonHasMockScript(repoRoot);
328
+ if (scriptResult.hasScript) {
329
+ reasons.push(`package.json has mock scripts: ${scriptResult.scriptNames.join(", ")}`);
330
+ evidencePaths.push("package.json");
331
+ // A script alone is not strong evidence unless we also find config/entry
332
+ }
333
+ // 2. Check for direct mock framework dependencies
334
+ const mockDeps = ["msw", "mockjs", "miragejs", "json-server", "vite-plugin-mock"];
335
+ const foundDeps = [];
336
+ for (const dep of mockDeps) {
337
+ if (await hasDirectDependency(repoRoot, dep)) {
338
+ foundDeps.push(dep);
339
+ evidencePaths.push(`package.json (${dep})`);
340
+ }
341
+ }
342
+ // 3. Check for handler/fixture/bootstrap files
343
+ const serviceRootHint = taskConfig.frontendMock?.serviceRoot;
344
+ const handlerResult = await discoverMockHandlerFiles(repoRoot, serviceRootHint);
345
+ const bootstrapImports = await discoverMockBootstrapImports(repoRoot);
346
+ if (handlerResult.framework) {
347
+ framework = handlerResult.framework;
348
+ }
349
+ if (handlerResult.paths.length > 0) {
350
+ evidencePaths.push(...handlerResult.paths);
351
+ reasons.push(`Mock handler/fixture paths found: ${handlerResult.paths.join(", ")}`);
352
+ }
353
+ if (bootstrapImports.length > 0) {
354
+ evidencePaths.push(...bootstrapImports);
355
+ reasons.push(`Application/test bootstrap imports Mock code: ${bootstrapImports.join(", ")}`);
356
+ }
357
+ // Evaluate strong evidence
358
+ // Case: direct dep + handler files
359
+ if (foundDeps.length > 0 && handlerResult.paths.length > 0) {
360
+ strongEvidenceCount++;
361
+ reasons.push(`Direct mock dependency (${foundDeps.join(", ")}) with handler files`);
362
+ }
363
+ // Case: mock script in package.json + corresponding config/entry
364
+ if (scriptResult.hasScript && handlerResult.paths.length > 0) {
365
+ strongEvidenceCount++;
366
+ reasons.push("Mock scripts and handler files both present");
367
+ }
368
+ if (bootstrapImports.length > 0 && handlerResult.paths.length > 0) {
369
+ strongEvidenceCount++;
370
+ reasons.push("Application/test bootstrap and project Mock files both present");
371
+ }
372
+ // Case: project specs define mock service root
373
+ if (taskConfig.frontendMock?.serviceRoot && handlerResult.paths.length > 0) {
374
+ strongEvidenceCount++;
375
+ serviceRoot = taskConfig.frontendMock.serviceRoot;
376
+ reasons.push(`task config specifies serviceRoot=${serviceRoot}`);
377
+ }
378
+ // Handle ambiguous: some signals but not enough for "present"
379
+ if (strongEvidenceCount === 0 &&
380
+ (foundDeps.length > 0 ||
381
+ scriptResult.hasScript ||
382
+ handlerResult.paths.length > 0 ||
383
+ bootstrapImports.length > 0)) {
384
+ ambiguousSignals++;
385
+ if (foundDeps.length > 0 && handlerResult.paths.length === 0) {
386
+ reasons.push(`Mock dependency found (${foundDeps.join(", ")}) but no handler/bootstrap files detected`);
387
+ }
388
+ if (scriptResult.hasScript && handlerResult.paths.length === 0 && foundDeps.length === 0) {
389
+ reasons.push("Mock scripts exist but no handler files or direct mock dependencies found");
390
+ }
391
+ }
392
+ // Build verify commands from task config
393
+ const configuredVerifyCommands = (taskConfig.frontendMock?.verifyCommands ?? []).map((cmd) => ({
394
+ label: cmd.label,
395
+ args: ["bash", "-lc", cmd.command],
396
+ cwd: repoRoot,
397
+ timeoutMs: cmd.timeoutMs,
398
+ }));
399
+ const verifyCommands = [...configuredVerifyCommands, ...scriptResult.verifyCommands]
400
+ .filter((command, index, commands) => commands.findIndex((candidate) => candidate.label === command.label) === index);
401
+ if (strongEvidenceCount > 0) {
402
+ return {
403
+ status: "present",
404
+ framework,
405
+ serviceRoot: serviceRoot ?? taskConfig.frontendMock?.serviceRoot,
406
+ evidencePaths,
407
+ verifyCommands,
408
+ reasons,
409
+ };
410
+ }
411
+ if (ambiguousSignals > 0) {
412
+ return {
413
+ status: "ambiguous",
414
+ framework,
415
+ serviceRoot: taskConfig.frontendMock?.serviceRoot,
416
+ evidencePaths,
417
+ verifyCommands,
418
+ reasons,
419
+ };
420
+ }
421
+ return {
422
+ status: "absent",
423
+ evidencePaths,
424
+ verifyCommands,
425
+ reasons: ["No mock service evidence found in project"],
426
+ };
427
+ }
428
+ function patternStaticPrefix(pattern) {
429
+ const normalized = pattern.replace(/\\/g, "/").replace(/^\.\//, "");
430
+ const wildcard = normalized.search(/[?*]/);
431
+ return (wildcard >= 0 ? normalized.slice(0, wildcard) : normalized).replace(/\/+$/, "");
432
+ }
433
+ function frontendMockServiceRootAllowed(taskConfig) {
434
+ const serviceRoot = taskConfig.frontendMock?.serviceRoot;
435
+ if (!serviceRoot)
436
+ return true;
437
+ const normalized = serviceRoot.replace(/\\/g, "/").replace(/^\.\//, "");
438
+ if (normalized === "." ||
439
+ normalized === ".." ||
440
+ normalized.startsWith("../") ||
441
+ path.isAbsolute(serviceRoot) ||
442
+ /[?*]/.test(normalized)) {
443
+ return false;
444
+ }
445
+ const allowed = taskConfig.allowedPaths.some((allowedPath) => pathMatchesPattern(normalized, allowedPath) ||
446
+ pathMatchesPattern(`${normalized}/_probe_`, allowedPath));
447
+ if (!allowed)
448
+ return false;
449
+ return !mergeForbiddenPaths(taskConfig).some((forbiddenPath) => {
450
+ const forbiddenPrefix = patternStaticPrefix(forbiddenPath);
451
+ return (pathMatchesPattern(normalized, forbiddenPath) ||
452
+ (forbiddenPrefix.length > 0 &&
453
+ pathMatchesPattern(forbiddenPrefix, normalized)));
454
+ });
455
+ }
456
+ async function frontendMockServiceRootSafetyViolation(repoRoot, taskConfig) {
457
+ const serviceRoot = taskConfig.frontendMock?.serviceRoot;
458
+ if (!serviceRoot)
459
+ return undefined;
460
+ if (!frontendMockServiceRootAllowed(taskConfig)) {
461
+ return `frontendMock.serviceRoot is outside allowedPaths or overlaps forbiddenPaths: ${serviceRoot}`;
462
+ }
463
+ try {
464
+ const [repoRealPath, serviceRealPath] = await Promise.all([
465
+ realpath(repoRoot),
466
+ realpath(path.resolve(repoRoot, serviceRoot)),
467
+ ]);
468
+ const relativeRealPath = path.relative(repoRealPath, serviceRealPath);
469
+ if (relativeRealPath === ".." ||
470
+ relativeRealPath.startsWith(`..${path.sep}`) ||
471
+ path.isAbsolute(relativeRealPath)) {
472
+ return `frontendMock.serviceRoot resolves outside the repository: ${serviceRoot}`;
473
+ }
474
+ const normalizedRealPath = relativeRealPath.split(path.sep).join("/") || ".";
475
+ if (!frontendMockServiceRootAllowed({
476
+ ...taskConfig,
477
+ frontendMock: {
478
+ ...(taskConfig.frontendMock ?? { policy: "auto", verifyCommands: [] }),
479
+ serviceRoot: normalizedRealPath,
480
+ },
481
+ })) {
482
+ return `frontendMock.serviceRoot resolves outside its allowed boundary: ${serviceRoot}`;
483
+ }
484
+ }
485
+ catch (error) {
486
+ // A missing configured root is capability absence, not a path escape. The
487
+ // discovery pass below will report it without traversing another location.
488
+ if (error.code !== "ENOENT") {
489
+ return `frontendMock.serviceRoot safety could not be verified: ${serviceRoot}`;
490
+ }
491
+ }
492
+ return undefined;
493
+ }
494
+ /**
495
+ * Heuristic: does the task have interface/async data dependencies?
496
+ *
497
+ * Checks (in priority order):
498
+ * 1. taskConfig.frontendMock.policy === "required"
499
+ * 2. Requirement references API docs, schemas, endpoints
500
+ * 3. Acceptance criteria mention requests, async data, or service states
501
+ * 4. Contract/scout confirmed existing API call chain (not available at generation time)
502
+ */
503
+ export function hasApiDependency(sources) {
504
+ if (sources.taskConfig.frontendMock?.policy === "required") {
505
+ return true;
506
+ }
507
+ const requirement = normalizeTaskRequirementText(sources.requirementMarkdown)
508
+ .replace(/`[^`\n]*`/g, " ");
509
+ const dependencyPatterns = [
510
+ /(?:接口文档|接口定义|接口协议|后端接口|服务端接口|接口联调|请求|响应|远程数据|异步数据|数据获取|模拟接口|模拟数据)/,
511
+ /(?<![A-Za-z0-9_])API(?![A-Za-z0-9_])/i,
512
+ /\b(?:endpoint|request|response|fetch|axios|schema|mock|backend\s+api|server\s+api)\b/i,
513
+ ];
514
+ const negationPatterns = [
515
+ /(?:不涉及|无需|不需要|不依赖|不调用|不请求|没有|禁止|不得).{0,16}(?:接口|后端|服务端|远程数据|异步数据|API)/i,
516
+ /\b(?:no|without|does\s+not|do\s+not|must\s+not)\b.{0,24}\b(?:api|endpoint|request|backend|server)\b/i,
517
+ ];
518
+ return requirement
519
+ .split(/[。!?!?;;,,\r\n]+/)
520
+ .map((clause) => clause.trim())
521
+ .filter(Boolean)
522
+ .some((clause) => !negationPatterns.some((pattern) => pattern.test(clause)) &&
523
+ dependencyPatterns.some((pattern) => pattern.test(clause)));
524
+ }
525
+ /**
526
+ * Resolve frontend Mock mode from capability seed, task config, and interface dependency analysis.
527
+ *
528
+ * Decision matrix (from docs/design/frontend-mock-data-workflow.md):
529
+ *
530
+ * | 接口/异步数据依赖 | 既有 Mock 服务 | policy | 结果 |
531
+ * |---|---|---|---|
532
+ * | 无 | 任意 | auto | not-required |
533
+ * | 有 | present | auto | required |
534
+ * | 有 | present/absent/ambiguous | auto | required (strategy chooses a safe mechanism) |
535
+ * | 任意 | present | required | required |
536
+ * | 任意 | absent/ambiguous | required | blocked |
537
+ * | 任意 | 任意 | disabled | not-required (if spec allows) else blocked |
538
+ */
539
+ export function resolveFrontendMockMode(capability, taskConfig, hasApiDep) {
540
+ const policy = taskConfig.frontendMock?.policy ?? "auto";
541
+ const hasDeterministicMockVerification = capability.verifyCommands.length > 0;
542
+ if (capability.safetyViolation || !frontendMockServiceRootAllowed(taskConfig)) {
543
+ return "blocked";
544
+ }
545
+ // disabled policy: must respect project mock rules (can't override spec)
546
+ if (policy === "disabled") {
547
+ // When disabled but the project spec mandates mock, it's blocked
548
+ if (capability.status === "present") {
549
+ // Project has mock service; disabled is an explicit override that still allows not-required
550
+ return "not-required";
551
+ }
552
+ return "not-required";
553
+ }
554
+ // required policy
555
+ if (policy === "required") {
556
+ if (capability.status === "present" && hasDeterministicMockVerification) {
557
+ return "required";
558
+ }
559
+ return "blocked";
560
+ }
561
+ // auto policy
562
+ if (!hasApiDep) {
563
+ return "not-required";
564
+ }
565
+ // In auto mode, capability discovery is evidence for the strategy node, not
566
+ // a final mechanism decision. Projects without a native Mock service may use
567
+ // an existing browser interception harness or a reversible request adapter.
568
+ // The deterministic strategy gate blocks before the writer when none can be
569
+ // verified by the DAG's frozen static/behavior entrypoints.
570
+ return "required";
571
+ }
151
572
  function mapTaskComplexity(complexity) {
152
573
  if (complexity === "small")
153
574
  return "LOW";
@@ -312,6 +733,10 @@ function markdownVerifyCommand(repoRoot, command) {
312
733
  label: command,
313
734
  };
314
735
  }
736
+ function isSupportedMarkdownVerifyCommand(command) {
737
+ return (/^(npm|pnpm|yarn|bun)\s+(run\s+)?[a-z0-9:_-]+(?:\s.*)?$/i.test(command) ||
738
+ /^(npx|pnpm\s+exec|yarn\s+exec|bunx)\s+(vitest|jest|playwright|cypress|tsc|eslint)(?:\s.*)?$/i.test(command));
739
+ }
315
740
  function extractFrontendVerifyCommandsFromMarkdown(input) {
316
741
  if (!input.repoRoot)
317
742
  return { staticCommands: [], behaviorCommands: [] };
@@ -328,8 +753,7 @@ function extractFrontendVerifyCommandsFromMarkdown(input) {
328
753
  const codeSpanCommands = Array.from(bulletless.matchAll(/`([^`]+)`/g), (match) => match[1].trim());
329
754
  const candidates = codeSpanCommands.length > 0 ? codeSpanCommands : [bulletless];
330
755
  for (const candidate of candidates) {
331
- if (/^(npm|pnpm|yarn|bun)\s+(run\s+)?[a-z0-9:_-]+(?:\s.*)?$/i.test(candidate) ||
332
- /^(npx|pnpm\s+exec|yarn\s+exec|bunx)\s+(vitest|jest|playwright|cypress|tsc|eslint)(?:\s.*)?$/i.test(candidate)) {
756
+ if (isSupportedMarkdownVerifyCommand(candidate)) {
333
757
  commands.add(candidate);
334
758
  }
335
759
  }
@@ -350,6 +774,26 @@ function extractFrontendVerifyCommandsFromMarkdown(input) {
350
774
  }
351
775
  return { staticCommands, behaviorCommands };
352
776
  }
777
+ function extractFrontendMockVerifyCommandsFromMarkdown(input) {
778
+ const commands = [];
779
+ const markdown = [
780
+ input.requirementMarkdown,
781
+ input.constraintMarkdown ?? "",
782
+ ].join("\n");
783
+ for (const line of markdown.split(/\r?\n/)) {
784
+ if (!/(?:mock|模拟服务|接口桩)/i.test(line))
785
+ continue;
786
+ for (const match of line.matchAll(/`([^`]+)`/g)) {
787
+ const commandText = match[1].trim();
788
+ if (!isSupportedMarkdownVerifyCommand(commandText))
789
+ continue;
790
+ const command = markdownVerifyCommand(input.repoRoot, commandText);
791
+ if (command)
792
+ commands.push(command);
793
+ }
794
+ }
795
+ return commands.filter((command, index, all) => all.findIndex((candidate) => candidate.args.join("\0") === command.args.join("\0")) === index);
796
+ }
353
797
  function chooseFrontendVerifyCommands(input) {
354
798
  if (input.parsedCommands.length > 0) {
355
799
  return { commands: input.parsedCommands, commandSource: "inline" };
@@ -411,6 +855,39 @@ function toTaskRelativeSourcePath(sources, absolutePath) {
411
855
  .relative(sources.taskDir, absolutePath)
412
856
  .replaceAll(path.sep, "/");
413
857
  }
858
+ function extractExplicitRequirementIds(...markdownInputs) {
859
+ const ids = [];
860
+ const seen = new Set();
861
+ for (const markdown of markdownInputs) {
862
+ if (!markdown)
863
+ continue;
864
+ for (const match of markdown.matchAll(/\b(?:REQ|BR|AC)-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/gi)) {
865
+ const id = match[0].toUpperCase();
866
+ if (!seen.has(id)) {
867
+ seen.add(id);
868
+ ids.push(id);
869
+ }
870
+ }
871
+ }
872
+ return ids;
873
+ }
874
+ function buildDagSourceBinding(sources) {
875
+ const sourceEntries = [
876
+ { kind: "requirement", path: sources.requirementPath, markdown: sources.requirementMarkdown },
877
+ ...(sources.constraintMarkdown ? [{ kind: "constraint", path: sources.constraintPath, markdown: sources.constraintMarkdown }] : []),
878
+ ...(sources.referenceDocuments ?? []).map((reference) => ({ kind: "reference", path: reference.path, markdown: reference.markdown })),
879
+ ];
880
+ return {
881
+ schemaVersion: 1,
882
+ taskId: sources.taskId,
883
+ sources: sourceEntries.map((source) => ({
884
+ kind: source.kind,
885
+ path: toTaskRelativeSourcePath(sources, source.path),
886
+ sha256: createHash("sha256").update(source.markdown, "utf8").digest("hex"),
887
+ })),
888
+ requirementIds: extractExplicitRequirementIds(sources.requirementMarkdown, sources.constraintMarkdown, ...(sources.referenceDocuments ?? []).map((reference) => reference.markdown)),
889
+ };
890
+ }
414
891
  function buildSourceContextBlock(sources) {
415
892
  const requirementRef = toTaskRelativeSourcePath(sources, sources.requirementPath);
416
893
  const requirementExcerpt = excerptMarkdown(sources.requirementMarkdown, {
@@ -535,7 +1012,7 @@ export async function loadTaskHybridSources(repoRoot, taskId) {
535
1012
  catch (error) {
536
1013
  throw new Error(`failed to load verification commands for task "${taskId}": ${error instanceof Error ? error.message : String(error)}`);
537
1014
  }
538
- return {
1015
+ const sources = {
539
1016
  taskId,
540
1017
  repoRoot,
541
1018
  taskDir: paths.taskDir,
@@ -551,6 +1028,26 @@ export async function loadTaskHybridSources(repoRoot, taskId) {
551
1028
  verifyCommands,
552
1029
  sddEmbeddedSkills: await probeRepoLocalSddSkills(repoRoot),
553
1030
  };
1031
+ return sources;
1032
+ }
1033
+ async function prepareFrontendMockSources(sources) {
1034
+ const repoRoot = sources.repoRoot ?? process.cwd();
1035
+ const capability = await discoverFrontendMockCapability(repoRoot, sources.taskConfig);
1036
+ const sourceMockVerifyCommands = extractFrontendMockVerifyCommandsFromMarkdown({
1037
+ repoRoot,
1038
+ requirementMarkdown: sources.requirementMarkdown,
1039
+ constraintMarkdown: sources.constraintMarkdown,
1040
+ });
1041
+ for (const command of sourceMockVerifyCommands) {
1042
+ if (!capability.verifyCommands.some((existing) => existing.args.join("\0") === command.args.join("\0"))) {
1043
+ capability.verifyCommands.push(command);
1044
+ }
1045
+ }
1046
+ return {
1047
+ ...sources,
1048
+ frontendMockCapability: capability,
1049
+ frontendMockMode: resolveFrontendMockMode(capability, sources.taskConfig, hasApiDependency(sources)),
1050
+ };
554
1051
  }
555
1052
  function mergeFinalVerifyCommands(repoRoot, taskConfig, adapterCommands) {
556
1053
  const taskCommands = taskConfig.verifyCommands.map((command) => ({
@@ -758,12 +1255,282 @@ export function buildStandardHybridDagFromTask(sources) {
758
1255
  assertValidDagSpec(spec);
759
1256
  return spec;
760
1257
  }
1258
+ function buildFrontendMockAssessNode(sources, sourceContext, mockContextBlock, fixedVerificationContext, readOnlyPaths, forbiddenPaths) {
1259
+ return {
1260
+ id: "frontend-mock-assess-pi",
1261
+ depends_on: ["frontend-contract-pi", "frontend-scout-pi"],
1262
+ role: "planner",
1263
+ executor: "pi",
1264
+ complexity: "MED",
1265
+ writePolicy: "read-only",
1266
+ allowedPaths: readOnlyPaths,
1267
+ forbiddenPaths,
1268
+ skills: FRONTEND_IMPLEMENTATION_SKILLS,
1269
+ outputContract: "Plain Markdown whose first non-empty line is MOCK_STRATEGY: native|browser-intercept|request-adapter|not-needed|blocked, followed by Mock Decision, API Contract Evidence, Specification Evidence, Service Evidence, Backend Readiness, Selection Evidence, Endpoint / Fixture Matrix, Activation, Target Files, Production Safety, Verification Plan, Real Integration Gap, and Blocking Issues. No file writes.",
1270
+ subtask_prompt: [
1271
+ "Perform read-only Mock assessment and select one safe frontend data strategy.",
1272
+ "The first non-empty line must be exactly one of: MOCK_STRATEGY: native, MOCK_STRATEGY: browser-intercept, MOCK_STRATEGY: request-adapter, MOCK_STRATEGY: not-needed, or MOCK_STRATEGY: blocked.",
1273
+ "Prefer an existing native Mock facility. Use browser-intercept only with an existing browser/e2e harness. When no Mock exists but the API layer is writable, use request-adapter by adding a minimal reversible adapter/DI seam within the approved writeSet; the real adapter must remain the production default.",
1274
+ "Select not-needed only with positive evidence that no remote API is involved, a stable real backend will be exercised, or existing fixtures already cover the contract without changes. not-needed still requires the fixed behavior entrypoint to exercise applicable real or no-remote behavior verification. When configured policy is required, not-needed is forbidden.",
1275
+ "Configured policy disabled requests no Mock but cannot override project specifications; if an actually-read project rule requires Mock, select blocked.",
1276
+ "",
1277
+ "## Required Output Sections:",
1278
+ "- Mock Decision: required | not-required | blocked (with reasoning)",
1279
+ "- API Contract Evidence and Specification Evidence: actual Mock/API/schema specs read (paths + excerpts)",
1280
+ "- Service Evidence: detected Mock framework, service root, handler/fixture/bootstrap paths",
1281
+ "- Backend Readiness and Selection Evidence: why the selected mechanism is available and appropriate",
1282
+ "- Endpoint / Fixture Matrix: method/path, source, request, success, empty, error, permission, consumer, fixture/evidence",
1283
+ "- Activation and Target Files: explicit dev/test activation and authorized implementation paths",
1284
+ "- Production Safety: how Mock stays off and the real request remains default",
1285
+ "- Verification Plan: map the strategy to the fixed entrypoints below; do not propose replacement shell commands",
1286
+ "- Real Integration Gap: what remains unproved until the real backend is ready",
1287
+ "- Blocking Issues: any spec gaps, path violations, missing verify commands, or conflicts",
1288
+ "",
1289
+ "## Rules:",
1290
+ "- Read project Mock/API/schema specifications before making any judgment.",
1291
+ "- Do not infer Mock service from lockfile-only or transitive dependency evidence.",
1292
+ "- Output MOCK_STRATEGY: blocked if capability evidence conflicts, contract fields are missing/conflicting, paths or dependencies are unauthorized, specs were not actually read, sources conflict, production-default-off cannot be proven, the API layer is not writable for a new adapter, or the frozen entrypoints cannot verify the selected strategy.",
1293
+ "- Never comment out or replace the real request with inline data, hard-code Mock enablement, import test mocks from a production entrypoint, invent API fields, or place secrets/real user data in fixtures.",
1294
+ "- Mock-backed behavior evidence proves the documented frontend contract only; it never proves real API integration.",
1295
+ "",
1296
+ "Read-only: do not modify repository files.",
1297
+ fixedVerificationContext,
1298
+ sourceContext,
1299
+ mockContextBlock,
1300
+ ].join("\n\n"),
1301
+ };
1302
+ }
1303
+ function buildFrontendMockContractGateNode(mockMode, configuredPolicy, readOnlyPaths, forbiddenPaths) {
1304
+ // A generation-time blocked decision is a hard fail-closed contract. Keep a
1305
+ // syntactically valid, impossible verdict so the shell gate can never pass
1306
+ // regardless of what the assessment model emits.
1307
+ const acceptedStrategies = mockMode === "blocked"
1308
+ ? ["MOCK_STRATEGY: __blocked__"]
1309
+ : configuredPolicy === "disabled"
1310
+ ? ["MOCK_STRATEGY: not-needed"]
1311
+ : [
1312
+ "MOCK_STRATEGY: native",
1313
+ "MOCK_STRATEGY: browser-intercept",
1314
+ "MOCK_STRATEGY: request-adapter",
1315
+ ...(configuredPolicy !== "required"
1316
+ ? ["MOCK_STRATEGY: not-needed"]
1317
+ : []),
1318
+ ];
1319
+ return {
1320
+ id: "frontend-mock-contract-gate-shell",
1321
+ depends_on: ["frontend-mock-assess-pi"],
1322
+ role: "verifier",
1323
+ executor: "shell",
1324
+ complexity: "LOW",
1325
+ writePolicy: "read-only",
1326
+ allowedPaths: readOnlyPaths,
1327
+ forbiddenPaths,
1328
+ outputContract: "Deterministic Mock contract gate: exit 0 only when frontend-mock-assess-pi selects an allowed non-blocked strategy. Does not authorize code writes.",
1329
+ subtask_prompt: "Deterministic gate: block plan/design/implement when frontend-mock-assess-pi selected blocked, selected not-needed under explicit policy=required, or emitted malformed output. Failure route: ContractMismatch.",
1330
+ shell: {
1331
+ commands: [],
1332
+ verdictGate: {
1333
+ fromNodeId: "frontend-mock-assess-pi",
1334
+ accept: acceptedStrategies,
1335
+ label: "frontend mock contract",
1336
+ lineMode: "first-non-empty",
1337
+ },
1338
+ cwd: ".",
1339
+ timeoutMs: 60000,
1340
+ },
1341
+ };
1342
+ }
1343
+ function buildFrontendMockVerifyNode(sources, implementId, readOnlyPaths, forbiddenPaths) {
1344
+ const capability = sources.frontendMockCapability;
1345
+ const taskConfig = sources.taskConfig;
1346
+ // Collect verify commands from task config, capability seed, and manifest
1347
+ const verifyCommands = [];
1348
+ // 1. Task config commands (highest priority)
1349
+ for (const cmd of taskConfig.frontendMock?.verifyCommands ?? []) {
1350
+ verifyCommands.push({
1351
+ label: cmd.label,
1352
+ args: ["bash", "-lc", cmd.command],
1353
+ cwd: sources.repoRoot ?? ".",
1354
+ timeoutMs: cmd.timeoutMs,
1355
+ });
1356
+ }
1357
+ // 2. Capability seed commands (from discovery)
1358
+ if (capability) {
1359
+ for (const cmd of capability.verifyCommands) {
1360
+ if (!verifyCommands.some((existing) => existing.label === cmd.label)) {
1361
+ verifyCommands.push(cmd);
1362
+ }
1363
+ }
1364
+ }
1365
+ // Fail closed: no commands = no verify shell
1366
+ const commands = verifyCommands.length > 0
1367
+ ? buildVerifyShellCommands({
1368
+ repoRoot: sources.repoRoot ?? ".",
1369
+ commands: verifyCommands,
1370
+ fallbackCommands: [],
1371
+ })
1372
+ : [];
1373
+ return {
1374
+ id: "frontend-mock-verify-shell",
1375
+ depends_on: [implementId],
1376
+ role: "verifier",
1377
+ executor: "shell",
1378
+ complexity: "LOW",
1379
+ writePolicy: "read-only",
1380
+ allowedPaths: readOnlyPaths,
1381
+ forbiddenPaths,
1382
+ outputContract: "Archived shell stdout/stderr with exit codes for deterministic Mock-specific verification; no worktree writes.",
1383
+ subtask_prompt: "Run deterministic Mock-specific verification (handler loading, endpoint matrix, fixture consumption, production boundary). Commands are frozen from generation-time trusted sources only.",
1384
+ shell: {
1385
+ commands,
1386
+ verifyEvidence: buildVerifyEvidence({
1387
+ phase: "intermediate",
1388
+ quota: "full",
1389
+ commandSource: commands.length > 0 ? "inline" : "adapter",
1390
+ commands: verifyCommands.length > 0 ? verifyCommands : undefined,
1391
+ fallbackCommands: [],
1392
+ }),
1393
+ cwd: ".",
1394
+ timeoutMs: 300000,
1395
+ },
1396
+ };
1397
+ }
1398
+ function buildBlockedFrontendMockDag(sources, sourceContext, readOnlyPaths, forbiddenPaths, globalConstraints) {
1399
+ const { taskConfig } = sources;
1400
+ const mockContextBlock = resolveFrontendMockContextBlock(sources);
1401
+ const spec = {
1402
+ version: 3,
1403
+ title: `Frontend implementation DAG (BLOCKED Mock): ${taskConfig.title}`,
1404
+ runtimeContract: GENERATED_DAG_RUNTIME_CONTRACT,
1405
+ outputLanguage: sources.outputLanguage ?? DEFAULT_DAG_OUTPUT_LANGUAGE,
1406
+ objective: extractObjective(sources.requirementMarkdown, taskConfig.title),
1407
+ successCriteria: extractSuccessCriteria(sources.requirementMarkdown, sources.taskId),
1408
+ globalConstraints: [
1409
+ ...globalConstraints,
1410
+ "Mock contract is BLOCKED: writer nodes must not be reachable. Resolve blocking issues and re-generate DAG.",
1411
+ "Do not execute any write, verify, or closeout nodes. The DAG ends at the Mock contract gate.",
1412
+ ],
1413
+ defaults: {
1414
+ ...FRONTEND_DEFAULTS,
1415
+ contextProfile: taskConfig.contextProfile,
1416
+ },
1417
+ skillsByRole: FRONTEND_SKILLS_BY_ROLE,
1418
+ executorModels: sources.executorModelMatrix ?? DEFAULT_DAG_EXECUTOR_MODELS,
1419
+ tasks: [
1420
+ {
1421
+ id: "frontend-contract-pi",
1422
+ depends_on: [],
1423
+ role: "planner",
1424
+ executor: "pi",
1425
+ complexity: "MED",
1426
+ writePolicy: "read-only",
1427
+ allowedPaths: readOnlyPaths,
1428
+ forbiddenPaths,
1429
+ skills: FRONTEND_IMPLEMENTATION_SKILLS,
1430
+ outputContract: "Markdown contract with Scope, Non-goals, Acceptance Criteria, UI States, Target Runtime Environment, Risks, and Verification Expectations. No file writes.",
1431
+ subtask_prompt: [
1432
+ "Read task source and produce a concise frontend implementation contract.",
1433
+ "Cover scope, non-goals, acceptance criteria, UI states, target runtime environment, risks, and verification expectations.",
1434
+ "Read-only: do not modify code, docs, artifacts, or repository files.",
1435
+ sourceContext,
1436
+ ].join("\n\n"),
1437
+ },
1438
+ {
1439
+ id: "frontend-scout-pi",
1440
+ depends_on: ["frontend-contract-pi"],
1441
+ role: "scout",
1442
+ executor: "pi",
1443
+ complexity: mapTaskComplexity(taskConfig.complexity),
1444
+ writePolicy: "read-only",
1445
+ allowedPaths: readOnlyPaths,
1446
+ forbiddenPaths,
1447
+ skills: FRONTEND_IMPLEMENTATION_SKILLS,
1448
+ outputContract: "Markdown scout report covering frontend stack, routes, components, styling system, existing design conventions, state/data flow, test entry points, reuse opportunities, and risks. No file writes.",
1449
+ subtask_prompt: [
1450
+ "Inspect frontend code, routing, components, styles, package scripts, and tests.",
1451
+ "Return code and design observations, existing reuse opportunities, and verification entry points.",
1452
+ "Read-only: do not modify repository files.",
1453
+ sourceContext,
1454
+ ].join("\n\n"),
1455
+ },
1456
+ buildFrontendMockAssessNode(sources, sourceContext, mockContextBlock, "No verification entrypoints were materialized because the generation-time Mock contract is blocked.", readOnlyPaths, forbiddenPaths),
1457
+ buildFrontendMockContractGateNode("blocked", taskConfig.frontendMock?.policy ?? "auto", readOnlyPaths, forbiddenPaths),
1458
+ ],
1459
+ };
1460
+ applyDefaultReadOnlyRetryPolicy(spec);
1461
+ parseDagSpec(spec);
1462
+ assertValidDagSpec(spec);
1463
+ return spec;
1464
+ }
1465
+ function resolveFrontendMockContextBlock(sources) {
1466
+ const capability = sources.frontendMockCapability;
1467
+ const mode = sources.frontendMockMode ?? "not-required";
1468
+ if (!capability)
1469
+ return "";
1470
+ const parts = [
1471
+ "## Frontend Mock Context",
1472
+ "",
1473
+ `Configured Policy: ${sources.taskConfig.frontendMock?.policy ?? "auto"}`,
1474
+ `Mock Decision: ${mode}`,
1475
+ `Capability Status: ${capability.status}`,
1476
+ ];
1477
+ if (capability.framework) {
1478
+ parts.push(`Detected Framework: ${capability.framework}`);
1479
+ }
1480
+ if (capability.serviceRoot) {
1481
+ parts.push(`Service Root: ${capability.serviceRoot}`);
1482
+ }
1483
+ if (capability.evidencePaths.length > 0) {
1484
+ parts.push(`Evidence Paths: ${capability.evidencePaths.join(", ")}`);
1485
+ }
1486
+ if (capability.verifyCommands.length > 0) {
1487
+ parts.push(`Frozen Mock Verify Commands: ${capability.verifyCommands
1488
+ .map((command) => command.label)
1489
+ .join(", ")}`);
1490
+ }
1491
+ if (capability.safetyViolation) {
1492
+ parts.push(`Safety Violation: ${capability.safetyViolation}`);
1493
+ }
1494
+ if (capability.reasons.length > 0) {
1495
+ parts.push(`Reasons: ${capability.reasons.join("; ")}`);
1496
+ }
1497
+ if (mode === "required") {
1498
+ parts.push("Mock-backed frontend verification is required. Prefer the detected native service; otherwise the assessment may select an existing browser interception harness or reversible request adapter. Any handler, fixture, adapter, and UI changes stay in the single frontend-implement-pi writeSet.");
1499
+ }
1500
+ if (mode === "not-required") {
1501
+ parts.push("Generation-time evidence does not require Mock. The assessment must still use contract/scout evidence: select not-needed only positively, or select a safe Mock strategy if an API dependency is confirmed.");
1502
+ }
1503
+ if (mode === "blocked") {
1504
+ parts.push("Mock contract is blocked. The DAG must stop before any write node executes.");
1505
+ }
1506
+ return parts.join("\n");
1507
+ }
761
1508
  function buildFrontendHybridDagFromTask(sources) {
762
1509
  const { taskConfig } = sources;
1510
+ const mockCapability = sources.frontendMockCapability ?? {
1511
+ status: "absent",
1512
+ evidencePaths: [],
1513
+ verifyCommands: [],
1514
+ reasons: ["Frontend Mock capability was not precomputed; assessment must verify repository evidence."],
1515
+ };
1516
+ const mockMode = sources.frontendMockMode ??
1517
+ resolveFrontendMockMode(mockCapability, taskConfig, hasApiDependency(sources));
1518
+ const frontendSources = {
1519
+ ...sources,
1520
+ frontendMockCapability: mockCapability,
1521
+ frontendMockMode: mockMode,
1522
+ };
763
1523
  const forbiddenPaths = mergeForbiddenPaths(taskConfig);
764
1524
  const implementPaths = resolveImplementPaths(taskConfig);
765
1525
  const implementId = frontendImplementationNodeId();
766
1526
  const sourceContext = buildSourceContextBlock(sources);
1527
+ const mockContextBlock = resolveFrontendMockContextBlock(frontendSources);
1528
+ const hasMockVerifyCommands = (taskConfig.frontendMock?.verifyCommands.length ?? 0) > 0 ||
1529
+ mockCapability.verifyCommands.length > 0;
1530
+ const requirementIds = buildDagSourceBinding(sources).requirementIds;
1531
+ const requirementCoverageInstruction = requirementIds.length > 0
1532
+ ? `Include a Requirement Coverage section that lists every exact source identifier: ${requirementIds.join(", ")}. Preserve each identifier verbatim and map it to concrete implementation and verification steps.`
1533
+ : "";
767
1534
  const strategy = resolveDagVerifyStrategy(taskConfig);
768
1535
  const readOnlyPaths = taskConfig.allowedPaths.length > 0 ? taskConfig.allowedPaths : ["**"];
769
1536
  const behaviorPaths = deriveFrontendBehaviorPaths(taskConfig);
@@ -777,9 +1544,16 @@ function buildFrontendHybridDagFromTask(sources) {
777
1544
  "Final design gate pass is the only authorization for frontend implementation writes.",
778
1545
  "Plan revision remains read-only and never edits business code.",
779
1546
  "Design revision failures route to replan-and-rerun, never dev-fix.",
1547
+ "Frontend planning must consume the read-only Mock assessment strategy produced after scouting; MOCK_STRATEGY: blocked must not pass the deterministic Mock contract gate.",
1548
+ "Mock implementations must preserve the real request path as the default, require explicit test/dev activation, and never rely on commenting out the real request.",
1549
+ "Mock-backed behavior evidence proves only the documented frontend contract, never real API integration.",
780
1550
  "frontend-implementation DAGs must complete deterministic static verification and behavior verification before final review.",
781
1551
  "frontend review must block closeout unless review verdict is exactly VERDICT: pass.",
782
1552
  ];
1553
+ // Guard: blocked mode — generate assessment-only DAG with no writer reachable
1554
+ if (mockMode === "blocked") {
1555
+ return buildBlockedFrontendMockDag(frontendSources, sourceContext, readOnlyPaths, forbiddenPaths, globalConstraints);
1556
+ }
783
1557
  const staticFallbackCommands = ["npm run typecheck", "npm run build"];
784
1558
  const behaviorFallbackCommands = ["npm test"];
785
1559
  const parsedFrontendVerifyCommands = extractFrontendVerifyCommandsFromMarkdown({
@@ -795,6 +1569,39 @@ function buildFrontendHybridDagFromTask(sources) {
795
1569
  parsedCommands: parsedFrontendVerifyCommands.behaviorCommands,
796
1570
  adapterCommands: sources.verifyCommands?.final,
797
1571
  });
1572
+ const staticShellCommands = buildVerifyShellCommands({
1573
+ repoRoot: sources.repoRoot,
1574
+ commands: staticVerifyCommands.commands,
1575
+ fallbackCommands: staticFallbackCommands,
1576
+ });
1577
+ const behaviorShellCommands = buildVerifyShellCommands({
1578
+ repoRoot: sources.repoRoot,
1579
+ commands: behaviorVerifyCommands.commands,
1580
+ fallbackCommands: behaviorFallbackCommands,
1581
+ });
1582
+ const staticVerifyEvidence = buildVerifyEvidence({
1583
+ phase: "intermediate",
1584
+ quota: strategy.intermediateQuota ?? "full",
1585
+ commandSource: staticVerifyCommands.commandSource,
1586
+ commands: staticVerifyCommands.commands,
1587
+ fallbackCommands: staticFallbackCommands,
1588
+ });
1589
+ const behaviorVerifyEvidence = buildVerifyEvidence({
1590
+ phase: "final",
1591
+ quota: "full",
1592
+ commandSource: behaviorVerifyCommands.commandSource,
1593
+ commands: behaviorVerifyCommands.commands,
1594
+ fallbackCommands: behaviorFallbackCommands,
1595
+ finalFullRequired: true,
1596
+ });
1597
+ const fixedVerificationContext = [
1598
+ "## Fixed frontend verification entrypoints",
1599
+ "These shell entrypoints are fixed at DAG generation and are the only commands the static and behavior shell nodes execute. A strategy or plan may add tests behind an existing entrypoint inside writeSet, but must not invent or replace commands or assume subtask_prompt executes a command.",
1600
+ `- Static command source: ${staticVerifyEvidence.commandSource}`,
1601
+ ...staticVerifyEvidence.commandLabels.map((command) => ` - ${JSON.stringify(command)}`),
1602
+ `- Behavior command source: ${behaviorVerifyEvidence.commandSource}`,
1603
+ ...behaviorVerifyEvidence.commandLabels.map((command) => ` - ${JSON.stringify(command)}`),
1604
+ ].join("\n");
798
1605
  const spec = {
799
1606
  version: 3,
800
1607
  title: `Frontend implementation DAG: ${taskConfig.title}`,
@@ -846,9 +1653,17 @@ function buildFrontendHybridDagFromTask(sources) {
846
1653
  sourceContext,
847
1654
  ].join("\n\n"),
848
1655
  },
1656
+ // Mock assessment is always read-only and runs before planning.
1657
+ buildFrontendMockAssessNode(frontendSources, sourceContext, mockContextBlock, fixedVerificationContext, readOnlyPaths, forbiddenPaths),
1658
+ buildFrontendMockContractGateNode(mockMode, taskConfig.frontendMock?.policy ?? "auto", readOnlyPaths, forbiddenPaths),
849
1659
  {
850
1660
  id: "frontend-plan-pi",
851
- depends_on: ["frontend-scout-pi"],
1661
+ depends_on: [
1662
+ "frontend-contract-pi",
1663
+ "frontend-scout-pi",
1664
+ "frontend-mock-assess-pi",
1665
+ "frontend-mock-contract-gate-shell",
1666
+ ],
852
1667
  role: "planner",
853
1668
  executor: "pi",
854
1669
  complexity: "MED",
@@ -856,17 +1671,21 @@ function buildFrontendHybridDagFromTask(sources) {
856
1671
  allowedPaths: readOnlyPaths,
857
1672
  forbiddenPaths,
858
1673
  skills: FRONTEND_IMPLEMENTATION_SKILLS,
859
- outputContract: "Markdown implementation plan with Implementation Steps, Target Files, UI State Handling, Styling / Component Strategy, Interaction Notes, Dependency Policy, Verification Plan, and Residual Risks. No file writes.",
1674
+ outputContract: "Markdown implementation plan with Requirement Coverage, Implementation Steps, Target Files, UI State Handling, Styling / Component Strategy, Interaction Notes, Mock / API Strategy, Dependency Policy, Verification Plan, Real Integration Gap, and Residual Risks. No file writes.",
860
1675
  subtask_prompt: [
861
- "Based on frontend-contract-pi and frontend-scout-pi, return a minimal frontend implementation plan.",
862
- "Include ordered steps, target files, UI state handling, styling/component strategy, interaction notes, dependency policy, and deterministic verification commands.",
1676
+ "Based on frontend-contract-pi, frontend-scout-pi, and the gated frontend-mock-assess-pi strategy, return a minimal frontend implementation plan.",
1677
+ "Carry the selected Mock / API strategy, endpoint/fixture mapping, explicit activation, production-default-off rule, verification commands, and Real Integration Gap into the plan.",
1678
+ "Include ordered steps, target files, UI state handling, styling/component strategy, interaction notes, Mock/API strategy, dependency policy, deterministic verification entrypoints, and residual risks. Use only the fixed entrypoints below; implementation may add tests behind them but cannot replace them.",
1679
+ requirementCoverageInstruction,
863
1680
  "Read-only: do not modify code, docs, artifacts, or repository files.",
1681
+ fixedVerificationContext,
864
1682
  sourceContext,
1683
+ mockContextBlock,
865
1684
  ].join("\n\n"),
866
1685
  },
867
1686
  {
868
1687
  id: "frontend-design-gate-pi",
869
- depends_on: ["frontend-plan-pi"],
1688
+ depends_on: ["frontend-plan-pi", "frontend-mock-assess-pi"],
870
1689
  role: "reviewer",
871
1690
  executor: "pi",
872
1691
  complexity: "MED",
@@ -878,8 +1697,10 @@ function buildFrontendHybridDagFromTask(sources) {
878
1697
  subtask_prompt: [
879
1698
  "Audit the frontend plan before implementation.",
880
1699
  "First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
881
- "Request revision for missing applicable UI states, unsupported dependency additions, design-system drift without reason, weak interaction coverage, broad scope, or missing deterministic verification commands.",
1700
+ "Request revision when the Mock strategy is MOCK_STRATEGY: blocked, missing, unsupported by repository evidence, inconsistent with the API contract, outside authorized paths/dependencies, unable to prove production-default-off behavior with the fixed production/default-real-path static check, or missing deterministic behavior verification for the selected strategy. Mock strategies require Mock-backed evidence; not-needed requires applicable real or no-remote behavior evidence.",
1701
+ "Also request revision for missing applicable UI states, unsupported dependency additions, design-system drift without reason, weak interaction coverage, broad scope, inline fake data, schema drift, or missing deterministic verification commands.",
882
1702
  "Read-only: do not modify repository files.",
1703
+ fixedVerificationContext,
883
1704
  sourceContext,
884
1705
  ].join("\n\n"),
885
1706
  },
@@ -908,7 +1729,12 @@ function buildFrontendHybridDagFromTask(sources) {
908
1729
  },
909
1730
  {
910
1731
  id: "frontend-plan-revision-pi",
911
- depends_on: ["frontend-first-design-gate-shell", "frontend-plan-pi", "frontend-design-gate-pi"],
1732
+ depends_on: [
1733
+ "frontend-first-design-gate-shell",
1734
+ "frontend-plan-pi",
1735
+ "frontend-design-gate-pi",
1736
+ "frontend-mock-assess-pi",
1737
+ ],
912
1738
  role: "planner",
913
1739
  executor: "pi",
914
1740
  complexity: "MED",
@@ -916,22 +1742,49 @@ function buildFrontendHybridDagFromTask(sources) {
916
1742
  allowedPaths: readOnlyPaths,
917
1743
  forbiddenPaths,
918
1744
  skills: FRONTEND_IMPLEMENTATION_SKILLS,
919
- outputContract: "Markdown revision plan (pass case: first line PASS_NO_REVISION_NEEDED with confirmation of original plan; request-revision case: complete revised implementation plan with corrections from design findings). No file writes.",
1745
+ outputContract: "Markdown revision plan (pass case: first line PASS_NO_REVISION_NEEDED with Requirement Coverage confirmation; request-revision case: complete revised implementation plan with corrections from design findings and Requirement Coverage). No file writes.",
920
1746
  subtask_prompt: [
921
1747
  "Consume frontend-plan-pi (original plan) and frontend-design-gate-pi (first design review findings).",
922
1748
  "If the first design gate passed (VERDICT: pass from frontend-design-gate-pi), output exactly:",
923
1749
  "PASS_NO_REVISION_NEEDED",
924
1750
  "The original plan from frontend-plan-pi is confirmed and does not require changes.",
1751
+ "Then reproduce a complete Requirement Coverage section containing every explicit REQ-/BR-/AC- identifier from the authoritative task sources so this node is the single effective-plan evidence source for the deterministic coverage gate.",
925
1752
  "",
926
1753
  "If the first design gate requested revision (VERDICT: request-revision), produce a complete revised implementation plan that addresses every Required Plan Correction from the design findings.",
927
- "The revised plan must include Implementation Steps, Target Files, UI State Handling, Styling / Component Strategy, Interaction Notes, Dependency Policy, Verification Plan, and Residual Risks.",
1754
+ "The revised plan must include Requirement Coverage, Implementation Steps, Target Files, UI State Handling, Styling / Component Strategy, Interaction Notes, Mock / API Strategy, Dependency Policy, Verification Plan, Real Integration Gap, and Residual Risks.",
1755
+ requirementCoverageInstruction,
1756
+ "Do not turn MOCK_STRATEGY: blocked into an implementable strategy without new repository or contract evidence that resolves every blocker.",
928
1757
  "Read-only: do not modify code, docs, artifacts, or repository files. This node revises the plan only.",
929
1758
  sourceContext,
930
1759
  ].join("\n\n"),
931
1760
  },
1761
+ ...(requirementIds.length > 0 ? [{
1762
+ id: "frontend-requirement-coverage-shell",
1763
+ depends_on: ["frontend-plan-revision-pi"],
1764
+ role: "verifier",
1765
+ executor: "shell",
1766
+ complexity: "LOW",
1767
+ writePolicy: "read-only",
1768
+ allowedPaths: readOnlyPaths,
1769
+ forbiddenPaths,
1770
+ outputContract: "Deterministic current-run evidence that the original or revised frontend plan retains every explicit REQ-/BR-/AC- identifier from the bound task sources.",
1771
+ subtask_prompt: "Block final design review when the current run's plan facts omit any explicit requirement identifier from the authoritative task sources.",
1772
+ shell: {
1773
+ commands: [],
1774
+ requirementCoverageGate: { fromNodeIds: ["frontend-plan-revision-pi"], requiredIds: requirementIds, label: "frontend requirement coverage" },
1775
+ cwd: ".",
1776
+ timeoutMs: 60000,
1777
+ },
1778
+ }] : []),
932
1779
  {
933
1780
  id: "frontend-final-design-review-pi",
934
- depends_on: ["frontend-plan-revision-pi"],
1781
+ depends_on: [
1782
+ "frontend-plan-revision-pi",
1783
+ "frontend-plan-pi",
1784
+ "frontend-design-gate-pi",
1785
+ "frontend-mock-assess-pi",
1786
+ ...(requirementIds.length > 0 ? ["frontend-requirement-coverage-shell"] : []),
1787
+ ],
935
1788
  role: "reviewer",
936
1789
  executor: "pi",
937
1790
  complexity: "MED",
@@ -945,8 +1798,11 @@ function buildFrontendHybridDagFromTask(sources) {
945
1798
  "First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
946
1799
  "If frontend-plan-revision-pi returned PASS_NO_REVISION_NEEDED, confirm the original plan against all design constraints and task requirements. Re-verify that all applicable UI states are covered, dependencies are authorized, and deterministic verification commands are present.",
947
1800
  "If frontend-plan-revision-pi revised the plan, verify that every Required Plan Correction from the first design review has been fully addressed.",
1801
+ "Recheck the selected Mock / API strategy, contract-to-fixture mapping, authorized paths/dependencies, explicit activation, production-default-off behavior, behavior verification, and Real Integration Gap. MOCK_STRATEGY: blocked cannot receive VERDICT: pass.",
1802
+ "The frontend requirement coverage gate has verified that every explicit REQ-/BR-/AC- identifier remains present in the current-run plan evidence; review the mapped behavior rather than accepting identifier presence alone.",
948
1803
  "Request revision if any design gap remains, if corrections are incomplete, or if the revised plan introduces new unaddressed issues.",
949
1804
  "Read-only: do not modify repository files.",
1805
+ fixedVerificationContext,
950
1806
  sourceContext,
951
1807
  ].join("\n\n"),
952
1808
  },
@@ -975,13 +1831,17 @@ function buildFrontendHybridDagFromTask(sources) {
975
1831
  },
976
1832
  {
977
1833
  id: implementId,
978
- depends_on: ["frontend-final-design-gate-shell", "frontend-plan-revision-pi", "frontend-final-design-review-pi"],
1834
+ depends_on: [
1835
+ "frontend-final-design-gate-shell",
1836
+ "frontend-plan-revision-pi",
1837
+ "frontend-final-design-review-pi",
1838
+ "frontend-plan-pi",
1839
+ "frontend-mock-assess-pi",
1840
+ ],
979
1841
  role: "implementer",
980
1842
  executor: "pi",
981
1843
  toolProfile: "write",
982
- complexity: taskConfig.complexity === "large"
983
- ? "HIGH"
984
- : "MED",
1844
+ complexity: resolveWriterComplexity(taskConfig),
985
1845
  writePolicy: "exclusive",
986
1846
  writeSet: implementPaths.writeSet,
987
1847
  allowedPaths: implementPaths.allowedPaths,
@@ -990,14 +1850,25 @@ function buildFrontendHybridDagFromTask(sources) {
990
1850
  outputContract: "Markdown summary with Changed Files, Implemented Behavior, UI States Covered, Styling / Component Notes, Verification Attempted, and Residual Risks.",
991
1851
  subtask_prompt: [
992
1852
  "Implement the final approved frontend plan (from frontend-plan-revision-pi) with minimal focused changes.",
1853
+ "Implement only the approved Mock strategy from frontend-mock-assess-pi as carried through the approved plan. Preserve the real request path as the default, require explicit test/dev activation, and never comment out or replace the real request with inline data.",
993
1854
  "The frontend-final-design-review-pi verdict confirmed the plan is ready. Stay within writeSet and preserve unrelated files.",
1855
+ "For native, browser-intercept, or request-adapter, implement contract-aligned fixtures/states and a dev/test-only activation boundary in this same writer. For not-needed, do not add Mock files or a framework and state the positive reason.",
994
1856
  "Do not write root artifacts/** unless explicitly included in writeSet.",
1857
+ writerDeliveryContract(taskConfig),
995
1858
  sourceContext,
996
- ].join("\n\n"),
1859
+ mockContextBlock,
1860
+ ].filter((value) => Boolean(value)).join("\n\n"),
997
1861
  },
1862
+ // Optional dedicated Mock verification exists only when trusted commands
1863
+ // were frozen at generation time. Behavior verification remains required.
1864
+ ...(mockMode === "required" && hasMockVerifyCommands
1865
+ ? [buildFrontendMockVerifyNode(frontendSources, implementId, readOnlyPaths, forbiddenPaths)]
1866
+ : []),
998
1867
  {
999
1868
  id: "frontend-static-verify-shell",
1000
- depends_on: [implementId],
1869
+ depends_on: mockMode === "required" && hasMockVerifyCommands
1870
+ ? ["frontend-mock-verify-shell"]
1871
+ : [implementId],
1001
1872
  role: "verifier",
1002
1873
  executor: "shell",
1003
1874
  complexity: "LOW",
@@ -1005,20 +1876,10 @@ function buildFrontendHybridDagFromTask(sources) {
1005
1876
  allowedPaths: readOnlyPaths,
1006
1877
  forbiddenPaths,
1007
1878
  outputContract: "Archived shell stdout/stderr with exit codes for deterministic static verification; no worktree writes.",
1008
- subtask_prompt: "Run deterministic static verification for the frontend implementation.",
1879
+ subtask_prompt: "Run deterministic static verification for the frontend implementation, including a production/default-real-path build with Mock activation off when Mock applies. Report only what the commands actually exercise.",
1009
1880
  shell: {
1010
- commands: buildVerifyShellCommands({
1011
- repoRoot: sources.repoRoot,
1012
- commands: staticVerifyCommands.commands,
1013
- fallbackCommands: staticFallbackCommands,
1014
- }),
1015
- verifyEvidence: buildVerifyEvidence({
1016
- phase: "intermediate",
1017
- quota: strategy.intermediateQuota ?? "full",
1018
- commandSource: staticVerifyCommands.commandSource,
1019
- commands: staticVerifyCommands.commands,
1020
- fallbackCommands: staticFallbackCommands,
1021
- }),
1881
+ commands: staticShellCommands,
1882
+ verifyEvidence: staticVerifyEvidence,
1022
1883
  cwd: ".",
1023
1884
  timeoutMs: 300000,
1024
1885
  },
@@ -1033,28 +1894,29 @@ function buildFrontendHybridDagFromTask(sources) {
1033
1894
  allowedPaths: behaviorPaths,
1034
1895
  forbiddenPaths,
1035
1896
  outputContract: "Archived shell stdout/stderr with exit codes for deterministic behavior verification; no worktree writes.",
1036
- subtask_prompt: "Run deterministic behavior verification for frontend flows, states, and integration points.",
1897
+ subtask_prompt: "Run the fixed deterministic behavior entrypoints for the selected strategy. For native, browser-intercept, or request-adapter, cover the approved Mock activation and applicable success/loading/empty/error states; for not-needed, exercise applicable real or no-remote behavior. Report only what the commands actually exercise.",
1037
1898
  shell: {
1038
- commands: buildVerifyShellCommands({
1039
- repoRoot: sources.repoRoot,
1040
- commands: behaviorVerifyCommands.commands,
1041
- fallbackCommands: behaviorFallbackCommands,
1042
- }),
1043
- verifyEvidence: buildVerifyEvidence({
1044
- phase: "final",
1045
- quota: "full",
1046
- commandSource: behaviorVerifyCommands.commandSource,
1047
- commands: behaviorVerifyCommands.commands,
1048
- fallbackCommands: behaviorFallbackCommands,
1049
- finalFullRequired: true,
1050
- }),
1899
+ commands: behaviorShellCommands,
1900
+ verifyEvidence: behaviorVerifyEvidence,
1051
1901
  cwd: ".",
1052
1902
  timeoutMs: 300000,
1053
1903
  },
1054
1904
  },
1055
1905
  {
1056
1906
  id: "frontend-review-pi",
1057
- depends_on: ["frontend-behavior-verify-shell"],
1907
+ depends_on: [
1908
+ "frontend-static-verify-shell",
1909
+ "frontend-behavior-verify-shell",
1910
+ implementId,
1911
+ "frontend-contract-pi",
1912
+ "frontend-plan-pi",
1913
+ "frontend-plan-revision-pi",
1914
+ "frontend-final-design-review-pi",
1915
+ "frontend-mock-assess-pi",
1916
+ ...(mockMode === "required" && hasMockVerifyCommands
1917
+ ? ["frontend-mock-verify-shell"]
1918
+ : []),
1919
+ ],
1058
1920
  role: "reviewer",
1059
1921
  executor: "pi",
1060
1922
  complexity: "HIGH",
@@ -1067,8 +1929,13 @@ function buildFrontendHybridDagFromTask(sources) {
1067
1929
  "Review the frontend implementation and verification evidence.",
1068
1930
  "First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
1069
1931
  "Any Critical or Important finding must force VERDICT: request-revision.",
1932
+ "Use the direct contract, original plan, revision/no-op result, and final design review to reconstruct the approved plan and design verdict; do not infer them from the implementation summary.",
1933
+ "Treat a commented-out real request, default-enabled Mock, production entrypoint importing test mocks, API/fixture contract drift, unauthorized Mock dependency/path, or missing behavior evidence for the selected strategy as at least Important. Mock strategies require Mock-backed evidence; not-needed requires applicable real or no-remote behavior evidence. Verify that the real request remains the default when Mock activation is absent.",
1934
+ "Inspect the production/default-real-path static evidence directly and require Mock activation to be off for that check.",
1935
+ "Distinguish Mock-backed evidence from real API integration evidence and preserve the Real Integration Gap when the backend was not exercised.",
1070
1936
  "Review implementation quality, behavior/state coverage, verification evidence, and maintainability. Read-only: do not modify files.",
1071
1937
  sourceContext,
1938
+ mockContextBlock,
1072
1939
  ].join("\n\n"),
1073
1940
  },
1074
1941
  {
@@ -1096,7 +1963,16 @@ function buildFrontendHybridDagFromTask(sources) {
1096
1963
  },
1097
1964
  {
1098
1965
  id: "frontend-closeout-pi",
1099
- depends_on: ["frontend-review-gate-shell"],
1966
+ depends_on: [
1967
+ "frontend-review-gate-shell",
1968
+ "frontend-review-pi",
1969
+ "frontend-static-verify-shell",
1970
+ "frontend-behavior-verify-shell",
1971
+ "frontend-mock-assess-pi",
1972
+ ...(mockMode === "required" && hasMockVerifyCommands
1973
+ ? ["frontend-mock-verify-shell"]
1974
+ : []),
1975
+ ],
1100
1976
  role: "closeout",
1101
1977
  executor: "pi",
1102
1978
  complexity: "MED",
@@ -1106,11 +1982,13 @@ function buildFrontendHybridDagFromTask(sources) {
1106
1982
  : ["**", "docs/**"],
1107
1983
  forbiddenPaths,
1108
1984
  skills: FRONTEND_VERIFICATION_SKILLS,
1109
- outputContract: "Markdown closeout summary with Changes, Verification Evidence, Review Result, Known Risks, and Follow-up. No file writes.",
1985
+ outputContract: "Markdown closeout summary with Changes, Mock Decision / Strategy / Files / Verification / Production Boundary, Verification Evidence, Review Result, Frontend Status, Real Integration Status, Known Risks, and Follow-up. No file writes.",
1110
1986
  subtask_prompt: [
1111
- "Return a frontend closeout summary covering changes, verification evidence, review result, known risks, and follow-up.",
1987
+ "Return a frontend closeout summary covering Mock decision/strategy/files/verification/production boundary, changes, verification evidence, review result, known risks, and follow-up.",
1988
+ `When only Mock-backed evidence passed, state exactly Frontend status: mock-validated and Real integration: pending, summarize the Real Integration Gap, and name ${taskConfig.taskId}-real-api-integration-verify as the explicit follow-up task to create/run after backend readiness. This follow-up is not auto-created or auto-executed. Never describe Mock evidence as real API integration.`,
1112
1989
  "Read-only: do not modify code, docs, artifacts, or .harness/dag-runs/.",
1113
1990
  sourceContext,
1991
+ mockContextBlock,
1114
1992
  ].join("\n\n"),
1115
1993
  },
1116
1994
  ],
@@ -1133,53 +2011,48 @@ function buildAnalyzeInputsNode(sources) {
1133
2011
  writePolicy: "read-only",
1134
2012
  allowedPaths: commonReadOnlyPaths(sources),
1135
2013
  forbiddenPaths: commonForbiddenPaths(sources),
1136
- outputContract: "Structured Markdown extracting core content from source documents. No file writes.",
2014
+ outputContract: "Pure Backend Test Analysis v1 JSON object matching docs/templates/backend-test-analysis.schema.json. No Markdown prose and no file writes.",
1137
2015
  subtask_prompt: [
1138
- "Read the task source materials and extract the following structured content for downstream test generation.",
1139
- "",
1140
- "## Required Output Sections:",
1141
- "",
1142
- "### 1. API Endpoints",
1143
- "List all API endpoints: Method, Path, Description, Request params, Response format.",
1144
- "",
1145
- "### 2. Data Model",
1146
- "For each table/collection: fields, types, constraints, descriptions.",
1147
- "",
1148
- "### 3. Business Logic",
1149
- "Core business rules, validation rules, calculation formulas.",
1150
- "",
1151
- "### 4. State Transitions",
1152
- "State machines (e.g. order status: pending → paid → shipped → completed).",
1153
- "",
1154
- "### 5. Error Scenarios & Error Codes",
1155
- "All error codes, error messages, and when they occur.",
1156
- "",
1157
- "### 6. External Dependencies",
1158
- "Third-party services, databases, message queues. Include timeout settings if documented.",
1159
- "",
1160
- "### 7. Acceptance Criteria",
1161
- "Extract ALL acceptance criteria from 需求.md. Number them AC-001, AC-002, etc. If not explicitly listed, derive from functional requirements.",
1162
- "",
1163
- "### 8. Risk Areas",
1164
- "High-risk areas requiring extra test coverage.",
1165
- "",
1166
- "## Conditional Sections (include ONLY if mentioned in requirements):",
1167
- "- Authentication & Authorization: include ONLY if requirements mention auth mechanism (JWT, OAuth2, API Key, etc.)",
1168
- "- Timeout Handling: include ONLY if requirements mention timeout configuration or degradation strategy",
1169
- "- Concurrency & Idempotency: include ONLY if requirements mention concurrency, idempotency rules, or locking mechanisms",
1170
- "- State Transitions: include ONLY if requirements mention business state machines",
1171
- "- If not mentioned in requirements, do NOT include these sections",
1172
- "",
1173
- "This output will be used directly by downstream nodes. Be thorough and structured.",
2016
+ "Read the task source materials and return exactly one JSON object matching Backend Test Analysis v1.",
2017
+ "Do not wrap it in explanatory prose. A single fenced json block is tolerated, but pure JSON is preferred.",
2018
+ "Copy taskId, requirementPath, requirementSha256, referencePaths, and requirementIds exactly from the DAG source binding shown below.",
2019
+ "Preserve existing AC IDs. Do not invent endpoint methods, paths, fields, errors, boundaries, or business rules; record unknowns in evidenceGaps.",
2020
+ "Use empty arrays for categories not documented. Never include credentials, tokens, private keys, or secret values.",
2021
+ "Required top-level keys: schemaVersion, sourceBinding, acceptanceCriteria, endpoints, dataModels, businessRules, stateTransitions, boundaryConstraints, externalDependencies, risks, evidenceGaps.",
1174
2022
  "Read-only: do not modify code, docs, artifacts, or repository files.",
1175
2023
  buildSourceContextBlock(sources),
1176
2024
  ].join("\n\n"),
1177
2025
  };
1178
2026
  }
2027
+ function buildBackendTestAnalysisContractGateNode(sources) {
2028
+ return {
2029
+ id: "backend-test-analysis-contract-shell",
2030
+ depends_on: ["analyze-inputs-pi"],
2031
+ role: "verifier",
2032
+ executor: "shell",
2033
+ complexity: "LOW",
2034
+ writePolicy: "read-only",
2035
+ allowedPaths: commonReadOnlyPaths(sources),
2036
+ forbiddenPaths: commonForbiddenPaths(sources),
2037
+ outputContract: "Validated run-owned Backend Test Analysis v1 artifact pointer, schema ID, and SHA-256.",
2038
+ subtask_prompt: "Materialize and validate the backend-test analysis contract under the current DAG run.",
2039
+ shell: {
2040
+ commands: [],
2041
+ jsonArtifactGate: {
2042
+ fromNodeId: "analyze-inputs-pi",
2043
+ schemaId: "backend-test-analysis-v1",
2044
+ artifactName: "backend-test-analysis.json",
2045
+ outputDir: "contracts",
2046
+ },
2047
+ cwd: ".",
2048
+ timeoutMs: 60000,
2049
+ },
2050
+ };
2051
+ }
1179
2052
  function buildGenerateBackendFunctionalCasesNode(sources) {
1180
2053
  return {
1181
2054
  id: "generate-backend-functional-cases-pi",
1182
- depends_on: ["analyze-inputs-pi"],
2055
+ depends_on: ["backend-test-analysis-contract-shell"],
1183
2056
  role: "implementer",
1184
2057
  executor: "pi",
1185
2058
  toolProfile: "write",
@@ -1191,7 +2064,7 @@ function buildGenerateBackendFunctionalCasesNode(sources) {
1191
2064
  // 注意:Pi 节点超时由 executor 层控制(默认 30 分钟)
1192
2065
  // 如需调整,在 harness.json 的 executors.pi 中配置 modelConfig.timeoutMs
1193
2066
  subtask_prompt: [
1194
- "Based on the upstream analyze-inputs-pi output, generate structured backend functional test cases.",
2067
+ "Read the validated structured artifact pointer from backend-test-analysis-contract-shell and generate cases only from that JSON contract.", ,
1195
2068
  "",
1196
2069
  "## Output Steps (do in order):",
1197
2070
  "1. First, output a brief summary: how many modules, how many cases planned per module",
@@ -1206,9 +2079,9 @@ function buildGenerateBackendFunctionalCasesNode(sources) {
1206
2079
  "## Coverage Requirements:",
1207
2080
  "- Positive paths: happy path for each acceptance criterion",
1208
2081
  "- Negative paths: error scenarios (invalid input, not found, state violations)",
1209
- "- Boundary conditions: empty input, max length, edge values",
1210
2082
  "",
1211
2083
  "## Conditional Coverage (include ONLY if mentioned in upstream analysis):",
2084
+ "- Boundary conditions: include ONLY if upstream analyze-inputs-pi mentions value ranges, length limits, numeric bounds, or format constraints",
1212
2085
  "- State transitions: include ONLY if upstream analyze-inputs-pi mentions state machine",
1213
2086
  "- Authentication scenarios: include ONLY if upstream analyze-inputs-pi mentions auth mechanism",
1214
2087
  "- Timeout scenarios: include ONLY if upstream analyze-inputs-pi mentions timeout handling",
@@ -1217,7 +2090,7 @@ function buildGenerateBackendFunctionalCasesNode(sources) {
1217
2090
  "",
1218
2091
  "## Constraints:",
1219
2092
  "- Stay within writeSet: testcase/md/**",
1220
- "- Do NOT re-read source documents — use the upstream analyze-inputs-pi output only",
2093
+ "- Do NOT re-read source documents or fall back to free-form analysis — use the validated structured artifact only", ,
1221
2094
  "- Do not write root artifacts/**",
1222
2095
  ].join("\n\n"),
1223
2096
  };
@@ -1225,7 +2098,7 @@ function buildGenerateBackendFunctionalCasesNode(sources) {
1225
2098
  function buildReviewBackendCasesNode(sources) {
1226
2099
  return {
1227
2100
  id: "review-backend-cases-pi",
1228
- depends_on: ["generate-backend-functional-cases-pi"],
2101
+ depends_on: ["generate-backend-functional-cases-pi", "backend-test-analysis-contract-shell"],
1229
2102
  role: "reviewer",
1230
2103
  executor: "pi",
1231
2104
  complexity: "HIGH",
@@ -1243,12 +2116,12 @@ function buildReviewBackendCasesNode(sources) {
1243
2116
  "- ID format: every case uses BE-<MODULE>-<NNN>",
1244
2117
  "- Positive coverage: each acceptance criterion (AC-xxx) has happy-path case",
1245
2118
  "- Negative coverage: error scenarios (invalid input, not found, state violations)",
1246
- "- Boundary coverage: edge cases (empty, max length, edge values)",
1247
2119
  "- Traceability: each AC maps to at least one case ID",
1248
2120
  "- Case structure: ID, Title, Precondition, Steps, Expected Result",
1249
2121
  "- No duplicate IDs across files",
1250
2122
  "",
1251
2123
  "## Conditional Coverage (check ONLY if mentioned in upstream analysis):",
2124
+ "- Boundary coverage: check ONLY if analyze-inputs-pi mentions value ranges, length limits, numeric bounds, or format constraints",
1252
2125
  "- State transition coverage: check ONLY if analyze-inputs-pi mentions state machine",
1253
2126
  "- Authentication coverage: check ONLY if analyze-inputs-pi mentions auth mechanism",
1254
2127
  "- Timeout coverage: check ONLY if analyze-inputs-pi mentions timeout handling",
@@ -1266,7 +2139,7 @@ function buildReviewBackendCasesNode(sources) {
1266
2139
  "",
1267
2140
  "## Constraints:",
1268
2141
  "- Read-only: do not modify files",
1269
- "- Do NOT re-read source documents use upstream analyze-inputs-pi output for acceptance criteria",
2142
+ "- Read the validated backend-test analysis artifact pointer from upstream and use it for AC/source-binding coverage checks", ,
1270
2143
  "- Use testcase/md/ files for case review",
1271
2144
  ].join("\n\n"),
1272
2145
  };
@@ -1305,8 +2178,15 @@ function buildGenerateBackendPytestNode(sources) {
1305
2178
  toolProfile: "write",
1306
2179
  complexity: "HIGH",
1307
2180
  writePolicy: "exclusive",
1308
- writeSet: ["testcase/**/test_*.py"],
1309
- allowedPaths: ["testcase/**/test_*.py"],
2181
+ // test_*.py plus optional helpers/factories under testcase/ (not conftest/config)
2182
+ writeSet: [
2183
+ "testcase/**/test_*.py",
2184
+ "testcase/**/helpers/**",
2185
+ "testcase/**/factories/**",
2186
+ ],
2187
+ // Union task allowedPaths with testcase/** so writeSet stays in scope even when
2188
+ // task.json only lists product paths (e.g. ./src/**). Writes still gated by writeSet.
2189
+ allowedPaths: Array.from(new Set([...commonReadOnlyPaths(sources), "testcase/**"])),
1310
2190
  forbiddenPaths: commonForbiddenPaths(sources),
1311
2191
  // 注意:Pi 节点超时由 executor 层控制(默认 30 分钟)
1312
2192
  // 如需调整,在 harness.json 的 executors.pi 中配置 modelConfig.timeoutMs
@@ -1325,26 +2205,94 @@ function buildGenerateBackendPytestNode(sources) {
1325
2205
  "",
1326
2206
  "## Implementation Rules:",
1327
2207
  "- Use assert statements, not unittest assertions",
1328
- "- Assert specific values, not just 'no exception'",
1329
- "- Use @pytest.mark.parametrize for boundary cases",
2208
+ "- Use @pytest.mark.parametrize for boundary cases when the case defines edge values",
1330
2209
  "- Use markers: @pytest.mark.positive, @pytest.mark.negative, @pytest.mark.boundary",
1331
2210
  "",
2211
+ "## Test Data Preparation Rules (MUST follow):",
2212
+ "",
2213
+ "### When Setup is Needed",
2214
+ "Setup phase is REQUIRED only when test cases need pre-existing data:",
2215
+ "- Query/Read APIs: need data to exist before querying",
2216
+ "- Update/Delete APIs: need data to exist before modifying",
2217
+ "- State transition tests: need data in specific state",
2218
+ "",
2219
+ "Setup phase is NOT needed for:",
2220
+ "- Create APIs: testing the creation itself",
2221
+ "- Validation tests: testing input validation with invalid data",
2222
+ "",
2223
+ "### Data Setup Strategy",
2224
+ "When setup is needed:",
2225
+ "1. Prefer function-scoped fixtures for isolation; use module/session scope only when cases explicitly share immutable fixtures",
2226
+ "2. Prefer API-based setup from the upstream analyze-inputs-pi API list and reviewed cases",
2227
+ "3. If a required helper/factory is missing, create NEW files only under testcase/**/helpers/** or testcase/**/factories/**",
2228
+ "",
2229
+ "### Data Construction Priority",
2230
+ "1. API-first: construct data via documented APIs from analyze-inputs-pi / reviewed cases",
2231
+ "2. Reuse existing conftest fixtures when present (read-only)",
2232
+ "3. Direct DB writes are LAST RESORT and only if conftest already exposes a safe test DB fixture with rollback/isolation",
2233
+ "4. If neither API nor safe DB fixture exists, skip the case with an explicit gap note — do NOT invent production DB credentials or write live data",
2234
+ "",
2235
+ "### API Data Construction",
2236
+ "- Prefer the analyze-inputs-pi API Endpoints section and reviewed cases for method/path/fields",
2237
+ "- Chain API calls only when cases document multi-step preconditions",
2238
+ "- Store created resource IDs in fixtures for reuse",
2239
+ "- Do NOT broadly search host route/controller trees for secrets, .env, private keys, or production configs",
2240
+ "- Read host API definitions only when needed to resolve a field name already referenced by reviewed cases; stay out of credential/config paths",
2241
+ "",
2242
+ "### Database Data Construction (restricted)",
2243
+ "- Allowed only via existing conftest test-DB fixtures with transaction rollback or equivalent isolation",
2244
+ "- Never hardcode connection strings, passwords, tokens, or cloud credentials",
2245
+ "- Never target production/shared non-test databases",
2246
+ "- If isolation is unclear, report the gap instead of writing DB rows",
2247
+ "",
2248
+ "## Assertion Rules (MUST follow):",
2249
+ "",
2250
+ "### Positive Path",
2251
+ "MUST assert ALL of the following:",
2252
+ "1. HTTP status code: as defined in API spec (e.g. 200, 201)",
2253
+ "2. Response structure: key fields exist in response body",
2254
+ "3. Specific values: each field equals expected value from test case",
2255
+ "4. Data type: each field is correct type",
2256
+ "",
2257
+ "### Negative Path",
2258
+ "MUST assert ALL of the following:",
2259
+ "1. HTTP status code: as defined in API spec (e.g. 400, 404, 500)",
2260
+ "2. Error code field: field name from API spec (e.g. code, error_code, errcode, ret)",
2261
+ "3. Error message field: field name from API spec (e.g. message, msg, errmsg, error)",
2262
+ "",
2263
+ "### Field Name Resolution",
2264
+ "Field names MUST come from the upstream analyze-inputs-pi output (API Endpoints section) or reviewed cases, NOT guessed. For example:",
2265
+ "- If API spec defines {\"ret\": 0, \"msg\": \"success\"}, assert response.json()['ret'] and response.json()['msg']",
2266
+ "- If API spec defines {\"code\": 4001, \"message\": \"error\"}, assert response.json()['code'] and response.json()['message']",
2267
+ "",
1332
2268
  "## Conditional Implementation (include ONLY if test cases exist):",
1333
2269
  "- Authentication tests: implement ONLY if testcase/md/ contains auth-related cases",
1334
2270
  "- Timeout tests: implement ONLY if testcase/md/ contains timeout-related cases",
2271
+ "- Boundary tests: implement ONLY when cases define value ranges, length limits, or format constraints",
1335
2272
  "- Use @pytest.mark.auth for auth tests, @pytest.mark.timeout for timeout tests",
1336
2273
  "- If no such cases exist, do NOT add these tests",
1337
2274
  "",
1338
2275
  "## Constraints:",
1339
- "- Only create NEW files, do NOT modify existing framework files (conftest.py, pytest.ini, pyproject.toml)",
1340
- "- If filename exists, add suffix: test_order.py test_order_01.py",
1341
- "- Stay within writeSet: testcase/**/test_*.py",
1342
- "- Do NOT re-read source documents — use the reviewed cases under testcase/md/ only",
2276
+ "- Only create NEW files under writeSet: testcase/**/test_*.py, testcase/**/helpers/**, testcase/**/factories/**",
2277
+ "- Do NOT modify existing framework files (conftest.py, pytest.ini, pyproject.toml, setup.cfg, __init__.py)",
2278
+ "- If a test filename exists, add suffix: test_order.py → test_order_01.py",
2279
+ "- Do NOT re-read source documents — use reviewed cases under testcase/md/ and upstream analyze-inputs-pi output only",
1343
2280
  "- Read existing conftest.py/pytest.ini to understand conventions, but do NOT modify them",
1344
2281
  ].join("\n\n"),
1345
2282
  };
1346
2283
  }
1347
2284
  function buildExecuteBackendPytestNode(sources) {
2285
+ // Keep the target worktree read-only: JUnit is runner-owned evidence under
2286
+ // the current DAG run and moves with active → completed/paused lifecycle.
2287
+ const pytestCommand = [
2288
+ 'test -n "${HARNESS_DAG_RUN_DIR:-}" || { echo "missing HARNESS_DAG_RUN_DIR for backend pytest report" >&2; exit 2; }',
2289
+ 'REPORT="${HARNESS_DAG_RUN_DIR}/reports/backend-test-junit.xml"',
2290
+ 'mkdir -p "$(dirname "${REPORT}")"',
2291
+ 'PYTHONDONTWRITEBYTECODE=1 python -m pytest testcase/ -v -p no:cacheprovider --junitxml="${REPORT}"',
2292
+ 'STATUS=$?',
2293
+ 'printf "JUnit report: %s\\n" "${REPORT}"',
2294
+ 'exit "${STATUS}"',
2295
+ ].join("; ");
1348
2296
  return {
1349
2297
  id: "execute-backend-pytest-shell",
1350
2298
  depends_on: ["generate-backend-pytest-pi"],
@@ -1354,19 +2302,15 @@ function buildExecuteBackendPytestNode(sources) {
1354
2302
  writePolicy: "read-only",
1355
2303
  allowedPaths: commonReadOnlyPaths(sources),
1356
2304
  forbiddenPaths: commonForbiddenPaths(sources),
1357
- outputContract: "Archived pytest stdout/stderr with exit codes and HTML report path; no source or test file modifications.",
1358
- subtask_prompt: "Run pytest for the backend test suite and capture results.",
2305
+ outputContract: "Archived pytest stdout/stderr with exit codes; JUnit XML is runner-owned evidence at $HARNESS_DAG_RUN_DIR/reports/backend-test-junit.xml. Must not modify worktree files, testcase sources, production code, or assertions.",
2306
+ subtask_prompt: "Run pytest for the backend test suite; write JUnit evidence only under the current HARNESS_DAG_RUN_DIR/reports/.",
1359
2307
  shell: {
1360
- commands: [
1361
- "python -m pytest testcase/ --html=reports/backend-test-report.html -v",
1362
- ],
2308
+ commands: [pytestCommand],
1363
2309
  verifyEvidence: buildVerifyEvidence({
1364
2310
  phase: "final",
1365
2311
  quota: "full",
1366
2312
  commandSource: "inline",
1367
- fallbackCommands: [
1368
- "python -m pytest testcase/ --html=reports/backend-test-report.html -v",
1369
- ],
2313
+ fallbackCommands: [pytestCommand],
1370
2314
  finalFullRequired: true,
1371
2315
  }),
1372
2316
  cwd: ".",
@@ -1438,9 +2382,9 @@ function buildBackendTestHybridDag(sources) {
1438
2382
  ...STANDARD_GLOBAL_CONSTRAINTS,
1439
2383
  "backend-test-dag nodes must maintain traceability from requirements to functional cases to pytest automation.",
1440
2384
  "Functional test case IDs must use BE-<MODULE>-<NNN> format.",
1441
- "pytest execution must produce HTML reports under reports/.",
2385
+ "pytest execution must keep the target worktree read-only and write machine-readable results only under the current HARNESS_DAG_RUN_DIR/reports/** (e.g. JUnit XML).",
1442
2386
  "pytest automation scripts must use test_ filename prefix for pytest discovery.",
1443
- "generate-backend-pytest-pi must only create new test files under testcase/; modifying existing framework files (conftest.py, pytest.ini, pyproject.toml) is forbidden.",
2387
+ "generate-backend-pytest-pi may create only new files under testcase/**/test_*.py, testcase/**/helpers/**, and testcase/**/factories/**; modifying conftest.py, pytest.ini, pyproject.toml, or production code is forbidden.",
1444
2388
  "review-backend-cases-gate-shell must block pytest generation unless the review verdict is exactly VERDICT: pass.",
1445
2389
  "If a target test filename already exists under testcase/, add a numeric suffix (_01, _02, ...); never overwrite or append to existing files.",
1446
2390
  "execute-backend-pytest-shell must not modify test assertions or production code to make tests pass; test failures indicate potential implementation issues and must be reported honestly.",
@@ -1453,18 +2397,9 @@ function buildBackendTestHybridDag(sources) {
1453
2397
  objective: extractObjective(sources.requirementMarkdown, taskConfig.title),
1454
2398
  successCriteria: extractSuccessCriteria(sources.requirementMarkdown, sources.taskId),
1455
2399
  globalConstraints,
1456
- convergence: {
1457
- enabled: true,
1458
- maxPasses: 3,
1459
- stopOnVerdictPass: true,
1460
- stopOnHardVerifyPass: true,
1461
- pauseOnRegression: true,
1462
- chainNodeIds: [
1463
- "generate-backend-functional-cases-pi",
1464
- "review-backend-cases-pi",
1465
- "review-backend-cases-gate-shell",
1466
- ],
1467
- },
2400
+ // No convergence loop: review gate is fail-closed. request-revision stops
2401
+ // the DAG; regenerate after fixing cases. Controller still keys off
2402
+ // hard-verify-shell, which this template does not include.
1468
2403
  defaults: {
1469
2404
  ...BACKEND_TEST_DEFAULTS,
1470
2405
  contextProfile: taskConfig.contextProfile,
@@ -1473,6 +2408,7 @@ function buildBackendTestHybridDag(sources) {
1473
2408
  executorModels: sources.executorModelMatrix ?? DEFAULT_DAG_EXECUTOR_MODELS,
1474
2409
  tasks: [
1475
2410
  buildAnalyzeInputsNode(sources),
2411
+ buildBackendTestAnalysisContractGateNode(sources),
1476
2412
  buildGenerateBackendFunctionalCasesNode(sources),
1477
2413
  buildReviewBackendCasesNode(sources),
1478
2414
  buildReviewBackendCasesGateNode(sources),
@@ -1487,6 +2423,104 @@ function buildBackendTestHybridDag(sources) {
1487
2423
  return spec;
1488
2424
  }
1489
2425
  // ---------------------------------------------------------------------------
2426
+ // Frontend browser-test RAG DAG template
2427
+ // ---------------------------------------------------------------------------
2428
+ function buildFrontendTestHybridDag(sources) {
2429
+ const config = sources.taskConfig.frontendTest ?? { maxCasesPerBatch: 20 };
2430
+ const hasFrontendTestWriteScope = sources.taskConfig.allowedPaths.some((pattern) => pattern === "testcase/frontend/**" || pattern === "testcase/**" || pattern === "**");
2431
+ const hasReportWriteScope = sources.taskConfig.allowedPaths.some((pattern) => pattern === "docs/test-reports/**" || pattern === "docs/**" || pattern === "**");
2432
+ if (!hasFrontendTestWriteScope || !hasReportWriteScope) {
2433
+ throw new Error('frontend-test requires task.json allowedPaths to include both "testcase/frontend/**" and "docs/test-reports/**" (or explicit containing globs).');
2434
+ }
2435
+ const forbidden = commonForbiddenPaths(sources);
2436
+ const ragWriteSet = ["testcase/frontend/rag/**"];
2437
+ const casesWriteSet = ["testcase/frontend/cases/**"];
2438
+ const evidenceRoot = "testcase/frontend/evidence";
2439
+ const manifestValidation = [
2440
+ "node -e",
2441
+ JSON.stringify([
2442
+ "const fs=require('fs'),path=require('path');",
2443
+ "const file='testcase/frontend/cases/manifest.json'; if(!fs.existsSync(file)) throw new Error('missing '+file);",
2444
+ "const manifest=JSON.parse(fs.readFileSync(file,'utf8')); if(manifest.schemaVersion!==1||!Array.isArray(manifest.cases)) throw new Error('invalid frontend case manifest');",
2445
+ "const dims=new Set(['core','boundary','flow','backend']);",
2446
+ "const seen=new Set(); const seenCasePath=new Set(); const seenEvidenceDir=new Set();",
2447
+ "for(const c of manifest.cases){",
2448
+ " if(!c||typeof c.caseId!=='string'||!/^FE-[A-Za-z0-9][A-Za-z0-9-]*$/.test(c.caseId)||seen.has(c.caseId)) throw new Error('invalid or duplicate caseId');",
2449
+ " seen.add(c.caseId);",
2450
+ " if(typeof c.dimension!=='string'||!dims.has(c.dimension)) throw new Error('invalid dimension');",
2451
+ " if(!Array.isArray(c.acIds)||c.acIds.length===0||c.acIds.some(a=>typeof a!=='string'||!a.trim())) throw new Error('invalid acIds');",
2452
+ " for(const k of ['casePath','evidenceDir']){ const v=c[k]; if(typeof v!=='string'||path.isAbsolute(v)||v.includes('..')) throw new Error('unsafe '+k); }",
2453
+ " if(c.casePath!=='testcase/frontend/cases/'+c.caseId+'.md') throw new Error('casePath must match caseId');",
2454
+ " if(!c.evidenceDir.startsWith('testcase/frontend/evidence/'+c.caseId+'/')) throw new Error('case path escapes frontend test roots');",
2455
+ " if(seenCasePath.has(c.casePath)) throw new Error('duplicate casePath'); seenCasePath.add(c.casePath);",
2456
+ " if(seenEvidenceDir.has(c.evidenceDir)) throw new Error('duplicate evidenceDir'); seenEvidenceDir.add(c.evidenceDir);",
2457
+ "}",
2458
+ "process.stdout.write(JSON.stringify({cases:manifest.cases}));",
2459
+ ].join("")),
2460
+ ].join(" ");
2461
+ const spec = {
2462
+ version: 3,
2463
+ title: `Frontend test DAG: ${sources.taskConfig.title}`,
2464
+ runtimeContract: GENERATED_DAG_RUNTIME_CONTRACT,
2465
+ outputLanguage: sources.outputLanguage ?? DEFAULT_DAG_OUTPUT_LANGUAGE,
2466
+ objective: extractObjective(sources.requirementMarkdown, sources.taskConfig.title),
2467
+ successCriteria: extractSuccessCriteria(sources.requirementMarkdown, sources.taskId),
2468
+ globalConstraints: [
2469
+ ...sources.taskConfig.hardConstraints,
2470
+ ...STANDARD_GLOBAL_CONSTRAINTS,
2471
+ "frontend-test-dag generates Markdown cases and browser evidence only; it must not generate pytest or Playwright test source code.",
2472
+ "Each browser case runs serially in a fresh Pi execution boundary. Persist its evidence before starting the next case.",
2473
+ "Use only the declared isolated test environment. Production URLs, real credentials, and unauthorized data are blocked.",
2474
+ "Browser startup for generated cases must be playwright-cli open --browser=chrome --headed <base-url>.",
2475
+ "Token settings are post-case stop thresholds, never a hard provider token cap. Unstarted cases after a threshold are blocked: token-budget-exhausted.",
2476
+ ],
2477
+ defaults: { ...HYBRID_DEFAULTS, writePolicy: "read-only", contextProfile: sources.taskConfig.contextProfile },
2478
+ skillsByRole: {
2479
+ planner: ["loop-agent"], scout: ["playwright-cli"], implementer: ["playwright-cli-case-generator", "playwright-cli", "webapp-testing"], reviewer: ["requesting-code-review"], verifier: ["playwright-cli", "webapp-testing"], closeout: ["loop-agent", "verification-before-completion"],
2480
+ },
2481
+ executorModels: sources.executorModelMatrix ?? DEFAULT_DAG_EXECUTOR_MODELS,
2482
+ tasks: [
2483
+ {
2484
+ id: "retrieve-frontend-test-context-pi", depends_on: [], role: "planner", executor: "pi", toolProfile: "write", complexity: "HIGH", writePolicy: "exclusive", writeSet: ragWriteSet, allowedPaths: [...commonReadOnlyPaths(sources), ...ragWriteSet], forbiddenPaths: forbidden,
2485
+ outputContract: "Write testcase/frontend/rag/context.md and coverage-map.md with traceable UI/API/test-environment facts.",
2486
+ subtask_prompt: ["Build the frontend test RAG package.", "Read task source, relevant routes/components/API or Mock facts, existing tests, and execution contract. Write only testcase/frontend/rag/context.md and coverage-map.md.", "Record AC IDs, source paths, routes, states, roles, fixture/data prerequisites, API mapping status, risks, and isolated execution contract. Do not guess unavailable facts.", buildSourceContextBlock(sources)].join("\n\n"),
2487
+ },
2488
+ {
2489
+ id: "generate-frontend-functional-cases-pi", depends_on: ["retrieve-frontend-test-context-pi"], role: "implementer", executor: "pi", toolProfile: "write", complexity: "HIGH", writePolicy: "exclusive", writeSet: casesWriteSet, allowedPaths: [...ragWriteSet, ...casesWriteSet], forbiddenPaths: forbidden,
2490
+ outputContract: "Write executable Markdown frontend cases, index.md, and manifest.json schemaVersion 1; no test source code.",
2491
+ subtask_prompt: ["Use skill playwright-cli-case-generator.", "Read only testcase/frontend/rag/context.md, testcase/frontend/rag/coverage-map.md, and existing testcase/frontend/cases/. Write only testcase/frontend/cases/**.", "Generate Markdown cases, index.md and manifest.json (schemaVersion 1; cases[] with caseId, casePath, dimension, acIds, evidenceDir). IDs use FE-<FEATURE>-<NNN>-<dimension>; dimensions core|boundary|flow|backend.", "Never infer API fields, constraints, SLA, credentials, or unrecorded test data. Do not create pytest or Playwright source. Every browser start command is: playwright-cli open --browser=chrome --headed <base-url>.", "Each case must be independent, declare its session/preconditions/data cleanup, UI assertions, evidence paths under testcase/frontend/evidence/<case-id>/, and mark unsafe/missing dependencies blocked."].join("\n\n"),
2492
+ },
2493
+ {
2494
+ id: "review-frontend-cases-pi", depends_on: ["generate-frontend-functional-cases-pi"], role: "reviewer", executor: "pi", complexity: "HIGH", writePolicy: "read-only", allowedPaths: [...ragWriteSet, ...casesWriteSet], forbiddenPaths: forbidden,
2495
+ outputContract: "First line VERDICT: pass or VERDICT: request-revision, followed by AC-to-case coverage and execution risk findings; no writes.", subtask_prompt: "Review only the RAG package and frontend Markdown cases. Verify traceability, independent execution, safe data/environment handling, manifest correctness, and evidence requirements. The verdict is advisory and does not block case execution.",
2496
+ },
2497
+ {
2498
+ id: "materialize-frontend-case-manifest-shell", depends_on: ["review-frontend-cases-pi"], role: "verifier", executor: "shell", complexity: "LOW", writePolicy: "read-only", allowedPaths: casesWriteSet, forbiddenPaths: forbidden,
2499
+ outputContract: "stdout is exactly JSON { cases: [...] } after deterministic frontend manifest validation.", subtask_prompt: "Validate and materialize the generated frontend case manifest.", shell: { commands: [manifestValidation], cwd: ".", timeoutMs: 120000 },
2500
+ },
2501
+ {
2502
+ id: "execute-frontend-cases-map", depends_on: ["materialize-frontend-case-manifest-shell"], role: "verifier", executor: "static", complexity: "LOW", writePolicy: "none", allowedPaths: [], forbiddenPaths: forbidden,
2503
+ outputContract: "Serial aggregate of case execution summaries, evidence paths, tokens, and token-budget blocked cases.", subtask_prompt: "Expand and execute the validated frontend case manifest serially.", static: { resultMarkdown: "Frontend case map expansion barrier." },
2504
+ dynamicExpansion: { type: "map_agent", workflowNodeId: "execute-frontend-cases-map", itemsFrom: "$.nodes['materialize-frontend-case-manifest-shell'].output.cases", itemName: "case", maxItems: config.maxCasesPerBatch, maxExpandedNodes: config.maxCasesPerBatch, childIdPrefix: "execute-frontend-case", tokenBudget: { maxTokensPerCase: config.maxTokensPerCase, maxTotalTokens: config.maxTotalTokens }, childTask: {
2505
+ executor: "pi", role: "verifier", skills: ["playwright-cli", "webapp-testing"], toolProfile: "write", complexity: "MED", writePolicy: "exclusive", allowedPaths: ["testcase/frontend/cases/{{case.caseId}}.md", "testcase/frontend/rag/context.md", "testcase/frontend/rag/coverage-map.md", `${evidenceRoot}/{{case.caseId}}/**`], forbiddenPaths: forbidden, writeSet: [`${evidenceRoot}/{{case.caseId}}/**`], outputContract: "Compact JSON <=1200 characters with case status, evidence paths, error summary, and tokens.", subtaskPromptTemplate: ["Execute exactly case {{case.caseId}} from {{case.casePath}} using playwright-cli and webapp-testing. This is a fresh Pi session; do not use /new.", "Use only the declared isolated test environment. If CLI/browser/base URL/credentials/fixture isolation is missing, record blocked rather than installing tools or guessing.", "Use playwright-cli open --browser=chrome --headed <base-url>. Persist execution.md, case-result.json, screenshots/trace/video/logs under {{case.evidenceDir}} before returning.", "A business failed or blocked case is a recorded result, not a node failure. Close the session and return only compact JSON (<=1200 chars): {caseId,status,evidencePaths,errorSummary,tokens}."].join("\n\n")
2506
+ } },
2507
+ },
2508
+ {
2509
+ id: "review-frontend-execution-pi", depends_on: ["execute-frontend-cases-map"], role: "reviewer", executor: "pi", complexity: "HIGH", writePolicy: "read-only", allowedPaths: ["testcase/frontend/**"], forbiddenPaths: forbidden,
2510
+ outputContract: "Read-only AC-to-case-to-browser-evidence review, including failed, blocked and token-budget-exhausted cases.", subtask_prompt: "Review the frontend case aggregate and on-disk case/evidence artifacts. A passed case requires assertion plus screenshot or equivalent browser evidence; failed/blocked cases require reasons. Do not replace browser evidence with model conclusions.",
2511
+ },
2512
+ {
2513
+ id: "frontend-test-retrospect-pi", depends_on: ["review-frontend-execution-pi"], role: "closeout", executor: "pi", toolProfile: "write", complexity: "MED", writePolicy: "exclusive", writeSet: ["docs/test-reports/**"], allowedPaths: ["testcase/frontend/**", "docs/test-reports/**"], forbiddenPaths: forbidden,
2514
+ outputContract: "Write frontend-test-retrospect-<date>.md with coverage, pass/fail/blocked, risks, findings, and A/B/C/D rating.", subtask_prompt: "Write the frontend test retrospective under docs/test-reports/. Summarize coverage, passed/failed/blocked cases (including token-budget-exhausted), review findings, browser anomalies, residual risks, and A/B/C/D rating. Blocked cases never count as passed.",
2515
+ },
2516
+ ],
2517
+ };
2518
+ applyDefaultReadOnlyRetryPolicy(spec);
2519
+ parseDagSpec(spec);
2520
+ assertValidDagSpec(spec);
2521
+ return spec;
2522
+ }
2523
+ // ---------------------------------------------------------------------------
1490
2524
  // Knowledge-sync DAG template
1491
2525
  // ---------------------------------------------------------------------------
1492
2526
  const KNOWLEDGE_SYNC_DEFAULTS = {
@@ -1501,6 +2535,104 @@ const KNOWLEDGE_SYNC_SKILLS_BY_ROLE = {
1501
2535
  verifier: ["verification-before-completion", "systematic-debugging"],
1502
2536
  closeout: ["loop-agent", "verification-before-completion"],
1503
2537
  };
2538
+ /**
2539
+ * Deterministic aggregate gate: all listed review nodes must emit VERDICT: pass
2540
+ * (first VERDICT: line in assistantText/stdout). Uses HARNESS_DAG_RUN_DIR JSON artifacts.
2541
+ */
2542
+ export function buildMultiPerspectiveReviewAggregateScript(fromNodeIds, label) {
2543
+ if (fromNodeIds.length === 0) {
2544
+ throw new Error("multi-perspective aggregate requires at least one review node id");
2545
+ }
2546
+ const idsLiteral = JSON.stringify([...fromNodeIds]);
2547
+ const labelLiteral = JSON.stringify(label);
2548
+ return [
2549
+ "node",
2550
+ "-e",
2551
+ JSON.stringify([
2552
+ "const fs=require('fs');",
2553
+ "const path=require('path');",
2554
+ `const ids=${idsLiteral};`,
2555
+ `const label=${labelLiteral};`,
2556
+ "const runDir=process.env.HARNESS_DAG_RUN_DIR;",
2557
+ "if(!runDir){ console.error(label+': missing HARNESS_DAG_RUN_DIR'); process.exit(1); }",
2558
+ "function normalize(line){",
2559
+ " const t=String(line).trim();",
2560
+ " const m=t.match(/^\\*{1,3}\\s*(VERDICT:[^*]+?)\\s*\\*{1,3}$/);",
2561
+ " return (m?m[1]:t).trim();",
2562
+ "}",
2563
+ "function firstVerdict(text){",
2564
+ " for (const line of String(text||'').split(/\\r?\\n/)) {",
2565
+ " const n=normalize(line);",
2566
+ " if(/^VERDICT:/.test(n)) return n;",
2567
+ " }",
2568
+ " return '';",
2569
+ "}",
2570
+ "const failures=[];",
2571
+ "for (const id of ids) {",
2572
+ " const file=path.join(runDir, id+'.json');",
2573
+ " if(!fs.existsSync(file)){ failures.push(id+': missing JSON '+file); continue; }",
2574
+ " let raw; try { raw=JSON.parse(fs.readFileSync(file,'utf8')); } catch(e){ failures.push(id+': invalid JSON'); continue; }",
2575
+ " const verdict=firstVerdict(raw.assistantText ?? raw.stdout ?? '');",
2576
+ " if(verdict!=='VERDICT: pass') failures.push(id+': '+(verdict||'missing VERDICT line'));",
2577
+ " else console.log(id+': VERDICT: pass');",
2578
+ "}",
2579
+ "if(failures.length){ console.error(label+' blocked:\\n'+failures.join('\\n')); process.exit(1); }",
2580
+ "console.log(label+': all perspectives VERDICT: pass ('+ids.length+')');",
2581
+ ].join("")),
2582
+ ].join(" ");
2583
+ }
2584
+ function buildMultiPerspectiveReviewNodes(input) {
2585
+ const reviewNodes = input.perspectives.map((p) => ({
2586
+ id: `${input.nodePrefix}${p.id}-pi`,
2587
+ depends_on: [...input.dependsOn],
2588
+ role: "reviewer",
2589
+ executor: "pi",
2590
+ complexity: "HIGH",
2591
+ writePolicy: "read-only",
2592
+ allowedPaths: input.allowedPaths,
2593
+ forbiddenPaths: commonForbiddenPaths(input.sources),
2594
+ outputContract: `Plain Markdown; first non-empty line is VERDICT: pass or VERDICT: request-revision. Perspective: ${p.perspective}. No file writes.`,
2595
+ subtask_prompt: [
2596
+ `You are the **${p.perspective}** reviewer in a multi-perspective review panel.`,
2597
+ "Other perspectives run in parallel; do not assume their conclusions. Stay in your role.",
2598
+ "First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
2599
+ "Any Critical or Important finding in your domain must force VERDICT: request-revision.",
2600
+ "Structure: VERDICT line, then Findings (Critical/Important/Minor), then Checked Items, then Residual Risks.",
2601
+ "Cite concrete paths/ids as evidence. Read-only: do not modify files.",
2602
+ ...input.sharedBrief,
2603
+ "Focus for this perspective:",
2604
+ ...p.focus.map((line) => `- ${line}`),
2605
+ buildSourceContextBlock(input.sources),
2606
+ ].join("\n\n"),
2607
+ }));
2608
+ const reviewIds = reviewNodes.map((n) => n.id);
2609
+ const aggregateScript = buildMultiPerspectiveReviewAggregateScript(reviewIds, input.gateLabel);
2610
+ const gateNode = {
2611
+ id: input.gateId,
2612
+ depends_on: reviewIds,
2613
+ role: "verifier",
2614
+ executor: "shell",
2615
+ complexity: "LOW",
2616
+ writePolicy: "read-only",
2617
+ allowedPaths: commonReadOnlyPaths(input.sources),
2618
+ forbiddenPaths: commonForbiddenPaths(input.sources),
2619
+ outputContract: `Deterministic multi-perspective gate: exit 0 only when every review node among ${reviewIds.join(", ")} emits VERDICT: pass.`,
2620
+ subtask_prompt: `Aggregate gate for ${input.gateLabel}: all perspectives must pass before downstream apply/promote.`,
2621
+ shell: {
2622
+ commands: [aggregateScript],
2623
+ verifyEvidence: buildVerifyEvidence({
2624
+ phase: "final",
2625
+ quota: "full",
2626
+ commandSource: "inline",
2627
+ fallbackCommands: [aggregateScript],
2628
+ finalFullRequired: true,
2629
+ }),
2630
+ cwd: ".",
2631
+ timeoutMs: 60000,
2632
+ },
2633
+ };
2634
+ return [...reviewNodes, gateNode];
2635
+ }
1504
2636
  /** Safe Feature directory id: F-… without path separators. */
1505
2637
  const KNOWLEDGE_SYNC_FEATURE_ID_RE = /^F-[A-Za-z0-9][A-Za-z0-9._-]*$/;
1506
2638
  export function assertSafeKnowledgeSyncFeatureId(featureId) {
@@ -1682,12 +2814,61 @@ function buildKnowledgeSyncValidateNode(sources, featureId) {
1682
2814
  },
1683
2815
  };
1684
2816
  }
2817
+ const KNOWLEDGE_SYNC_MULTI_REVIEW_PERSPECTIVES = [
2818
+ {
2819
+ id: "qa",
2820
+ perspective: "QA / acceptance",
2821
+ focus: [
2822
+ "acceptanceVerdict and AC coverage vs evidencePointers",
2823
+ "caseIndex completeness and non-invented pass results",
2824
+ "defects registry consistency with open issues",
2825
+ ],
2826
+ },
2827
+ {
2828
+ id: "domain",
2829
+ perspective: "domain / product",
2830
+ focus: [
2831
+ "requirement-delta risk and silent requirement rewrites",
2832
+ "operations[] targets stay under the bound featureId",
2833
+ "business meaning of coverage/matrix changes",
2834
+ ],
2835
+ },
2836
+ {
2837
+ id: "evidence",
2838
+ perspective: "evidence / audit",
2839
+ focus: [
2840
+ "finalVerification authority is shell evidence, not prose",
2841
+ "high-risk ops and gates.requireHumanIfHighRisk",
2842
+ "draft schema fields and pointer-only log policy",
2843
+ ],
2844
+ },
2845
+ ];
2846
+ function buildKnowledgeSyncMultiReviewNodes(sources, featureId) {
2847
+ return buildMultiPerspectiveReviewNodes({
2848
+ sources,
2849
+ dependsOn: ["knowledge-sync-validate-shell"],
2850
+ nodePrefix: "knowledge-sync-review-",
2851
+ gateId: "knowledge-sync-multi-review-gate-shell",
2852
+ gateLabel: "knowledge-sync multi-perspective review",
2853
+ perspectives: KNOWLEDGE_SYNC_MULTI_REVIEW_PERSPECTIVES,
2854
+ allowedPaths: [
2855
+ ...commonReadOnlyPaths(sources),
2856
+ `features/${featureId}/**`,
2857
+ "docs/test-reports/**",
2858
+ ],
2859
+ sharedBrief: [
2860
+ `Bound featureId: ${featureId}. Only review draft/ops for this Feature.`,
2861
+ `Primary draft path: ${knowledgeSyncDraftRelPath(featureId)}.`,
2862
+ "Apply is blocked until all perspectives pass. Do not approve fabricated verification pass.",
2863
+ ],
2864
+ });
2865
+ }
1685
2866
  function buildKnowledgeSyncApplyNode(sources, featureId) {
1686
2867
  const writeSet = knowledgeSyncWriteSet(featureId);
1687
2868
  const draftPath = knowledgeSyncDraftRelPath(featureId);
1688
2869
  return {
1689
2870
  id: "knowledge-sync-apply-pi",
1690
- depends_on: ["knowledge-sync-validate-shell"],
2871
+ depends_on: ["knowledge-sync-multi-review-gate-shell"],
1691
2872
  role: "implementer",
1692
2873
  executor: "pi",
1693
2874
  toolProfile: "write",
@@ -1756,11 +2937,13 @@ function buildKnowledgeSyncHybridDag(sources) {
1756
2937
  ...STANDARD_GLOBAL_CONSTRAINTS,
1757
2938
  `knowledge-sync-dag is bound to featureId=${featureId}; writes only features/${featureId}/testing/**, features/${featureId}/requirement-delta.md, and docs/test-reports/**.`,
1758
2939
  "knowledge-sync must not modify other features/**, src/**, .harness/**, knowledge/testing/standards/**, or pytest framework files.",
1759
- "Apply is blocked unless knowledge-sync-validate-shell passes; final verification evidence remains the completion authority.",
2940
+ "Apply is blocked unless knowledge-sync-validate-shell and multi-perspective review gate pass; final verification evidence remains the completion authority.",
2941
+ "Multi-perspective review: QA/acceptance, domain/product, and evidence/audit must each emit VERDICT: pass before apply.",
1760
2942
  "High-risk requirement/acceptance body changes require human approval via requirement-delta; do not silently rewrite requirement.md.",
1761
2943
  "Raw shell logs stay as path pointers; knowledge base stores stable facts only.",
1762
2944
  "Prefer structured YAML/Markdown L1 knowledge pack over vector-store-only writes.",
1763
2945
  ];
2946
+ const multiReview = buildKnowledgeSyncMultiReviewNodes(sources, featureId);
1764
2947
  const spec = {
1765
2948
  version: 2,
1766
2949
  title: `Knowledge-sync DAG (${featureId}): ${taskConfig.title}`,
@@ -1778,6 +2961,7 @@ function buildKnowledgeSyncHybridDag(sources) {
1778
2961
  buildKnowledgeSyncCollectNode(sources, featureId),
1779
2962
  buildKnowledgeSyncDraftNode(sources, featureId),
1780
2963
  buildKnowledgeSyncValidateNode(sources, featureId),
2964
+ ...multiReview,
1781
2965
  buildKnowledgeSyncApplyNode(sources, featureId),
1782
2966
  buildKnowledgeSyncPointerNode(sources, featureId),
1783
2967
  ],
@@ -1991,51 +3175,55 @@ function buildKgBootstrapValidateNode(sources) {
1991
3175
  },
1992
3176
  };
1993
3177
  }
1994
- function buildKgBootstrapReviewNode(sources) {
1995
- return {
1996
- id: "kg-bootstrap-review-pi",
1997
- depends_on: ["kg-bootstrap-validate-shell"],
1998
- role: "reviewer",
1999
- executor: "pi",
2000
- complexity: "HIGH",
2001
- writePolicy: "read-only",
2002
- allowedPaths: ["knowledge/bootstrap/**", "knowledge/**", "features/**", "docs/**"],
2003
- forbiddenPaths: commonForbiddenPaths(sources),
2004
- outputContract: "Plain Markdown whose first non-empty line is VERDICT: pass or VERDICT: request-revision; human still promotes asserted entities.",
2005
- subtask_prompt: [
2006
- "Review staging knowledge-graph proposals for evidence quality, over-claiming, missing inventory coverage, and unsafe edges.",
2007
- "First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
2008
- "pass means proposals are ready for human promotion consideration — it does NOT mark entities asserted.",
2009
- "request-revision if critical entities lack evidence, ids collide conceptually, or formal trees were written outside staging.",
2010
- "Read-only: do not modify files.",
2011
- buildSourceContextBlock(sources),
2012
- ].join("\n\n"),
2013
- };
2014
- }
2015
- function buildKgBootstrapReviewGateNode(sources) {
2016
- return {
2017
- id: "kg-bootstrap-review-gate-shell",
2018
- depends_on: ["kg-bootstrap-review-pi"],
2019
- role: "verifier",
2020
- executor: "shell",
2021
- complexity: "LOW",
2022
- writePolicy: "read-only",
2023
- allowedPaths: commonReadOnlyPaths(sources),
2024
- forbiddenPaths: commonForbiddenPaths(sources),
2025
- outputContract: "Deterministic gate: exit 0 only when kg-bootstrap-review-pi first verdict line is VERDICT: pass.",
2026
- subtask_prompt: "Block promote/materialize unless review verdict is pass.",
2027
- shell: {
2028
- commands: [],
2029
- verdictGate: {
2030
- fromNodeId: "kg-bootstrap-review-pi",
2031
- accept: ["VERDICT: pass"],
2032
- label: "kg-bootstrap-review",
2033
- lineMode: "first-verdict-line",
2034
- },
2035
- cwd: ".",
2036
- timeoutMs: 60000,
2037
- },
2038
- };
3178
+ const KG_BOOTSTRAP_MULTI_REVIEW_PERSPECTIVES = [
3179
+ {
3180
+ id: "structure",
3181
+ perspective: "architecture / structure",
3182
+ focus: [
3183
+ "domain/service/module partition vs inventory candidates",
3184
+ "id uniqueness and naming conventions",
3185
+ "edges that create impossible or circular dependencies",
3186
+ ],
3187
+ },
3188
+ {
3189
+ id: "evidence",
3190
+ perspective: "evidence / anti-hallucination",
3191
+ focus: [
3192
+ "every entity/edge has concrete evidence paths",
3193
+ "no confidence: asserted in staging",
3194
+ "no invented APIs or production details without files",
3195
+ ],
3196
+ },
3197
+ {
3198
+ id: "safety",
3199
+ perspective: "write-boundary / promote safety",
3200
+ focus: [
3201
+ "writes stayed in knowledge/bootstrap/staging/** (and runs/**)",
3202
+ "no formal knowledge/domains|services trees or graph indexes written by AI",
3203
+ "incremental scope respected when update_mode: incremental",
3204
+ ],
3205
+ },
3206
+ ];
3207
+ function buildKgBootstrapMultiReviewNodes(sources) {
3208
+ return buildMultiPerspectiveReviewNodes({
3209
+ sources,
3210
+ dependsOn: ["kg-bootstrap-validate-shell"],
3211
+ nodePrefix: "kg-bootstrap-review-",
3212
+ gateId: "kg-bootstrap-multi-review-gate-shell",
3213
+ gateLabel: "kg-bootstrap multi-perspective review",
3214
+ perspectives: KG_BOOTSTRAP_MULTI_REVIEW_PERSPECTIVES,
3215
+ allowedPaths: [
3216
+ "knowledge/bootstrap/**",
3217
+ "knowledge/**",
3218
+ "features/**",
3219
+ "docs/**",
3220
+ ],
3221
+ sharedBrief: [
3222
+ "Review staging knowledge-graph proposals before promote.",
3223
+ "pass means ready for promote consideration — it does NOT mark entities asserted.",
3224
+ "request-revision if critical entities lack evidence, ids collide, or formal trees were written outside staging.",
3225
+ ],
3226
+ });
2039
3227
  }
2040
3228
  function buildKgBootstrapPromoteNode(sources) {
2041
3229
  const script = buildKgBootstrapInlineNodeScript([
@@ -2065,7 +3253,9 @@ function buildKgBootstrapPromoteNode(sources) {
2065
3253
  "if(fs.existsSync(linksDir)){",
2066
3254
  " for(const f of fs.readdirSync(linksDir)){",
2067
3255
  " if(!f.endsWith('.yaml')&&!f.endsWith('.yml')) continue;",
3256
+ " if(!/^F-[A-Za-z0-9][A-Za-z0-9._-]*\\.ya?ml$/.test(f)){ console.error('invalid feature link proposal filename: '+f); process.exit(1); }",
2068
3257
  " const id=f.replace(/\\.ya?ml$/,'');",
3258
+ " if(id.includes('..')){ console.error('invalid feature link proposal filename: '+f); process.exit(1); }",
2069
3259
  " const dest=path.join(root,'features',id,'knowledge-links.yaml');",
2070
3260
  " if(!fs.existsSync(path.join(root,'features',id))) continue;",
2071
3261
  " if(!fs.existsSync(dest)){ fs.mkdirSync(path.dirname(dest),{recursive:true}); fs.copyFileSync(path.join(linksDir,f),dest); copied++; }",
@@ -2075,7 +3265,7 @@ function buildKgBootstrapPromoteNode(sources) {
2075
3265
  ]);
2076
3266
  return {
2077
3267
  id: "kg-bootstrap-promote-shell",
2078
- depends_on: ["kg-bootstrap-review-gate-shell"],
3268
+ depends_on: ["kg-bootstrap-multi-review-gate-shell"],
2079
3269
  role: "verifier",
2080
3270
  executor: "shell",
2081
3271
  complexity: "LOW",
@@ -2195,7 +3385,9 @@ function buildKnowledgeGraphBootstrapHybridDag(sources) {
2195
3385
  "Promote must not overwrite existing formal files (merge-new-only).",
2196
3386
  "Require knowledge/bootstrap/scope.yaml before propose (B0/B1 skeleton).",
2197
3387
  "Graph indexes are written only by materialize-shell, not by propose-pi.",
3388
+ "Multi-perspective review (structure, evidence, safety) must all VERDICT: pass before promote.",
2198
3389
  ];
3390
+ const multiReview = buildKgBootstrapMultiReviewNodes(sources);
2199
3391
  const spec = {
2200
3392
  version: 2,
2201
3393
  title: `Knowledge-graph bootstrap DAG: ${taskConfig.title}`,
@@ -2214,8 +3406,7 @@ function buildKnowledgeGraphBootstrapHybridDag(sources) {
2214
3406
  buildKgBootstrapInventoryNode(sources),
2215
3407
  buildKgBootstrapProposeNode(sources),
2216
3408
  buildKgBootstrapValidateNode(sources),
2217
- buildKgBootstrapReviewNode(sources),
2218
- buildKgBootstrapReviewGateNode(sources),
3409
+ ...multiReview,
2219
3410
  buildKgBootstrapPromoteNode(sources),
2220
3411
  buildKgBootstrapMaterializeNode(sources),
2221
3412
  ],
@@ -2224,30 +3415,42 @@ function buildKnowledgeGraphBootstrapHybridDag(sources) {
2224
3415
  assertValidDagSpec(spec);
2225
3416
  return spec;
2226
3417
  }
3418
+ function buildHybridDagForTemplate(sources, template) {
3419
+ let spec;
3420
+ if (template === "frontend-implementation") {
3421
+ spec = buildFrontendHybridDagFromTask(sources);
3422
+ }
3423
+ else if (template === "frontend-test-dag")
3424
+ spec = buildFrontendTestHybridDag(sources);
3425
+ else if (template === "backend-test-dag")
3426
+ spec = buildBackendTestHybridDag(sources);
3427
+ else if (template === "knowledge-sync-dag")
3428
+ spec = buildKnowledgeSyncHybridDag(sources);
3429
+ else if (template === "knowledge-graph-bootstrap-dag")
3430
+ spec = buildKnowledgeGraphBootstrapHybridDag(sources);
3431
+ else {
3432
+ const standard = buildStandardHybridDagFromTask(sources);
3433
+ if (template === "standard-dag")
3434
+ spec = standard;
3435
+ else if (template === "review-gated-dag")
3436
+ spec = buildReviewGatedHybridDag(standard, sources);
3437
+ else
3438
+ spec = buildSupervisedHybridDag(standard, sources);
3439
+ }
3440
+ spec.sourceBinding = buildDagSourceBinding(sources);
3441
+ parseDagSpec(spec);
3442
+ assertValidDagSpec(spec);
3443
+ return spec;
3444
+ }
2227
3445
  export function buildHybridDagFromTask(sources, options = {}) {
2228
- if (sources.taskConfig.taskKind === "frontend-implementation" ||
2229
- options.template === "frontend-implementation") {
2230
- return buildFrontendHybridDagFromTask(sources);
2231
- }
2232
- if (sources.taskConfig.taskKind === "backend-test" ||
2233
- options.template === "backend-test-dag") {
2234
- return buildBackendTestHybridDag(sources);
2235
- }
2236
- if (sources.taskConfig.taskKind === "knowledge-sync" ||
2237
- options.template === "knowledge-sync-dag") {
2238
- return buildKnowledgeSyncHybridDag(sources);
2239
- }
2240
- if (sources.taskConfig.taskKind === "knowledge-graph-bootstrap" ||
2241
- options.template === "knowledge-graph-bootstrap-dag") {
2242
- return buildKnowledgeGraphBootstrapHybridDag(sources);
2243
- }
2244
- const standard = buildStandardHybridDagFromTask(sources);
2245
- const template = options.template ?? "standard-dag";
2246
- if (template === "standard-dag")
2247
- return standard;
2248
- if (template === "review-gated-dag")
2249
- return buildReviewGatedHybridDag(standard, sources);
2250
- return buildSupervisedHybridDag(standard, sources);
3446
+ const selection = resolveTaskDagTemplateSelection({
3447
+ taskKind: sources.taskConfig.taskKind,
3448
+ title: sources.taskConfig.title,
3449
+ requirementMarkdown: sources.requirementMarkdown,
3450
+ allowedPaths: sources.taskConfig.allowedPaths,
3451
+ requestedTemplate: options.template,
3452
+ });
3453
+ return buildHybridDagForTemplate(sources, selection.template);
2251
3454
  }
2252
3455
  function cloneTask(task, patch = {}) {
2253
3456
  return { ...task, ...patch };
@@ -2635,8 +3838,19 @@ export function defaultHybridDagOutputPath(taskId) {
2635
3838
  return path.join(os.tmpdir(), `${taskId}-hybrid-dag.json`);
2636
3839
  }
2637
3840
  export async function writeHybridDagDraft(sources, outputPath, options = {}) {
2638
- const template = options.template ?? "standard-dag";
2639
- const spec = buildHybridDagFromTask(sources, { template });
3841
+ const templateSelection = resolveTaskDagTemplateSelection({
3842
+ taskKind: sources.taskConfig.taskKind,
3843
+ title: sources.taskConfig.title,
3844
+ requirementMarkdown: sources.requirementMarkdown,
3845
+ allowedPaths: sources.taskConfig.allowedPaths,
3846
+ requestedTemplate: options.template,
3847
+ });
3848
+ const template = templateSelection.template;
3849
+ assertTaskAllowedPathsPreflight(sources.taskConfig);
3850
+ const preparedSources = template === "frontend-implementation"
3851
+ ? await prepareFrontendMockSources(sources)
3852
+ : sources;
3853
+ const spec = buildHybridDagForTemplate(preparedSources, template);
2640
3854
  await writeFile(outputPath, `${JSON.stringify(spec, null, 2)}\n`, "utf-8");
2641
3855
  return {
2642
3856
  taskId: sources.taskId,
@@ -2644,6 +3858,7 @@ export async function writeHybridDagDraft(sources, outputPath, options = {}) {
2644
3858
  taskCount: spec.tasks.length,
2645
3859
  nodeIds: spec.tasks.map((task) => task.id),
2646
3860
  template,
3861
+ templateSelection,
2647
3862
  };
2648
3863
  }
2649
3864
  export async function initHybridDagFromTask(repoRoot, taskId, options = {}) {