@tea-agent/loop-agent 0.13.0-alpha.0 → 0.13.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 (199) hide show
  1. package/AGENTS.md +4 -0
  2. package/CHANGELOG.md +57 -53
  3. package/README.md +12 -3
  4. package/dist/application/dag/generate-task-dag.js +28 -58
  5. package/dist/application/evaluation/candidate-hash.js +75 -0
  6. package/dist/application/evaluation/candidate.js +52 -0
  7. package/dist/application/evaluation/replay.js +289 -0
  8. package/dist/application/evaluation/types.js +130 -0
  9. package/dist/cli/command-definitions.js +17 -4
  10. package/dist/cli/program.js +8 -4
  11. package/dist/commands/eval.js +235 -0
  12. package/dist/commands/init.js +131 -24
  13. package/dist/executors/pi-sdk-executor.js +38 -24
  14. package/dist/executors/shell-executor.js +226 -15
  15. package/dist/executors/shell-presets.js +20 -0
  16. package/dist/executors/shell-verification.js +7 -0
  17. package/dist/governance/manifest-types.js +1 -0
  18. package/dist/infrastructure/evaluation/candidate-store.js +439 -0
  19. package/dist/infrastructure/evaluation/store.js +40 -0
  20. package/dist/task/config-types.js +23 -0
  21. package/dist/worker/observe/routes.js +18 -3
  22. package/dist/worker/observe/spec-evidence.js +1 -1
  23. package/dist/worker/observe/static/dom.js +160 -1
  24. package/dist/worker/observe/static/state.js +14 -0
  25. package/dist/worker/observe/static/views/dag-inspector.js +35 -4
  26. package/dist/worker/observe/static/views/dag.js +9 -0
  27. package/dist/worker/observe/static/views/dashboard.js +702 -445
  28. package/dist/worker/observe/static/views/session-timeline.js +15 -1
  29. package/dist/workflows/dag/backend-test-analysis-contract.js +120 -0
  30. package/dist/workflows/dag/backend-test-case-manifest.js +503 -0
  31. package/dist/workflows/dag/backend-test-execution-contract.js +353 -0
  32. package/dist/workflows/dag/backend-test-result-contract.js +568 -0
  33. package/dist/workflows/dag/decision-envelope.js +57 -2
  34. package/dist/workflows/dag/dynamic-runtime/map.js +90 -2
  35. package/dist/workflows/dag/frontend-implementation-contract.js +240 -0
  36. package/dist/workflows/dag/frontend-project-capability.js +309 -0
  37. package/dist/workflows/dag/frontend-repair.js +341 -0
  38. package/dist/workflows/dag/frontend-risk.js +161 -0
  39. package/dist/workflows/dag/frontend-verification-trace.js +190 -0
  40. package/dist/workflows/dag/init-hybrid.js +2407 -297
  41. package/dist/workflows/dag/node-execution.js +9 -0
  42. package/dist/workflows/dag/prompt.js +9 -0
  43. package/dist/workflows/dag/repair-artifact.js +43 -3
  44. package/dist/workflows/dag/report.js +35 -1
  45. package/dist/workflows/dag/runner.js +28 -2
  46. package/dist/workflows/dag/skill-instructions.js +4 -2
  47. package/dist/workflows/dag/task-demand-routing.js +383 -0
  48. package/dist/workflows/dag/types.js +71 -13
  49. package/dist/workflows/dag/upstream-artifacts.js +1 -0
  50. package/dist/workflows/dag/validate.js +59 -1
  51. package/docs/README.md +6 -3
  52. package/docs/agent-dag-recovery-playbook.md +5 -3
  53. package/docs/agent-dag-runner.md +3 -3
  54. package/docs/architecture/README.md +3 -3
  55. package/docs/architecture/dag-execution.md +1 -1
  56. package/docs/architecture/evolution.md +13 -13
  57. package/docs/architecture/facts-and-state.md +1 -1
  58. package/docs/architecture/runtime-boundaries.md +7 -7
  59. package/docs/architecture/system-overview.md +3 -3
  60. package/docs/architecture/worker-and-feature.md +3 -3
  61. package/docs/design/README.md +124 -42
  62. package/docs/development-principles.md +4 -4
  63. package/docs/exec-plans/active/README.md +12 -11
  64. package/docs/exec-plans/completed/README.md +33 -0
  65. package/docs/feature-workflow.md +114 -39
  66. package/docs/init-surface.manifest.json +30 -3
  67. package/docs/loop-agent-harness.md +9 -8
  68. package/docs/production-readiness.md +1 -1
  69. package/docs/progress/README.md +23 -1
  70. package/docs/reports/README.md +65 -6
  71. package/docs/skills/vetted-skill-registry.md +2 -0
  72. package/docs/templates/agent-dag.schema.json +29 -1
  73. package/docs/templates/agent-dag.supervised-implementation.json +127 -8
  74. package/docs/templates/backend-test-analysis.schema.json +44 -0
  75. package/docs/templates/backend-test-case-manifest.schema.json +190 -0
  76. package/docs/templates/backend-test-dag.classify.prompt.md +75 -0
  77. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +81 -16
  78. package/docs/templates/backend-test-dag.json +311 -40
  79. package/docs/templates/backend-test-dag.retrospect.prompt.md +44 -30
  80. package/docs/templates/backend-test-dag.review-cases.prompt.md +10 -8
  81. package/docs/templates/backend-test-execution.schema.json +133 -0
  82. package/docs/templates/backend-test-result.schema.json +99 -0
  83. package/docs/templates/branch-merge-report.md +93 -0
  84. package/docs/templates/frontend-design-contract.md +9 -0
  85. package/docs/templates/frontend-eval/fixtures/failures/01-type-build-error.md +17 -0
  86. package/docs/templates/frontend-eval/fixtures/failures/02-unit-component-test-fail.md +16 -0
  87. package/docs/templates/frontend-eval/fixtures/failures/03-fixture-schema-drift.md +16 -0
  88. package/docs/templates/frontend-eval/fixtures/failures/04-missing-loading-empty-error-state.md +16 -0
  89. package/docs/templates/frontend-eval/fixtures/failures/05-forbidden-write-writeset-expansion.md +16 -0
  90. package/docs/templates/frontend-eval/fixtures/failures/06-unapproved-dependency-add.md +16 -0
  91. package/docs/templates/frontend-eval/fixtures/failures/07-mock-production-on.md +21 -0
  92. package/docs/templates/frontend-eval/fixtures/functional/01-simple-component-style.md +29 -0
  93. package/docs/templates/frontend-eval/fixtures/functional/02-form-validation.md +28 -0
  94. package/docs/templates/frontend-eval/fixtures/functional/03-list-detail-page.md +28 -0
  95. package/docs/templates/frontend-eval/fixtures/functional/04-api-mock.md +29 -0
  96. package/docs/templates/frontend-eval/fixtures/functional/05-permission-auth-gated-ui.md +27 -0
  97. package/docs/templates/frontend-eval/fixtures/functional/06-ssr-server-client-boundary.md +28 -0
  98. package/docs/templates/frontend-eval/fixtures/functional/07-shared-public-component-api.md +28 -0
  99. package/docs/templates/frontend-eval/fixtures/functional/08-pure-local-no-remote.md +27 -0
  100. package/docs/templates/frontend-eval/metrics.md +138 -0
  101. package/docs/templates/frontend-eval/smoke-targets.md +53 -0
  102. package/docs/templates/frontend-implementation-contract.schema.json +27 -0
  103. package/docs/templates/frontend-task-constraints.md +10 -0
  104. package/docs/templates/frontend-task-requirement.md +9 -0
  105. package/docs/templates/frontend-test-dag.generate-cases.prompt.md +5 -0
  106. package/docs/templates/frontend-test-dag.json +23 -0
  107. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +3 -0
  108. package/docs/templates/frontend-test-dag.retrospect.prompt.md +3 -0
  109. package/docs/templates/frontend-test-dag.review-cases.prompt.md +3 -0
  110. package/docs/templates/frontend-test-dag.review-execution.prompt.md +3 -0
  111. package/docs/templates/knowledge-graph-bootstrap-dag.json +1 -1
  112. package/docs/templates/knowledge-sync-dag.json +1 -0
  113. package/docs/verification-matrix.md +4 -1
  114. package/examples/decision-gate-agent-dag.json +4 -4
  115. package/examples/hybrid-loop-agent-dag.json +1 -1
  116. package/package.json +2 -2
  117. package/scripts/kb-bootstrap-init-skeleton.sh +2 -1
  118. package/scripts/kb-graph-incremental-prepare.mjs +19 -5
  119. package/scripts/kb-graph-promote.mjs +12 -1
  120. package/skills/ai-engineering-context/SKILL.md +2 -2
  121. package/skills/analyze-product-dependencies/SKILL.md +67 -0
  122. package/skills/analyze-product-dependencies/agents/openai.yaml +4 -0
  123. package/skills/analyze-product-dependencies/references/api-documentation-schema.md +30 -0
  124. package/skills/analyze-product-dependencies/references/dependency-analysis-schema.md +28 -0
  125. package/skills/analyze-product-dependencies/references/example.md +76 -0
  126. package/skills/analyze-product-dependencies/references/forward-test-cases.md +35 -0
  127. package/skills/analyze-product-dependencies/references/input-contract.md +11 -0
  128. package/skills/analyze-product-dependencies/references/scouting-rules.md +61 -0
  129. package/skills/analyze-product-dependencies/scripts/test-validators.mjs +267 -0
  130. package/skills/analyze-product-dependencies/scripts/validate-api-documentation.mjs +101 -0
  131. package/skills/analyze-product-dependencies/scripts/validate-dependency-analysis.mjs +142 -0
  132. package/skills/analyze-product-dependencies/scripts/validate-product-requirement-input.mjs +76 -0
  133. package/skills/analyze-product-dependencies/scripts/validation-helpers.mjs +146 -0
  134. package/skills/analyze-product-requirements/SKILL.md +90 -0
  135. package/skills/analyze-product-requirements/agents/openai.yaml +4 -0
  136. package/skills/analyze-product-requirements/references/acceptance-criteria.md +91 -0
  137. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +56 -0
  138. package/skills/analyze-product-requirements/references/example.md +86 -0
  139. package/skills/analyze-product-requirements/references/forward-test-cases.md +66 -0
  140. package/skills/analyze-product-requirements/references/product-analysis-schema.md +32 -0
  141. package/skills/analyze-product-requirements/references/product-requirement-schema.md +33 -0
  142. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +35 -0
  143. package/skills/analyze-product-requirements/scripts/test-validators.mjs +193 -0
  144. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +69 -0
  145. package/skills/analyze-product-requirements/scripts/validate-product-requirement.mjs +97 -0
  146. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +98 -0
  147. package/skills/analyze-product-requirements/scripts/validation-helpers.mjs +156 -0
  148. package/skills/browser-tools/SKILL.md +196 -0
  149. package/skills/browser-tools/browser-content.js +103 -0
  150. package/skills/browser-tools/browser-cookies.js +35 -0
  151. package/skills/browser-tools/browser-eval.js +53 -0
  152. package/skills/browser-tools/browser-hn-scraper.js +108 -0
  153. package/skills/browser-tools/browser-nav.js +44 -0
  154. package/skills/browser-tools/browser-pick.js +162 -0
  155. package/skills/browser-tools/browser-screenshot.js +34 -0
  156. package/skills/browser-tools/browser-start.js +86 -0
  157. package/skills/browser-tools/package-lock.json +2556 -0
  158. package/skills/browser-tools/package.json +19 -0
  159. package/skills/frontend-design-review/SKILL.md +6 -1
  160. package/skills/frontend-design-review/references/review-checklist.md +25 -4
  161. package/skills/frontend-implementation/SKILL.md +25 -30
  162. package/skills/frontend-implementation/references/code-standards.md +20 -22
  163. package/skills/frontend-implementation/references/node-contracts.md +17 -53
  164. package/skills/frontend-review/SKILL.md +10 -4
  165. package/skills/frontend-review/references/review-findings.md +8 -3
  166. package/skills/frontend-verification/SKILL.md +22 -9
  167. package/skills/frontend-verification/references/verification-checklist.md +17 -5
  168. package/skills/grill-with-docs/SKILL.md +5 -5
  169. package/skills/grill-with-docs/adr-format.md +3 -3
  170. package/skills/init-capability-evolution/SKILL.md +5 -5
  171. package/skills/loop-agent/SKILL.md +5 -5
  172. package/skills/loop-agent/references/README.md +3 -3
  173. package/skills/loop-agent/references/command-reference.md +98 -24
  174. package/skills/loop-agent/references/docs-converge.md +15 -15
  175. package/skills/loop-agent/references/harness-policy.md +2 -2
  176. package/skills/loop-agent/references/hybrid-dag.md +32 -22
  177. package/skills/loop-agent/references/multi-worktree.md +1 -1
  178. package/skills/loop-agent/references/orchestrator-and-interventions.md +8 -8
  179. package/skills/loop-agent/references/task-workflow.md +1 -1
  180. package/skills/loop-agent/references/verification-and-failure-handling.md +6 -4
  181. package/skills/playwright-cli/SKILL.md +420 -0
  182. package/skills/playwright-cli/references/element-attributes.md +23 -0
  183. package/skills/playwright-cli/references/playwright-tests.md +39 -0
  184. package/skills/playwright-cli/references/request-mocking.md +87 -0
  185. package/skills/playwright-cli/references/running-code.md +241 -0
  186. package/skills/playwright-cli/references/session-management.md +225 -0
  187. package/skills/playwright-cli/references/storage-state.md +275 -0
  188. package/skills/playwright-cli/references/test-generation.md +433 -0
  189. package/skills/playwright-cli/references/tracing.md +139 -0
  190. package/skills/playwright-cli/references/video-recording.md +143 -0
  191. package/skills/playwright-cli-case-generator/SKILL.md +74 -0
  192. package/skills/requesting-code-review/SKILL.md +1 -1
  193. package/skills/systematic-debugging/CREATION-LOG.md +3 -3
  194. package/skills/systematic-debugging/SKILL.md +1 -1
  195. package/skills/systematic-debugging/test-academic.md +1 -1
  196. package/skills/systematic-debugging/test-pressure-1.md +1 -1
  197. package/skills/systematic-debugging/test-pressure-2.md +1 -1
  198. package/skills/systematic-debugging/test-pressure-3.md +1 -1
  199. package/skills/verification-before-completion/SKILL.md +1 -1
@@ -1,11 +1,13 @@
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";
5
6
  import { DAG_AGENT_RUNTIME_PI_ONLY, DAG_REPAIR_WRITER_PROTOCOL_EXPLICIT_NODE_V1, DAG_RUNTIME_CONTRACT_SCHEMA_VERSION, DEFAULT_DAG_OUTPUT_LANGUAGE, DEFAULT_DAG_EXECUTOR_MODELS, parseDagSpec, } from "./types.js";
6
7
  import { pathMatchesPattern } from "../../shared/git-progress.js";
7
8
  import { BASELINE_FORBIDDEN_PATHS } from "./governance-constants.js";
8
- import { DEFAULT_READ_ONLY_PI_RETRY_POLICY, isSafeReadOnlyPiRetryCandidate } from "./retry-policy.js";
9
+ import { buildDecisionEnvelopePromptContract } from "./decision-envelope.js";
10
+ import { DEFAULT_READ_ONLY_PI_RETRY_POLICY, isSafeReadOnlyPiRetryCandidate, } from "./retry-policy.js";
9
11
  import { resolveAdapter } from "../../adapters/index.js";
10
12
  import { loadHarnessManifest } from "../../governance/harness.js";
11
13
  import { buildAuthoritySurfaceAuditNode, buildAuthoritySurfaceGateNode, resolveAuthoritySurfaceAudit, } from "./authority-surface.js";
@@ -14,6 +16,11 @@ import { getTaskPaths, loadTaskConfig } from "../../task/runtime.js";
14
16
  import { materializeTaskReferenceDocs } from "../../task/source-references.js";
15
17
  import { resolveVerifyPreset } from "../../executors/shell-verification.js";
16
18
  import { resolveExecutorModelMatrices } from "../../executors/model-routing.js";
19
+ import { normalizeTaskRequirementText, resolveTaskDagTemplateSelection, } from "./task-demand-routing.js";
20
+ import { BACKEND_TEST_EXECUTION_DEFAULT_TEST_ROOT, buildBackendTestExecutionPreflightShellSnippet, } from "./backend-test-execution-contract.js";
21
+ import { buildBackendTestOutcomeGateShellSnippet } from "./backend-test-result-contract.js";
22
+ import { classifyFrontendRisk, } from "./frontend-risk.js";
23
+ import { discoverFrontendProjectCapability, } from "./frontend-project-capability.js";
17
24
  const REQUIREMENT_FILE = "需求.md";
18
25
  const CONSTRAINT_FILE = "执行约束.md";
19
26
  const REFERENCE_DIRECTORY = "references";
@@ -148,6 +155,445 @@ const STANDARD_GLOBAL_CONSTRAINTS = [
148
155
  "exclusive implementer nodes must use narrow, concrete writeSet paths; never keep ** or repo root",
149
156
  `Replace ${IMPLEMENT_WRITESET_PLACEHOLDER} with concrete paths before executing the implementation writer`,
150
157
  ];
158
+ // ---------------------------------------------------------------------------
159
+ // Frontend Mock capability discovery & mode resolution
160
+ // ---------------------------------------------------------------------------
161
+ /** Evidence-based check: does package.json contain a mock-related script? */
162
+ async function packageJsonHasMockScript(repoRoot) {
163
+ try {
164
+ const raw = await readFile(path.join(repoRoot, "package.json"), "utf-8");
165
+ const pkg = JSON.parse(raw);
166
+ const scripts = pkg.scripts ?? {};
167
+ const mockScripts = Object.keys(scripts).filter((name) => name === "mock" ||
168
+ name.startsWith("mock:") ||
169
+ name.startsWith("dev:mock") ||
170
+ /mock/i.test(name));
171
+ const verifyCommands = mockScripts
172
+ .filter((name) => /(?:test|check|verify|contract)/i.test(name))
173
+ .map((name) => ({
174
+ label: `npm run ${name}`,
175
+ args: ["npm", "run", name],
176
+ cwd: repoRoot,
177
+ }));
178
+ return {
179
+ hasScript: mockScripts.length > 0,
180
+ scriptNames: mockScripts,
181
+ verifyCommands,
182
+ };
183
+ }
184
+ catch {
185
+ return { hasScript: false, scriptNames: [], verifyCommands: [] };
186
+ }
187
+ }
188
+ /** Check whether a direct (non-transitive) dependency exists in package.json. */
189
+ async function hasDirectDependency(repoRoot, depName) {
190
+ try {
191
+ const raw = await readFile(path.join(repoRoot, "package.json"), "utf-8");
192
+ const pkg = JSON.parse(raw);
193
+ const deps = {
194
+ ...(pkg.dependencies ?? {}),
195
+ ...(pkg.devDependencies ?? {}),
196
+ };
197
+ return depName in deps;
198
+ }
199
+ catch {
200
+ return false;
201
+ }
202
+ }
203
+ /** Check whether handler/fixture/bootstrap files exist for known mock frameworks. */
204
+ async function discoverMockHandlerFiles(repoRoot, serviceRoot) {
205
+ const exactCandidates = [
206
+ ...[
207
+ "src/mocks/handlers.ts",
208
+ "src/mocks/handlers.js",
209
+ "src/mocks/browser.ts",
210
+ "src/mocks/browser.js",
211
+ "src/mocks/server.ts",
212
+ "src/mocks/server.js",
213
+ "mocks/handlers.ts",
214
+ "mocks/handlers.js",
215
+ "mocks/browser.ts",
216
+ "mocks/browser.js",
217
+ ].map((candidatePath) => ({ framework: "msw", path: candidatePath })),
218
+ ...["db.json", "mock/db.json", "src/mock/db.json"].map((candidatePath) => ({
219
+ framework: "json-server",
220
+ path: candidatePath,
221
+ })),
222
+ ];
223
+ const directoryCandidates = [
224
+ ...(serviceRoot ? [{ path: serviceRoot }] : []),
225
+ { framework: "mockjs", path: "src/mock" },
226
+ { framework: "mockjs", path: "mock" },
227
+ { framework: "msw", path: "src/mocks" },
228
+ { framework: "msw", path: "mocks" },
229
+ { framework: "mirage", path: "src/mirage" },
230
+ { framework: "mirage", path: "mirage" },
231
+ ];
232
+ const foundPaths = new Set();
233
+ let foundFramework;
234
+ for (const candidate of exactCandidates) {
235
+ try {
236
+ await access(path.join(repoRoot, candidate.path));
237
+ foundPaths.add(candidate.path);
238
+ foundFramework ??= candidate.framework;
239
+ }
240
+ catch {
241
+ // Exact candidate does not exist.
242
+ }
243
+ }
244
+ async function collectFiles(directory, relativeDirectory) {
245
+ let entries;
246
+ try {
247
+ entries = await readdir(directory, { withFileTypes: true });
248
+ }
249
+ catch {
250
+ return;
251
+ }
252
+ for (const entry of entries) {
253
+ if (foundPaths.size >= 24)
254
+ return;
255
+ const absoluteEntry = path.join(directory, entry.name);
256
+ const relativeEntry = path.posix.join(relativeDirectory.replace(/\\/g, "/"), entry.name);
257
+ if (entry.isDirectory()) {
258
+ await collectFiles(absoluteEntry, relativeEntry);
259
+ }
260
+ else if (entry.isFile()) {
261
+ foundPaths.add(relativeEntry);
262
+ }
263
+ }
264
+ }
265
+ for (const candidate of directoryCandidates) {
266
+ const before = foundPaths.size;
267
+ await collectFiles(path.join(repoRoot, candidate.path), candidate.path);
268
+ if (foundPaths.size > before && candidate.framework) {
269
+ foundFramework ??= candidate.framework;
270
+ }
271
+ }
272
+ return { framework: foundFramework, paths: [...foundPaths].sort() };
273
+ }
274
+ async function discoverMockBootstrapImports(repoRoot) {
275
+ const entryCandidates = [
276
+ "src/main.ts",
277
+ "src/main.tsx",
278
+ "src/main.js",
279
+ "src/main.jsx",
280
+ "src/index.ts",
281
+ "src/index.tsx",
282
+ "src/index.js",
283
+ "src/index.jsx",
284
+ "src/setupTests.ts",
285
+ "src/setupTests.js",
286
+ "test/setup.ts",
287
+ "test/setup.js",
288
+ ];
289
+ const imports = [];
290
+ for (const candidate of entryCandidates) {
291
+ try {
292
+ const content = await readFile(path.join(repoRoot, candidate), "utf-8");
293
+ if (/(?:from\s*|import\s*)["'][^"']*(?:mock|msw|mirage)[^"']*["']/i.test(content)) {
294
+ imports.push(candidate);
295
+ }
296
+ }
297
+ catch {
298
+ // Candidate entry does not exist or is unreadable.
299
+ }
300
+ }
301
+ return imports;
302
+ }
303
+ /**
304
+ * Deterministic frontend Mock capability discovery.
305
+ *
306
+ * Strong evidence (at least one must be hit to judge "present"):
307
+ * 1. package.json mock script + corresponding config/entry
308
+ * 2. Direct dependency (MSW, Mock.js, Mirage, json-server, Vite Mock plugin) + handler files
309
+ * 3. Application bootstrap imports project mock files
310
+ * 4. Project specs explicitly define mock service root, handler dir, and startup method
311
+ *
312
+ * Anti-evidence (cannot alone judge "present"):
313
+ * - lockfile-only or transitive dependency
314
+ * - test variable named "mock"
315
+ * - fixtures without service registration
316
+ * - neighboring project mock services
317
+ * - model-directory-name guessing
318
+ */
319
+ export async function discoverFrontendMockCapability(repoRoot, taskConfig) {
320
+ const safetyViolation = await frontendMockServiceRootSafetyViolation(repoRoot, taskConfig);
321
+ if (safetyViolation) {
322
+ return {
323
+ status: "ambiguous",
324
+ serviceRoot: taskConfig.frontendMock?.serviceRoot,
325
+ safetyViolation,
326
+ evidencePaths: [],
327
+ verifyCommands: [],
328
+ reasons: [safetyViolation],
329
+ };
330
+ }
331
+ const evidencePaths = [];
332
+ const reasons = [];
333
+ let framework;
334
+ let serviceRoot;
335
+ let strongEvidenceCount = 0;
336
+ let ambiguousSignals = 0;
337
+ // 1. Check package.json mock scripts
338
+ const scriptResult = await packageJsonHasMockScript(repoRoot);
339
+ if (scriptResult.hasScript) {
340
+ reasons.push(`package.json has mock scripts: ${scriptResult.scriptNames.join(", ")}`);
341
+ evidencePaths.push("package.json");
342
+ // A script alone is not strong evidence unless we also find config/entry
343
+ }
344
+ // 2. Check for direct mock framework dependencies
345
+ const mockDeps = [
346
+ "msw",
347
+ "mockjs",
348
+ "miragejs",
349
+ "json-server",
350
+ "vite-plugin-mock",
351
+ ];
352
+ const foundDeps = [];
353
+ for (const dep of mockDeps) {
354
+ if (await hasDirectDependency(repoRoot, dep)) {
355
+ foundDeps.push(dep);
356
+ evidencePaths.push(`package.json (${dep})`);
357
+ }
358
+ }
359
+ // 3. Check for handler/fixture/bootstrap files
360
+ const serviceRootHint = taskConfig.frontendMock?.serviceRoot;
361
+ const handlerResult = await discoverMockHandlerFiles(repoRoot, serviceRootHint);
362
+ const bootstrapImports = await discoverMockBootstrapImports(repoRoot);
363
+ if (handlerResult.framework) {
364
+ framework = handlerResult.framework;
365
+ }
366
+ if (handlerResult.paths.length > 0) {
367
+ evidencePaths.push(...handlerResult.paths);
368
+ reasons.push(`Mock handler/fixture paths found: ${handlerResult.paths.join(", ")}`);
369
+ }
370
+ if (bootstrapImports.length > 0) {
371
+ evidencePaths.push(...bootstrapImports);
372
+ reasons.push(`Application/test bootstrap imports Mock code: ${bootstrapImports.join(", ")}`);
373
+ }
374
+ // Evaluate strong evidence
375
+ // Case: direct dep + handler files
376
+ if (foundDeps.length > 0 && handlerResult.paths.length > 0) {
377
+ strongEvidenceCount++;
378
+ reasons.push(`Direct mock dependency (${foundDeps.join(", ")}) with handler files`);
379
+ }
380
+ // Case: mock script in package.json + corresponding config/entry
381
+ if (scriptResult.hasScript && handlerResult.paths.length > 0) {
382
+ strongEvidenceCount++;
383
+ reasons.push("Mock scripts and handler files both present");
384
+ }
385
+ if (bootstrapImports.length > 0 && handlerResult.paths.length > 0) {
386
+ strongEvidenceCount++;
387
+ reasons.push("Application/test bootstrap and project Mock files both present");
388
+ }
389
+ // Case: project specs define mock service root
390
+ if (taskConfig.frontendMock?.serviceRoot && handlerResult.paths.length > 0) {
391
+ strongEvidenceCount++;
392
+ serviceRoot = taskConfig.frontendMock.serviceRoot;
393
+ reasons.push(`task config specifies serviceRoot=${serviceRoot}`);
394
+ }
395
+ // Handle ambiguous: some signals but not enough for "present"
396
+ if (strongEvidenceCount === 0 &&
397
+ (foundDeps.length > 0 ||
398
+ scriptResult.hasScript ||
399
+ handlerResult.paths.length > 0 ||
400
+ bootstrapImports.length > 0)) {
401
+ ambiguousSignals++;
402
+ if (foundDeps.length > 0 && handlerResult.paths.length === 0) {
403
+ reasons.push(`Mock dependency found (${foundDeps.join(", ")}) but no handler/bootstrap files detected`);
404
+ }
405
+ if (scriptResult.hasScript &&
406
+ handlerResult.paths.length === 0 &&
407
+ foundDeps.length === 0) {
408
+ reasons.push("Mock scripts exist but no handler files or direct mock dependencies found");
409
+ }
410
+ }
411
+ // Build verify commands from task config
412
+ const configuredVerifyCommands = (taskConfig.frontendMock?.verifyCommands ?? []).map((cmd) => ({
413
+ label: cmd.label,
414
+ args: ["bash", "-lc", cmd.command],
415
+ cwd: repoRoot,
416
+ timeoutMs: cmd.timeoutMs,
417
+ }));
418
+ const verifyCommands = [
419
+ ...configuredVerifyCommands,
420
+ ...scriptResult.verifyCommands,
421
+ ].filter((command, index, commands) => commands.findIndex((candidate) => candidate.label === command.label) ===
422
+ index);
423
+ if (strongEvidenceCount > 0) {
424
+ return {
425
+ status: "present",
426
+ framework,
427
+ serviceRoot: serviceRoot ?? taskConfig.frontendMock?.serviceRoot,
428
+ evidencePaths,
429
+ verifyCommands,
430
+ reasons,
431
+ };
432
+ }
433
+ if (ambiguousSignals > 0) {
434
+ return {
435
+ status: "ambiguous",
436
+ framework,
437
+ serviceRoot: taskConfig.frontendMock?.serviceRoot,
438
+ evidencePaths,
439
+ verifyCommands,
440
+ reasons,
441
+ };
442
+ }
443
+ return {
444
+ status: "absent",
445
+ evidencePaths,
446
+ verifyCommands,
447
+ reasons: ["No mock service evidence found in project"],
448
+ };
449
+ }
450
+ function patternStaticPrefix(pattern) {
451
+ const normalized = pattern.replace(/\\/g, "/").replace(/^\.\//, "");
452
+ const wildcard = normalized.search(/[?*]/);
453
+ return (wildcard >= 0 ? normalized.slice(0, wildcard) : normalized).replace(/\/+$/, "");
454
+ }
455
+ function frontendMockServiceRootAllowed(taskConfig) {
456
+ const serviceRoot = taskConfig.frontendMock?.serviceRoot;
457
+ if (!serviceRoot)
458
+ return true;
459
+ const normalized = serviceRoot.replace(/\\/g, "/").replace(/^\.\//, "");
460
+ if (normalized === "." ||
461
+ normalized === ".." ||
462
+ normalized.startsWith("../") ||
463
+ path.isAbsolute(serviceRoot) ||
464
+ /[?*]/.test(normalized)) {
465
+ return false;
466
+ }
467
+ const allowed = taskConfig.allowedPaths.some((allowedPath) => pathMatchesPattern(normalized, allowedPath) ||
468
+ pathMatchesPattern(`${normalized}/_probe_`, allowedPath));
469
+ if (!allowed)
470
+ return false;
471
+ return !mergeForbiddenPaths(taskConfig).some((forbiddenPath) => {
472
+ const forbiddenPrefix = patternStaticPrefix(forbiddenPath);
473
+ return (pathMatchesPattern(normalized, forbiddenPath) ||
474
+ (forbiddenPrefix.length > 0 &&
475
+ pathMatchesPattern(forbiddenPrefix, normalized)));
476
+ });
477
+ }
478
+ async function frontendMockServiceRootSafetyViolation(repoRoot, taskConfig) {
479
+ const serviceRoot = taskConfig.frontendMock?.serviceRoot;
480
+ if (!serviceRoot)
481
+ return undefined;
482
+ if (!frontendMockServiceRootAllowed(taskConfig)) {
483
+ return `frontendMock.serviceRoot is outside allowedPaths or overlaps forbiddenPaths: ${serviceRoot}`;
484
+ }
485
+ try {
486
+ const [repoRealPath, serviceRealPath] = await Promise.all([
487
+ realpath(repoRoot),
488
+ realpath(path.resolve(repoRoot, serviceRoot)),
489
+ ]);
490
+ const relativeRealPath = path.relative(repoRealPath, serviceRealPath);
491
+ if (relativeRealPath === ".." ||
492
+ relativeRealPath.startsWith(`..${path.sep}`) ||
493
+ path.isAbsolute(relativeRealPath)) {
494
+ return `frontendMock.serviceRoot resolves outside the repository: ${serviceRoot}`;
495
+ }
496
+ const normalizedRealPath = relativeRealPath.split(path.sep).join("/") || ".";
497
+ if (!frontendMockServiceRootAllowed({
498
+ ...taskConfig,
499
+ frontendMock: {
500
+ ...(taskConfig.frontendMock ?? {
501
+ policy: "auto",
502
+ verifyCommands: [],
503
+ }),
504
+ serviceRoot: normalizedRealPath,
505
+ },
506
+ })) {
507
+ return `frontendMock.serviceRoot resolves outside its allowed boundary: ${serviceRoot}`;
508
+ }
509
+ }
510
+ catch (error) {
511
+ // A missing configured root is capability absence, not a path escape. The
512
+ // discovery pass below will report it without traversing another location.
513
+ if (error.code !== "ENOENT") {
514
+ return `frontendMock.serviceRoot safety could not be verified: ${serviceRoot}`;
515
+ }
516
+ }
517
+ return undefined;
518
+ }
519
+ /**
520
+ * Heuristic: does the task have interface/async data dependencies?
521
+ *
522
+ * Checks (in priority order):
523
+ * 1. taskConfig.frontendMock.policy === "required"
524
+ * 2. Requirement references API docs, schemas, endpoints
525
+ * 3. Acceptance criteria mention requests, async data, or service states
526
+ * 4. Contract/scout confirmed existing API call chain (not available at generation time)
527
+ */
528
+ export function hasApiDependency(sources) {
529
+ if (sources.taskConfig.frontendMock?.policy === "required") {
530
+ return true;
531
+ }
532
+ const requirement = normalizeTaskRequirementText(sources.requirementMarkdown).replace(/`[^`\n]*`/g, " ");
533
+ const dependencyPatterns = [
534
+ /(?:接口文档|接口定义|接口协议|后端接口|服务端接口|接口联调|请求|响应|远程数据|异步数据|数据获取|模拟接口|模拟数据)/,
535
+ /(?<![A-Za-z0-9_])API(?![A-Za-z0-9_])/i,
536
+ /\b(?:endpoint|request|response|fetch|axios|schema|mock|backend\s+api|server\s+api)\b/i,
537
+ ];
538
+ const negationPatterns = [
539
+ /(?:不涉及|无需|不需要|不依赖|不调用|不请求|没有|禁止|不得).{0,16}(?:接口|后端|服务端|远程数据|异步数据|API)/i,
540
+ /\b(?:no|without|does\s+not|do\s+not|must\s+not)\b.{0,24}\b(?:api|endpoint|request|backend|server)\b/i,
541
+ ];
542
+ return requirement
543
+ .split(/[。!?!?;;,,\r\n]+/)
544
+ .map((clause) => clause.trim())
545
+ .filter(Boolean)
546
+ .some((clause) => !negationPatterns.some((pattern) => pattern.test(clause)) &&
547
+ dependencyPatterns.some((pattern) => pattern.test(clause)));
548
+ }
549
+ /**
550
+ * Resolve frontend Mock mode from capability seed, task config, and interface dependency analysis.
551
+ *
552
+ * Decision matrix (from docs/design/frontend-mock-data-workflow.md):
553
+ *
554
+ * | 接口/异步数据依赖 | 既有 Mock 服务 | policy | 结果 |
555
+ * |---|---|---|---|
556
+ * | 无 | 任意 | auto | not-required |
557
+ * | 有 | present | auto | required |
558
+ * | 有 | present/absent/ambiguous | auto | required (strategy chooses a safe mechanism) |
559
+ * | 任意 | present | required | required |
560
+ * | 任意 | absent/ambiguous | required | blocked |
561
+ * | 任意 | 任意 | disabled | not-required (if spec allows) else blocked |
562
+ */
563
+ export function resolveFrontendMockMode(capability, taskConfig, hasApiDep) {
564
+ const policy = taskConfig.frontendMock?.policy ?? "auto";
565
+ const hasDeterministicMockVerification = capability.verifyCommands.length > 0;
566
+ if (capability.safetyViolation ||
567
+ !frontendMockServiceRootAllowed(taskConfig)) {
568
+ return "blocked";
569
+ }
570
+ // disabled policy: must respect project mock rules (can't override spec)
571
+ if (policy === "disabled") {
572
+ // When disabled but the project spec mandates mock, it's blocked
573
+ if (capability.status === "present") {
574
+ // Project has mock service; disabled is an explicit override that still allows not-required
575
+ return "not-required";
576
+ }
577
+ return "not-required";
578
+ }
579
+ // required policy
580
+ if (policy === "required") {
581
+ if (capability.status === "present" && hasDeterministicMockVerification) {
582
+ return "required";
583
+ }
584
+ return "blocked";
585
+ }
586
+ // auto policy
587
+ if (!hasApiDep) {
588
+ return "not-required";
589
+ }
590
+ // In auto mode, capability discovery is evidence for the strategy node, not
591
+ // a final mechanism decision. Projects without a native Mock service may use
592
+ // an existing browser interception harness or a reversible request adapter.
593
+ // The deterministic strategy gate blocks before the writer when none can be
594
+ // verified by the DAG's frozen static/behavior entrypoints.
595
+ return "required";
596
+ }
151
597
  function mapTaskComplexity(complexity) {
152
598
  if (complexity === "small")
153
599
  return "LOW";
@@ -194,7 +640,8 @@ function isMetadataLine(line) {
194
640
  }
195
641
  if (/^(权威来源|SHA-256|冲突时以)/.test(trimmed))
196
642
  return true;
197
- if (/^>/.test(trimmed) && /(权威来源|SHA-256|原始 PRD|reference)/i.test(trimmed)) {
643
+ if (/^>/.test(trimmed) &&
644
+ /(权威来源|SHA-256|原始 PRD|reference)/i.test(trimmed)) {
198
645
  return true;
199
646
  }
200
647
  return false;
@@ -281,7 +728,9 @@ function shellQuote(value) {
281
728
  }
282
729
  function verifyCommandToShell(repoRoot, command) {
283
730
  const relativeCwd = path.relative(repoRoot, command.cwd);
284
- const cwdPrefix = relativeCwd && !relativeCwd.startsWith("..") && !path.isAbsolute(relativeCwd)
731
+ const cwdPrefix = relativeCwd &&
732
+ !relativeCwd.startsWith("..") &&
733
+ !path.isAbsolute(relativeCwd)
285
734
  ? `cd ${shellQuote(relativeCwd)} && `
286
735
  : command.cwd !== repoRoot
287
736
  ? `cd ${shellQuote(command.cwd)} && `
@@ -312,6 +761,10 @@ function markdownVerifyCommand(repoRoot, command) {
312
761
  label: command,
313
762
  };
314
763
  }
764
+ function isSupportedMarkdownVerifyCommand(command) {
765
+ return (/^(npm|pnpm|yarn|bun)\s+(run\s+)?[a-z0-9:_-]+(?:\s.*)?$/i.test(command) ||
766
+ /^(npx|pnpm\s+exec|yarn\s+exec|bunx)\s+(vitest|jest|playwright|cypress|tsc|eslint)(?:\s.*)?$/i.test(command));
767
+ }
315
768
  function extractFrontendVerifyCommandsFromMarkdown(input) {
316
769
  if (!input.repoRoot)
317
770
  return { staticCommands: [], behaviorCommands: [] };
@@ -328,8 +781,7 @@ function extractFrontendVerifyCommandsFromMarkdown(input) {
328
781
  const codeSpanCommands = Array.from(bulletless.matchAll(/`([^`]+)`/g), (match) => match[1].trim());
329
782
  const candidates = codeSpanCommands.length > 0 ? codeSpanCommands : [bulletless];
330
783
  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)) {
784
+ if (isSupportedMarkdownVerifyCommand(candidate)) {
333
785
  commands.add(candidate);
334
786
  }
335
787
  }
@@ -350,6 +802,26 @@ function extractFrontendVerifyCommandsFromMarkdown(input) {
350
802
  }
351
803
  return { staticCommands, behaviorCommands };
352
804
  }
805
+ function extractFrontendMockVerifyCommandsFromMarkdown(input) {
806
+ const commands = [];
807
+ const markdown = [
808
+ input.requirementMarkdown,
809
+ input.constraintMarkdown ?? "",
810
+ ].join("\n");
811
+ for (const line of markdown.split(/\r?\n/)) {
812
+ if (!/(?:mock|模拟服务|接口桩)/i.test(line))
813
+ continue;
814
+ for (const match of line.matchAll(/`([^`]+)`/g)) {
815
+ const commandText = match[1].trim();
816
+ if (!isSupportedMarkdownVerifyCommand(commandText))
817
+ continue;
818
+ const command = markdownVerifyCommand(input.repoRoot, commandText);
819
+ if (command)
820
+ commands.push(command);
821
+ }
822
+ }
823
+ return commands.filter((command, index, all) => all.findIndex((candidate) => candidate.args.join("\0") === command.args.join("\0")) === index);
824
+ }
353
825
  function chooseFrontendVerifyCommands(input) {
354
826
  if (input.parsedCommands.length > 0) {
355
827
  return { commands: input.parsedCommands, commandSource: "inline" };
@@ -407,19 +879,65 @@ function deriveFrontendBehaviorPaths(taskConfig) {
407
879
  return taskConfig.allowedPaths;
408
880
  }
409
881
  function toTaskRelativeSourcePath(sources, absolutePath) {
410
- return path
411
- .relative(sources.taskDir, absolutePath)
412
- .replaceAll(path.sep, "/");
882
+ return path.relative(sources.taskDir, absolutePath).replaceAll(path.sep, "/");
883
+ }
884
+ function extractExplicitRequirementIds(...markdownInputs) {
885
+ const ids = [];
886
+ const seen = new Set();
887
+ for (const markdown of markdownInputs) {
888
+ if (!markdown)
889
+ continue;
890
+ for (const match of markdown.matchAll(/\b(?:REQ|BR|AC)-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/gi)) {
891
+ const id = match[0].toUpperCase();
892
+ if (!seen.has(id)) {
893
+ seen.add(id);
894
+ ids.push(id);
895
+ }
896
+ }
897
+ }
898
+ return ids;
899
+ }
900
+ function buildDagSourceBinding(sources) {
901
+ const sourceEntries = [
902
+ {
903
+ kind: "requirement",
904
+ path: sources.requirementPath,
905
+ markdown: sources.requirementMarkdown,
906
+ },
907
+ ...(sources.constraintMarkdown
908
+ ? [
909
+ {
910
+ kind: "constraint",
911
+ path: sources.constraintPath,
912
+ markdown: sources.constraintMarkdown,
913
+ },
914
+ ]
915
+ : []),
916
+ ...(sources.referenceDocuments ?? []).map((reference) => ({
917
+ kind: "reference",
918
+ path: reference.path,
919
+ markdown: reference.markdown,
920
+ })),
921
+ ];
922
+ return {
923
+ schemaVersion: 1,
924
+ taskId: sources.taskId,
925
+ sources: sourceEntries.map((source) => ({
926
+ kind: source.kind,
927
+ path: toTaskRelativeSourcePath(sources, source.path),
928
+ sha256: createHash("sha256")
929
+ .update(source.markdown, "utf8")
930
+ .digest("hex"),
931
+ })),
932
+ requirementIds: extractExplicitRequirementIds(sources.requirementMarkdown, sources.constraintMarkdown, ...(sources.referenceDocuments ?? []).map((reference) => reference.markdown)),
933
+ };
413
934
  }
414
935
  function buildSourceContextBlock(sources) {
415
936
  const requirementRef = toTaskRelativeSourcePath(sources, sources.requirementPath);
416
937
  const requirementExcerpt = excerptMarkdown(sources.requirementMarkdown, {
417
938
  sourceRef: requirementRef,
418
939
  });
419
- const parts = [
420
- "## Task source: 需求.md",
421
- requirementExcerpt.text,
422
- ];
940
+ const parts = ["## Task source: 需求.md", requirementExcerpt.text];
423
941
  if (sources.constraintMarkdown) {
424
942
  const constraintRef = toTaskRelativeSourcePath(sources, sources.constraintPath);
425
943
  const constraintExcerpt = excerptMarkdown(sources.constraintMarkdown, {
@@ -463,7 +981,8 @@ async function loadMaterializedSourceReferences(sourceDir) {
463
981
  }
464
982
  else if (entry.isFile()) {
465
983
  // Skip index/manifest sidecars; keep only user/source reference content.
466
- if (entry.name === "index.json" || entry.name === "source-manifest.json") {
984
+ if (entry.name === "index.json" ||
985
+ entry.name === "source-manifest.json") {
467
986
  continue;
468
987
  }
469
988
  referencePaths.push(entryPath);
@@ -472,7 +991,9 @@ async function loadMaterializedSourceReferences(sourceDir) {
472
991
  }
473
992
  await collect(referenceDir);
474
993
  referencePaths.sort((left, right) => left.localeCompare(right));
475
- return Promise.all(referencePaths.slice(0, MAX_SOURCE_REFERENCE_DOCUMENTS).map(async (filePath) => ({
994
+ return Promise.all(referencePaths
995
+ .slice(0, MAX_SOURCE_REFERENCE_DOCUMENTS)
996
+ .map(async (filePath) => ({
476
997
  path: filePath,
477
998
  markdown: await readFile(filePath, "utf-8"),
478
999
  })));
@@ -535,7 +1056,7 @@ export async function loadTaskHybridSources(repoRoot, taskId) {
535
1056
  catch (error) {
536
1057
  throw new Error(`failed to load verification commands for task "${taskId}": ${error instanceof Error ? error.message : String(error)}`);
537
1058
  }
538
- return {
1059
+ const sources = {
539
1060
  taskId,
540
1061
  repoRoot,
541
1062
  taskDir: paths.taskDir,
@@ -551,6 +1072,42 @@ export async function loadTaskHybridSources(repoRoot, taskId) {
551
1072
  verifyCommands,
552
1073
  sddEmbeddedSkills: await probeRepoLocalSddSkills(repoRoot),
553
1074
  };
1075
+ return sources;
1076
+ }
1077
+ async function prepareFrontendMockSources(sources) {
1078
+ const repoRoot = sources.repoRoot ?? process.cwd();
1079
+ const capability = await discoverFrontendMockCapability(repoRoot, sources.taskConfig);
1080
+ const sourceMockVerifyCommands = extractFrontendMockVerifyCommandsFromMarkdown({
1081
+ repoRoot,
1082
+ requirementMarkdown: sources.requirementMarkdown,
1083
+ constraintMarkdown: sources.constraintMarkdown,
1084
+ });
1085
+ for (const command of sourceMockVerifyCommands) {
1086
+ if (!capability.verifyCommands.some((existing) => existing.args.join("\0") === command.args.join("\0"))) {
1087
+ capability.verifyCommands.push(command);
1088
+ }
1089
+ }
1090
+ const projectCapability = await discoverFrontendProjectCapability(repoRoot);
1091
+ const frontendRisk = classifyFrontendRisk({
1092
+ title: sources.taskConfig.title,
1093
+ requirementMarkdown: sources.requirementMarkdown,
1094
+ constraintMarkdown: sources.constraintMarkdown ?? undefined,
1095
+ allowedPaths: sources.taskConfig.allowedPaths,
1096
+ forbiddenPaths: sources.taskConfig.forbiddenPaths,
1097
+ complexity: sources.taskConfig.complexity,
1098
+ manifestEvidence: [
1099
+ projectCapability.framework,
1100
+ projectCapability.frameworkVersion ?? "",
1101
+ projectCapability.adapterGuidance,
1102
+ ].join("\n"),
1103
+ });
1104
+ return {
1105
+ ...sources,
1106
+ frontendMockCapability: capability,
1107
+ frontendMockMode: resolveFrontendMockMode(capability, sources.taskConfig, hasApiDependency(sources)),
1108
+ frontendProjectCapability: projectCapability,
1109
+ frontendRisk,
1110
+ };
554
1111
  }
555
1112
  function mergeFinalVerifyCommands(repoRoot, taskConfig, adapterCommands) {
556
1113
  const taskCommands = taskConfig.verifyCommands.map((command) => ({
@@ -583,14 +1140,17 @@ export function buildStandardHybridDagFromTask(sources) {
583
1140
  fallbackCommands: [],
584
1141
  });
585
1142
  const verifyShellTask = verifyShellCommands.length > 0
586
- ? [{
1143
+ ? [
1144
+ {
587
1145
  id: "verify-shell",
588
1146
  depends_on: [implementId],
589
1147
  role: "verifier",
590
1148
  executor: "shell",
591
1149
  complexity: "LOW",
592
1150
  writePolicy: "read-only",
593
- allowedPaths: taskConfig.allowedPaths.length > 0 ? taskConfig.allowedPaths : ["**"],
1151
+ allowedPaths: taskConfig.allowedPaths.length > 0
1152
+ ? taskConfig.allowedPaths
1153
+ : ["**"],
594
1154
  forbiddenPaths,
595
1155
  outputContract: "Archived final shell verification stdout/stderr with exit codes; no worktree writes.",
596
1156
  subtask_prompt: "Run the adapter-resolved final verification commands before read-only verification review.",
@@ -607,7 +1167,8 @@ export function buildStandardHybridDagFromTask(sources) {
607
1167
  cwd: ".",
608
1168
  timeoutMs: 300000,
609
1169
  },
610
- }]
1170
+ },
1171
+ ]
611
1172
  : [];
612
1173
  const sourceContext = buildSourceContextBlock(sources);
613
1174
  const globalConstraints = [
@@ -712,7 +1273,9 @@ export function buildStandardHybridDagFromTask(sources) {
712
1273
  "Stay within writeSet. Do not write root artifacts/** unless artifacts paths are explicitly declared in writeSet.",
713
1274
  writerDeliveryContract(taskConfig),
714
1275
  sourceContext,
715
- ].filter((value) => Boolean(value)).join("\n\n"),
1276
+ ]
1277
+ .filter((value) => Boolean(value))
1278
+ .join("\n\n"),
716
1279
  },
717
1280
  ...verifyShellTask,
718
1281
  {
@@ -758,57 +1321,167 @@ export function buildStandardHybridDagFromTask(sources) {
758
1321
  assertValidDagSpec(spec);
759
1322
  return spec;
760
1323
  }
761
- function buildFrontendHybridDagFromTask(sources) {
1324
+ function buildFrontendMockAssessNode(sources, sourceContext, mockContextBlock, fixedVerificationContext, readOnlyPaths, forbiddenPaths) {
1325
+ return {
1326
+ id: "frontend-mock-assess-pi",
1327
+ depends_on: ["frontend-contract-pi", "frontend-scout-pi"],
1328
+ role: "planner",
1329
+ executor: "pi",
1330
+ complexity: "MED",
1331
+ writePolicy: "read-only",
1332
+ allowedPaths: readOnlyPaths,
1333
+ forbiddenPaths,
1334
+ skills: FRONTEND_IMPLEMENTATION_SKILLS,
1335
+ 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.",
1336
+ subtask_prompt: [
1337
+ "Perform read-only Mock assessment and select one safe frontend data strategy.",
1338
+ "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.",
1339
+ "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.",
1340
+ "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.",
1341
+ "Configured policy disabled requests no Mock but cannot override project specifications; if an actually-read project rule requires Mock, select blocked.",
1342
+ "",
1343
+ "## Required Output Sections:",
1344
+ "- Mock Decision: required | not-required | blocked (with reasoning)",
1345
+ "- API Contract Evidence and Specification Evidence: actual Mock/API/schema specs read (paths + excerpts)",
1346
+ "- Service Evidence: detected Mock framework, service root, handler/fixture/bootstrap paths",
1347
+ "- Backend Readiness and Selection Evidence: why the selected mechanism is available and appropriate",
1348
+ "- Endpoint / Fixture Matrix: method/path, source, request, success, empty, error, permission, consumer, fixture/evidence",
1349
+ "- Activation and Target Files: explicit dev/test activation and authorized implementation paths",
1350
+ "- Production Safety: how Mock stays off and the real request remains default",
1351
+ "- Verification Plan: map the strategy to the fixed entrypoints below; do not propose replacement shell commands",
1352
+ "- Real Integration Gap: what remains unproved until the real backend is ready",
1353
+ "- Blocking Issues: any spec gaps, path violations, missing verify commands, or conflicts",
1354
+ "",
1355
+ "## Rules:",
1356
+ "- Read project Mock/API/schema specifications before making any judgment.",
1357
+ "- Do not infer Mock service from lockfile-only or transitive dependency evidence.",
1358
+ "- 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.",
1359
+ "- 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.",
1360
+ "- Mock-backed behavior evidence proves the documented frontend contract only; it never proves real API integration.",
1361
+ "",
1362
+ "Read-only: do not modify repository files.",
1363
+ fixedVerificationContext,
1364
+ sourceContext,
1365
+ mockContextBlock,
1366
+ ].join("\n\n"),
1367
+ };
1368
+ }
1369
+ function buildFrontendMockContractGateNode(mockMode, configuredPolicy, readOnlyPaths, forbiddenPaths) {
1370
+ // A generation-time blocked decision is a hard fail-closed contract. Keep a
1371
+ // syntactically valid, impossible verdict so the shell gate can never pass
1372
+ // regardless of what the assessment model emits.
1373
+ const acceptedStrategies = mockMode === "blocked"
1374
+ ? ["MOCK_STRATEGY: __blocked__"]
1375
+ : configuredPolicy === "disabled"
1376
+ ? ["MOCK_STRATEGY: not-needed"]
1377
+ : [
1378
+ "MOCK_STRATEGY: native",
1379
+ "MOCK_STRATEGY: browser-intercept",
1380
+ "MOCK_STRATEGY: request-adapter",
1381
+ ...(configuredPolicy !== "required"
1382
+ ? ["MOCK_STRATEGY: not-needed"]
1383
+ : []),
1384
+ ];
1385
+ return {
1386
+ id: "frontend-mock-contract-gate-shell",
1387
+ depends_on: ["frontend-mock-assess-pi"],
1388
+ role: "verifier",
1389
+ executor: "shell",
1390
+ complexity: "LOW",
1391
+ writePolicy: "read-only",
1392
+ allowedPaths: readOnlyPaths,
1393
+ forbiddenPaths,
1394
+ outputContract: "Deterministic Mock contract gate: exit 0 only when frontend-mock-assess-pi selects an allowed non-blocked strategy. Does not authorize code writes.",
1395
+ 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.",
1396
+ shell: {
1397
+ commands: [],
1398
+ verdictGate: {
1399
+ fromNodeId: "frontend-mock-assess-pi",
1400
+ accept: acceptedStrategies,
1401
+ label: "frontend mock contract",
1402
+ lineMode: "first-non-empty",
1403
+ },
1404
+ cwd: ".",
1405
+ timeoutMs: 60000,
1406
+ },
1407
+ };
1408
+ }
1409
+ function buildFrontendMockVerifyNode(sources, implementId, readOnlyPaths, forbiddenPaths) {
1410
+ const capability = sources.frontendMockCapability;
1411
+ const taskConfig = sources.taskConfig;
1412
+ // Collect verify commands from task config, capability seed, and manifest
1413
+ const verifyCommands = [];
1414
+ // 1. Task config commands (highest priority)
1415
+ for (const cmd of taskConfig.frontendMock?.verifyCommands ?? []) {
1416
+ verifyCommands.push({
1417
+ label: cmd.label,
1418
+ args: ["bash", "-lc", cmd.command],
1419
+ cwd: sources.repoRoot ?? ".",
1420
+ timeoutMs: cmd.timeoutMs,
1421
+ });
1422
+ }
1423
+ // 2. Capability seed commands (from discovery)
1424
+ if (capability) {
1425
+ for (const cmd of capability.verifyCommands) {
1426
+ if (!verifyCommands.some((existing) => existing.label === cmd.label)) {
1427
+ verifyCommands.push(cmd);
1428
+ }
1429
+ }
1430
+ }
1431
+ // Fail closed: no commands = no verify shell
1432
+ const commands = verifyCommands.length > 0
1433
+ ? buildVerifyShellCommands({
1434
+ repoRoot: sources.repoRoot ?? ".",
1435
+ commands: verifyCommands,
1436
+ fallbackCommands: [],
1437
+ })
1438
+ : [];
1439
+ return {
1440
+ id: "frontend-mock-verify-shell",
1441
+ depends_on: [implementId],
1442
+ role: "verifier",
1443
+ executor: "shell",
1444
+ complexity: "LOW",
1445
+ writePolicy: "read-only",
1446
+ allowedPaths: readOnlyPaths,
1447
+ forbiddenPaths,
1448
+ outputContract: "Archived shell stdout/stderr with exit codes for deterministic Mock-specific verification; no worktree writes.",
1449
+ subtask_prompt: "Run deterministic Mock-specific verification (handler loading, endpoint matrix, fixture consumption, production boundary). Commands are frozen from generation-time trusted sources only.",
1450
+ shell: {
1451
+ commands,
1452
+ verifyEvidence: buildVerifyEvidence({
1453
+ phase: "intermediate",
1454
+ quota: "full",
1455
+ commandSource: commands.length > 0 ? "inline" : "adapter",
1456
+ commands: verifyCommands.length > 0 ? verifyCommands : undefined,
1457
+ fallbackCommands: [],
1458
+ }),
1459
+ cwd: ".",
1460
+ timeoutMs: 300000,
1461
+ },
1462
+ };
1463
+ }
1464
+ function buildBlockedFrontendMockDag(sources, sourceContext, readOnlyPaths, forbiddenPaths, globalConstraints) {
762
1465
  const { taskConfig } = sources;
763
- const forbiddenPaths = mergeForbiddenPaths(taskConfig);
764
- const implementPaths = resolveImplementPaths(taskConfig);
765
- const implementId = frontendImplementationNodeId();
766
- const sourceContext = buildSourceContextBlock(sources);
767
- const strategy = resolveDagVerifyStrategy(taskConfig);
768
- const readOnlyPaths = taskConfig.allowedPaths.length > 0 ? taskConfig.allowedPaths : ["**"];
769
- const behaviorPaths = deriveFrontendBehaviorPaths(taskConfig);
770
- const globalConstraints = [
771
- ...taskConfig.hardConstraints,
772
- ...(sources.constraintMarkdown
773
- ? [`See 执行约束.md in task source (${sources.taskId})`]
774
- : []),
775
- ...STANDARD_GLOBAL_CONSTRAINTS,
776
- "Frontend implementation DAGs must pass the final design verdict gate before any write node executes; the first design gate also accepts request-revision for plan revision only.",
777
- "Final design gate pass is the only authorization for frontend implementation writes.",
778
- "Plan revision remains read-only and never edits business code.",
779
- "Design revision failures route to replan-and-rerun, never dev-fix.",
780
- "frontend-implementation DAGs must complete deterministic static verification and behavior verification before final review.",
781
- "frontend review must block closeout unless review verdict is exactly VERDICT: pass.",
782
- ];
783
- const staticFallbackCommands = ["npm run typecheck", "npm run build"];
784
- const behaviorFallbackCommands = ["npm test"];
785
- const parsedFrontendVerifyCommands = extractFrontendVerifyCommandsFromMarkdown({
786
- repoRoot: sources.repoRoot,
787
- requirementMarkdown: sources.requirementMarkdown,
788
- constraintMarkdown: sources.constraintMarkdown,
789
- });
790
- const staticVerifyCommands = chooseFrontendVerifyCommands({
791
- parsedCommands: parsedFrontendVerifyCommands.staticCommands,
792
- adapterCommands: sources.verifyCommands?.intermediate,
793
- });
794
- const behaviorVerifyCommands = chooseFrontendVerifyCommands({
795
- parsedCommands: parsedFrontendVerifyCommands.behaviorCommands,
796
- adapterCommands: sources.verifyCommands?.final,
797
- });
1466
+ const mockContextBlock = resolveFrontendMockContextBlock(sources);
798
1467
  const spec = {
799
1468
  version: 3,
800
- title: `Frontend implementation DAG: ${taskConfig.title}`,
1469
+ title: `Frontend implementation DAG (BLOCKED Mock): ${taskConfig.title}`,
801
1470
  runtimeContract: GENERATED_DAG_RUNTIME_CONTRACT,
1471
+ outputLanguage: sources.outputLanguage ?? DEFAULT_DAG_OUTPUT_LANGUAGE,
802
1472
  objective: extractObjective(sources.requirementMarkdown, taskConfig.title),
803
1473
  successCriteria: extractSuccessCriteria(sources.requirementMarkdown, sources.taskId),
804
- globalConstraints,
1474
+ globalConstraints: [
1475
+ ...globalConstraints,
1476
+ "Mock contract is BLOCKED: writer nodes must not be reachable. Resolve blocking issues and re-generate DAG.",
1477
+ "Do not execute any write, verify, or closeout nodes. The DAG ends at the Mock contract gate.",
1478
+ ],
805
1479
  defaults: {
806
1480
  ...FRONTEND_DEFAULTS,
807
1481
  contextProfile: taskConfig.contextProfile,
808
1482
  },
809
1483
  skillsByRole: FRONTEND_SKILLS_BY_ROLE,
810
1484
  executorModels: sources.executorModelMatrix ?? DEFAULT_DAG_EXECUTOR_MODELS,
811
- verifyStrategy: resolveDagVerifyStrategy(taskConfig),
812
1485
  tasks: [
813
1486
  {
814
1487
  id: "frontend-contract-pi",
@@ -846,27 +1519,376 @@ function buildFrontendHybridDagFromTask(sources) {
846
1519
  sourceContext,
847
1520
  ].join("\n\n"),
848
1521
  },
849
- {
850
- id: "frontend-plan-pi",
851
- depends_on: ["frontend-scout-pi"],
852
- role: "planner",
853
- executor: "pi",
854
- complexity: "MED",
855
- writePolicy: "read-only",
856
- allowedPaths: readOnlyPaths,
857
- forbiddenPaths,
858
- 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.",
1522
+ buildFrontendMockAssessNode(sources, sourceContext, mockContextBlock, "No verification entrypoints were materialized because the generation-time Mock contract is blocked.", readOnlyPaths, forbiddenPaths),
1523
+ buildFrontendMockContractGateNode("blocked", taskConfig.frontendMock?.policy ?? "auto", readOnlyPaths, forbiddenPaths),
1524
+ ],
1525
+ };
1526
+ applyDefaultReadOnlyRetryPolicy(spec);
1527
+ parseDagSpec(spec);
1528
+ assertValidDagSpec(spec);
1529
+ return spec;
1530
+ }
1531
+ function resolveFrontendMockContextBlock(sources) {
1532
+ const capability = sources.frontendMockCapability;
1533
+ const mode = sources.frontendMockMode ?? "not-required";
1534
+ if (!capability)
1535
+ return "";
1536
+ const parts = [
1537
+ "## Frontend Mock Context",
1538
+ "",
1539
+ `Configured Policy: ${sources.taskConfig.frontendMock?.policy ?? "auto"}`,
1540
+ `Mock Decision: ${mode}`,
1541
+ `Capability Status: ${capability.status}`,
1542
+ ];
1543
+ if (capability.framework) {
1544
+ parts.push(`Detected Framework: ${capability.framework}`);
1545
+ }
1546
+ if (capability.serviceRoot) {
1547
+ parts.push(`Service Root: ${capability.serviceRoot}`);
1548
+ }
1549
+ if (capability.evidencePaths.length > 0) {
1550
+ parts.push(`Evidence Paths: ${capability.evidencePaths.join(", ")}`);
1551
+ }
1552
+ if (capability.verifyCommands.length > 0) {
1553
+ parts.push(`Frozen Mock Verify Commands: ${capability.verifyCommands
1554
+ .map((command) => command.label)
1555
+ .join(", ")}`);
1556
+ }
1557
+ if (capability.safetyViolation) {
1558
+ parts.push(`Safety Violation: ${capability.safetyViolation}`);
1559
+ }
1560
+ if (capability.reasons.length > 0) {
1561
+ parts.push(`Reasons: ${capability.reasons.join("; ")}`);
1562
+ }
1563
+ if (mode === "required") {
1564
+ 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.");
1565
+ }
1566
+ if (mode === "not-required") {
1567
+ 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.");
1568
+ }
1569
+ if (mode === "blocked") {
1570
+ parts.push("Mock contract is blocked. The DAG must stop before any write node executes.");
1571
+ }
1572
+ return parts.join("\n");
1573
+ }
1574
+ function resolveFrontendCapabilityContextBlock(sources) {
1575
+ const risk = sources.frontendRisk;
1576
+ const capability = sources.frontendProjectCapability;
1577
+ const parts = ["## Frontend risk & project capability", ""];
1578
+ if (risk) {
1579
+ parts.push(`Risk level: ${risk.selectedRisk}`, `Risk reason: ${risk.reason}`, `Risk signals: ${risk.signals.join(", ") || "(none)"}`, `Rejected signals: ${risk.rejectedSignals.join(", ") || "(none)"}`, `Force full gates: ${risk.forceFullGates}`);
1580
+ }
1581
+ else {
1582
+ parts.push("Risk level: standard (not precomputed)");
1583
+ }
1584
+ if (capability) {
1585
+ parts.push("", capability.adapterGuidance);
1586
+ if (capability.designEvidence.normativePaths.length > 0) {
1587
+ parts.push(`openSpec normative candidates: ${capability.designEvidence.normativePaths.slice(0, 12).join(", ")}`);
1588
+ }
1589
+ parts.push(`A11y capability: ${capability.a11y.status}` +
1590
+ (capability.a11y.tools.length
1591
+ ? ` (${capability.a11y.tools.join(", ")})`
1592
+ : ""));
1593
+ parts.push("Browser accessibility verification: not-run (out of scope for this workflow).");
1594
+ }
1595
+ return parts.join("\n");
1596
+ }
1597
+ function pruneFrontendTasksForRisk(tasks, risk) {
1598
+ if (risk.forceFullGates || risk.selectedRisk !== "small") {
1599
+ return tasks;
1600
+ }
1601
+ // small topology: drop first design gate + plan revision + second design cycle;
1602
+ // keep mock assess/gate, single design review (final), contract, implement, verify, repair chain, review.
1603
+ const drop = new Set([
1604
+ "frontend-design-gate-pi",
1605
+ "frontend-first-design-gate-shell",
1606
+ "frontend-plan-revision-pi",
1607
+ ]);
1608
+ // If we drop plan-revision, contract shell must depend on plan-pi instead; final design review depends on plan.
1609
+ const filtered = tasks.filter((task) => !drop.has(task.id));
1610
+ const byId = new Map(filtered.map((task) => [task.id, task]));
1611
+ const remap = (deps) => {
1612
+ if (!deps)
1613
+ return [];
1614
+ const next = [];
1615
+ for (const dep of deps) {
1616
+ if (dep === "frontend-plan-revision-pi") {
1617
+ if (byId.has("frontend-plan-pi"))
1618
+ next.push("frontend-plan-pi");
1619
+ continue;
1620
+ }
1621
+ if (dep === "frontend-first-design-gate-shell" ||
1622
+ dep === "frontend-design-gate-pi") {
1623
+ // skip removed gates
1624
+ continue;
1625
+ }
1626
+ if (byId.has(dep) || dep === "frontend-implement-pi")
1627
+ next.push(dep);
1628
+ }
1629
+ return [...new Set(next)];
1630
+ };
1631
+ return filtered.map((task) => {
1632
+ const depends_on = remap(task.depends_on);
1633
+ // Ensure final design review still has plan + mock + contract path
1634
+ if (task.id === "frontend-final-design-review-pi") {
1635
+ for (const need of [
1636
+ "frontend-plan-pi",
1637
+ "frontend-mock-assess-pi",
1638
+ "frontend-implementation-contract-shell",
1639
+ ]) {
1640
+ if (byId.has(need) && !depends_on.includes(need))
1641
+ depends_on.push(need);
1642
+ }
1643
+ }
1644
+ if (task.id === "frontend-requirement-coverage-shell") {
1645
+ const shell = task.shell?.requirementCoverageGate
1646
+ ? {
1647
+ ...task.shell,
1648
+ requirementCoverageGate: {
1649
+ ...task.shell.requirementCoverageGate,
1650
+ fromNodeIds: task.shell.requirementCoverageGate.fromNodeIds.map((nodeId) => nodeId === "frontend-plan-revision-pi"
1651
+ ? "frontend-plan-pi"
1652
+ : nodeId),
1653
+ },
1654
+ }
1655
+ : task.shell;
1656
+ return { ...task, depends_on, shell };
1657
+ }
1658
+ if (task.id === "frontend-implementation-contract-shell") {
1659
+ const nextDeps = depends_on.filter((dep) => dep !== "frontend-plan-revision-pi");
1660
+ if (!nextDeps.includes("frontend-plan-pi") &&
1661
+ byId.has("frontend-plan-pi")) {
1662
+ nextDeps.push("frontend-plan-pi");
1663
+ }
1664
+ const shell = task.shell
1665
+ ? {
1666
+ ...task.shell,
1667
+ jsonArtifactGate: task.shell.jsonArtifactGate
1668
+ ? {
1669
+ ...task.shell.jsonArtifactGate,
1670
+ fromNodeId: "frontend-plan-pi",
1671
+ }
1672
+ : task.shell.jsonArtifactGate,
1673
+ }
1674
+ : task.shell;
1675
+ return { ...task, depends_on: nextDeps, shell };
1676
+ }
1677
+ if (task.id === "frontend-implement-pi") {
1678
+ // still requires final design gate
1679
+ for (const need of [
1680
+ "frontend-final-design-gate-shell",
1681
+ "frontend-implementation-contract-shell",
1682
+ ]) {
1683
+ if (byId.has(need) && !depends_on.includes(need))
1684
+ depends_on.push(need);
1685
+ }
1686
+ }
1687
+ return { ...task, depends_on };
1688
+ });
1689
+ }
1690
+ function buildFrontendHybridDagFromTask(sources) {
1691
+ const { taskConfig } = sources;
1692
+ const mockCapability = sources.frontendMockCapability ?? {
1693
+ status: "absent",
1694
+ evidencePaths: [],
1695
+ verifyCommands: [],
1696
+ reasons: [
1697
+ "Frontend Mock capability was not precomputed; assessment must verify repository evidence.",
1698
+ ],
1699
+ };
1700
+ const mockMode = sources.frontendMockMode ??
1701
+ resolveFrontendMockMode(mockCapability, taskConfig, hasApiDependency(sources));
1702
+ const frontendSources = {
1703
+ ...sources,
1704
+ frontendMockCapability: mockCapability,
1705
+ frontendMockMode: mockMode,
1706
+ };
1707
+ const forbiddenPaths = mergeForbiddenPaths(taskConfig);
1708
+ const implementPaths = resolveImplementPaths(taskConfig);
1709
+ const implementId = frontendImplementationNodeId();
1710
+ const mockContextBlock = resolveFrontendMockContextBlock(frontendSources);
1711
+ const capabilityContextBlock = resolveFrontendCapabilityContextBlock(frontendSources);
1712
+ const frontendRisk = frontendSources.frontendRisk ??
1713
+ classifyFrontendRisk({
1714
+ title: taskConfig.title,
1715
+ requirementMarkdown: sources.requirementMarkdown,
1716
+ constraintMarkdown: sources.constraintMarkdown ?? undefined,
1717
+ allowedPaths: taskConfig.allowedPaths,
1718
+ complexity: taskConfig.complexity,
1719
+ });
1720
+ const sourceContext = [
1721
+ buildSourceContextBlock(sources),
1722
+ capabilityContextBlock,
1723
+ ]
1724
+ .filter(Boolean)
1725
+ .join("\n\n");
1726
+ const hasMockVerifyCommands = (taskConfig.frontendMock?.verifyCommands.length ?? 0) > 0 ||
1727
+ mockCapability.verifyCommands.length > 0;
1728
+ const requirementIds = buildDagSourceBinding(sources).requirementIds;
1729
+ const requirementCoverageInstruction = requirementIds.length > 0
1730
+ ? `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.`
1731
+ : "";
1732
+ const strategy = resolveDagVerifyStrategy(taskConfig);
1733
+ const readOnlyPaths = taskConfig.allowedPaths.length > 0 ? taskConfig.allowedPaths : ["**"];
1734
+ const behaviorPaths = deriveFrontendBehaviorPaths(taskConfig);
1735
+ const globalConstraints = [
1736
+ ...taskConfig.hardConstraints,
1737
+ ...(sources.constraintMarkdown
1738
+ ? [`See 执行约束.md in task source (${sources.taskId})`]
1739
+ : []),
1740
+ ...STANDARD_GLOBAL_CONSTRAINTS,
1741
+ "Frontend implementation DAGs must pass the final design verdict gate before any write node executes; the first design gate also accepts request-revision for plan revision only.",
1742
+ "Final design gate pass is the only authorization for frontend implementation writes.",
1743
+ "Plan revision remains read-only and never edits business code.",
1744
+ "Design revision failures route to replan-and-rerun, never dev-fix.",
1745
+ "Frontend planning must consume the read-only Mock assessment strategy produced after scouting; MOCK_STRATEGY: blocked must not pass the deterministic Mock contract gate.",
1746
+ "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.",
1747
+ "Mock-backed behavior evidence proves only the documented frontend contract, never real API integration.",
1748
+ "frontend-implementation DAGs must complete deterministic static verification and behavior verification before final review.",
1749
+ "frontend review must block closeout unless review verdict is exactly VERDICT: pass.",
1750
+ `Frontend risk classification: ${frontendRisk.selectedRisk} — ${frontendRisk.reason}`,
1751
+ frontendRisk.forceFullGates
1752
+ ? "High-risk or supervised: keep full design gates; do not weaken write boundaries."
1753
+ : "Risk-aware topology may omit redundant design revision nodes for small tasks only.",
1754
+ ];
1755
+ // Guard: blocked mode — generate assessment-only DAG with no writer reachable
1756
+ if (mockMode === "blocked") {
1757
+ return buildBlockedFrontendMockDag(frontendSources, sourceContext, readOnlyPaths, forbiddenPaths, globalConstraints);
1758
+ }
1759
+ const staticFallbackCommands = ["npm run typecheck", "npm run build"];
1760
+ const behaviorFallbackCommands = ["npm test"];
1761
+ const parsedFrontendVerifyCommands = extractFrontendVerifyCommandsFromMarkdown({
1762
+ repoRoot: sources.repoRoot,
1763
+ requirementMarkdown: sources.requirementMarkdown,
1764
+ constraintMarkdown: sources.constraintMarkdown,
1765
+ });
1766
+ const staticVerifyCommands = chooseFrontendVerifyCommands({
1767
+ parsedCommands: parsedFrontendVerifyCommands.staticCommands,
1768
+ adapterCommands: sources.verifyCommands?.intermediate,
1769
+ });
1770
+ const behaviorVerifyCommands = chooseFrontendVerifyCommands({
1771
+ parsedCommands: parsedFrontendVerifyCommands.behaviorCommands,
1772
+ adapterCommands: sources.verifyCommands?.final,
1773
+ });
1774
+ const staticShellCommands = buildVerifyShellCommands({
1775
+ repoRoot: sources.repoRoot,
1776
+ commands: staticVerifyCommands.commands,
1777
+ fallbackCommands: staticFallbackCommands,
1778
+ });
1779
+ const behaviorShellCommands = buildVerifyShellCommands({
1780
+ repoRoot: sources.repoRoot,
1781
+ commands: behaviorVerifyCommands.commands,
1782
+ fallbackCommands: behaviorFallbackCommands,
1783
+ });
1784
+ const staticVerifyEvidence = buildVerifyEvidence({
1785
+ phase: "intermediate",
1786
+ quota: strategy.intermediateQuota ?? "full",
1787
+ commandSource: staticVerifyCommands.commandSource,
1788
+ commands: staticVerifyCommands.commands,
1789
+ fallbackCommands: staticFallbackCommands,
1790
+ });
1791
+ const behaviorVerifyEvidence = buildVerifyEvidence({
1792
+ phase: "final",
1793
+ quota: "full",
1794
+ commandSource: behaviorVerifyCommands.commandSource,
1795
+ commands: behaviorVerifyCommands.commands,
1796
+ fallbackCommands: behaviorFallbackCommands,
1797
+ finalFullRequired: true,
1798
+ });
1799
+ const fixedVerificationContext = [
1800
+ "## Fixed frontend verification entrypoints",
1801
+ "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.",
1802
+ `- Static command source: ${staticVerifyEvidence.commandSource}`,
1803
+ ...staticVerifyEvidence.commandLabels.map((command) => ` - ${JSON.stringify(command)}`),
1804
+ `- Behavior command source: ${behaviorVerifyEvidence.commandSource}`,
1805
+ ...behaviorVerifyEvidence.commandLabels.map((command) => ` - ${JSON.stringify(command)}`),
1806
+ ].join("\n");
1807
+ const spec = {
1808
+ version: 3,
1809
+ title: `Frontend implementation DAG: ${taskConfig.title}`,
1810
+ runtimeContract: GENERATED_DAG_RUNTIME_CONTRACT,
1811
+ objective: extractObjective(sources.requirementMarkdown, taskConfig.title),
1812
+ successCriteria: extractSuccessCriteria(sources.requirementMarkdown, sources.taskId),
1813
+ globalConstraints,
1814
+ defaults: {
1815
+ ...FRONTEND_DEFAULTS,
1816
+ contextProfile: taskConfig.contextProfile,
1817
+ },
1818
+ skillsByRole: FRONTEND_SKILLS_BY_ROLE,
1819
+ executorModels: sources.executorModelMatrix ?? DEFAULT_DAG_EXECUTOR_MODELS,
1820
+ verifyStrategy: resolveDagVerifyStrategy(taskConfig),
1821
+ tasks: [
1822
+ {
1823
+ id: "frontend-contract-pi",
1824
+ depends_on: [],
1825
+ role: "planner",
1826
+ executor: "pi",
1827
+ complexity: "MED",
1828
+ writePolicy: "read-only",
1829
+ allowedPaths: readOnlyPaths,
1830
+ forbiddenPaths,
1831
+ skills: FRONTEND_IMPLEMENTATION_SKILLS,
1832
+ outputContract: "Markdown contract with Scope, Non-goals, Acceptance Criteria, UI States, Target Runtime Environment, Risks, and Verification Expectations. No file writes.",
1833
+ subtask_prompt: [
1834
+ "Read task source and produce a concise frontend implementation contract.",
1835
+ "Cover scope, non-goals, acceptance criteria, UI states, target runtime environment, risks, and verification expectations.",
1836
+ "Read-only: do not modify code, docs, artifacts, or repository files.",
1837
+ sourceContext,
1838
+ ].join("\n\n"),
1839
+ },
1840
+ {
1841
+ id: "frontend-scout-pi",
1842
+ depends_on: ["frontend-contract-pi"],
1843
+ role: "scout",
1844
+ executor: "pi",
1845
+ complexity: mapTaskComplexity(taskConfig.complexity),
1846
+ writePolicy: "read-only",
1847
+ allowedPaths: readOnlyPaths,
1848
+ forbiddenPaths,
1849
+ skills: FRONTEND_IMPLEMENTATION_SKILLS,
1850
+ 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.",
1851
+ subtask_prompt: [
1852
+ "Inspect frontend code, routing, components, styles, package scripts, and tests.",
1853
+ "Return code and design observations, existing reuse opportunities, and verification entry points.",
1854
+ "Read-only: do not modify repository files.",
1855
+ sourceContext,
1856
+ ].join("\n\n"),
1857
+ },
1858
+ // Mock assessment is always read-only and runs before planning.
1859
+ buildFrontendMockAssessNode(frontendSources, sourceContext, mockContextBlock, fixedVerificationContext, readOnlyPaths, forbiddenPaths),
1860
+ buildFrontendMockContractGateNode(mockMode, taskConfig.frontendMock?.policy ?? "auto", readOnlyPaths, forbiddenPaths),
1861
+ {
1862
+ id: "frontend-plan-pi",
1863
+ depends_on: [
1864
+ "frontend-contract-pi",
1865
+ "frontend-scout-pi",
1866
+ "frontend-mock-assess-pi",
1867
+ "frontend-mock-contract-gate-shell",
1868
+ ],
1869
+ role: "planner",
1870
+ executor: "pi",
1871
+ complexity: "MED",
1872
+ writePolicy: "read-only",
1873
+ allowedPaths: readOnlyPaths,
1874
+ forbiddenPaths,
1875
+ skills: FRONTEND_IMPLEMENTATION_SKILLS,
1876
+ 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, followed by exactly one fenced json object conforming to frontend-implementation-contract-v1 when this node is the effective plan source. No file writes.",
860
1877
  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.",
1878
+ "Based on frontend-contract-pi, frontend-scout-pi, and the gated frontend-mock-assess-pi strategy, return a minimal frontend implementation plan.",
1879
+ "Carry the selected Mock / API strategy, endpoint/fixture mapping, explicit activation, production-default-off rule, verification commands, and Real Integration Gap into the plan.",
1880
+ "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.",
1881
+ "End with exactly one fenced json object conforming to frontend-implementation-contract-v1 so small topology can materialize the contract without plan-revision.",
1882
+ requirementCoverageInstruction,
863
1883
  "Read-only: do not modify code, docs, artifacts, or repository files.",
1884
+ fixedVerificationContext,
864
1885
  sourceContext,
1886
+ mockContextBlock,
865
1887
  ].join("\n\n"),
866
1888
  },
867
1889
  {
868
1890
  id: "frontend-design-gate-pi",
869
- depends_on: ["frontend-plan-pi"],
1891
+ depends_on: ["frontend-plan-pi", "frontend-mock-assess-pi"],
870
1892
  role: "reviewer",
871
1893
  executor: "pi",
872
1894
  complexity: "MED",
@@ -878,8 +1900,10 @@ function buildFrontendHybridDagFromTask(sources) {
878
1900
  subtask_prompt: [
879
1901
  "Audit the frontend plan before implementation.",
880
1902
  "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.",
1903
+ "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.",
1904
+ "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
1905
  "Read-only: do not modify repository files.",
1906
+ fixedVerificationContext,
883
1907
  sourceContext,
884
1908
  ].join("\n\n"),
885
1909
  },
@@ -908,7 +1932,12 @@ function buildFrontendHybridDagFromTask(sources) {
908
1932
  },
909
1933
  {
910
1934
  id: "frontend-plan-revision-pi",
911
- depends_on: ["frontend-first-design-gate-shell", "frontend-plan-pi", "frontend-design-gate-pi"],
1935
+ depends_on: [
1936
+ "frontend-first-design-gate-shell",
1937
+ "frontend-plan-pi",
1938
+ "frontend-design-gate-pi",
1939
+ "frontend-mock-assess-pi",
1940
+ ],
912
1941
  role: "planner",
913
1942
  executor: "pi",
914
1943
  complexity: "MED",
@@ -916,22 +1945,84 @@ function buildFrontendHybridDagFromTask(sources) {
916
1945
  allowedPaths: readOnlyPaths,
917
1946
  forbiddenPaths,
918
1947
  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.",
1948
+ outputContract: "Markdown revision plan followed by exactly one fenced json object conforming to frontend-implementation-contract-v1. The JSON is the authoritative materialization input. No file writes.",
920
1949
  subtask_prompt: [
921
1950
  "Consume frontend-plan-pi (original plan) and frontend-design-gate-pi (first design review findings).",
922
1951
  "If the first design gate passed (VERDICT: pass from frontend-design-gate-pi), output exactly:",
923
1952
  "PASS_NO_REVISION_NEEDED",
924
1953
  "The original plan from frontend-plan-pi is confirmed and does not require changes.",
1954
+ "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
1955
  "",
926
1956
  "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.",
1957
+ "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.",
1958
+ requirementCoverageInstruction,
1959
+ "Do not turn MOCK_STRATEGY: blocked into an implementable strategy without new repository or contract evidence that resolves every blocker.",
928
1960
  "Read-only: do not modify code, docs, artifacts, or repository files. This node revises the plan only.",
1961
+ "End the response with exactly one fenced json object conforming to frontend-implementation-contract-v1. Bind it to the supplied task sources; map every requirement and applicable UI state to concrete implementation and verification targets or an explicit blocking evidence gap. Do not include secrets or unsafe paths.",
929
1962
  sourceContext,
930
1963
  ].join("\n\n"),
931
1964
  },
1965
+ ...(requirementIds.length > 0
1966
+ ? [
1967
+ {
1968
+ id: "frontend-requirement-coverage-shell",
1969
+ depends_on: ["frontend-plan-revision-pi"],
1970
+ role: "verifier",
1971
+ executor: "shell",
1972
+ complexity: "LOW",
1973
+ writePolicy: "read-only",
1974
+ allowedPaths: readOnlyPaths,
1975
+ forbiddenPaths,
1976
+ outputContract: "Deterministic current-run evidence that the original or revised frontend plan retains every explicit REQ-/BR-/AC- identifier from the bound task sources.",
1977
+ subtask_prompt: "Block final design review when the current run's plan facts omit any explicit requirement identifier from the authoritative task sources.",
1978
+ shell: {
1979
+ commands: [],
1980
+ requirementCoverageGate: {
1981
+ fromNodeIds: ["frontend-plan-revision-pi"],
1982
+ requiredIds: requirementIds,
1983
+ label: "frontend requirement coverage",
1984
+ },
1985
+ cwd: ".",
1986
+ timeoutMs: 60000,
1987
+ },
1988
+ },
1989
+ ]
1990
+ : []),
932
1991
  {
933
- id: "frontend-final-design-review-pi",
1992
+ id: "frontend-implementation-contract-shell",
934
1993
  depends_on: ["frontend-plan-revision-pi"],
1994
+ role: "verifier",
1995
+ executor: "shell",
1996
+ complexity: "LOW",
1997
+ writePolicy: "read-only",
1998
+ allowedPaths: readOnlyPaths,
1999
+ forbiddenPaths,
2000
+ outputContract: "Run-owned validated frontend-implementation-contract-v1 artifact path, schema id, and SHA-256.",
2001
+ subtask_prompt: "Materialize the effective frontend plan as a source-bound structured contract; fail closed on missing or invalid output.",
2002
+ shell: {
2003
+ commands: [],
2004
+ jsonArtifactGate: {
2005
+ fromNodeId: "frontend-plan-revision-pi",
2006
+ schemaId: "frontend-implementation-contract-v1",
2007
+ artifactName: "frontend-implementation-contract.json",
2008
+ outputDir: "contracts",
2009
+ },
2010
+ cwd: ".",
2011
+ timeoutMs: 60000,
2012
+ },
2013
+ },
2014
+ {
2015
+ id: "frontend-final-design-review-pi",
2016
+ depends_on: [
2017
+ "frontend-plan-revision-pi",
2018
+ "frontend-plan-pi",
2019
+ "frontend-design-gate-pi",
2020
+ "frontend-mock-assess-pi",
2021
+ "frontend-implementation-contract-shell",
2022
+ ...(requirementIds.length > 0
2023
+ ? ["frontend-requirement-coverage-shell"]
2024
+ : []),
2025
+ ],
935
2026
  role: "reviewer",
936
2027
  executor: "pi",
937
2028
  complexity: "MED",
@@ -945,8 +2036,11 @@ function buildFrontendHybridDagFromTask(sources) {
945
2036
  "First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
946
2037
  "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
2038
  "If frontend-plan-revision-pi revised the plan, verify that every Required Plan Correction from the first design review has been fully addressed.",
2039
+ "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.",
2040
+ "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
2041
  "Request revision if any design gap remains, if corrections are incomplete, or if the revised plan introduces new unaddressed issues.",
949
2042
  "Read-only: do not modify repository files.",
2043
+ fixedVerificationContext,
950
2044
  sourceContext,
951
2045
  ].join("\n\n"),
952
2046
  },
@@ -975,29 +2069,51 @@ function buildFrontendHybridDagFromTask(sources) {
975
2069
  },
976
2070
  {
977
2071
  id: implementId,
978
- depends_on: ["frontend-final-design-gate-shell", "frontend-plan-revision-pi", "frontend-final-design-review-pi"],
2072
+ depends_on: [
2073
+ "frontend-final-design-gate-shell",
2074
+ "frontend-implementation-contract-shell",
2075
+ "frontend-plan-revision-pi",
2076
+ "frontend-final-design-review-pi",
2077
+ "frontend-plan-pi",
2078
+ "frontend-mock-assess-pi",
2079
+ ],
979
2080
  role: "implementer",
980
2081
  executor: "pi",
981
2082
  toolProfile: "write",
982
- complexity: taskConfig.complexity === "large"
983
- ? "HIGH"
984
- : "MED",
2083
+ complexity: resolveWriterComplexity(taskConfig),
985
2084
  writePolicy: "exclusive",
986
2085
  writeSet: implementPaths.writeSet,
987
2086
  allowedPaths: implementPaths.allowedPaths,
988
2087
  forbiddenPaths,
989
2088
  skills: FRONTEND_IMPLEMENTATION_SKILLS,
990
- outputContract: "Markdown summary with Changed Files, Implemented Behavior, UI States Covered, Styling / Component Notes, Verification Attempted, and Residual Risks.",
2089
+ outputContract: "Markdown delivery summary with Contract Ref (path/schema/hash), Changed Files, Requirements Implemented, UI States, Tests Changed, Verification Attempts, Deviations, and Residual Risks. Follow fixed stages: contract confirm → tests → component/state → API/Mock → focused checks → diff cleanup.",
991
2090
  subtask_prompt: [
992
- "Implement the final approved frontend plan (from frontend-plan-revision-pi) with minimal focused changes.",
2091
+ "Implement against the validated run-owned Frontend Implementation Contract from frontend-implementation-contract-shell (path/schema/hash). Do not rebuild the contract from Markdown alone.",
2092
+ "Execute in fixed stages and report each in the delivery summary: (1) Contract confirm, (2) Tests sync, (3) Component/UI state implementation, (4) API/Mock wiring per contract.mockApi, (5) Focused checks behind frozen entrypoints only, (6) Diff cleanup.",
2093
+ "Map every requirement id and applicable UI state from the contract to concrete files. Do not invent shell verification commands; only frozen static/behavior entrypoints will run.",
2094
+ "Implement only the approved Mock strategy from frontend-mock-assess-pi as carried in the contract. 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
2095
  "The frontend-final-design-review-pi verdict confirmed the plan is ready. Stay within writeSet and preserve unrelated files.",
994
- "Do not write root artifacts/** unless explicitly included in writeSet.",
2096
+ "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.",
2097
+ "Do not write root artifacts/** unless explicitly included in writeSet. Do not claim Browser/visual verification.",
2098
+ writerDeliveryContract(taskConfig),
995
2099
  sourceContext,
996
- ].join("\n\n"),
2100
+ mockContextBlock,
2101
+ ]
2102
+ .filter((value) => Boolean(value))
2103
+ .join("\n\n"),
997
2104
  },
2105
+ // Optional dedicated Mock verification exists only when trusted commands
2106
+ // were frozen at generation time. Behavior verification remains required.
2107
+ ...(mockMode === "required" && hasMockVerifyCommands
2108
+ ? [
2109
+ buildFrontendMockVerifyNode(frontendSources, implementId, readOnlyPaths, forbiddenPaths),
2110
+ ]
2111
+ : []),
998
2112
  {
999
2113
  id: "frontend-static-verify-shell",
1000
- depends_on: [implementId],
2114
+ depends_on: mockMode === "required" && hasMockVerifyCommands
2115
+ ? ["frontend-mock-verify-shell"]
2116
+ : [implementId],
1001
2117
  role: "verifier",
1002
2118
  executor: "shell",
1003
2119
  complexity: "LOW",
@@ -1005,22 +2121,13 @@ function buildFrontendHybridDagFromTask(sources) {
1005
2121
  allowedPaths: readOnlyPaths,
1006
2122
  forbiddenPaths,
1007
2123
  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.",
2124
+ 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
2125
  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
- }),
2126
+ commands: staticShellCommands,
2127
+ verifyEvidence: staticVerifyEvidence,
1022
2128
  cwd: ".",
1023
2129
  timeoutMs: 300000,
2130
+ nonZeroExitPolicy: "record",
1024
2131
  },
1025
2132
  },
1026
2133
  {
@@ -1033,28 +2140,181 @@ function buildFrontendHybridDagFromTask(sources) {
1033
2140
  allowedPaths: behaviorPaths,
1034
2141
  forbiddenPaths,
1035
2142
  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.",
2143
+ 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
2144
  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
- }),
2145
+ commands: behaviorShellCommands,
2146
+ verifyEvidence: behaviorVerifyEvidence,
2147
+ cwd: ".",
2148
+ timeoutMs: 300000,
2149
+ nonZeroExitPolicy: "record",
2150
+ },
2151
+ },
2152
+ {
2153
+ id: "frontend-verification-trace-shell",
2154
+ depends_on: [
2155
+ "frontend-behavior-verify-shell",
2156
+ "frontend-static-verify-shell",
2157
+ "frontend-implementation-contract-shell",
2158
+ ],
2159
+ role: "verifier",
2160
+ executor: "shell",
2161
+ complexity: "LOW",
2162
+ writePolicy: "read-only",
2163
+ allowedPaths: readOnlyPaths,
2164
+ forbiddenPaths,
2165
+ outputContract: "Deterministic verification trace: contract verificationTargets bound to current-run static/behavior commandLabels; target files/symbols exist; Browser/visual not-run. No worktree writes.",
2166
+ subtask_prompt: "Validate AC/UI/verification targets against frozen shell evidence and workspace files. Do not invent commands. Does not prove semantic test quality.",
2167
+ shell: {
2168
+ commands: ["frontend-verification-trace-gate"],
2169
+ cwd: ".",
2170
+ timeoutMs: 120000,
2171
+ nonZeroExitPolicy: "record",
2172
+ },
2173
+ },
2174
+ {
2175
+ id: "frontend-failure-assess-shell",
2176
+ depends_on: [
2177
+ "frontend-verification-trace-shell",
2178
+ "frontend-behavior-verify-shell",
2179
+ "frontend-static-verify-shell",
2180
+ "frontend-implementation-contract-shell",
2181
+ ],
2182
+ role: "verifier",
2183
+ executor: "shell",
2184
+ complexity: "LOW",
2185
+ writePolicy: "read-only",
2186
+ allowedPaths: readOnlyPaths,
2187
+ forbiddenPaths,
2188
+ outputContract: "Run-owned frontend-repair-assessment-v1 at contracts/frontend-repair-assessment.json classifying verify/trace failures as repairable or not.",
2189
+ subtask_prompt: "Assess current-run static/behavior/trace failure facts against the validated contract. Do not repair code.",
2190
+ shell: {
2191
+ commands: ["frontend-failure-assess-gate"],
2192
+ cwd: ".",
2193
+ timeoutMs: 60000,
2194
+ },
2195
+ },
2196
+ {
2197
+ id: "frontend-repair-contract-shell",
2198
+ depends_on: ["frontend-failure-assess-shell"],
2199
+ role: "verifier",
2200
+ executor: "shell",
2201
+ complexity: "LOW",
2202
+ writePolicy: "read-only",
2203
+ allowedPaths: readOnlyPaths,
2204
+ forbiddenPaths,
2205
+ outputContract: "Deterministic repair eligibility gate: pass when no failure or repairable assessment; fail-closed on non-repairable classes.",
2206
+ subtask_prompt: "Validate frontend-repair-assessment writeSet subset, attempt limit, and eligibility before repair writer.",
2207
+ shell: {
2208
+ commands: ["frontend-repair-contract-gate"],
2209
+ cwd: ".",
2210
+ timeoutMs: 60000,
2211
+ },
2212
+ },
2213
+ {
2214
+ id: "frontend-repair-pi",
2215
+ depends_on: [
2216
+ "frontend-repair-contract-shell",
2217
+ "frontend-failure-assess-shell",
2218
+ implementId,
2219
+ ],
2220
+ role: "implementer",
2221
+ executor: "pi",
2222
+ toolProfile: "write",
2223
+ complexity: resolveWriterComplexity(taskConfig),
2224
+ writePolicy: "exclusive",
2225
+ writeSet: implementPaths.writeSet,
2226
+ allowedPaths: implementPaths.allowedPaths,
2227
+ forbiddenPaths,
2228
+ skills: FRONTEND_IMPLEMENTATION_SKILLS,
2229
+ outputContract: "Repair summary or explicit no-op when assessment has no failure/eligible=false path already gated. Must not expand writeSet, re-interpret requirements, skip tests, or enable Mock by default.",
2230
+ subtask_prompt: [
2231
+ "Read contracts/frontend-repair-assessment.json and the validated frontend implementation contract.",
2232
+ "If failedNodeIds is empty, return no-op with evidence. If eligible repairable failure, apply the smallest fix inside the original implement writeSet only.",
2233
+ "Do not change lint/type/test config, do not add .skip/.only, do not comment out real requests, do not default-enable Mock, do not add dependencies.",
2234
+ "Do not re-plan requirements or expand allowed paths. Browser/visual remain not-run.",
2235
+ writerDeliveryContract(taskConfig),
2236
+ sourceContext,
2237
+ mockContextBlock,
2238
+ ]
2239
+ .filter((value) => Boolean(value))
2240
+ .join("\n\n"),
2241
+ },
2242
+ {
2243
+ id: "frontend-static-reverify-shell",
2244
+ depends_on: ["frontend-repair-pi"],
2245
+ role: "verifier",
2246
+ executor: "shell",
2247
+ complexity: "LOW",
2248
+ writePolicy: "read-only",
2249
+ allowedPaths: readOnlyPaths,
2250
+ forbiddenPaths,
2251
+ outputContract: "Archived static re-verification after repair using the same frozen commands; fail on nonzero.",
2252
+ subtask_prompt: "Re-run frozen static entrypoints after repair. Fresh evidence only; do not rewrite prior failure artifacts.",
2253
+ shell: {
2254
+ commands: staticShellCommands,
2255
+ verifyEvidence: staticVerifyEvidence,
2256
+ cwd: ".",
2257
+ timeoutMs: 300000,
2258
+ },
2259
+ },
2260
+ {
2261
+ id: "frontend-behavior-reverify-shell",
2262
+ depends_on: ["frontend-static-reverify-shell"],
2263
+ role: "verifier",
2264
+ executor: "shell",
2265
+ complexity: "LOW",
2266
+ writePolicy: "read-only",
2267
+ allowedPaths: behaviorPaths,
2268
+ forbiddenPaths,
2269
+ outputContract: "Archived behavior re-verification after repair using the same frozen commands; fail on nonzero.",
2270
+ subtask_prompt: "Re-run frozen behavior entrypoints after repair.",
2271
+ shell: {
2272
+ commands: behaviorShellCommands,
2273
+ verifyEvidence: behaviorVerifyEvidence,
1051
2274
  cwd: ".",
1052
2275
  timeoutMs: 300000,
1053
2276
  },
1054
2277
  },
2278
+ {
2279
+ id: "frontend-verification-retrace-shell",
2280
+ depends_on: [
2281
+ "frontend-behavior-reverify-shell",
2282
+ "frontend-static-reverify-shell",
2283
+ "frontend-implementation-contract-shell",
2284
+ ],
2285
+ role: "verifier",
2286
+ executor: "shell",
2287
+ complexity: "LOW",
2288
+ writePolicy: "read-only",
2289
+ allowedPaths: readOnlyPaths,
2290
+ forbiddenPaths,
2291
+ outputContract: "Re-run verification trace against contract and reverify shell evidence after repair.",
2292
+ subtask_prompt: "Trace AC/UI/verification targets against post-repair static/behavior evidence.",
2293
+ shell: {
2294
+ commands: ["frontend-verification-trace-gate"],
2295
+ cwd: ".",
2296
+ timeoutMs: 120000,
2297
+ },
2298
+ },
1055
2299
  {
1056
2300
  id: "frontend-review-pi",
1057
- depends_on: ["frontend-behavior-verify-shell"],
2301
+ depends_on: [
2302
+ "frontend-verification-retrace-shell",
2303
+ "frontend-static-reverify-shell",
2304
+ "frontend-behavior-reverify-shell",
2305
+ "frontend-repair-pi",
2306
+ "frontend-failure-assess-shell",
2307
+ implementId,
2308
+ "frontend-implementation-contract-shell",
2309
+ "frontend-contract-pi",
2310
+ "frontend-plan-pi",
2311
+ "frontend-plan-revision-pi",
2312
+ "frontend-final-design-review-pi",
2313
+ "frontend-mock-assess-pi",
2314
+ ...(mockMode === "required" && hasMockVerifyCommands
2315
+ ? ["frontend-mock-verify-shell"]
2316
+ : []),
2317
+ ],
1058
2318
  role: "reviewer",
1059
2319
  executor: "pi",
1060
2320
  complexity: "HIGH",
@@ -1067,8 +2327,15 @@ function buildFrontendHybridDagFromTask(sources) {
1067
2327
  "Review the frontend implementation and verification evidence.",
1068
2328
  "First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
1069
2329
  "Any Critical or Important finding must force VERDICT: request-revision.",
2330
+ "Read the validated frontend-implementation-contract, frontend-verification-trace evidence, static/behavior shell facts, and actual diff. Trace proves command/file/symbol binding only—not semantic correctness.",
2331
+ "Flag .skip/.only, deleted or weakened tests, unauthorized config changes, Mock-only evidence claimed as real integration, and Browser/visual claims (always not-run in this workflow).",
2332
+ "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.",
2333
+ "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.",
2334
+ "Inspect the production/default-real-path static evidence directly and require Mock activation to be off for that check.",
2335
+ "Distinguish Mock-backed evidence from real API integration evidence and preserve the Real Integration Gap when the backend was not exercised.",
1070
2336
  "Review implementation quality, behavior/state coverage, verification evidence, and maintainability. Read-only: do not modify files.",
1071
2337
  sourceContext,
2338
+ mockContextBlock,
1072
2339
  ].join("\n\n"),
1073
2340
  },
1074
2341
  {
@@ -1096,7 +2363,20 @@ function buildFrontendHybridDagFromTask(sources) {
1096
2363
  },
1097
2364
  {
1098
2365
  id: "frontend-closeout-pi",
1099
- depends_on: ["frontend-review-gate-shell"],
2366
+ depends_on: [
2367
+ "frontend-review-gate-shell",
2368
+ "frontend-review-pi",
2369
+ "frontend-verification-retrace-shell",
2370
+ "frontend-static-reverify-shell",
2371
+ "frontend-behavior-reverify-shell",
2372
+ "frontend-repair-pi",
2373
+ "frontend-failure-assess-shell",
2374
+ "frontend-implementation-contract-shell",
2375
+ "frontend-mock-assess-pi",
2376
+ ...(mockMode === "required" && hasMockVerifyCommands
2377
+ ? ["frontend-mock-verify-shell"]
2378
+ : []),
2379
+ ],
1100
2380
  role: "closeout",
1101
2381
  executor: "pi",
1102
2382
  complexity: "MED",
@@ -1106,15 +2386,19 @@ function buildFrontendHybridDagFromTask(sources) {
1106
2386
  : ["**", "docs/**"],
1107
2387
  forbiddenPaths,
1108
2388
  skills: FRONTEND_VERIFICATION_SKILLS,
1109
- outputContract: "Markdown closeout summary with Changes, Verification Evidence, Review Result, Known Risks, and Follow-up. No file writes.",
2389
+ 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
2390
  subtask_prompt: [
1111
- "Return a frontend closeout summary covering changes, verification evidence, review result, known risks, and follow-up.",
2391
+ "Return a frontend closeout summary covering Mock decision/strategy/files/verification/production boundary, changes, verification evidence, review result, known risks, and follow-up.",
2392
+ "Include a coverage matrix for each requirement id, applicable UI state, and verification target/check with status passed|failed|not-run|blocked|unavailable. Always state Browser accessibility verification: not-run and Visual regression: not-run. Use frontend-verification-trace facts; do not invent Browser evidence from component tests.",
2393
+ `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
2394
  "Read-only: do not modify code, docs, artifacts, or .harness/dag-runs/.",
1113
2395
  sourceContext,
2396
+ mockContextBlock,
1114
2397
  ].join("\n\n"),
1115
2398
  },
1116
2399
  ],
1117
2400
  };
2401
+ spec.tasks = pruneFrontendTasksForRisk(spec.tasks, frontendRisk);
1118
2402
  applyDefaultReadOnlyRetryPolicy(spec);
1119
2403
  parseDagSpec(spec);
1120
2404
  assertValidDagSpec(spec);
@@ -1133,53 +2417,102 @@ function buildAnalyzeInputsNode(sources) {
1133
2417
  writePolicy: "read-only",
1134
2418
  allowedPaths: commonReadOnlyPaths(sources),
1135
2419
  forbiddenPaths: commonForbiddenPaths(sources),
1136
- outputContract: "Structured Markdown extracting core content from source documents. No file writes.",
2420
+ outputContract: "Pure Backend Test Analysis v1 JSON object matching docs/templates/backend-test-analysis.schema.json. No Markdown prose and no file writes.",
1137
2421
  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.",
2422
+ "Read the task source materials and return exactly one JSON object matching Backend Test Analysis v1.",
2423
+ "Do not wrap it in explanatory prose. A single fenced json block is tolerated, but pure JSON is preferred.",
2424
+ "Copy taskId, requirementPath, requirementSha256, referencePaths, and requirementIds exactly from the DAG source binding shown below.",
2425
+ "Preserve existing AC IDs. Do not invent endpoint methods, paths, fields, errors, boundaries, or business rules; record unknowns in evidenceGaps.",
2426
+ "Use empty arrays for categories not documented. Never include credentials, tokens, private keys, or secret values.",
2427
+ "Required top-level keys: schemaVersion, sourceBinding, acceptanceCriteria, endpoints, dataModels, businessRules, stateTransitions, boundaryConstraints, externalDependencies, risks, evidenceGaps.",
1174
2428
  "Read-only: do not modify code, docs, artifacts, or repository files.",
1175
2429
  buildSourceContextBlock(sources),
1176
2430
  ].join("\n\n"),
1177
2431
  };
1178
2432
  }
2433
+ function buildBackendTestAnalysisContractGateNode(sources) {
2434
+ return {
2435
+ id: "backend-test-analysis-contract-shell",
2436
+ depends_on: ["analyze-inputs-pi"],
2437
+ role: "verifier",
2438
+ executor: "shell",
2439
+ complexity: "LOW",
2440
+ writePolicy: "read-only",
2441
+ allowedPaths: commonReadOnlyPaths(sources),
2442
+ forbiddenPaths: commonForbiddenPaths(sources),
2443
+ outputContract: "Validated run-owned Backend Test Analysis v1 artifact pointer, schema ID, and SHA-256.",
2444
+ subtask_prompt: "Materialize and validate the backend-test analysis contract under the current DAG run.",
2445
+ shell: {
2446
+ commands: [],
2447
+ jsonArtifactGate: {
2448
+ fromNodeId: "analyze-inputs-pi",
2449
+ schemaId: "backend-test-analysis-v1",
2450
+ artifactName: "backend-test-analysis.json",
2451
+ outputDir: "contracts",
2452
+ },
2453
+ cwd: ".",
2454
+ timeoutMs: 60000,
2455
+ },
2456
+ };
2457
+ }
2458
+ function buildBackendTestEnvironmentScoutNode(sources) {
2459
+ return {
2460
+ id: "backend-test-environment-scout-pi",
2461
+ depends_on: ["backend-test-analysis-contract-shell"],
2462
+ role: "scout",
2463
+ executor: "pi",
2464
+ complexity: "MED",
2465
+ writePolicy: "read-only",
2466
+ allowedPaths: commonReadOnlyPaths(sources),
2467
+ forbiddenPaths: commonForbiddenPaths(sources),
2468
+ outputContract: "Pure Backend Test Execution Contract v1 JSON object matching docs/templates/backend-test-execution.schema.json. No Markdown prose and no file writes.",
2469
+ subtask_prompt: [
2470
+ "Read-only environment scout for backend-test pytest MVP.",
2471
+ "Return exactly one JSON object matching Backend Test Execution Contract v1 (schema docs/templates/backend-test-execution.schema.json).",
2472
+ "Prefer pure JSON; a single fenced json block is tolerated; no trailing prose.",
2473
+ "Discover only non-secret evidence: pytest config files (pytest.ini / pyproject.toml / setup.cfg test paths), candidate test roots, existing fixtures/clients, documented run commands, and env *names* (not values).",
2474
+ "Do NOT search the whole repo for secrets, .env values, tokens, private keys, or production credentials.",
2475
+ 'framework must be "pytest". Default targetMode to "in-process" unless evidence clearly shows an external service base URL env name or documented managed start/stop with sourceRef.',
2476
+ 'Do NOT select targetMode "managed-command" unless task source documents a safe start/stop command with an explicit sourceRef; otherwise leave managedCommand absent and record the gap in evidenceGaps.',
2477
+ "testRoot and workingDirectory must be repo-relative posix paths without .. or absolute form. Adapter default testRoot is testcase when evidence is incomplete.",
2478
+ "runner must not include secret values. report.format must be junit with a relativeHint under the run (e.g. reports/backend-test-junit.xml).",
2479
+ "requiredEnvNames lists env NAMES only. baseUrlEnvName is required only for external-running-service and must match ^[A-Z_][A-Z0-9_]*$.",
2480
+ "Record incomplete discovery in evidenceGaps. Populate evidenceRefs with repo-relative paths actually read.",
2481
+ "Required top-level keys: schemaVersion, framework, runner, testRoot, workingDirectory, report, targetMode, existingFixtures, authenticationMode, requiredEnvNames, dataIsolation, evidenceGaps, evidenceRefs.",
2482
+ "Read-only: do not modify code, docs, artifacts, or repository files.",
2483
+ buildSourceContextBlock(sources),
2484
+ ].join("\n\n"),
2485
+ };
2486
+ }
2487
+ function buildBackendTestExecutionContractGateNode(sources) {
2488
+ return {
2489
+ id: "backend-test-execution-contract-shell",
2490
+ depends_on: ["backend-test-environment-scout-pi"],
2491
+ role: "verifier",
2492
+ executor: "shell",
2493
+ complexity: "LOW",
2494
+ writePolicy: "read-only",
2495
+ allowedPaths: commonReadOnlyPaths(sources),
2496
+ forbiddenPaths: commonForbiddenPaths(sources),
2497
+ outputContract: "Validated run-owned Backend Test Execution Contract v1 artifact pointer, schema ID, and SHA-256.",
2498
+ subtask_prompt: "Materialize and validate the backend-test execution contract under the current DAG run.",
2499
+ shell: {
2500
+ commands: [],
2501
+ jsonArtifactGate: {
2502
+ fromNodeId: "backend-test-environment-scout-pi",
2503
+ schemaId: "backend-test-execution-v1",
2504
+ artifactName: "backend-test-execution.json",
2505
+ outputDir: "contracts",
2506
+ },
2507
+ cwd: ".",
2508
+ timeoutMs: 60000,
2509
+ },
2510
+ };
2511
+ }
1179
2512
  function buildGenerateBackendFunctionalCasesNode(sources) {
1180
2513
  return {
1181
2514
  id: "generate-backend-functional-cases-pi",
1182
- depends_on: ["analyze-inputs-pi"],
2515
+ depends_on: ["backend-test-execution-contract-shell"],
1183
2516
  role: "implementer",
1184
2517
  executor: "pi",
1185
2518
  toolProfile: "write",
@@ -1191,7 +2524,8 @@ function buildGenerateBackendFunctionalCasesNode(sources) {
1191
2524
  // 注意:Pi 节点超时由 executor 层控制(默认 30 分钟)
1192
2525
  // 如需调整,在 harness.json 的 executors.pi 中配置 modelConfig.timeoutMs
1193
2526
  subtask_prompt: [
1194
- "Based on the upstream analyze-inputs-pi output, generate structured backend functional test cases.",
2527
+ "Read the validated structured artifact pointer from backend-test-analysis-contract-shell and generate cases only from that JSON contract.",
2528
+ ,
1195
2529
  "",
1196
2530
  "## Output Steps (do in order):",
1197
2531
  "1. First, output a brief summary: how many modules, how many cases planned per module",
@@ -1206,9 +2540,9 @@ function buildGenerateBackendFunctionalCasesNode(sources) {
1206
2540
  "## Coverage Requirements:",
1207
2541
  "- Positive paths: happy path for each acceptance criterion",
1208
2542
  "- Negative paths: error scenarios (invalid input, not found, state violations)",
1209
- "- Boundary conditions: empty input, max length, edge values",
1210
2543
  "",
1211
2544
  "## Conditional Coverage (include ONLY if mentioned in upstream analysis):",
2545
+ "- Boundary conditions: include ONLY if upstream analyze-inputs-pi mentions value ranges, length limits, numeric bounds, or format constraints",
1212
2546
  "- State transitions: include ONLY if upstream analyze-inputs-pi mentions state machine",
1213
2547
  "- Authentication scenarios: include ONLY if upstream analyze-inputs-pi mentions auth mechanism",
1214
2548
  "- Timeout scenarios: include ONLY if upstream analyze-inputs-pi mentions timeout handling",
@@ -1217,15 +2551,92 @@ function buildGenerateBackendFunctionalCasesNode(sources) {
1217
2551
  "",
1218
2552
  "## Constraints:",
1219
2553
  "- Stay within writeSet: testcase/md/**",
1220
- "- Do NOT re-read source documents — use the upstream analyze-inputs-pi output only",
2554
+ "- Do NOT re-read source documents or fall back to free-form analysis — use the validated structured artifact only",
2555
+ ,
1221
2556
  "- Do not write root artifacts/**",
1222
2557
  ].join("\n\n"),
1223
2558
  };
1224
2559
  }
2560
+ function buildEmitBackendCaseManifestNode(sources) {
2561
+ return {
2562
+ id: "emit-backend-case-manifest-pi",
2563
+ depends_on: [
2564
+ "generate-backend-functional-cases-pi",
2565
+ "backend-test-analysis-contract-shell",
2566
+ ],
2567
+ role: "scout",
2568
+ executor: "pi",
2569
+ complexity: "MED",
2570
+ writePolicy: "read-only",
2571
+ allowedPaths: commonReadOnlyPaths(sources),
2572
+ forbiddenPaths: commonForbiddenPaths(sources),
2573
+ outputContract: "Pure Backend Test Case Manifest v1 JSON (schema docs/templates/backend-test-case-manifest.schema.json). No file writes; model must not write .harness/**.",
2574
+ subtask_prompt: [
2575
+ "Emit Backend Test Case Manifest v1 as pure JSON (or one fenced json block with no trailing text).",
2576
+ "Read-only: use validated contracts/backend-test-analysis.json pointer + testcase/md/** only. Do not write repository files or .harness/**.",
2577
+ "sourceBinding must match the analysis contract / DAG source binding exactly (taskId, requirementPath, requirementSha256, referencePaths, requirementIds).",
2578
+ "For each functional case under testcase/md/: caseId BE-<MODULE>-<NNN>, acIds[], title, category, automationStatus.",
2579
+ "After case generation (pre-pytest), default automationStatus=planned. Use skipped/unsupported only with gapReason. Use generated only when file+symbol already exist.",
2580
+ "evidenceGaps: structured gaps for explicit AC-* that cannot be mapped to a case.",
2581
+ "Do NOT invent coverage percentages. Optional coverageSummary must match deterministic counts (gate recomputes/validates).",
2582
+ "No secrets or credential-shaped fields.",
2583
+ ].join("\n\n"),
2584
+ };
2585
+ }
2586
+ function buildBackendTestCaseManifestGateNode(sources) {
2587
+ return {
2588
+ id: "backend-test-case-manifest-shell",
2589
+ depends_on: ["emit-backend-case-manifest-pi"],
2590
+ role: "verifier",
2591
+ executor: "shell",
2592
+ complexity: "LOW",
2593
+ writePolicy: "read-only",
2594
+ allowedPaths: commonReadOnlyPaths(sources),
2595
+ forbiddenPaths: commonForbiddenPaths(sources),
2596
+ outputContract: "Validated run-owned Backend Test Case Manifest v1 at contracts/backend-test-case-manifest.json (schemaId backend-test-case-manifest-v1) with deterministic AC coverage.",
2597
+ subtask_prompt: "Materialize and validate Backend Test Case Manifest v1; fail closed on duplicate IDs, unknown AC, missing AC coverage without gap, or skipped without gapReason.",
2598
+ shell: {
2599
+ commands: [],
2600
+ jsonArtifactGate: {
2601
+ fromNodeId: "emit-backend-case-manifest-pi",
2602
+ schemaId: "backend-test-case-manifest-v1",
2603
+ artifactName: "backend-test-case-manifest.json",
2604
+ outputDir: "contracts",
2605
+ },
2606
+ cwd: ".",
2607
+ timeoutMs: 60000,
2608
+ },
2609
+ };
2610
+ }
2611
+ function buildBackendTestTraceabilityGateNode(sources) {
2612
+ return {
2613
+ id: "backend-test-traceability-gate-shell",
2614
+ depends_on: [
2615
+ "generate-backend-pytest-pi",
2616
+ "backend-test-case-manifest-shell",
2617
+ ],
2618
+ role: "verifier",
2619
+ executor: "shell",
2620
+ complexity: "LOW",
2621
+ writePolicy: "read-only",
2622
+ allowedPaths: commonReadOnlyPaths(sources),
2623
+ forbiddenPaths: commonForbiddenPaths(sources),
2624
+ outputContract: "Deterministic traceability: generated cases have real file/symbol; skipped/unsupported have gapReason; convention symbols scanned under testcase/**/test_*.py.",
2625
+ subtask_prompt: "Fail closed when generated automation claims do not resolve to workspace pytest symbols, or skip/unsupported lacks gapReason.",
2626
+ shell: {
2627
+ commands: ["backend-test-traceability-gate"],
2628
+ cwd: ".",
2629
+ timeoutMs: 60000,
2630
+ },
2631
+ };
2632
+ }
1225
2633
  function buildReviewBackendCasesNode(sources) {
1226
2634
  return {
1227
2635
  id: "review-backend-cases-pi",
1228
- depends_on: ["generate-backend-functional-cases-pi"],
2636
+ depends_on: [
2637
+ "backend-test-case-manifest-shell",
2638
+ "backend-test-analysis-contract-shell",
2639
+ ],
1229
2640
  role: "reviewer",
1230
2641
  executor: "pi",
1231
2642
  complexity: "HIGH",
@@ -1234,7 +2645,7 @@ function buildReviewBackendCasesNode(sources) {
1234
2645
  forbiddenPaths: commonForbiddenPaths(sources),
1235
2646
  outputContract: "Plain Markdown whose first non-empty line is VERDICT: pass or VERDICT: request-revision; followed by Findings and Coverage Assessment. No file writes.",
1236
2647
  subtask_prompt: [
1237
- "Review the generated backend functional test cases under testcase/md/.",
2648
+ "Review the generated backend functional test cases under testcase/md/ and the validated Case Manifest v1.",
1238
2649
  "",
1239
2650
  "## Mandatory First Line:",
1240
2651
  "First non-empty line must be exactly: VERDICT: pass or VERDICT: request-revision",
@@ -1243,12 +2654,13 @@ function buildReviewBackendCasesNode(sources) {
1243
2654
  "- ID format: every case uses BE-<MODULE>-<NNN>",
1244
2655
  "- Positive coverage: each acceptance criterion (AC-xxx) has happy-path case",
1245
2656
  "- Negative coverage: error scenarios (invalid input, not found, state violations)",
1246
- "- Boundary coverage: edge cases (empty, max length, edge values)",
1247
- "- Traceability: each AC maps to at least one case ID",
2657
+ "- Traceability: each AC maps to at least one case ID (prefer contracts/backend-test-case-manifest.json coverageSummary)",
1248
2658
  "- Case structure: ID, Title, Precondition, Steps, Expected Result",
1249
2659
  "- No duplicate IDs across files",
2660
+ "- Manifest consistency: MD cases align with manifest caseId/acIds; do not invent coverage %",
1250
2661
  "",
1251
2662
  "## Conditional Coverage (check ONLY if mentioned in upstream analysis):",
2663
+ "- Boundary coverage: check ONLY if analyze-inputs-pi mentions value ranges, length limits, numeric bounds, or format constraints",
1252
2664
  "- State transition coverage: check ONLY if analyze-inputs-pi mentions state machine",
1253
2665
  "- Authentication coverage: check ONLY if analyze-inputs-pi mentions auth mechanism",
1254
2666
  "- Timeout coverage: check ONLY if analyze-inputs-pi mentions timeout handling",
@@ -1260,13 +2672,13 @@ function buildReviewBackendCasesNode(sources) {
1260
2672
  "- Any Critical fails OR Important > 2 → VERDICT: request-revision",
1261
2673
  "",
1262
2674
  "## Output After Verdict:",
1263
- "1. Coverage Assessment table (AC → case IDs)",
2675
+ "1. Coverage Assessment table (AC → case IDs) using manifest + MD",
1264
2676
  "2. Findings list (Critical/Important/Informational)",
1265
2677
  "3. Statistics (total cases, positive/negative/boundary breakdown)",
1266
2678
  "",
1267
2679
  "## Constraints:",
1268
2680
  "- Read-only: do not modify files",
1269
- "- Do NOT re-read source documents use upstream analyze-inputs-pi output for acceptance criteria",
2681
+ "- Read validated analysis + case manifest artifacts; do not recompute coverage percentages",
1270
2682
  "- Use testcase/md/ files for case review",
1271
2683
  ].join("\n\n"),
1272
2684
  };
@@ -1299,20 +2711,36 @@ function buildReviewBackendCasesGateNode(sources) {
1299
2711
  function buildGenerateBackendPytestNode(sources) {
1300
2712
  return {
1301
2713
  id: "generate-backend-pytest-pi",
1302
- depends_on: ["review-backend-cases-gate-shell"],
2714
+ depends_on: [
2715
+ "review-backend-cases-gate-shell",
2716
+ "backend-test-execution-contract-shell",
2717
+ ],
1303
2718
  role: "implementer",
1304
2719
  executor: "pi",
1305
2720
  toolProfile: "write",
1306
2721
  complexity: "HIGH",
1307
2722
  writePolicy: "exclusive",
1308
- writeSet: ["testcase/**/test_*.py"],
1309
- allowedPaths: ["testcase/**/test_*.py"],
2723
+ // test_*.py plus optional helpers/factories under testcase/ (not conftest/config)
2724
+ writeSet: [
2725
+ "testcase/**/test_*.py",
2726
+ "testcase/**/helpers/**",
2727
+ "testcase/**/factories/**",
2728
+ ],
2729
+ // Union task allowedPaths with testcase/** so writeSet stays in scope even when
2730
+ // task.json only lists product paths (e.g. ./src/**). Writes still gated by writeSet.
2731
+ allowedPaths: Array.from(new Set([...commonReadOnlyPaths(sources), "testcase/**"])),
1310
2732
  forbiddenPaths: commonForbiddenPaths(sources),
1311
2733
  // 注意:Pi 节点超时由 executor 层控制(默认 30 分钟)
1312
2734
  // 如需调整,在 harness.json 的 executors.pi 中配置 modelConfig.timeoutMs
1313
2735
  subtask_prompt: [
1314
2736
  "Convert the reviewed test cases under testcase/md/ into pytest automation code.",
1315
2737
  "",
2738
+ "## Inputs (MUST use validated contracts):",
2739
+ "- Reviewed cases under testcase/md/ (after review-backend-cases-gate-shell).",
2740
+ "- Validated Backend Test Analysis v1 under the current run contracts/ (analysis gate).",
2741
+ "- Validated Backend Test Execution Contract v1 under contracts/backend-test-execution.json (execution gate).",
2742
+ "Use only fixture names, env NAMES, testRoot, targetMode, and field/API facts already present in those contracts or reviewed cases. Do not invent production credentials or secret values.",
2743
+ "",
1316
2744
  "## Output Steps (do in order):",
1317
2745
  "1. First, output a brief summary: how many files, how many test functions planned",
1318
2746
  "2. Then write each test file under testcase/",
@@ -1325,48 +2753,127 @@ function buildGenerateBackendPytestNode(sources) {
1325
2753
  "",
1326
2754
  "## Implementation Rules:",
1327
2755
  "- Use assert statements, not unittest assertions",
1328
- "- Assert specific values, not just 'no exception'",
1329
- "- Use @pytest.mark.parametrize for boundary cases",
2756
+ "- Use @pytest.mark.parametrize for boundary cases when the case defines edge values",
1330
2757
  "- Use markers: @pytest.mark.positive, @pytest.mark.negative, @pytest.mark.boundary",
1331
2758
  "",
2759
+ "## Test Data Preparation Rules (MUST follow):",
2760
+ "",
2761
+ "### When Setup is Needed",
2762
+ "Setup phase is REQUIRED only when test cases need pre-existing data:",
2763
+ "- Query/Read APIs: need data to exist before querying",
2764
+ "- Update/Delete APIs: need data to exist before modifying",
2765
+ "- State transition tests: need data in specific state",
2766
+ "",
2767
+ "Setup phase is NOT needed for:",
2768
+ "- Create APIs: testing the creation itself",
2769
+ "- Validation tests: testing input validation with invalid data",
2770
+ "",
2771
+ "### Data Setup Strategy",
2772
+ "When setup is needed:",
2773
+ "1. Prefer function-scoped fixtures for isolation; use module/session scope only when cases explicitly share immutable fixtures",
2774
+ "2. Prefer API-based setup from the upstream analyze-inputs-pi API list and reviewed cases",
2775
+ "3. If a required helper/factory is missing, create NEW files only under testcase/**/helpers/** or testcase/**/factories/**",
2776
+ "",
2777
+ "### Data Construction Priority",
2778
+ "1. API-first: construct data via documented APIs from analyze-inputs-pi / reviewed cases",
2779
+ "2. Reuse existing conftest fixtures when present (read-only)",
2780
+ "3. Direct DB writes are LAST RESORT and only if conftest already exposes a safe test DB fixture with rollback/isolation",
2781
+ "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",
2782
+ "",
2783
+ "### API Data Construction",
2784
+ "- Prefer the analyze-inputs-pi API Endpoints section and reviewed cases for method/path/fields",
2785
+ "- Chain API calls only when cases document multi-step preconditions",
2786
+ "- Store created resource IDs in fixtures for reuse",
2787
+ "- Do NOT broadly search host route/controller trees for secrets, .env, private keys, or production configs",
2788
+ "- Read host API definitions only when needed to resolve a field name already referenced by reviewed cases; stay out of credential/config paths",
2789
+ "",
2790
+ "### Database Data Construction (restricted)",
2791
+ "- Allowed only via existing conftest test-DB fixtures with transaction rollback or equivalent isolation",
2792
+ "- Never hardcode connection strings, passwords, tokens, or cloud credentials",
2793
+ "- Never target production/shared non-test databases",
2794
+ "- If isolation is unclear, report the gap instead of writing DB rows",
2795
+ "",
2796
+ "## Assertion Rules (MUST follow):",
2797
+ "",
2798
+ "### Positive Path",
2799
+ "MUST assert ALL of the following:",
2800
+ "1. HTTP status code: as defined in API spec (e.g. 200, 201)",
2801
+ "2. Response structure: key fields exist in response body",
2802
+ "3. Specific values: each field equals expected value from test case",
2803
+ "4. Data type: each field is correct type",
2804
+ "",
2805
+ "### Negative Path",
2806
+ "MUST assert ALL of the following:",
2807
+ "1. HTTP status code: as defined in API spec (e.g. 400, 404, 500)",
2808
+ "2. Error code field: field name from API spec (e.g. code, error_code, errcode, ret)",
2809
+ "3. Error message field: field name from API spec (e.g. message, msg, errmsg, error)",
2810
+ "",
2811
+ "### Field Name Resolution",
2812
+ "Field names MUST come from the upstream analyze-inputs-pi output (API Endpoints section) or reviewed cases, NOT guessed. For example:",
2813
+ '- If API spec defines {"ret": 0, "msg": "success"}, assert response.json()[\'ret\'] and response.json()[\'msg\']',
2814
+ '- If API spec defines {"code": 4001, "message": "error"}, assert response.json()[\'code\'] and response.json()[\'message\']',
2815
+ "",
1332
2816
  "## Conditional Implementation (include ONLY if test cases exist):",
1333
2817
  "- Authentication tests: implement ONLY if testcase/md/ contains auth-related cases",
1334
2818
  "- Timeout tests: implement ONLY if testcase/md/ contains timeout-related cases",
2819
+ "- Boundary tests: implement ONLY when cases define value ranges, length limits, or format constraints",
1335
2820
  "- Use @pytest.mark.auth for auth tests, @pytest.mark.timeout for timeout tests",
1336
2821
  "- If no such cases exist, do NOT add these tests",
1337
2822
  "",
1338
2823
  "## 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",
2824
+ "- Only create NEW files under writeSet: testcase/**/test_*.py, testcase/**/helpers/**, testcase/**/factories/**",
2825
+ "- Do NOT modify existing framework files (conftest.py, pytest.ini, pyproject.toml, setup.cfg, __init__.py)",
2826
+ "- If a test filename exists, add suffix: test_order.py → test_order_01.py",
2827
+ "- Do NOT re-read source documents — use reviewed cases under testcase/md/ and upstream analyze-inputs-pi output only",
1343
2828
  "- Read existing conftest.py/pytest.ini to understand conventions, but do NOT modify them",
1344
2829
  ].join("\n\n"),
1345
2830
  };
1346
2831
  }
1347
2832
  function buildExecuteBackendPytestNode(sources) {
2833
+ // Keep the target worktree read-only: JUnit is runner-owned evidence under
2834
+ // the current DAG run and moves with active → completed/paused lifecycle.
2835
+ // Adapter default testRoot is frozen at DAG generation time (auditable) and
2836
+ // cross-checked against the materialized execution contract in preflight.
2837
+ const frozenTestRoot = BACKEND_TEST_EXECUTION_DEFAULT_TEST_ROOT;
2838
+ const preflightCommand = buildBackendTestExecutionPreflightShellSnippet({
2839
+ expectedTestRoot: frozenTestRoot,
2840
+ });
2841
+ // Map pytest exit 0/1 → node success ONLY when JUnit exists (assertion-fail is a
2842
+ // legal result). Do not change global shell ok semantics. Persist raw exit for parse.
2843
+ const pytestCommand = [
2844
+ preflightCommand,
2845
+ 'REPORT="${HARNESS_DAG_RUN_DIR}/reports/backend-test-junit.xml"',
2846
+ 'EXIT_FILE="${HARNESS_DAG_RUN_DIR}/reports/backend-test-pytest-exit.txt"',
2847
+ 'mkdir -p "$(dirname "${REPORT}")"',
2848
+ `PYTHONDONTWRITEBYTECODE=1 python -m pytest ${frozenTestRoot}/ -v -p no:cacheprovider --junitxml="\${REPORT}"`,
2849
+ "STATUS=$?",
2850
+ 'printf "%s" "${STATUS}" > "${EXIT_FILE}"',
2851
+ 'printf "JUnit report: %s\\n" "${REPORT}"',
2852
+ 'printf "pytestExitCode=%s\\n" "${STATUS}"',
2853
+ 'if { [ "${STATUS}" -eq 0 ] || [ "${STATUS}" -eq 1 ]; } && [ -s "${REPORT}" ]; then exit 0; fi',
2854
+ 'exit "${STATUS}"',
2855
+ ].join("; ");
1348
2856
  return {
1349
2857
  id: "execute-backend-pytest-shell",
1350
- depends_on: ["generate-backend-pytest-pi"],
2858
+ depends_on: [
2859
+ "backend-test-traceability-gate-shell",
2860
+ "backend-test-execution-contract-shell",
2861
+ ],
1351
2862
  role: "verifier",
1352
2863
  executor: "shell",
1353
2864
  complexity: "LOW",
1354
2865
  writePolicy: "read-only",
1355
2866
  allowedPaths: commonReadOnlyPaths(sources),
1356
2867
  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.",
2868
+ outputContract: "Archived pytest stdout/stderr; raw pytestExitCode side-channel + JUnit at $HARNESS_DAG_RUN_DIR/reports/**. Exit 0/1 with non-empty JUnit finishes the node so parse/classify/retrospect can run; assertion failures remain recorded in exit file.",
2869
+ subtask_prompt: "Run pytest for the backend test suite; write JUnit + pytestExitCode evidence only under the current HARNESS_DAG_RUN_DIR/reports/.",
1359
2870
  shell: {
1360
- commands: [
1361
- "python -m pytest testcase/ --html=reports/backend-test-report.html -v",
1362
- ],
2871
+ commands: [pytestCommand],
1363
2872
  verifyEvidence: buildVerifyEvidence({
1364
2873
  phase: "final",
1365
2874
  quota: "full",
1366
2875
  commandSource: "inline",
1367
- fallbackCommands: [
1368
- "python -m pytest testcase/ --html=reports/backend-test-report.html -v",
1369
- ],
2876
+ fallbackCommands: [pytestCommand],
1370
2877
  finalFullRequired: true,
1371
2878
  }),
1372
2879
  cwd: ".",
@@ -1374,10 +2881,61 @@ function buildExecuteBackendPytestNode(sources) {
1374
2881
  },
1375
2882
  };
1376
2883
  }
2884
+ function buildParseBackendTestResultNode(sources) {
2885
+ return {
2886
+ id: "parse-backend-test-result-shell",
2887
+ depends_on: ["execute-backend-pytest-shell"],
2888
+ role: "verifier",
2889
+ executor: "shell",
2890
+ complexity: "LOW",
2891
+ writePolicy: "read-only",
2892
+ allowedPaths: commonReadOnlyPaths(sources),
2893
+ forbiddenPaths: commonForbiddenPaths(sources),
2894
+ outputContract: "Validated run-owned Backend Test Result v1 at contracts/backend-test-result.json (schemaId backend-test-result-v1) with outcome/counts/failures from deterministic JUnit parse.",
2895
+ subtask_prompt: "Materialize Backend Test Result v1 from JUnit + pytestExitCode under the current DAG run (fail-closed on missing/corrupt report).",
2896
+ shell: {
2897
+ commands: [],
2898
+ jsonArtifactGate: {
2899
+ fromNodeId: "execute-backend-pytest-shell",
2900
+ schemaId: "backend-test-result-v1",
2901
+ artifactName: "backend-test-result.json",
2902
+ outputDir: "contracts",
2903
+ },
2904
+ cwd: ".",
2905
+ timeoutMs: 60000,
2906
+ },
2907
+ };
2908
+ }
2909
+ function buildClassifyBackendTestResultNode(sources) {
2910
+ return {
2911
+ id: "classify-backend-test-result-pi",
2912
+ depends_on: ["parse-backend-test-result-shell"],
2913
+ role: "reviewer",
2914
+ executor: "pi",
2915
+ complexity: "MED",
2916
+ writePolicy: "read-only",
2917
+ allowedPaths: commonReadOnlyPaths(sources),
2918
+ forbiddenPaths: commonForbiddenPaths(sources),
2919
+ outputContract: "Pure JSON classification: category in {ProductBug,TestBug,EnvFailure,ContractMismatch,FlakyTest,Unknown}, evidence[], confidence (capped), notes. No file writes.",
2920
+ subtask_prompt: [
2921
+ "Read-only classifier for Backend Test Result v1.",
2922
+ "Return exactly one JSON object (prefer pure JSON; single fenced json block tolerated; no trailing prose).",
2923
+ "Read contracts/backend-test-result.json (run-owned Result v1). Do NOT invent pass rates from raw logs.",
2924
+ "category must be one of: ProductBug, TestBug, EnvFailure, ContractMismatch, FlakyTest, Unknown.",
2925
+ "Hard constraints:",
2926
+ "- Single-run failure MUST NOT use FlakyTest (use Unknown, TestBug, or ProductBug).",
2927
+ "- executionStatus/outcome collection-error, command-error, or report-error MUST NOT use ProductBug.",
2928
+ "- Prefer EnvFailure/Unknown/TestBug for env, import, collection, and missing-report cases.",
2929
+ "- confidence must respect deterministic caps (≤0.75 for assertion failures; ≤0.6 for env/collection).",
2930
+ "Include evidence[] referencing result fields (outcome, failed, failures[].name, executionStatus).",
2931
+ "Read-only: do not modify code, docs, artifacts, or repository files.",
2932
+ ].join("\n\n"),
2933
+ };
2934
+ }
1377
2935
  function buildTestRetrospectNode(sources) {
1378
2936
  return {
1379
2937
  id: "test-retrospect-pi",
1380
- depends_on: ["execute-backend-pytest-shell"],
2938
+ depends_on: ["classify-backend-test-result-pi"],
1381
2939
  role: "closeout",
1382
2940
  executor: "pi",
1383
2941
  toolProfile: "write",
@@ -1387,23 +2945,29 @@ function buildTestRetrospectNode(sources) {
1387
2945
  allowedPaths: ["docs/test-reports/**"],
1388
2946
  forbiddenPaths: commonForbiddenPaths(sources),
1389
2947
  subtask_prompt: [
1390
- "Read upstream outputs (review report + pytest results) and generate a test retrospective report.",
2948
+ "Read upstream Result v1 + Case Manifest coverageSummary + classification and generate a test retrospective report.",
1391
2949
  "",
1392
2950
  "## Output Steps (do in order):",
1393
2951
  "1. First, output the maturity rating on the first line: Rating: A/B/C/D",
1394
2952
  "2. Then write the full report under docs/test-reports/",
1395
2953
  "",
2954
+ "## Stats authority (deterministic only):",
2955
+ "- Pass rate, failed/error/skipped counts, and failure list MUST come from contracts/backend-test-result.json only.",
2956
+ "- AC coverage ratio / case counts MUST come from contracts/backend-test-case-manifest.json coverageSummary (or gate-derived fields). Do NOT invent coverage %.",
2957
+ "- Use classify-backend-test-result-pi JSON as interpretive evidence only.",
2958
+ "- NEVER rewrite a failed result as passed. Outcome gate (not this report) is authoritative for task success.",
2959
+ "",
1396
2960
  "## Report Structure:",
1397
2961
  "1. Maturity Rating with rationale",
1398
- "2. Test Coverage Summary (total cases, pass rate, failed case analysis)",
2962
+ "2. Test Coverage Summary (manifest coverageSummary + Result v1 pass rate)",
1399
2963
  "3. Review Findings and resolution status",
1400
- "4. Failed Test Analysis (if any)",
2964
+ "4. Failed Test Analysis (if any) + classification category",
1401
2965
  "5. Recommendations for improvement",
1402
2966
  "",
1403
2967
  "## Rating Criteria:",
1404
- "- A: 100% acceptance criteria covered + 100% pytest pass + no Critical findings",
1405
- "- B: ≥80% coverage + ≥90% pass + Low findings only",
1406
- "- C: ≥60% coverage + ≥70% pass + no Critical findings",
2968
+ "- A: coverageSummary.acCoverageRatio=1 + 100% pytest pass + no Critical findings",
2969
+ "- B: acCoverageRatio0.8 + ≥90% pass + Low findings only",
2970
+ "- C: acCoverageRatio0.6 + ≥70% pass + no Critical findings",
1407
2971
  "- D: below C thresholds",
1408
2972
  "",
1409
2973
  "## Constraints:",
@@ -1413,6 +2977,33 @@ function buildTestRetrospectNode(sources) {
1413
2977
  ].join("\n\n"),
1414
2978
  };
1415
2979
  }
2980
+ function buildBackendTestOutcomeGateNode(sources) {
2981
+ const gateCommand = buildBackendTestOutcomeGateShellSnippet();
2982
+ return {
2983
+ id: "backend-test-outcome-gate-shell",
2984
+ depends_on: ["test-retrospect-pi"],
2985
+ role: "verifier",
2986
+ executor: "shell",
2987
+ complexity: "LOW",
2988
+ writePolicy: "read-only",
2989
+ allowedPaths: commonReadOnlyPaths(sources),
2990
+ forbiddenPaths: commonForbiddenPaths(sources),
2991
+ outputContract: "Shell exit 0 only when Result v1 outcome=passed with failed=0 and error=0; non-zero otherwise. Ignores retrospective Markdown.",
2992
+ subtask_prompt: "Gate the backend-test DAG on run-owned Result v1 shell facts only (not retrospective prose).",
2993
+ shell: {
2994
+ commands: [gateCommand],
2995
+ verifyEvidence: buildVerifyEvidence({
2996
+ phase: "final",
2997
+ quota: "full",
2998
+ commandSource: "inline",
2999
+ fallbackCommands: [gateCommand],
3000
+ finalFullRequired: true,
3001
+ }),
3002
+ cwd: ".",
3003
+ timeoutMs: 60000,
3004
+ },
3005
+ };
3006
+ }
1416
3007
  const BACKEND_TEST_DEFAULTS = {
1417
3008
  ...HYBRID_DEFAULTS,
1418
3009
  writePolicy: "read-only",
@@ -1438,12 +3029,15 @@ function buildBackendTestHybridDag(sources) {
1438
3029
  ...STANDARD_GLOBAL_CONSTRAINTS,
1439
3030
  "backend-test-dag nodes must maintain traceability from requirements to functional cases to pytest automation.",
1440
3031
  "Functional test case IDs must use BE-<MODULE>-<NNN> format.",
1441
- "pytest execution must produce HTML reports under reports/.",
3032
+ "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
3033
  "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.",
3034
+ "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
3035
  "review-backend-cases-gate-shell must block pytest generation unless the review verdict is exactly VERDICT: pass.",
1445
3036
  "If a target test filename already exists under testcase/, add a numeric suffix (_01, _02, ...); never overwrite or append to existing files.",
1446
3037
  "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.",
3038
+ "parse-backend-test-result-shell materializes Backend Test Result v1 from JUnit + pytestExitCode; classify/retrospect run on pass and assertion-fail; backend-test-outcome-gate-shell uses result.outcome only.",
3039
+ "backend-test-case-manifest-shell validates schemaId backend-test-case-manifest-v1 and materializes contracts/backend-test-case-manifest.json; AC coverage is fail-closed and deterministic.",
3040
+ "backend-test-traceability-gate-shell verifies generated file/symbol existence after pytest generation and before execute; models must not invent coverage percentages.",
1447
3041
  ];
1448
3042
  const spec = {
1449
3043
  version: 3,
@@ -1453,18 +3047,9 @@ function buildBackendTestHybridDag(sources) {
1453
3047
  objective: extractObjective(sources.requirementMarkdown, taskConfig.title),
1454
3048
  successCriteria: extractSuccessCriteria(sources.requirementMarkdown, sources.taskId),
1455
3049
  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
- },
3050
+ // No convergence loop: review gate is fail-closed. request-revision stops
3051
+ // the DAG; regenerate after fixing cases. Controller still keys off
3052
+ // hard-verify-shell, which this template does not include.
1468
3053
  defaults: {
1469
3054
  ...BACKEND_TEST_DEFAULTS,
1470
3055
  contextProfile: taskConfig.contextProfile,
@@ -1473,12 +3058,242 @@ function buildBackendTestHybridDag(sources) {
1473
3058
  executorModels: sources.executorModelMatrix ?? DEFAULT_DAG_EXECUTOR_MODELS,
1474
3059
  tasks: [
1475
3060
  buildAnalyzeInputsNode(sources),
3061
+ buildBackendTestAnalysisContractGateNode(sources),
3062
+ buildBackendTestEnvironmentScoutNode(sources),
3063
+ buildBackendTestExecutionContractGateNode(sources),
1476
3064
  buildGenerateBackendFunctionalCasesNode(sources),
3065
+ buildEmitBackendCaseManifestNode(sources),
3066
+ buildBackendTestCaseManifestGateNode(sources),
1477
3067
  buildReviewBackendCasesNode(sources),
1478
3068
  buildReviewBackendCasesGateNode(sources),
1479
3069
  buildGenerateBackendPytestNode(sources),
3070
+ buildBackendTestTraceabilityGateNode(sources),
1480
3071
  buildExecuteBackendPytestNode(sources),
3072
+ buildParseBackendTestResultNode(sources),
3073
+ buildClassifyBackendTestResultNode(sources),
1481
3074
  buildTestRetrospectNode(sources),
3075
+ buildBackendTestOutcomeGateNode(sources),
3076
+ ],
3077
+ };
3078
+ applyDefaultReadOnlyRetryPolicy(spec);
3079
+ parseDagSpec(spec);
3080
+ assertValidDagSpec(spec);
3081
+ return spec;
3082
+ }
3083
+ // ---------------------------------------------------------------------------
3084
+ // Frontend browser-test RAG DAG template
3085
+ // ---------------------------------------------------------------------------
3086
+ function buildFrontendTestHybridDag(sources) {
3087
+ const config = sources.taskConfig.frontendTest ?? { maxCasesPerBatch: 20 };
3088
+ const hasFrontendTestWriteScope = sources.taskConfig.allowedPaths.some((pattern) => pattern === "testcase/frontend/**" ||
3089
+ pattern === "testcase/**" ||
3090
+ pattern === "**");
3091
+ const hasReportWriteScope = sources.taskConfig.allowedPaths.some((pattern) => pattern === "docs/test-reports/**" ||
3092
+ pattern === "docs/**" ||
3093
+ pattern === "**");
3094
+ if (!hasFrontendTestWriteScope || !hasReportWriteScope) {
3095
+ throw new Error('frontend-test requires task.json allowedPaths to include both "testcase/frontend/**" and "docs/test-reports/**" (or explicit containing globs).');
3096
+ }
3097
+ const forbidden = commonForbiddenPaths(sources);
3098
+ const ragWriteSet = ["testcase/frontend/rag/**"];
3099
+ const casesWriteSet = ["testcase/frontend/cases/**"];
3100
+ const evidenceRoot = "testcase/frontend/evidence";
3101
+ const manifestValidation = [
3102
+ "node -e",
3103
+ JSON.stringify([
3104
+ "const fs=require('fs'),path=require('path');",
3105
+ "const file='testcase/frontend/cases/manifest.json'; if(!fs.existsSync(file)) throw new Error('missing '+file);",
3106
+ "const manifest=JSON.parse(fs.readFileSync(file,'utf8')); if(manifest.schemaVersion!==1||!Array.isArray(manifest.cases)) throw new Error('invalid frontend case manifest');",
3107
+ "const dims=new Set(['core','boundary','flow','backend']);",
3108
+ "const seen=new Set(); const seenCasePath=new Set(); const seenEvidenceDir=new Set();",
3109
+ "for(const c of manifest.cases){",
3110
+ " 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');",
3111
+ " seen.add(c.caseId);",
3112
+ " if(typeof c.dimension!=='string'||!dims.has(c.dimension)) throw new Error('invalid dimension');",
3113
+ " if(!Array.isArray(c.acIds)||c.acIds.length===0||c.acIds.some(a=>typeof a!=='string'||!a.trim())) throw new Error('invalid acIds');",
3114
+ " for(const k of ['casePath','evidenceDir']){ const v=c[k]; if(typeof v!=='string'||path.isAbsolute(v)||v.includes('..')) throw new Error('unsafe '+k); }",
3115
+ " if(c.casePath!=='testcase/frontend/cases/'+c.caseId+'.md') throw new Error('casePath must match caseId');",
3116
+ " if(!c.evidenceDir.startsWith('testcase/frontend/evidence/'+c.caseId+'/')) throw new Error('case path escapes frontend test roots');",
3117
+ " if(seenCasePath.has(c.casePath)) throw new Error('duplicate casePath'); seenCasePath.add(c.casePath);",
3118
+ " if(seenEvidenceDir.has(c.evidenceDir)) throw new Error('duplicate evidenceDir'); seenEvidenceDir.add(c.evidenceDir);",
3119
+ "}",
3120
+ "process.stdout.write(JSON.stringify({cases:manifest.cases}));",
3121
+ ].join("")),
3122
+ ].join(" ");
3123
+ const spec = {
3124
+ version: 3,
3125
+ title: `Frontend test DAG: ${sources.taskConfig.title}`,
3126
+ runtimeContract: GENERATED_DAG_RUNTIME_CONTRACT,
3127
+ outputLanguage: sources.outputLanguage ?? DEFAULT_DAG_OUTPUT_LANGUAGE,
3128
+ objective: extractObjective(sources.requirementMarkdown, sources.taskConfig.title),
3129
+ successCriteria: extractSuccessCriteria(sources.requirementMarkdown, sources.taskId),
3130
+ globalConstraints: [
3131
+ ...sources.taskConfig.hardConstraints,
3132
+ ...STANDARD_GLOBAL_CONSTRAINTS,
3133
+ "frontend-test-dag generates Markdown cases and browser evidence only; it must not generate pytest or Playwright test source code.",
3134
+ "Each browser case runs serially in a fresh Pi execution boundary. Persist its evidence before starting the next case.",
3135
+ "Use only the declared isolated test environment. Production URLs, real credentials, and unauthorized data are blocked.",
3136
+ "Browser startup for generated cases must be playwright-cli open --browser=chrome --headed <base-url>.",
3137
+ "Token settings are post-case stop thresholds, never a hard provider token cap. Unstarted cases after a threshold are blocked: token-budget-exhausted.",
3138
+ ],
3139
+ defaults: {
3140
+ ...HYBRID_DEFAULTS,
3141
+ writePolicy: "read-only",
3142
+ contextProfile: sources.taskConfig.contextProfile,
3143
+ },
3144
+ skillsByRole: {
3145
+ planner: ["loop-agent"],
3146
+ scout: ["playwright-cli"],
3147
+ implementer: [
3148
+ "playwright-cli-case-generator",
3149
+ "playwright-cli",
3150
+ "webapp-testing",
3151
+ ],
3152
+ reviewer: ["requesting-code-review"],
3153
+ verifier: ["playwright-cli", "webapp-testing"],
3154
+ closeout: ["loop-agent", "verification-before-completion"],
3155
+ },
3156
+ executorModels: sources.executorModelMatrix ?? DEFAULT_DAG_EXECUTOR_MODELS,
3157
+ tasks: [
3158
+ {
3159
+ id: "retrieve-frontend-test-context-pi",
3160
+ depends_on: [],
3161
+ role: "planner",
3162
+ executor: "pi",
3163
+ toolProfile: "write",
3164
+ complexity: "HIGH",
3165
+ writePolicy: "exclusive",
3166
+ writeSet: ragWriteSet,
3167
+ allowedPaths: [...commonReadOnlyPaths(sources), ...ragWriteSet],
3168
+ forbiddenPaths: forbidden,
3169
+ outputContract: "Write testcase/frontend/rag/context.md and coverage-map.md with traceable UI/API/test-environment facts.",
3170
+ subtask_prompt: [
3171
+ "Build the frontend test RAG package.",
3172
+ "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.",
3173
+ "Record AC IDs, source paths, routes, states, roles, fixture/data prerequisites, API mapping status, risks, and isolated execution contract. Do not guess unavailable facts.",
3174
+ buildSourceContextBlock(sources),
3175
+ ].join("\n\n"),
3176
+ },
3177
+ {
3178
+ id: "generate-frontend-functional-cases-pi",
3179
+ depends_on: ["retrieve-frontend-test-context-pi"],
3180
+ role: "implementer",
3181
+ executor: "pi",
3182
+ toolProfile: "write",
3183
+ complexity: "HIGH",
3184
+ writePolicy: "exclusive",
3185
+ writeSet: casesWriteSet,
3186
+ allowedPaths: [...ragWriteSet, ...casesWriteSet],
3187
+ forbiddenPaths: forbidden,
3188
+ outputContract: "Write executable Markdown frontend cases, index.md, and manifest.json schemaVersion 1; no test source code.",
3189
+ subtask_prompt: [
3190
+ "Use skill playwright-cli-case-generator.",
3191
+ "Read only testcase/frontend/rag/context.md, testcase/frontend/rag/coverage-map.md, and existing testcase/frontend/cases/. Write only testcase/frontend/cases/**.",
3192
+ "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.",
3193
+ "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>.",
3194
+ "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.",
3195
+ ].join("\n\n"),
3196
+ },
3197
+ {
3198
+ id: "review-frontend-cases-pi",
3199
+ depends_on: ["generate-frontend-functional-cases-pi"],
3200
+ role: "reviewer",
3201
+ executor: "pi",
3202
+ complexity: "HIGH",
3203
+ writePolicy: "read-only",
3204
+ allowedPaths: [...ragWriteSet, ...casesWriteSet],
3205
+ forbiddenPaths: forbidden,
3206
+ outputContract: "First line VERDICT: pass or VERDICT: request-revision, followed by AC-to-case coverage and execution risk findings; no writes.",
3207
+ 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.",
3208
+ },
3209
+ {
3210
+ id: "materialize-frontend-case-manifest-shell",
3211
+ depends_on: ["review-frontend-cases-pi"],
3212
+ role: "verifier",
3213
+ executor: "shell",
3214
+ complexity: "LOW",
3215
+ writePolicy: "read-only",
3216
+ allowedPaths: casesWriteSet,
3217
+ forbiddenPaths: forbidden,
3218
+ outputContract: "stdout is exactly JSON { cases: [...] } after deterministic frontend manifest validation.",
3219
+ subtask_prompt: "Validate and materialize the generated frontend case manifest.",
3220
+ shell: { commands: [manifestValidation], cwd: ".", timeoutMs: 120000 },
3221
+ },
3222
+ {
3223
+ id: "execute-frontend-cases-map",
3224
+ depends_on: ["materialize-frontend-case-manifest-shell"],
3225
+ role: "verifier",
3226
+ executor: "static",
3227
+ complexity: "LOW",
3228
+ writePolicy: "none",
3229
+ allowedPaths: [],
3230
+ forbiddenPaths: forbidden,
3231
+ outputContract: "Serial aggregate of case execution summaries, evidence paths, tokens, and token-budget blocked cases.",
3232
+ subtask_prompt: "Expand and execute the validated frontend case manifest serially.",
3233
+ static: { resultMarkdown: "Frontend case map expansion barrier." },
3234
+ dynamicExpansion: {
3235
+ type: "map_agent",
3236
+ workflowNodeId: "execute-frontend-cases-map",
3237
+ itemsFrom: "$.nodes['materialize-frontend-case-manifest-shell'].output.cases",
3238
+ itemName: "case",
3239
+ maxItems: config.maxCasesPerBatch,
3240
+ maxExpandedNodes: config.maxCasesPerBatch,
3241
+ childIdPrefix: "execute-frontend-case",
3242
+ tokenBudget: {
3243
+ maxTokensPerCase: config.maxTokensPerCase,
3244
+ maxTotalTokens: config.maxTotalTokens,
3245
+ },
3246
+ childTask: {
3247
+ executor: "pi",
3248
+ role: "verifier",
3249
+ skills: ["playwright-cli", "webapp-testing"],
3250
+ toolProfile: "write",
3251
+ complexity: "MED",
3252
+ writePolicy: "exclusive",
3253
+ allowedPaths: [
3254
+ "testcase/frontend/cases/{{case.caseId}}.md",
3255
+ "testcase/frontend/rag/context.md",
3256
+ "testcase/frontend/rag/coverage-map.md",
3257
+ `${evidenceRoot}/{{case.caseId}}/**`,
3258
+ ],
3259
+ forbiddenPaths: forbidden,
3260
+ writeSet: [`${evidenceRoot}/{{case.caseId}}/**`],
3261
+ outputContract: "Compact JSON <=1200 characters with case status, evidence paths, error summary, and tokens.",
3262
+ subtaskPromptTemplate: [
3263
+ "Execute exactly case {{case.caseId}} from {{case.casePath}} using playwright-cli and webapp-testing. This is a fresh Pi session; do not use /new.",
3264
+ "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.",
3265
+ "Use playwright-cli open --browser=chrome --headed <base-url>. Persist execution.md, case-result.json, screenshots/trace/video/logs under {{case.evidenceDir}} before returning.",
3266
+ "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}.",
3267
+ ].join("\n\n"),
3268
+ },
3269
+ },
3270
+ },
3271
+ {
3272
+ id: "review-frontend-execution-pi",
3273
+ depends_on: ["execute-frontend-cases-map"],
3274
+ role: "reviewer",
3275
+ executor: "pi",
3276
+ complexity: "HIGH",
3277
+ writePolicy: "read-only",
3278
+ allowedPaths: ["testcase/frontend/**"],
3279
+ forbiddenPaths: forbidden,
3280
+ outputContract: "Read-only AC-to-case-to-browser-evidence review, including failed, blocked and token-budget-exhausted cases.",
3281
+ 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.",
3282
+ },
3283
+ {
3284
+ id: "frontend-test-retrospect-pi",
3285
+ depends_on: ["review-frontend-execution-pi"],
3286
+ role: "closeout",
3287
+ executor: "pi",
3288
+ toolProfile: "write",
3289
+ complexity: "MED",
3290
+ writePolicy: "exclusive",
3291
+ writeSet: ["docs/test-reports/**"],
3292
+ allowedPaths: ["testcase/frontend/**", "docs/test-reports/**"],
3293
+ forbiddenPaths: forbidden,
3294
+ outputContract: "Write frontend-test-retrospect-<date>.md with coverage, pass/fail/blocked, risks, findings, and A/B/C/D rating.",
3295
+ 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.",
3296
+ },
1482
3297
  ],
1483
3298
  };
1484
3299
  applyDefaultReadOnlyRetryPolicy(spec);
@@ -1501,6 +3316,104 @@ const KNOWLEDGE_SYNC_SKILLS_BY_ROLE = {
1501
3316
  verifier: ["verification-before-completion", "systematic-debugging"],
1502
3317
  closeout: ["loop-agent", "verification-before-completion"],
1503
3318
  };
3319
+ /**
3320
+ * Deterministic aggregate gate: all listed review nodes must emit VERDICT: pass
3321
+ * (first VERDICT: line in assistantText/stdout). Uses HARNESS_DAG_RUN_DIR JSON artifacts.
3322
+ */
3323
+ export function buildMultiPerspectiveReviewAggregateScript(fromNodeIds, label) {
3324
+ if (fromNodeIds.length === 0) {
3325
+ throw new Error("multi-perspective aggregate requires at least one review node id");
3326
+ }
3327
+ const idsLiteral = JSON.stringify([...fromNodeIds]);
3328
+ const labelLiteral = JSON.stringify(label);
3329
+ return [
3330
+ "node",
3331
+ "-e",
3332
+ JSON.stringify([
3333
+ "const fs=require('fs');",
3334
+ "const path=require('path');",
3335
+ `const ids=${idsLiteral};`,
3336
+ `const label=${labelLiteral};`,
3337
+ "const runDir=process.env.HARNESS_DAG_RUN_DIR;",
3338
+ "if(!runDir){ console.error(label+': missing HARNESS_DAG_RUN_DIR'); process.exit(1); }",
3339
+ "function normalize(line){",
3340
+ " const t=String(line).trim();",
3341
+ " const m=t.match(/^\\*{1,3}\\s*(VERDICT:[^*]+?)\\s*\\*{1,3}$/);",
3342
+ " return (m?m[1]:t).trim();",
3343
+ "}",
3344
+ "function firstVerdict(text){",
3345
+ " for (const line of String(text||'').split(/\\r?\\n/)) {",
3346
+ " const n=normalize(line);",
3347
+ " if(/^VERDICT:/.test(n)) return n;",
3348
+ " }",
3349
+ " return '';",
3350
+ "}",
3351
+ "const failures=[];",
3352
+ "for (const id of ids) {",
3353
+ " const file=path.join(runDir, id+'.json');",
3354
+ " if(!fs.existsSync(file)){ failures.push(id+': missing JSON '+file); continue; }",
3355
+ " let raw; try { raw=JSON.parse(fs.readFileSync(file,'utf8')); } catch(e){ failures.push(id+': invalid JSON'); continue; }",
3356
+ " const verdict=firstVerdict(raw.assistantText ?? raw.stdout ?? '');",
3357
+ " if(verdict!=='VERDICT: pass') failures.push(id+': '+(verdict||'missing VERDICT line'));",
3358
+ " else console.log(id+': VERDICT: pass');",
3359
+ "}",
3360
+ "if(failures.length){ console.error(label+' blocked:\\n'+failures.join('\\n')); process.exit(1); }",
3361
+ "console.log(label+': all perspectives VERDICT: pass ('+ids.length+')');",
3362
+ ].join("")),
3363
+ ].join(" ");
3364
+ }
3365
+ function buildMultiPerspectiveReviewNodes(input) {
3366
+ const reviewNodes = input.perspectives.map((p) => ({
3367
+ id: `${input.nodePrefix}${p.id}-pi`,
3368
+ depends_on: [...input.dependsOn],
3369
+ role: "reviewer",
3370
+ executor: "pi",
3371
+ complexity: "HIGH",
3372
+ writePolicy: "read-only",
3373
+ allowedPaths: input.allowedPaths,
3374
+ forbiddenPaths: commonForbiddenPaths(input.sources),
3375
+ outputContract: `Plain Markdown; first non-empty line is VERDICT: pass or VERDICT: request-revision. Perspective: ${p.perspective}. No file writes.`,
3376
+ subtask_prompt: [
3377
+ `You are the **${p.perspective}** reviewer in a multi-perspective review panel.`,
3378
+ "Other perspectives run in parallel; do not assume their conclusions. Stay in your role.",
3379
+ "First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
3380
+ "Any Critical or Important finding in your domain must force VERDICT: request-revision.",
3381
+ "Structure: VERDICT line, then Findings (Critical/Important/Minor), then Checked Items, then Residual Risks.",
3382
+ "Cite concrete paths/ids as evidence. Read-only: do not modify files.",
3383
+ ...input.sharedBrief,
3384
+ "Focus for this perspective:",
3385
+ ...p.focus.map((line) => `- ${line}`),
3386
+ buildSourceContextBlock(input.sources),
3387
+ ].join("\n\n"),
3388
+ }));
3389
+ const reviewIds = reviewNodes.map((n) => n.id);
3390
+ const aggregateScript = buildMultiPerspectiveReviewAggregateScript(reviewIds, input.gateLabel);
3391
+ const gateNode = {
3392
+ id: input.gateId,
3393
+ depends_on: reviewIds,
3394
+ role: "verifier",
3395
+ executor: "shell",
3396
+ complexity: "LOW",
3397
+ writePolicy: "read-only",
3398
+ allowedPaths: commonReadOnlyPaths(input.sources),
3399
+ forbiddenPaths: commonForbiddenPaths(input.sources),
3400
+ outputContract: `Deterministic multi-perspective gate: exit 0 only when every review node among ${reviewIds.join(", ")} emits VERDICT: pass.`,
3401
+ subtask_prompt: `Aggregate gate for ${input.gateLabel}: all perspectives must pass before downstream apply/promote.`,
3402
+ shell: {
3403
+ commands: [aggregateScript],
3404
+ verifyEvidence: buildVerifyEvidence({
3405
+ phase: "final",
3406
+ quota: "full",
3407
+ commandSource: "inline",
3408
+ fallbackCommands: [aggregateScript],
3409
+ finalFullRequired: true,
3410
+ }),
3411
+ cwd: ".",
3412
+ timeoutMs: 60000,
3413
+ },
3414
+ };
3415
+ return [...reviewNodes, gateNode];
3416
+ }
1504
3417
  /** Safe Feature directory id: F-… without path separators. */
1505
3418
  const KNOWLEDGE_SYNC_FEATURE_ID_RE = /^F-[A-Za-z0-9][A-Za-z0-9._-]*$/;
1506
3419
  export function assertSafeKnowledgeSyncFeatureId(featureId) {
@@ -1508,7 +3421,9 @@ export function assertSafeKnowledgeSyncFeatureId(featureId) {
1508
3421
  if (!KNOWLEDGE_SYNC_FEATURE_ID_RE.test(trimmed)) {
1509
3422
  throw new Error(`knowledge-sync featureId must match F-<id> (letters/digits/._- only); got ${JSON.stringify(featureId)}`);
1510
3423
  }
1511
- if (trimmed.includes("..") || trimmed.includes("/") || trimmed.includes("\\")) {
3424
+ if (trimmed.includes("..") ||
3425
+ trimmed.includes("/") ||
3426
+ trimmed.includes("\\")) {
1512
3427
  throw new Error(`knowledge-sync featureId must not contain path segments: ${featureId}`);
1513
3428
  }
1514
3429
  return trimmed;
@@ -1682,12 +3597,61 @@ function buildKnowledgeSyncValidateNode(sources, featureId) {
1682
3597
  },
1683
3598
  };
1684
3599
  }
3600
+ const KNOWLEDGE_SYNC_MULTI_REVIEW_PERSPECTIVES = [
3601
+ {
3602
+ id: "qa",
3603
+ perspective: "QA / acceptance",
3604
+ focus: [
3605
+ "acceptanceVerdict and AC coverage vs evidencePointers",
3606
+ "caseIndex completeness and non-invented pass results",
3607
+ "defects registry consistency with open issues",
3608
+ ],
3609
+ },
3610
+ {
3611
+ id: "domain",
3612
+ perspective: "domain / product",
3613
+ focus: [
3614
+ "requirement-delta risk and silent requirement rewrites",
3615
+ "operations[] targets stay under the bound featureId",
3616
+ "business meaning of coverage/matrix changes",
3617
+ ],
3618
+ },
3619
+ {
3620
+ id: "evidence",
3621
+ perspective: "evidence / audit",
3622
+ focus: [
3623
+ "finalVerification authority is shell evidence, not prose",
3624
+ "high-risk ops and gates.requireHumanIfHighRisk",
3625
+ "draft schema fields and pointer-only log policy",
3626
+ ],
3627
+ },
3628
+ ];
3629
+ function buildKnowledgeSyncMultiReviewNodes(sources, featureId) {
3630
+ return buildMultiPerspectiveReviewNodes({
3631
+ sources,
3632
+ dependsOn: ["knowledge-sync-validate-shell"],
3633
+ nodePrefix: "knowledge-sync-review-",
3634
+ gateId: "knowledge-sync-multi-review-gate-shell",
3635
+ gateLabel: "knowledge-sync multi-perspective review",
3636
+ perspectives: KNOWLEDGE_SYNC_MULTI_REVIEW_PERSPECTIVES,
3637
+ allowedPaths: [
3638
+ ...commonReadOnlyPaths(sources),
3639
+ `features/${featureId}/**`,
3640
+ "docs/test-reports/**",
3641
+ ],
3642
+ sharedBrief: [
3643
+ `Bound featureId: ${featureId}. Only review draft/ops for this Feature.`,
3644
+ `Primary draft path: ${knowledgeSyncDraftRelPath(featureId)}.`,
3645
+ "Apply is blocked until all perspectives pass. Do not approve fabricated verification pass.",
3646
+ ],
3647
+ });
3648
+ }
1685
3649
  function buildKnowledgeSyncApplyNode(sources, featureId) {
1686
3650
  const writeSet = knowledgeSyncWriteSet(featureId);
1687
3651
  const draftPath = knowledgeSyncDraftRelPath(featureId);
1688
3652
  return {
1689
3653
  id: "knowledge-sync-apply-pi",
1690
- depends_on: ["knowledge-sync-validate-shell"],
3654
+ depends_on: ["knowledge-sync-multi-review-gate-shell"],
1691
3655
  role: "implementer",
1692
3656
  executor: "pi",
1693
3657
  toolProfile: "write",
@@ -1756,11 +3720,13 @@ function buildKnowledgeSyncHybridDag(sources) {
1756
3720
  ...STANDARD_GLOBAL_CONSTRAINTS,
1757
3721
  `knowledge-sync-dag is bound to featureId=${featureId}; writes only features/${featureId}/testing/**, features/${featureId}/requirement-delta.md, and docs/test-reports/**.`,
1758
3722
  "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.",
3723
+ "Apply is blocked unless knowledge-sync-validate-shell and multi-perspective review gate pass; final verification evidence remains the completion authority.",
3724
+ "Multi-perspective review: QA/acceptance, domain/product, and evidence/audit must each emit VERDICT: pass before apply.",
1760
3725
  "High-risk requirement/acceptance body changes require human approval via requirement-delta; do not silently rewrite requirement.md.",
1761
3726
  "Raw shell logs stay as path pointers; knowledge base stores stable facts only.",
1762
3727
  "Prefer structured YAML/Markdown L1 knowledge pack over vector-store-only writes.",
1763
3728
  ];
3729
+ const multiReview = buildKnowledgeSyncMultiReviewNodes(sources, featureId);
1764
3730
  const spec = {
1765
3731
  version: 2,
1766
3732
  title: `Knowledge-sync DAG (${featureId}): ${taskConfig.title}`,
@@ -1778,6 +3744,7 @@ function buildKnowledgeSyncHybridDag(sources) {
1778
3744
  buildKnowledgeSyncCollectNode(sources, featureId),
1779
3745
  buildKnowledgeSyncDraftNode(sources, featureId),
1780
3746
  buildKnowledgeSyncValidateNode(sources, featureId),
3747
+ ...multiReview,
1781
3748
  buildKnowledgeSyncApplyNode(sources, featureId),
1782
3749
  buildKnowledgeSyncPointerNode(sources, featureId),
1783
3750
  ],
@@ -1876,7 +3843,10 @@ function buildKgBootstrapInventoryNode(sources) {
1876
3843
  executor: "shell",
1877
3844
  complexity: "LOW",
1878
3845
  writePolicy: "exclusive",
1879
- writeSet: ["knowledge/bootstrap/inventory.json", "knowledge/bootstrap/status.yaml"],
3846
+ writeSet: [
3847
+ "knowledge/bootstrap/inventory.json",
3848
+ "knowledge/bootstrap/status.yaml",
3849
+ ],
1880
3850
  allowedPaths: ["knowledge/bootstrap/**", "./**"],
1881
3851
  forbiddenPaths: commonForbiddenPaths(sources),
1882
3852
  outputContract: "Deterministic inventory.json under knowledge/bootstrap/ from directory and feature signals.",
@@ -1903,10 +3873,7 @@ function buildKgBootstrapProposeNode(sources) {
1903
3873
  toolProfile: "write",
1904
3874
  complexity: "HIGH",
1905
3875
  writePolicy: "exclusive",
1906
- writeSet: [
1907
- "knowledge/bootstrap/staging/**",
1908
- "knowledge/bootstrap/runs/**",
1909
- ],
3876
+ writeSet: ["knowledge/bootstrap/staging/**", "knowledge/bootstrap/runs/**"],
1910
3877
  allowedPaths: [
1911
3878
  "knowledge/bootstrap/**",
1912
3879
  "knowledge/**",
@@ -1991,51 +3958,55 @@ function buildKgBootstrapValidateNode(sources) {
1991
3958
  },
1992
3959
  };
1993
3960
  }
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
- };
3961
+ const KG_BOOTSTRAP_MULTI_REVIEW_PERSPECTIVES = [
3962
+ {
3963
+ id: "structure",
3964
+ perspective: "architecture / structure",
3965
+ focus: [
3966
+ "domain/service/module partition vs inventory candidates",
3967
+ "id uniqueness and naming conventions",
3968
+ "edges that create impossible or circular dependencies",
3969
+ ],
3970
+ },
3971
+ {
3972
+ id: "evidence",
3973
+ perspective: "evidence / anti-hallucination",
3974
+ focus: [
3975
+ "every entity/edge has concrete evidence paths",
3976
+ "no confidence: asserted in staging",
3977
+ "no invented APIs or production details without files",
3978
+ ],
3979
+ },
3980
+ {
3981
+ id: "safety",
3982
+ perspective: "write-boundary / promote safety",
3983
+ focus: [
3984
+ "writes stayed in knowledge/bootstrap/staging/** (and runs/**)",
3985
+ "no formal knowledge/domains|services trees or graph indexes written by AI",
3986
+ "incremental scope respected when update_mode: incremental",
3987
+ ],
3988
+ },
3989
+ ];
3990
+ function buildKgBootstrapMultiReviewNodes(sources) {
3991
+ return buildMultiPerspectiveReviewNodes({
3992
+ sources,
3993
+ dependsOn: ["kg-bootstrap-validate-shell"],
3994
+ nodePrefix: "kg-bootstrap-review-",
3995
+ gateId: "kg-bootstrap-multi-review-gate-shell",
3996
+ gateLabel: "kg-bootstrap multi-perspective review",
3997
+ perspectives: KG_BOOTSTRAP_MULTI_REVIEW_PERSPECTIVES,
3998
+ allowedPaths: [
3999
+ "knowledge/bootstrap/**",
4000
+ "knowledge/**",
4001
+ "features/**",
4002
+ "docs/**",
4003
+ ],
4004
+ sharedBrief: [
4005
+ "Review staging knowledge-graph proposals before promote.",
4006
+ "pass means ready for promote consideration — it does NOT mark entities asserted.",
4007
+ "request-revision if critical entities lack evidence, ids collide, or formal trees were written outside staging.",
4008
+ ],
4009
+ });
2039
4010
  }
2040
4011
  function buildKgBootstrapPromoteNode(sources) {
2041
4012
  const script = buildKgBootstrapInlineNodeScript([
@@ -2065,7 +4036,9 @@ function buildKgBootstrapPromoteNode(sources) {
2065
4036
  "if(fs.existsSync(linksDir)){",
2066
4037
  " for(const f of fs.readdirSync(linksDir)){",
2067
4038
  " if(!f.endsWith('.yaml')&&!f.endsWith('.yml')) continue;",
4039
+ " 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
4040
  " const id=f.replace(/\\.ya?ml$/,'');",
4041
+ " if(id.includes('..')){ console.error('invalid feature link proposal filename: '+f); process.exit(1); }",
2069
4042
  " const dest=path.join(root,'features',id,'knowledge-links.yaml');",
2070
4043
  " if(!fs.existsSync(path.join(root,'features',id))) continue;",
2071
4044
  " if(!fs.existsSync(dest)){ fs.mkdirSync(path.dirname(dest),{recursive:true}); fs.copyFileSync(path.join(linksDir,f),dest); copied++; }",
@@ -2075,7 +4048,7 @@ function buildKgBootstrapPromoteNode(sources) {
2075
4048
  ]);
2076
4049
  return {
2077
4050
  id: "kg-bootstrap-promote-shell",
2078
- depends_on: ["kg-bootstrap-review-gate-shell"],
4051
+ depends_on: ["kg-bootstrap-multi-review-gate-shell"],
2079
4052
  role: "verifier",
2080
4053
  executor: "shell",
2081
4054
  complexity: "LOW",
@@ -2095,11 +4068,7 @@ function buildKgBootstrapPromoteNode(sources) {
2095
4068
  "knowledge/graph/**",
2096
4069
  "features/**",
2097
4070
  ],
2098
- forbiddenPaths: [
2099
- ...commonForbiddenPaths(sources),
2100
- "src/**",
2101
- "testcase/**",
2102
- ],
4071
+ forbiddenPaths: [...commonForbiddenPaths(sources), "src/**", "testcase/**"],
2103
4072
  outputContract: "Promote staging → formal knowledge trees without overwriting existing files; copy edges.manual.yaml if absent.",
2104
4073
  subtask_prompt: "B5 promote: merge new files only (no overwrite of existing asserted content).",
2105
4074
  shell: {
@@ -2195,7 +4164,9 @@ function buildKnowledgeGraphBootstrapHybridDag(sources) {
2195
4164
  "Promote must not overwrite existing formal files (merge-new-only).",
2196
4165
  "Require knowledge/bootstrap/scope.yaml before propose (B0/B1 skeleton).",
2197
4166
  "Graph indexes are written only by materialize-shell, not by propose-pi.",
4167
+ "Multi-perspective review (structure, evidence, safety) must all VERDICT: pass before promote.",
2198
4168
  ];
4169
+ const multiReview = buildKgBootstrapMultiReviewNodes(sources);
2199
4170
  const spec = {
2200
4171
  version: 2,
2201
4172
  title: `Knowledge-graph bootstrap DAG: ${taskConfig.title}`,
@@ -2214,8 +4185,7 @@ function buildKnowledgeGraphBootstrapHybridDag(sources) {
2214
4185
  buildKgBootstrapInventoryNode(sources),
2215
4186
  buildKgBootstrapProposeNode(sources),
2216
4187
  buildKgBootstrapValidateNode(sources),
2217
- buildKgBootstrapReviewNode(sources),
2218
- buildKgBootstrapReviewGateNode(sources),
4188
+ ...multiReview,
2219
4189
  buildKgBootstrapPromoteNode(sources),
2220
4190
  buildKgBootstrapMaterializeNode(sources),
2221
4191
  ],
@@ -2224,30 +4194,42 @@ function buildKnowledgeGraphBootstrapHybridDag(sources) {
2224
4194
  assertValidDagSpec(spec);
2225
4195
  return spec;
2226
4196
  }
4197
+ function buildHybridDagForTemplate(sources, template) {
4198
+ let spec;
4199
+ if (template === "frontend-implementation") {
4200
+ spec = buildFrontendHybridDagFromTask(sources);
4201
+ }
4202
+ else if (template === "frontend-test-dag")
4203
+ spec = buildFrontendTestHybridDag(sources);
4204
+ else if (template === "backend-test-dag")
4205
+ spec = buildBackendTestHybridDag(sources);
4206
+ else if (template === "knowledge-sync-dag")
4207
+ spec = buildKnowledgeSyncHybridDag(sources);
4208
+ else if (template === "knowledge-graph-bootstrap-dag")
4209
+ spec = buildKnowledgeGraphBootstrapHybridDag(sources);
4210
+ else {
4211
+ const standard = buildStandardHybridDagFromTask(sources);
4212
+ if (template === "standard-dag")
4213
+ spec = standard;
4214
+ else if (template === "review-gated-dag")
4215
+ spec = buildReviewGatedHybridDag(standard, sources);
4216
+ else
4217
+ spec = buildSupervisedHybridDag(standard, sources);
4218
+ }
4219
+ spec.sourceBinding = buildDagSourceBinding(sources);
4220
+ parseDagSpec(spec);
4221
+ assertValidDagSpec(spec);
4222
+ return spec;
4223
+ }
2227
4224
  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);
4225
+ const selection = resolveTaskDagTemplateSelection({
4226
+ taskKind: sources.taskConfig.taskKind,
4227
+ title: sources.taskConfig.title,
4228
+ requirementMarkdown: sources.requirementMarkdown,
4229
+ allowedPaths: sources.taskConfig.allowedPaths,
4230
+ requestedTemplate: options.template,
4231
+ });
4232
+ return buildHybridDagForTemplate(sources, selection.template);
2251
4233
  }
2252
4234
  function cloneTask(task, patch = {}) {
2253
4235
  return { ...task, ...patch };
@@ -2378,22 +4360,117 @@ function buildWriteSetAuditNode(sources) {
2378
4360
  ].join("\n\n"),
2379
4361
  };
2380
4362
  }
4363
+ function buildWriteSetAuditFormatRepairNode(sources, options) {
4364
+ return {
4365
+ id: options.id,
4366
+ depends_on: [options.auditNodeId],
4367
+ role: "reviewer",
4368
+ executor: "pi",
4369
+ complexity: "LOW",
4370
+ writePolicy: "read-only",
4371
+ allowedPaths: commonReadOnlyPaths(sources),
4372
+ forbiddenPaths: commonForbiddenPaths(sources),
4373
+ outputContract: "Plain Markdown whose first non-empty line is exactly VERDICT: pass or VERDICT: request-revision, followed by the original audit findings without substantive changes. No file writes.",
4374
+ subtask_prompt: [
4375
+ `Normalize the output format of ${options.auditNodeId}; this is the single read-only format-repair attempt for that audit.`,
4376
+ "First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
4377
+ "If the upstream audit already contains a valid verdict, preserve it exactly. If it omitted or malformed the verdict but states an unambiguous audit conclusion, add only the matching canonical verdict and preserve the findings.",
4378
+ "Do not add, remove, or reclassify substantive findings. If the upstream conclusion is ambiguous or cannot be preserved safely, emit VERDICT: request-revision and report the format ambiguity.",
4379
+ "Do not infer a pass from general prose, expand task allowedPaths, or edit files.",
4380
+ buildSourceContextBlock(sources),
4381
+ ].join("\n\n"),
4382
+ };
4383
+ }
4384
+ function buildWriteSetFormatGateNode(sources) {
4385
+ return {
4386
+ id: "write-set-format-gate-shell",
4387
+ depends_on: ["write-set-audit-format-repair-pi"],
4388
+ role: "verifier",
4389
+ executor: "shell",
4390
+ complexity: "LOW",
4391
+ writePolicy: "read-only",
4392
+ allowedPaths: commonReadOnlyPaths(sources),
4393
+ forbiddenPaths: commonForbiddenPaths(sources),
4394
+ outputContract: "Deterministic initial write-set verdict format gate: accept pass or request-revision so the bounded plan-revision stage can run; reject missing or unexpected verdicts.",
4395
+ subtask_prompt: "Validate the normalized initial write-set audit verdict before the bounded plan-revision stage. This gate does not authorize implementation writes.",
4396
+ shell: {
4397
+ commands: [],
4398
+ verdictGate: {
4399
+ fromNodeId: "write-set-audit-format-repair-pi",
4400
+ accept: ["VERDICT: pass", "VERDICT: request-revision"],
4401
+ label: "initial write-set audit format",
4402
+ lineMode: "first-verdict-line",
4403
+ },
4404
+ cwd: ".",
4405
+ timeoutMs: 60000,
4406
+ },
4407
+ };
4408
+ }
4409
+ function buildPlanRevisionNode(sources) {
4410
+ return {
4411
+ id: "plan-revision-pi",
4412
+ depends_on: [
4413
+ "write-set-format-gate-shell",
4414
+ "plan-pi",
4415
+ "write-set-audit-format-repair-pi",
4416
+ ],
4417
+ role: "planner",
4418
+ executor: "pi",
4419
+ complexity: "MED",
4420
+ writePolicy: "read-only",
4421
+ allowedPaths: commonReadOnlyPaths(sources),
4422
+ forbiddenPaths: commonForbiddenPaths(sources),
4423
+ outputContract: "PASS_NO_REVISION_NEEDED when the normalized initial audit passed, otherwise a complete revised implementation plan with a corrected WriteSet Coverage Matrix. No file writes.",
4424
+ subtask_prompt: [
4425
+ "Perform the single bounded plan-revision round after the normalized initial write-set audit.",
4426
+ "If the normalized verdict is VERDICT: pass, output PASS_NO_REVISION_NEEDED and do not change the original plan.",
4427
+ "If it is VERDICT: request-revision, return a complete revised plan that resolves every audit finding and includes a corrected WriteSet Coverage Matrix.",
4428
+ "Do not expand task.json.allowedPaths, weaken forbiddenPaths, or edit files.",
4429
+ buildSourceContextBlock(sources),
4430
+ ].join("\n\n"),
4431
+ };
4432
+ }
4433
+ function buildFinalWriteSetAuditNode(sources) {
4434
+ return {
4435
+ id: "final-write-set-audit-pi",
4436
+ depends_on: [
4437
+ "plan-pi",
4438
+ "plan-revision-pi",
4439
+ "write-set-audit-format-repair-pi",
4440
+ ],
4441
+ role: "reviewer",
4442
+ executor: "pi",
4443
+ complexity: "MED",
4444
+ writePolicy: "read-only",
4445
+ allowedPaths: commonReadOnlyPaths(sources),
4446
+ forbiddenPaths: commonForbiddenPaths(sources),
4447
+ outputContract: "Plain Markdown whose first non-empty line is VERDICT: pass or VERDICT: request-revision; includes final writeSet coverage findings after the single plan-revision round. No file writes.",
4448
+ subtask_prompt: [
4449
+ "Perform the final write-set audit after the single bounded plan-revision round.",
4450
+ "When plan-revision-pi returned PASS_NO_REVISION_NEEDED, audit the original plan-pi output. Otherwise audit the complete revised plan.",
4451
+ "First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
4452
+ "Request revision if required files still lack a single exclusive owner, writeSet is broad/placeholder, forbidden paths overlap, or any initial finding remains unresolved.",
4453
+ "Do not expand task allowedPaths or edit files.",
4454
+ buildSourceContextBlock(sources),
4455
+ ].join("\n\n"),
4456
+ };
4457
+ }
2381
4458
  function buildWriteSetGateNode(sources) {
2382
4459
  return {
2383
4460
  id: "write-set-gate-shell",
2384
- depends_on: ["write-set-audit-pi"],
4461
+ depends_on: ["final-write-set-audit-format-repair-pi"],
2385
4462
  role: "verifier",
2386
4463
  executor: "shell",
2387
4464
  complexity: "LOW",
2388
4465
  writePolicy: "read-only",
2389
4466
  allowedPaths: commonReadOnlyPaths(sources),
2390
4467
  forbiddenPaths: commonForbiddenPaths(sources),
2391
- outputContract: "Deterministic write-set audit verdict gate: exit 0 only when write-set-audit-pi first non-empty assistant output line is pass.",
2392
- subtask_prompt: "Deterministic gate: block the implementation writer unless write-set-audit-pi emitted VERDICT: pass.",
4468
+ outputContract: "Deterministic final write-set audit verdict gate: exit 0 only when final-write-set-audit-format-repair-pi emits VERDICT: pass after the bounded revision round.",
4469
+ subtask_prompt: "Deterministic gate: block the implementation writer unless the normalized final write-set audit emitted VERDICT: pass.",
2393
4470
  shell: {
2394
4471
  commands: [],
2395
4472
  verdictGate: {
2396
- fromNodeId: "write-set-audit-pi",
4473
+ fromNodeId: "final-write-set-audit-format-repair-pi",
2397
4474
  accept: ["VERDICT: pass"],
2398
4475
  label: "write-set audit",
2399
4476
  lineMode: "first-verdict-line",
@@ -2514,7 +4591,9 @@ function buildRepairNode(sources) {
2514
4591
  "If VERDICT: pass, return no-op with evidence. Re-run focused tests when you change code.",
2515
4592
  writerDeliveryContract(sources.taskConfig),
2516
4593
  buildSourceContextBlock(sources),
2517
- ].filter((value) => Boolean(value)).join("\n\n"),
4594
+ ]
4595
+ .filter((value) => Boolean(value))
4596
+ .join("\n\n"),
2518
4597
  };
2519
4598
  }
2520
4599
  function buildHardVerifyNode(sources) {
@@ -2566,7 +4645,8 @@ function buildDecisionNode(sources) {
2566
4645
  outputContract: "Markdown with exactly one DECISION_ENVELOPE_JSON fenced block plus evidence summary. No file writes.",
2567
4646
  subtask_prompt: [
2568
4647
  "Return an advisory Decision Gate envelope for the supervised DAG outcome.",
2569
- "Include exactly one DECISION_ENVELOPE_JSON fenced block and concise evidence. Read-only: do not modify files.",
4648
+ "Read-only: do not modify files. Review deterministic verification, review findings, write boundaries, and risks before deciding.",
4649
+ buildDecisionEnvelopePromptContract(),
2570
4650
  buildSourceContextBlock(sources),
2571
4651
  ].join("\n\n"),
2572
4652
  decisionGate: { enabled: true, schemaVersion: 1, mode: "record-only" },
@@ -2604,8 +4684,26 @@ function buildSupervisedHybridDag(standard, sources) {
2604
4684
  cloneTask(scoutTests),
2605
4685
  cloneTask(plan),
2606
4686
  buildWriteSetAuditNode(sources),
4687
+ buildWriteSetAuditFormatRepairNode(sources, {
4688
+ id: "write-set-audit-format-repair-pi",
4689
+ auditNodeId: "write-set-audit-pi",
4690
+ }),
4691
+ buildWriteSetFormatGateNode(sources),
4692
+ buildPlanRevisionNode(sources),
4693
+ buildFinalWriteSetAuditNode(sources),
4694
+ buildWriteSetAuditFormatRepairNode(sources, {
4695
+ id: "final-write-set-audit-format-repair-pi",
4696
+ auditNodeId: "final-write-set-audit-pi",
4697
+ }),
2607
4698
  buildWriteSetGateNode(sources),
2608
- cloneTask(implement, { depends_on: ["write-set-gate-shell"] }),
4699
+ cloneTask(implement, {
4700
+ depends_on: [
4701
+ "write-set-gate-shell",
4702
+ "plan-pi",
4703
+ "plan-revision-pi",
4704
+ "final-write-set-audit-format-repair-pi",
4705
+ ],
4706
+ }),
2609
4707
  buildSoftVerifyNode(sources),
2610
4708
  buildProcessSupervisorNode(sources),
2611
4709
  buildProcessGateNode(sources),
@@ -2635,8 +4733,19 @@ export function defaultHybridDagOutputPath(taskId) {
2635
4733
  return path.join(os.tmpdir(), `${taskId}-hybrid-dag.json`);
2636
4734
  }
2637
4735
  export async function writeHybridDagDraft(sources, outputPath, options = {}) {
2638
- const template = options.template ?? "standard-dag";
2639
- const spec = buildHybridDagFromTask(sources, { template });
4736
+ const templateSelection = resolveTaskDagTemplateSelection({
4737
+ taskKind: sources.taskConfig.taskKind,
4738
+ title: sources.taskConfig.title,
4739
+ requirementMarkdown: sources.requirementMarkdown,
4740
+ allowedPaths: sources.taskConfig.allowedPaths,
4741
+ requestedTemplate: options.template,
4742
+ });
4743
+ const template = templateSelection.template;
4744
+ assertTaskAllowedPathsPreflight(sources.taskConfig);
4745
+ const preparedSources = template === "frontend-implementation"
4746
+ ? await prepareFrontendMockSources(sources)
4747
+ : sources;
4748
+ const spec = buildHybridDagForTemplate(preparedSources, template);
2640
4749
  await writeFile(outputPath, `${JSON.stringify(spec, null, 2)}\n`, "utf-8");
2641
4750
  return {
2642
4751
  taskId: sources.taskId,
@@ -2644,6 +4753,7 @@ export async function writeHybridDagDraft(sources, outputPath, options = {}) {
2644
4753
  taskCount: spec.tasks.length,
2645
4754
  nodeIds: spec.tasks.map((task) => task.id),
2646
4755
  template,
4756
+ templateSelection,
2647
4757
  };
2648
4758
  }
2649
4759
  export async function initHybridDagFromTask(repoRoot, taskId, options = {}) {