@tea-agent/loop-agent 0.10.0 → 0.12.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 (207) hide show
  1. package/AGENTS.md +10 -2
  2. package/CHANGELOG.md +91 -24
  3. package/README.md +84 -12
  4. package/dist/application/dag/args.js +1 -12
  5. package/dist/application/dag/generate-task-dag.js +38 -2
  6. package/dist/application/dag/run-dag.js +11 -27
  7. package/dist/application/dag/validate-dag.js +13 -2
  8. package/dist/application/loop/run-action.js +0 -4
  9. package/dist/cli/command-definitions.js +44 -16
  10. package/dist/cli/program.js +40 -23
  11. package/dist/cli/update/notifier.js +117 -0
  12. package/dist/cli/update/npm-client.js +151 -0
  13. package/dist/cli/update/policy.js +58 -0
  14. package/dist/cli/update/state.js +68 -0
  15. package/dist/cli.js +33 -0
  16. package/dist/commands/cursor-prompt.js +42 -82
  17. package/dist/commands/dag-approve.js +36 -0
  18. package/dist/commands/delegate.js +75 -77
  19. package/dist/commands/doctor.js +0 -18
  20. package/dist/commands/init.js +547 -95
  21. package/dist/commands/instructions.js +7 -10
  22. package/dist/commands/loop.js +4 -20
  23. package/dist/commands/plan.js +50 -0
  24. package/dist/executors/config-core.js +0 -51
  25. package/dist/executors/dag-pi-executor.js +1 -1
  26. package/dist/executors/dag.js +0 -1
  27. package/dist/executors/index.js +0 -2
  28. package/dist/executors/model-routing.js +9 -9
  29. package/dist/executors/shell-executor.js +1 -1
  30. package/dist/governance/checks.js +6 -3
  31. package/dist/governance/exec-plans.js +545 -0
  32. package/dist/governance/manifest-types.js +24 -2
  33. package/dist/infrastructure/harness/loop-action-store.js +0 -3
  34. package/dist/records/harvest.js +2 -23
  35. package/dist/records/one-shot-runs.js +1 -1
  36. package/dist/shared/artifacts-core.js +24 -5
  37. package/dist/shared/output-truncation.js +37 -0
  38. package/dist/shared/package-metadata.js +488 -0
  39. package/dist/{executors/cursor-executor.js → sidecars/cursor-prompt/executor.js} +2 -42
  40. package/dist/sidecars/cursor-prompt/index.js +3 -0
  41. package/dist/sidecars/cursor-prompt/stream.js +121 -0
  42. package/dist/task/config-types.js +29 -12
  43. package/dist/task/delegate.js +9 -21
  44. package/dist/task/runtime.js +1 -2
  45. package/dist/worker/cli.js +32 -3
  46. package/dist/worker/delivery/final-verification.js +47 -11
  47. package/dist/worker/delivery/package.js +63 -10
  48. package/dist/worker/feature/run.js +60 -8
  49. package/dist/worker/loop-agent/loop-agent-client.js +329 -126
  50. package/dist/worker/observability/event-history.js +216 -0
  51. package/dist/worker/observability/read-model.js +338 -83
  52. package/dist/worker/observe/paths.js +17 -0
  53. package/dist/worker/observe/routes.js +165 -21
  54. package/dist/worker/observe/server.js +59 -1
  55. package/dist/worker/observe/static/api.js +27 -0
  56. package/dist/worker/observe/static/app.js +120 -2317
  57. package/dist/worker/observe/static/constants.js +148 -0
  58. package/dist/worker/observe/static/copy.js +67 -0
  59. package/dist/worker/observe/static/dag-helpers.js +172 -0
  60. package/dist/worker/observe/static/dag-model.js +72 -0
  61. package/dist/worker/observe/static/dom.js +61 -0
  62. package/dist/worker/observe/static/format-pool.js +67 -0
  63. package/dist/worker/observe/static/format.js +292 -0
  64. package/dist/worker/observe/static/index.html +300 -82
  65. package/dist/worker/observe/static/kpi.js +94 -0
  66. package/dist/worker/observe/static/relations.js +128 -0
  67. package/dist/worker/observe/static/router.js +85 -0
  68. package/dist/worker/observe/static/run-processing.js +148 -0
  69. package/dist/worker/observe/static/shell-chrome.js +68 -0
  70. package/dist/worker/observe/static/state.js +253 -0
  71. package/dist/worker/observe/static/styles.css +1720 -495
  72. package/dist/worker/observe/static/views/batch.js +226 -0
  73. package/dist/worker/observe/static/views/dag-graph.js +172 -0
  74. package/dist/worker/observe/static/views/dag-inspector.js +477 -0
  75. package/dist/worker/observe/static/views/dag.js +362 -0
  76. package/dist/worker/observe/static/views/dashboard.js +442 -0
  77. package/dist/worker/observe/static/views/failures.js +143 -0
  78. package/dist/worker/observe/static/views/feature.js +453 -0
  79. package/dist/worker/observe/static/views/pool.js +347 -0
  80. package/dist/worker/observe/static/views/run.js +453 -0
  81. package/dist/worker/observe/static/views/session-timeline.js +205 -0
  82. package/dist/worker/observe/static/views/shell.js +7 -0
  83. package/dist/worker/observe/static/views/task.js +260 -0
  84. package/dist/worker/observe/static/views/timeline.js +163 -0
  85. package/dist/worker/preflight.js +49 -1
  86. package/dist/worker/run-task/run-task.js +22 -12
  87. package/dist/worker/runner/run-ready.js +76 -12
  88. package/dist/worker/task-spec/schema.js +0 -1
  89. package/dist/workflows/dag/controller-identity.js +104 -0
  90. package/dist/workflows/dag/convergence/controller.js +1 -1
  91. package/dist/workflows/dag/executor-registry.js +0 -2
  92. package/dist/workflows/dag/init-hybrid.js +797 -27
  93. package/dist/workflows/dag/node-execution.js +183 -35
  94. package/dist/workflows/dag/repair-artifact.js +91 -0
  95. package/dist/workflows/dag/report.js +50 -0
  96. package/dist/workflows/dag/retry-policy.js +138 -0
  97. package/dist/workflows/dag/runner.js +77 -17
  98. package/dist/workflows/dag/runtime-contract.js +87 -0
  99. package/dist/workflows/dag/scheduler.js +7 -2
  100. package/dist/workflows/dag/sdd-embedded.js +128 -0
  101. package/dist/workflows/dag/skill-instructions.js +5 -4
  102. package/dist/workflows/dag/skill-snapshot.js +529 -0
  103. package/dist/workflows/dag/types.js +86 -10
  104. package/dist/workflows/dag/validate.js +73 -12
  105. package/dist/workflows/loop/actions/dag-action.js +0 -2
  106. package/dist/workflows/loop/actions/shared.js +1 -1
  107. package/dist/workflows/loop/actions.js +14 -31
  108. package/dist/workflows/loop/benchmark.js +1 -1
  109. package/dist/workflows/loop/index.js +1 -1
  110. package/dist/workflows/loop/policy/auto-policy.js +22 -14
  111. package/dist/workflows/loop/policy/path-patterns.js +13 -0
  112. package/docs/README.md +36 -33
  113. package/docs/agent-dag-recovery-playbook.md +1 -1
  114. package/docs/agent-dag-runner.md +28 -3
  115. package/docs/architecture/README.md +26 -0
  116. package/docs/architecture/dag-execution.md +140 -0
  117. package/docs/architecture/evolution.md +53 -0
  118. package/docs/architecture/facts-and-state.md +58 -0
  119. package/docs/architecture/runtime-boundaries.md +45 -17
  120. package/docs/architecture/system-overview.md +93 -0
  121. package/docs/architecture/worker-and-feature.md +81 -0
  122. package/docs/cursor-prompt-sidecar.md +36 -0
  123. package/docs/decisions/README.md +13 -1
  124. package/docs/design/README.md +43 -21
  125. package/docs/development-principles.md +2 -2
  126. package/docs/exec-plans/active/README.md +1 -3
  127. package/docs/exec-plans/completed/README.md +23 -0
  128. package/docs/feature-workflow.md +78 -4
  129. package/docs/harness-methodology-debugging.md +1 -1
  130. package/docs/harness-methodology-tdd.md +3 -3
  131. package/docs/init-surface.manifest.json +60 -25
  132. package/docs/loop-agent-harness.md +28 -4
  133. package/docs/progress/README.md +50 -1
  134. package/docs/reports/README.md +90 -18
  135. package/docs/skills/README.md +2 -1
  136. package/docs/skills/vetted-skill-registry.md +2 -1
  137. package/docs/templates/agent-dag-report.schema.json +23 -6
  138. package/docs/templates/agent-dag.base.json +0 -5
  139. package/docs/templates/agent-dag.final-verification.json +0 -5
  140. package/docs/templates/agent-dag.schema.json +70 -3
  141. package/docs/templates/agent-dag.supervised-implementation.json +9 -8
  142. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +139 -0
  143. package/docs/templates/backend-test-dag.json +276 -0
  144. package/docs/templates/backend-test-dag.retrospect.prompt.md +125 -0
  145. package/docs/templates/backend-test-dag.review-cases.prompt.md +81 -0
  146. package/docs/templates/frontend-design-contract.md +33 -0
  147. package/docs/templates/frontend-task-constraints.md +25 -0
  148. package/docs/templates/frontend-task-requirement.md +61 -0
  149. package/docs/templates/harness.schema.json +10 -12
  150. package/docs/templates/hybrid-dag.json +1 -6
  151. package/docs/templates/interactive-ui-round2-experiment.md +1 -1
  152. package/docs/templates/product-line/task.yaml +0 -1
  153. package/docs/templates/project-start-checklist.md +2 -2
  154. package/docs/templates/worker-dogfood-evidence.md +28 -0
  155. package/docs/templates/worker-dogfood-setup.md +20 -0
  156. package/docs/verification-matrix.md +10 -0
  157. package/examples/decision-gate-agent-dag.json +87 -33
  158. package/examples/example-dag.json +0 -5
  159. package/examples/hybrid-loop-agent-dag.json +0 -5
  160. package/harness.json +7 -15
  161. package/package.json +22 -46
  162. package/scripts/check-product-line-docs.sh +10 -7
  163. package/skills/agent-worker/SKILL.md +37 -0
  164. package/skills/agent-worker/references/agent-worker-operator.md +43 -0
  165. package/skills/frontend-design-review/SKILL.md +59 -0
  166. package/skills/frontend-design-review/references/review-checklist.md +37 -0
  167. package/skills/frontend-implementation/SKILL.md +51 -0
  168. package/skills/frontend-implementation/references/code-standards.md +34 -0
  169. package/skills/frontend-implementation/references/design-spec.md +46 -0
  170. package/skills/frontend-implementation/references/node-contracts.md +32 -0
  171. package/skills/frontend-review/SKILL.md +53 -0
  172. package/skills/frontend-review/references/review-findings.md +42 -0
  173. package/skills/frontend-verification/SKILL.md +40 -0
  174. package/skills/frontend-verification/references/verification-checklist.md +56 -0
  175. package/skills/grill-me/SKILL.md +10 -0
  176. package/skills/grill-with-docs/SKILL.md +88 -0
  177. package/skills/grill-with-docs/adr-format.md +47 -0
  178. package/skills/grill-with-docs/context-format.md +60 -0
  179. package/skills/loop-agent/SKILL.md +11 -9
  180. package/skills/loop-agent/references/command-reference.md +14 -15
  181. package/skills/loop-agent/references/docs-converge.md +126 -0
  182. package/skills/loop-agent/references/harness-policy.md +7 -7
  183. package/skills/loop-agent/references/hybrid-dag.md +36 -20
  184. package/skills/loop-agent/references/long-running-loop.md +4 -6
  185. package/skills/loop-agent/references/multi-worktree.md +6 -6
  186. package/skills/loop-agent/references/orchestrator-and-interventions.md +3 -3
  187. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +14 -11
  188. package/skills/loop-agent/references/task-workflow.md +1 -1
  189. package/skills/loop-agent/references/verification-and-failure-handling.md +6 -0
  190. package/skills/using-git-worktrees/SKILL.md +215 -0
  191. package/dist/commands/cursor-worker.js +0 -43
  192. package/dist/cursor-worker-entry.js +0 -8
  193. package/dist/executors/cursor-artifacts.js +0 -33
  194. package/dist/executors/cursor-execution-log.js +0 -81
  195. package/dist/executors/cursor-executor-artifacts.js +0 -134
  196. package/dist/executors/cursor-run.js +0 -115
  197. package/dist/executors/cursor-tool.js +0 -94
  198. package/dist/executors/cursor-worker-client.js +0 -223
  199. package/dist/executors/cursor-worker-protocol.js +0 -18
  200. package/dist/executors/cursor-worker-server.js +0 -54
  201. package/dist/executors/cursor-worker.js +0 -3
  202. package/dist/executors/cursor.js +0 -6
  203. package/dist/executors/dag-cursor-executor.js +0 -87
  204. package/dist/workflows/loop/actions/cursor-fix.js +0 -191
  205. package/dist/workflows/loop/policy/cursor-fix-policy.js +0 -31
  206. package/docs/cursor-executor-usage.md +0 -25
  207. package/docs/dynamic-workflow-dag-engine-roadmap.md +0 -1749
@@ -2,12 +2,14 @@ import { access, readdir, readFile, writeFile } from "node:fs/promises";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { assertValidDagSpec } from "./validate.js";
5
- import { DEFAULT_DAG_OUTPUT_LANGUAGE, DEFAULT_DAG_EXECUTOR_MODELS, parseDagSpec, } from "./types.js";
5
+ 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
6
  import { pathMatchesPattern } from "../../shared/git-progress.js";
7
7
  import { BASELINE_FORBIDDEN_PATHS } from "./governance-constants.js";
8
+ import { DEFAULT_READ_ONLY_PI_RETRY_POLICY, isSafeReadOnlyPiRetryCandidate } from "./retry-policy.js";
8
9
  import { resolveAdapter } from "../../adapters/index.js";
9
10
  import { loadHarnessManifest } from "../../governance/harness.js";
10
11
  import { buildAuthoritySurfaceAuditNode, buildAuthoritySurfaceGateNode, resolveAuthoritySurfaceAudit, } from "./authority-surface.js";
12
+ import { applySddEmbeddedEnhancements, probeRepoLocalSddSkills, } from "./sdd-embedded.js";
11
13
  import { getTaskPaths, loadTaskConfig } from "../../task/runtime.js";
12
14
  import { materializeTaskReferenceDocs } from "../../task/source-references.js";
13
15
  import { resolveVerifyPreset } from "../../executors/shell-verification.js";
@@ -58,27 +60,50 @@ const HYBRID_SKILLS_BY_ROLE = {
58
60
  verifier: ["verification-before-completion", "systematic-debugging"],
59
61
  closeout: ["loop-agent", "verification-before-completion"],
60
62
  };
63
+ const FRONTEND_DEFAULTS = {
64
+ ...HYBRID_DEFAULTS,
65
+ skills: [],
66
+ };
67
+ const FRONTEND_SKILLS_BY_ROLE = {
68
+ planner: [],
69
+ scout: [],
70
+ implementer: [],
71
+ reviewer: [],
72
+ supervisor: [],
73
+ verifier: [],
74
+ closeout: [],
75
+ };
76
+ const FRONTEND_IMPLEMENTATION_SKILLS = ["frontend-implementation"];
77
+ const FRONTEND_DESIGN_REVIEW_SKILLS = ["frontend-design-review"];
78
+ const FRONTEND_REVIEW_SKILLS = ["frontend-review"];
79
+ const FRONTEND_VERIFICATION_SKILLS = ["frontend-verification"];
61
80
  const IMPLEMENT_WRITESET_PLACEHOLDER = "REPLACE/WITH/NARROW/IMPLEMENT/PATHS/**";
81
+ /**
82
+ * Runtime contract stamped on every newly generated DAG. Capability fields are
83
+ * authoritative and are the primary compatibility gate. `minimumControllerVersion`
84
+ * is intentionally omitted here: it is an optional diagnostic hint, and hardcoding
85
+ * a future version would make a controller reject the very DAGs it generates.
86
+ * Operators can still pin a minimum version on hand-authored specs when needed.
87
+ */
88
+ export const GENERATED_DAG_RUNTIME_CONTRACT = {
89
+ schemaVersion: DAG_RUNTIME_CONTRACT_SCHEMA_VERSION,
90
+ agentRuntime: DAG_AGENT_RUNTIME_PI_ONLY,
91
+ repairWriterProtocol: DAG_REPAIR_WRITER_PROTOCOL_EXPLICIT_NODE_V1,
92
+ };
62
93
  function resolveEnabledExecutors(manifestExecutors) {
63
94
  return Object.entries(manifestExecutors)
64
95
  .filter(([, config]) => config.enabled !== false)
65
- .map(([name]) => name);
96
+ .map(([name]) => name)
97
+ .filter((name) => name === "pi");
66
98
  }
67
- function resolveImplementationExecutor(sources) {
68
- const enabled = sources.enabledExecutors
69
- ? new Set(sources.enabledExecutors)
70
- : undefined;
71
- if (!enabled || enabled.has("pi"))
72
- return "pi";
73
- if (enabled.has("cursor"))
74
- return "cursor";
75
- return "pi";
99
+ function implementationNodeId() {
100
+ return "implement-pi";
76
101
  }
77
- function implementationNodeId(executor) {
78
- return executor === "cursor" ? "implement-cursor" : "implement-pi";
102
+ function repairNodeId() {
103
+ return "repair-pi";
79
104
  }
80
- function repairNodeId(executor) {
81
- return executor === "cursor" ? "repair-cursor" : "repair-pi";
105
+ function frontendImplementationNodeId() {
106
+ return "frontend-implement-pi";
82
107
  }
83
108
  function probePathsForWriteSetEntry(entry) {
84
109
  const normalized = entry.replace(/\\/g, "/").replace(/^\.\//, "");
@@ -275,6 +300,65 @@ function buildVerifyShellCommands(input) {
275
300
  const repoRoot = input.repoRoot;
276
301
  return input.commands.map((command) => verifyCommandToShell(repoRoot, command));
277
302
  }
303
+ function markdownVerifyCommand(repoRoot, command) {
304
+ const args = command.split(/\s+/).filter(Boolean);
305
+ if (args.length === 0 ||
306
+ args.some((arg) => !/^[a-z0-9@._/:=,+-]+$/i.test(arg))) {
307
+ return null;
308
+ }
309
+ return {
310
+ args,
311
+ cwd: repoRoot,
312
+ label: command,
313
+ };
314
+ }
315
+ function extractFrontendVerifyCommandsFromMarkdown(input) {
316
+ if (!input.repoRoot)
317
+ return { staticCommands: [], behaviorCommands: [] };
318
+ const markdown = [
319
+ input.requirementMarkdown,
320
+ input.constraintMarkdown ?? "",
321
+ ].join("\n");
322
+ const commands = new Set();
323
+ for (const line of markdown.split(/\r?\n/)) {
324
+ const bulletless = line
325
+ .trim()
326
+ .replace(/^[-*]\s*(?:\[[ xX]\]\s*)?/, "")
327
+ .trim();
328
+ const codeSpanCommands = Array.from(bulletless.matchAll(/`([^`]+)`/g), (match) => match[1].trim());
329
+ const candidates = codeSpanCommands.length > 0 ? codeSpanCommands : [bulletless];
330
+ 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)) {
333
+ commands.add(candidate);
334
+ }
335
+ }
336
+ }
337
+ const staticCommands = [];
338
+ const behaviorCommands = [];
339
+ for (const command of commands) {
340
+ const verifyCommand = markdownVerifyCommand(input.repoRoot, command);
341
+ if (!verifyCommand)
342
+ continue;
343
+ if (/\b(typecheck|lint|eslint|tsc|build)\b/i.test(command)) {
344
+ staticCommands.push(verifyCommand);
345
+ continue;
346
+ }
347
+ if (/\b(test|vitest|jest|playwright|cypress|e2e)\b/i.test(command)) {
348
+ behaviorCommands.push(verifyCommand);
349
+ }
350
+ }
351
+ return { staticCommands, behaviorCommands };
352
+ }
353
+ function chooseFrontendVerifyCommands(input) {
354
+ if (input.parsedCommands.length > 0) {
355
+ return { commands: input.parsedCommands, commandSource: "inline" };
356
+ }
357
+ if (input.adapterCommands && input.adapterCommands.length > 0) {
358
+ return { commands: input.adapterCommands, commandSource: "adapter" };
359
+ }
360
+ return { commandSource: "inline" };
361
+ }
278
362
  function resolveDagVerifyStrategy(taskConfig) {
279
363
  return {
280
364
  intermediateQuota: taskConfig.dagVerifyStrategy?.intermediateQuota ?? taskConfig.verifyQuota,
@@ -311,6 +395,17 @@ function deriveParallelScoutPaths(taskConfig) {
311
395
  : allowed,
312
396
  };
313
397
  }
398
+ function deriveFrontendBehaviorPaths(taskConfig) {
399
+ if (taskConfig.allowedPaths.length === 0)
400
+ return ["**"];
401
+ const explicitTestPaths = taskConfig.allowedPaths.filter((entry) => entry.includes("/test/") ||
402
+ entry.includes("/tests/") ||
403
+ entry.includes("/e2e/") ||
404
+ entry.includes("/spec/"));
405
+ if (explicitTestPaths.length > 0)
406
+ return explicitTestPaths;
407
+ return taskConfig.allowedPaths;
408
+ }
314
409
  function toTaskRelativeSourcePath(sources, absolutePath) {
315
410
  return path
316
411
  .relative(sources.taskDir, absolutePath)
@@ -454,6 +549,7 @@ export async function loadTaskHybridSources(repoRoot, taskId) {
454
549
  enabledExecutors: resolveEnabledExecutors(manifest.executors),
455
550
  executorModelMatrix: resolveExecutorModelMatrices(manifest),
456
551
  verifyCommands,
552
+ sddEmbeddedSkills: await probeRepoLocalSddSkills(repoRoot),
457
553
  };
458
554
  }
459
555
  function mergeFinalVerifyCommands(repoRoot, taskConfig, adapterCommands) {
@@ -479,8 +575,7 @@ export function buildStandardHybridDagFromTask(sources) {
479
575
  const scoutPaths = deriveParallelScoutPaths(taskConfig);
480
576
  const scoutComplexity = mapTaskComplexity(taskConfig.complexity);
481
577
  const implementComplexity = resolveWriterComplexity(taskConfig);
482
- const writerExecutor = resolveImplementationExecutor(sources);
483
- const implementId = implementationNodeId(writerExecutor);
578
+ const implementId = implementationNodeId();
484
579
  const finalVerifyCommands = sources.verifyCommands?.final ?? [];
485
580
  const verifyShellCommands = buildVerifyShellCommands({
486
581
  repoRoot: sources.repoRoot,
@@ -523,8 +618,9 @@ export function buildStandardHybridDagFromTask(sources) {
523
618
  ...STANDARD_GLOBAL_CONSTRAINTS,
524
619
  ];
525
620
  const spec = {
526
- version: 2,
621
+ version: 3,
527
622
  title: `Hybrid DAG: ${taskConfig.title}`,
623
+ runtimeContract: GENERATED_DAG_RUNTIME_CONTRACT,
528
624
  outputLanguage: sources.outputLanguage ?? DEFAULT_DAG_OUTPUT_LANGUAGE,
529
625
  objective: extractObjective(sources.requirementMarkdown, taskConfig.title),
530
626
  successCriteria: extractSuccessCriteria(sources.requirementMarkdown, sources.taskId),
@@ -604,7 +700,7 @@ export function buildStandardHybridDagFromTask(sources) {
604
700
  id: implementId,
605
701
  depends_on: ["plan-pi"],
606
702
  role: "implementer",
607
- executor: writerExecutor,
703
+ executor: "pi",
608
704
  toolProfile: "write",
609
705
  complexity: implementComplexity,
610
706
  writePolicy: "exclusive",
@@ -656,11 +752,666 @@ export function buildStandardHybridDagFromTask(sources) {
656
752
  },
657
753
  ],
658
754
  };
755
+ applySddEmbeddedEnhancements(spec, sources.sddEmbeddedSkills ?? new Set());
756
+ applyDefaultReadOnlyRetryPolicy(spec);
757
+ parseDagSpec(spec);
758
+ assertValidDagSpec(spec);
759
+ return spec;
760
+ }
761
+ function buildFrontendHybridDagFromTask(sources) {
762
+ 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 design verdict gate before any write node executes.",
777
+ "frontend-implementation DAGs must complete deterministic static verification and behavior verification before final review.",
778
+ "frontend review must block closeout unless review verdict is exactly VERDICT: pass.",
779
+ ];
780
+ const staticFallbackCommands = ["npm run typecheck", "npm run build"];
781
+ const behaviorFallbackCommands = ["npm test"];
782
+ const parsedFrontendVerifyCommands = extractFrontendVerifyCommandsFromMarkdown({
783
+ repoRoot: sources.repoRoot,
784
+ requirementMarkdown: sources.requirementMarkdown,
785
+ constraintMarkdown: sources.constraintMarkdown,
786
+ });
787
+ const staticVerifyCommands = chooseFrontendVerifyCommands({
788
+ parsedCommands: parsedFrontendVerifyCommands.staticCommands,
789
+ adapterCommands: sources.verifyCommands?.intermediate,
790
+ });
791
+ const behaviorVerifyCommands = chooseFrontendVerifyCommands({
792
+ parsedCommands: parsedFrontendVerifyCommands.behaviorCommands,
793
+ adapterCommands: sources.verifyCommands?.final,
794
+ });
795
+ const spec = {
796
+ version: 3,
797
+ title: `Frontend implementation DAG: ${taskConfig.title}`,
798
+ runtimeContract: GENERATED_DAG_RUNTIME_CONTRACT,
799
+ objective: extractObjective(sources.requirementMarkdown, taskConfig.title),
800
+ successCriteria: extractSuccessCriteria(sources.requirementMarkdown, sources.taskId),
801
+ globalConstraints,
802
+ defaults: {
803
+ ...FRONTEND_DEFAULTS,
804
+ contextProfile: taskConfig.contextProfile,
805
+ },
806
+ skillsByRole: FRONTEND_SKILLS_BY_ROLE,
807
+ executorModels: sources.executorModelMatrix ?? DEFAULT_DAG_EXECUTOR_MODELS,
808
+ verifyStrategy: resolveDagVerifyStrategy(taskConfig),
809
+ tasks: [
810
+ {
811
+ id: "frontend-contract-pi",
812
+ depends_on: [],
813
+ role: "planner",
814
+ executor: "pi",
815
+ complexity: "MED",
816
+ writePolicy: "read-only",
817
+ allowedPaths: readOnlyPaths,
818
+ forbiddenPaths,
819
+ skills: FRONTEND_IMPLEMENTATION_SKILLS,
820
+ outputContract: "Markdown contract with Scope, Non-goals, Acceptance Criteria, UI States, Target Runtime Environment, Risks, and Verification Expectations. No file writes.",
821
+ subtask_prompt: [
822
+ "Read task source and produce a concise frontend implementation contract.",
823
+ "Cover scope, non-goals, acceptance criteria, UI states, target runtime environment, risks, and verification expectations.",
824
+ "Read-only: do not modify code, docs, artifacts, or repository files.",
825
+ sourceContext,
826
+ ].join("\n\n"),
827
+ },
828
+ {
829
+ id: "frontend-scout-pi",
830
+ depends_on: ["frontend-contract-pi"],
831
+ role: "scout",
832
+ executor: "pi",
833
+ complexity: mapTaskComplexity(taskConfig.complexity),
834
+ writePolicy: "read-only",
835
+ allowedPaths: readOnlyPaths,
836
+ forbiddenPaths,
837
+ skills: FRONTEND_IMPLEMENTATION_SKILLS,
838
+ 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.",
839
+ subtask_prompt: [
840
+ "Inspect frontend code, routing, components, styles, package scripts, and tests.",
841
+ "Return code and design observations, existing reuse opportunities, and verification entry points.",
842
+ "Read-only: do not modify repository files.",
843
+ sourceContext,
844
+ ].join("\n\n"),
845
+ },
846
+ {
847
+ id: "frontend-plan-pi",
848
+ depends_on: ["frontend-scout-pi"],
849
+ role: "planner",
850
+ executor: "pi",
851
+ complexity: "MED",
852
+ writePolicy: "read-only",
853
+ allowedPaths: readOnlyPaths,
854
+ forbiddenPaths,
855
+ skills: FRONTEND_IMPLEMENTATION_SKILLS,
856
+ 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.",
857
+ subtask_prompt: [
858
+ "Based on frontend-contract-pi and frontend-scout-pi, return a minimal frontend implementation plan.",
859
+ "Include ordered steps, target files, UI state handling, styling/component strategy, interaction notes, dependency policy, and deterministic verification commands.",
860
+ "Read-only: do not modify code, docs, artifacts, or repository files.",
861
+ sourceContext,
862
+ ].join("\n\n"),
863
+ },
864
+ {
865
+ id: "frontend-design-gate-pi",
866
+ depends_on: ["frontend-plan-pi"],
867
+ role: "reviewer",
868
+ executor: "pi",
869
+ complexity: "MED",
870
+ writePolicy: "read-only",
871
+ allowedPaths: readOnlyPaths,
872
+ forbiddenPaths,
873
+ skills: FRONTEND_DESIGN_REVIEW_SKILLS,
874
+ outputContract: "Plain Markdown whose first non-empty line is VERDICT: pass or VERDICT: request-revision, followed by Findings, Required Plan Corrections, and Checked Items. No file writes.",
875
+ subtask_prompt: [
876
+ "Audit the frontend plan before implementation.",
877
+ "First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
878
+ "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.",
879
+ "Read-only: do not modify repository files.",
880
+ sourceContext,
881
+ ].join("\n\n"),
882
+ },
883
+ {
884
+ id: "frontend-design-gate-shell",
885
+ depends_on: ["frontend-design-gate-pi"],
886
+ role: "verifier",
887
+ executor: "shell",
888
+ complexity: "LOW",
889
+ writePolicy: "read-only",
890
+ allowedPaths: readOnlyPaths,
891
+ forbiddenPaths,
892
+ outputContract: "Deterministic frontend design verdict gate: exit 0 only when frontend-design-gate-pi emits VERDICT: pass.",
893
+ subtask_prompt: "Deterministic gate: block frontend implementation unless frontend-design-gate-pi emitted VERDICT: pass.",
894
+ shell: {
895
+ commands: [],
896
+ verdictGate: {
897
+ fromNodeId: "frontend-design-gate-pi",
898
+ accept: ["VERDICT: pass"],
899
+ label: "frontend design gate",
900
+ lineMode: "first-verdict-line",
901
+ },
902
+ cwd: ".",
903
+ timeoutMs: 60000,
904
+ },
905
+ },
906
+ {
907
+ id: implementId,
908
+ depends_on: ["frontend-design-gate-shell"],
909
+ role: "implementer",
910
+ executor: "pi",
911
+ toolProfile: "write",
912
+ complexity: taskConfig.complexity === "large"
913
+ ? "HIGH"
914
+ : "MED",
915
+ writePolicy: "exclusive",
916
+ writeSet: implementPaths.writeSet,
917
+ allowedPaths: implementPaths.allowedPaths,
918
+ forbiddenPaths,
919
+ skills: FRONTEND_IMPLEMENTATION_SKILLS,
920
+ outputContract: "Markdown summary with Changed Files, Implemented Behavior, UI States Covered, Styling / Component Notes, Verification Attempted, and Residual Risks.",
921
+ subtask_prompt: [
922
+ "Implement the approved frontend plan with minimal focused changes.",
923
+ "Stay within writeSet and preserve unrelated files. Do not write root artifacts/** unless explicitly included in writeSet.",
924
+ sourceContext,
925
+ ].join("\n\n"),
926
+ },
927
+ {
928
+ id: "frontend-static-verify-shell",
929
+ depends_on: [implementId],
930
+ role: "verifier",
931
+ executor: "shell",
932
+ complexity: "LOW",
933
+ writePolicy: "read-only",
934
+ allowedPaths: readOnlyPaths,
935
+ forbiddenPaths,
936
+ outputContract: "Archived shell stdout/stderr with exit codes for deterministic static verification; no worktree writes.",
937
+ subtask_prompt: "Run deterministic static verification for the frontend implementation.",
938
+ shell: {
939
+ commands: buildVerifyShellCommands({
940
+ repoRoot: sources.repoRoot,
941
+ commands: staticVerifyCommands.commands,
942
+ fallbackCommands: staticFallbackCommands,
943
+ }),
944
+ verifyEvidence: buildVerifyEvidence({
945
+ phase: "intermediate",
946
+ quota: strategy.intermediateQuota ?? "full",
947
+ commandSource: staticVerifyCommands.commandSource,
948
+ commands: staticVerifyCommands.commands,
949
+ fallbackCommands: staticFallbackCommands,
950
+ }),
951
+ cwd: ".",
952
+ timeoutMs: 300000,
953
+ },
954
+ },
955
+ {
956
+ id: "frontend-behavior-verify-shell",
957
+ depends_on: ["frontend-static-verify-shell"],
958
+ role: "verifier",
959
+ executor: "shell",
960
+ complexity: "LOW",
961
+ writePolicy: "read-only",
962
+ allowedPaths: behaviorPaths,
963
+ forbiddenPaths,
964
+ outputContract: "Archived shell stdout/stderr with exit codes for deterministic behavior verification; no worktree writes.",
965
+ subtask_prompt: "Run deterministic behavior verification for frontend flows, states, and integration points.",
966
+ shell: {
967
+ commands: buildVerifyShellCommands({
968
+ repoRoot: sources.repoRoot,
969
+ commands: behaviorVerifyCommands.commands,
970
+ fallbackCommands: behaviorFallbackCommands,
971
+ }),
972
+ verifyEvidence: buildVerifyEvidence({
973
+ phase: "final",
974
+ quota: "full",
975
+ commandSource: behaviorVerifyCommands.commandSource,
976
+ commands: behaviorVerifyCommands.commands,
977
+ fallbackCommands: behaviorFallbackCommands,
978
+ finalFullRequired: true,
979
+ }),
980
+ cwd: ".",
981
+ timeoutMs: 300000,
982
+ },
983
+ },
984
+ {
985
+ id: "frontend-review-pi",
986
+ depends_on: ["frontend-behavior-verify-shell"],
987
+ role: "reviewer",
988
+ executor: "pi",
989
+ complexity: "HIGH",
990
+ writePolicy: "read-only",
991
+ allowedPaths: readOnlyPaths,
992
+ forbiddenPaths,
993
+ skills: FRONTEND_REVIEW_SKILLS,
994
+ outputContract: "Plain Markdown whose first non-empty line is VERDICT: pass or VERDICT: request-revision, followed by Findings, Verification Assessment, UX Assessment, and Residual Risks. No file writes.",
995
+ subtask_prompt: [
996
+ "Review the frontend implementation and verification evidence.",
997
+ "First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
998
+ "Any Critical or Important finding must force VERDICT: request-revision.",
999
+ "Review implementation quality, behavior/state coverage, verification evidence, and maintainability. Read-only: do not modify files.",
1000
+ sourceContext,
1001
+ ].join("\n\n"),
1002
+ },
1003
+ {
1004
+ id: "frontend-review-gate-shell",
1005
+ depends_on: ["frontend-review-pi"],
1006
+ role: "verifier",
1007
+ executor: "shell",
1008
+ complexity: "LOW",
1009
+ writePolicy: "read-only",
1010
+ allowedPaths: readOnlyPaths,
1011
+ forbiddenPaths,
1012
+ outputContract: "Deterministic frontend review verdict gate: exit 0 only when frontend-review-pi emits VERDICT: pass.",
1013
+ subtask_prompt: "Deterministic gate: block downstream closeout unless frontend-review-pi emitted VERDICT: pass.",
1014
+ shell: {
1015
+ commands: [],
1016
+ verdictGate: {
1017
+ fromNodeId: "frontend-review-pi",
1018
+ accept: ["VERDICT: pass"],
1019
+ label: "frontend review",
1020
+ lineMode: "first-verdict-line",
1021
+ },
1022
+ cwd: ".",
1023
+ timeoutMs: 60000,
1024
+ },
1025
+ },
1026
+ {
1027
+ id: "frontend-closeout-pi",
1028
+ depends_on: ["frontend-review-gate-shell"],
1029
+ role: "closeout",
1030
+ executor: "pi",
1031
+ complexity: "MED",
1032
+ writePolicy: "read-only",
1033
+ allowedPaths: taskConfig.allowedPaths.length > 0
1034
+ ? [...taskConfig.allowedPaths, "docs/**"]
1035
+ : ["**", "docs/**"],
1036
+ forbiddenPaths,
1037
+ skills: FRONTEND_VERIFICATION_SKILLS,
1038
+ outputContract: "Markdown closeout summary with Changes, Verification Evidence, Review Result, Known Risks, and Follow-up. No file writes.",
1039
+ subtask_prompt: [
1040
+ "Return a frontend closeout summary covering changes, verification evidence, review result, known risks, and follow-up.",
1041
+ "Read-only: do not modify code, docs, artifacts, or .harness/dag-runs/.",
1042
+ sourceContext,
1043
+ ].join("\n\n"),
1044
+ },
1045
+ ],
1046
+ };
1047
+ applyDefaultReadOnlyRetryPolicy(spec);
1048
+ parseDagSpec(spec);
1049
+ assertValidDagSpec(spec);
1050
+ return spec;
1051
+ }
1052
+ // ---------------------------------------------------------------------------
1053
+ // Backend test DAG template
1054
+ // ---------------------------------------------------------------------------
1055
+ function buildAnalyzeInputsNode(sources) {
1056
+ return {
1057
+ id: "analyze-inputs-pi",
1058
+ depends_on: [],
1059
+ role: "planner",
1060
+ executor: "pi",
1061
+ complexity: "MED",
1062
+ writePolicy: "read-only",
1063
+ allowedPaths: commonReadOnlyPaths(sources),
1064
+ forbiddenPaths: commonForbiddenPaths(sources),
1065
+ outputContract: "Structured Markdown extracting core content from source documents. No file writes.",
1066
+ subtask_prompt: [
1067
+ "Read the task source materials and extract the following structured content for downstream test generation.",
1068
+ "",
1069
+ "## Required Output Sections:",
1070
+ "",
1071
+ "### 1. API Endpoints",
1072
+ "List all API endpoints: Method, Path, Description, Request params, Response format.",
1073
+ "",
1074
+ "### 2. Data Model",
1075
+ "For each table/collection: fields, types, constraints, descriptions.",
1076
+ "",
1077
+ "### 3. Business Logic",
1078
+ "Core business rules, validation rules, calculation formulas.",
1079
+ "",
1080
+ "### 4. State Transitions",
1081
+ "State machines (e.g. order status: pending → paid → shipped → completed).",
1082
+ "",
1083
+ "### 5. Error Scenarios & Error Codes",
1084
+ "All error codes, error messages, and when they occur.",
1085
+ "",
1086
+ "### 6. External Dependencies",
1087
+ "Third-party services, databases, message queues. Include timeout settings if documented.",
1088
+ "",
1089
+ "### 7. Acceptance Criteria",
1090
+ "Extract ALL acceptance criteria from 需求.md. Number them AC-001, AC-002, etc. If not explicitly listed, derive from functional requirements.",
1091
+ "",
1092
+ "### 8. Risk Areas",
1093
+ "High-risk areas requiring extra test coverage.",
1094
+ "",
1095
+ "## Conditional Sections (include ONLY if mentioned in requirements):",
1096
+ "- Authentication & Authorization: include ONLY if requirements mention auth mechanism (JWT, OAuth2, API Key, etc.)",
1097
+ "- Timeout Handling: include ONLY if requirements mention timeout configuration or degradation strategy",
1098
+ "- Concurrency & Idempotency: include ONLY if requirements mention concurrency, idempotency rules, or locking mechanisms",
1099
+ "- State Transitions: include ONLY if requirements mention business state machines",
1100
+ "- If not mentioned in requirements, do NOT include these sections",
1101
+ "",
1102
+ "This output will be used directly by downstream nodes. Be thorough and structured.",
1103
+ "Read-only: do not modify code, docs, artifacts, or repository files.",
1104
+ buildSourceContextBlock(sources),
1105
+ ].join("\n\n"),
1106
+ };
1107
+ }
1108
+ function buildGenerateBackendFunctionalCasesNode(sources) {
1109
+ return {
1110
+ id: "generate-backend-functional-cases-pi",
1111
+ depends_on: ["analyze-inputs-pi"],
1112
+ role: "implementer",
1113
+ executor: "pi",
1114
+ toolProfile: "write",
1115
+ complexity: "MED",
1116
+ writePolicy: "exclusive",
1117
+ writeSet: ["testcase/md/**"],
1118
+ allowedPaths: ["testcase/md/**"],
1119
+ forbiddenPaths: commonForbiddenPaths(sources),
1120
+ // 注意:Pi 节点超时由 executor 层控制(默认 30 分钟)
1121
+ // 如需调整,在 harness.json 的 executors.pi 中配置 modelConfig.timeoutMs
1122
+ subtask_prompt: [
1123
+ "Based on the upstream analyze-inputs-pi output, generate structured backend functional test cases.",
1124
+ "",
1125
+ "## Output Steps (do in order):",
1126
+ "1. First, output a brief summary: how many modules, how many cases planned per module",
1127
+ "2. Then write each test case file under testcase/md/",
1128
+ "",
1129
+ "## Format Rules:",
1130
+ "- Each test case ID: BE-<MODULE>-<NNN> (e.g. BE-ORDER-001)",
1131
+ "- Each file covers one module",
1132
+ "- Case structure: ID, Title, Precondition, Steps, Expected Result",
1133
+ "- Map each case to acceptance criteria (AC-xxx)",
1134
+ "",
1135
+ "## Coverage Requirements:",
1136
+ "- Positive paths: happy path for each acceptance criterion",
1137
+ "- Negative paths: error scenarios (invalid input, not found, state violations)",
1138
+ "- Boundary conditions: empty input, max length, edge values",
1139
+ "",
1140
+ "## Conditional Coverage (include ONLY if mentioned in upstream analysis):",
1141
+ "- State transitions: include ONLY if upstream analyze-inputs-pi mentions state machine",
1142
+ "- Authentication scenarios: include ONLY if upstream analyze-inputs-pi mentions auth mechanism",
1143
+ "- Timeout scenarios: include ONLY if upstream analyze-inputs-pi mentions timeout handling",
1144
+ "- Concurrency scenarios: include ONLY if upstream analyze-inputs-pi mentions concurrency/idempotency rules",
1145
+ "- If not mentioned, do NOT generate these test cases",
1146
+ "",
1147
+ "## Constraints:",
1148
+ "- Stay within writeSet: testcase/md/**",
1149
+ "- Do NOT re-read source documents — use the upstream analyze-inputs-pi output only",
1150
+ "- Do not write root artifacts/**",
1151
+ ].join("\n\n"),
1152
+ };
1153
+ }
1154
+ function buildReviewBackendCasesNode(sources) {
1155
+ return {
1156
+ id: "review-backend-cases-pi",
1157
+ depends_on: ["generate-backend-functional-cases-pi"],
1158
+ role: "reviewer",
1159
+ executor: "pi",
1160
+ complexity: "HIGH",
1161
+ writePolicy: "read-only",
1162
+ allowedPaths: commonReadOnlyPaths(sources),
1163
+ forbiddenPaths: commonForbiddenPaths(sources),
1164
+ outputContract: "Plain Markdown whose first non-empty line is VERDICT: pass or VERDICT: request-revision; followed by Findings and Coverage Assessment. No file writes.",
1165
+ subtask_prompt: [
1166
+ "Review the generated backend functional test cases under testcase/md/.",
1167
+ "",
1168
+ "## Mandatory First Line:",
1169
+ "First non-empty line must be exactly: VERDICT: pass or VERDICT: request-revision",
1170
+ "",
1171
+ "## Review Checklist:",
1172
+ "- ID format: every case uses BE-<MODULE>-<NNN>",
1173
+ "- Positive coverage: each acceptance criterion (AC-xxx) has happy-path case",
1174
+ "- Negative coverage: error scenarios (invalid input, not found, state violations)",
1175
+ "- Boundary coverage: edge cases (empty, max length, edge values)",
1176
+ "- Traceability: each AC maps to at least one case ID",
1177
+ "- Case structure: ID, Title, Precondition, Steps, Expected Result",
1178
+ "- No duplicate IDs across files",
1179
+ "",
1180
+ "## Conditional Coverage (check ONLY if mentioned in upstream analysis):",
1181
+ "- State transition coverage: check ONLY if analyze-inputs-pi mentions state machine",
1182
+ "- Authentication coverage: check ONLY if analyze-inputs-pi mentions auth mechanism",
1183
+ "- Timeout coverage: check ONLY if analyze-inputs-pi mentions timeout handling",
1184
+ "- Concurrency coverage: check ONLY if analyze-inputs-pi mentions concurrency/idempotency rules",
1185
+ "- If not mentioned, do NOT flag as missing",
1186
+ "",
1187
+ "## Verdict Rules:",
1188
+ "- All Critical checks pass + Important findings ≤ 2 → VERDICT: pass",
1189
+ "- Any Critical fails OR Important > 2 → VERDICT: request-revision",
1190
+ "",
1191
+ "## Output After Verdict:",
1192
+ "1. Coverage Assessment table (AC → case IDs)",
1193
+ "2. Findings list (Critical/Important/Informational)",
1194
+ "3. Statistics (total cases, positive/negative/boundary breakdown)",
1195
+ "",
1196
+ "## Constraints:",
1197
+ "- Read-only: do not modify files",
1198
+ "- Do NOT re-read source documents — use upstream analyze-inputs-pi output for acceptance criteria",
1199
+ "- Use testcase/md/ files for case review",
1200
+ ].join("\n\n"),
1201
+ };
1202
+ }
1203
+ function buildReviewBackendCasesGateNode(sources) {
1204
+ return {
1205
+ id: "review-backend-cases-gate-shell",
1206
+ depends_on: ["review-backend-cases-pi"],
1207
+ role: "verifier",
1208
+ executor: "shell",
1209
+ complexity: "LOW",
1210
+ writePolicy: "read-only",
1211
+ allowedPaths: commonReadOnlyPaths(sources),
1212
+ forbiddenPaths: commonForbiddenPaths(sources),
1213
+ outputContract: "Deterministic backend case review gate: exit 0 only when review-backend-cases-pi emits VERDICT: pass.",
1214
+ subtask_prompt: "Deterministic gate: block pytest generation unless backend case review emitted VERDICT: pass.",
1215
+ shell: {
1216
+ commands: [],
1217
+ verdictGate: {
1218
+ fromNodeId: "review-backend-cases-pi",
1219
+ accept: ["VERDICT: pass"],
1220
+ label: "backend case review",
1221
+ lineMode: "first-verdict-line",
1222
+ },
1223
+ cwd: ".",
1224
+ timeoutMs: 60000,
1225
+ },
1226
+ };
1227
+ }
1228
+ function buildGenerateBackendPytestNode(sources) {
1229
+ return {
1230
+ id: "generate-backend-pytest-pi",
1231
+ depends_on: ["review-backend-cases-gate-shell"],
1232
+ role: "implementer",
1233
+ executor: "pi",
1234
+ toolProfile: "write",
1235
+ complexity: "HIGH",
1236
+ writePolicy: "exclusive",
1237
+ writeSet: ["testcase/**/test_*.py"],
1238
+ allowedPaths: ["testcase/**/test_*.py"],
1239
+ forbiddenPaths: commonForbiddenPaths(sources),
1240
+ // 注意:Pi 节点超时由 executor 层控制(默认 30 分钟)
1241
+ // 如需调整,在 harness.json 的 executors.pi 中配置 modelConfig.timeoutMs
1242
+ subtask_prompt: [
1243
+ "Convert the reviewed test cases under testcase/md/ into pytest automation code.",
1244
+ "",
1245
+ "## Output Steps (do in order):",
1246
+ "1. First, output a brief summary: how many files, how many test functions planned",
1247
+ "2. Then write each test file under testcase/",
1248
+ "",
1249
+ "## Format Rules:",
1250
+ "- File prefix: test_<module>.py",
1251
+ "- Function name: test_BE_<MODULE>_<NNN>_<description>",
1252
+ "- Docstring first line: BE-<MODULE>-<NNN>: <Case Title>",
1253
+ "- 1:1 mapping: each functional case → one pytest function",
1254
+ "",
1255
+ "## Implementation Rules:",
1256
+ "- Use assert statements, not unittest assertions",
1257
+ "- Assert specific values, not just 'no exception'",
1258
+ "- Use @pytest.mark.parametrize for boundary cases",
1259
+ "- Use markers: @pytest.mark.positive, @pytest.mark.negative, @pytest.mark.boundary",
1260
+ "",
1261
+ "## Conditional Implementation (include ONLY if test cases exist):",
1262
+ "- Authentication tests: implement ONLY if testcase/md/ contains auth-related cases",
1263
+ "- Timeout tests: implement ONLY if testcase/md/ contains timeout-related cases",
1264
+ "- Use @pytest.mark.auth for auth tests, @pytest.mark.timeout for timeout tests",
1265
+ "- If no such cases exist, do NOT add these tests",
1266
+ "",
1267
+ "## Constraints:",
1268
+ "- Only create NEW files, do NOT modify existing framework files (conftest.py, pytest.ini, pyproject.toml)",
1269
+ "- If filename exists, add suffix: test_order.py → test_order_01.py",
1270
+ "- Stay within writeSet: testcase/**/test_*.py",
1271
+ "- Do NOT re-read source documents — use the reviewed cases under testcase/md/ only",
1272
+ "- Read existing conftest.py/pytest.ini to understand conventions, but do NOT modify them",
1273
+ ].join("\n\n"),
1274
+ };
1275
+ }
1276
+ function buildExecuteBackendPytestNode(sources) {
1277
+ return {
1278
+ id: "execute-backend-pytest-shell",
1279
+ depends_on: ["generate-backend-pytest-pi"],
1280
+ role: "verifier",
1281
+ executor: "shell",
1282
+ complexity: "LOW",
1283
+ writePolicy: "read-only",
1284
+ allowedPaths: commonReadOnlyPaths(sources),
1285
+ forbiddenPaths: commonForbiddenPaths(sources),
1286
+ outputContract: "Archived pytest stdout/stderr with exit codes and HTML report path; no source or test file modifications.",
1287
+ subtask_prompt: "Run pytest for the backend test suite and capture results.",
1288
+ shell: {
1289
+ commands: [
1290
+ "python -m pytest testcase/ --html=reports/backend-test-report.html -v",
1291
+ ],
1292
+ verifyEvidence: buildVerifyEvidence({
1293
+ phase: "final",
1294
+ quota: "full",
1295
+ commandSource: "inline",
1296
+ fallbackCommands: [
1297
+ "python -m pytest testcase/ --html=reports/backend-test-report.html -v",
1298
+ ],
1299
+ finalFullRequired: true,
1300
+ }),
1301
+ cwd: ".",
1302
+ timeoutMs: 300000,
1303
+ },
1304
+ };
1305
+ }
1306
+ function buildTestRetrospectNode(sources) {
1307
+ return {
1308
+ id: "test-retrospect-pi",
1309
+ depends_on: ["execute-backend-pytest-shell"],
1310
+ role: "closeout",
1311
+ executor: "pi",
1312
+ toolProfile: "write",
1313
+ complexity: "MED",
1314
+ writePolicy: "exclusive",
1315
+ writeSet: ["docs/test-reports/**"],
1316
+ allowedPaths: ["docs/test-reports/**"],
1317
+ forbiddenPaths: commonForbiddenPaths(sources),
1318
+ subtask_prompt: [
1319
+ "Read upstream outputs (review report + pytest results) and generate a test retrospective report.",
1320
+ "",
1321
+ "## Output Steps (do in order):",
1322
+ "1. First, output the maturity rating on the first line: Rating: A/B/C/D",
1323
+ "2. Then write the full report under docs/test-reports/",
1324
+ "",
1325
+ "## Report Structure:",
1326
+ "1. Maturity Rating with rationale",
1327
+ "2. Test Coverage Summary (total cases, pass rate, failed case analysis)",
1328
+ "3. Review Findings and resolution status",
1329
+ "4. Failed Test Analysis (if any)",
1330
+ "5. Recommendations for improvement",
1331
+ "",
1332
+ "## Rating Criteria:",
1333
+ "- A: 100% acceptance criteria covered + 100% pytest pass + no Critical findings",
1334
+ "- B: ≥80% coverage + ≥90% pass + Low findings only",
1335
+ "- C: ≥60% coverage + ≥70% pass + no Critical findings",
1336
+ "- D: below C thresholds",
1337
+ "",
1338
+ "## Constraints:",
1339
+ "- Stay within writeSet: docs/test-reports/**",
1340
+ "- Do NOT re-read source documents — use upstream outputs only",
1341
+ "- Do not write root artifacts/**",
1342
+ ].join("\n\n"),
1343
+ };
1344
+ }
1345
+ const BACKEND_TEST_DEFAULTS = {
1346
+ ...HYBRID_DEFAULTS,
1347
+ writePolicy: "read-only",
1348
+ };
1349
+ const BACKEND_TEST_SKILLS_BY_ROLE = {
1350
+ planner: ["loop-agent"],
1351
+ scout: [],
1352
+ implementer: ["test-driven-development", "verification-before-completion"],
1353
+ reviewer: ["requesting-code-review", "code-review-core"],
1354
+ verifier: ["verification-before-completion", "systematic-debugging"],
1355
+ closeout: ["loop-agent", "verification-before-completion"],
1356
+ };
1357
+ function buildBackendTestHybridDag(sources) {
1358
+ const { taskConfig } = sources;
1359
+ const sourceContext = buildSourceContextBlock(sources);
1360
+ const readOnlyPaths = commonReadOnlyPaths(sources);
1361
+ const forbiddenPaths = commonForbiddenPaths(sources);
1362
+ const globalConstraints = [
1363
+ ...taskConfig.hardConstraints,
1364
+ ...(sources.constraintMarkdown
1365
+ ? [`See 执行约束.md in task source (${sources.taskId})`]
1366
+ : []),
1367
+ ...STANDARD_GLOBAL_CONSTRAINTS,
1368
+ "backend-test-dag nodes must maintain traceability from requirements to functional cases to pytest automation.",
1369
+ "Functional test case IDs must use BE-<MODULE>-<NNN> format.",
1370
+ "pytest execution must produce HTML reports under reports/.",
1371
+ "pytest automation scripts must use test_ filename prefix for pytest discovery.",
1372
+ "generate-backend-pytest-pi must only create new test files under testcase/; modifying existing framework files (conftest.py, pytest.ini, pyproject.toml) is forbidden.",
1373
+ "review-backend-cases-gate-shell must block pytest generation unless the review verdict is exactly VERDICT: pass.",
1374
+ "If a target test filename already exists under testcase/, add a numeric suffix (_01, _02, ...); never overwrite or append to existing files.",
1375
+ "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.",
1376
+ ];
1377
+ const spec = {
1378
+ version: 3,
1379
+ title: `Backend test DAG: ${taskConfig.title}`,
1380
+ runtimeContract: GENERATED_DAG_RUNTIME_CONTRACT,
1381
+ outputLanguage: sources.outputLanguage ?? DEFAULT_DAG_OUTPUT_LANGUAGE,
1382
+ objective: extractObjective(sources.requirementMarkdown, taskConfig.title),
1383
+ successCriteria: extractSuccessCriteria(sources.requirementMarkdown, sources.taskId),
1384
+ globalConstraints,
1385
+ defaults: {
1386
+ ...BACKEND_TEST_DEFAULTS,
1387
+ contextProfile: taskConfig.contextProfile,
1388
+ },
1389
+ skillsByRole: BACKEND_TEST_SKILLS_BY_ROLE,
1390
+ executorModels: sources.executorModelMatrix ?? DEFAULT_DAG_EXECUTOR_MODELS,
1391
+ tasks: [
1392
+ buildAnalyzeInputsNode(sources),
1393
+ buildGenerateBackendFunctionalCasesNode(sources),
1394
+ buildReviewBackendCasesNode(sources),
1395
+ buildReviewBackendCasesGateNode(sources),
1396
+ buildGenerateBackendPytestNode(sources),
1397
+ buildExecuteBackendPytestNode(sources),
1398
+ buildTestRetrospectNode(sources),
1399
+ ],
1400
+ };
1401
+ applyDefaultReadOnlyRetryPolicy(spec);
659
1402
  parseDagSpec(spec);
660
1403
  assertValidDagSpec(spec);
661
1404
  return spec;
662
1405
  }
663
1406
  export function buildHybridDagFromTask(sources, options = {}) {
1407
+ if (sources.taskConfig.taskKind === "frontend-implementation" ||
1408
+ options.template === "frontend-implementation") {
1409
+ return buildFrontendHybridDagFromTask(sources);
1410
+ }
1411
+ if (sources.taskConfig.taskKind === "backend-test" ||
1412
+ options.template === "backend-test-dag") {
1413
+ return buildBackendTestHybridDag(sources);
1414
+ }
664
1415
  const standard = buildStandardHybridDagFromTask(sources);
665
1416
  const template = options.template ?? "standard-dag";
666
1417
  if (template === "standard-dag")
@@ -672,6 +1423,21 @@ export function buildHybridDagFromTask(sources, options = {}) {
672
1423
  function cloneTask(task, patch = {}) {
673
1424
  return { ...task, ...patch };
674
1425
  }
1426
+ /**
1427
+ * Apply the default read-only Pi retry policy to safe planner/scout/reviewer/
1428
+ * verifier/closeout Pi nodes in the generated DAG. Writers, supervisors,
1429
+ * dynamic, shell, static, and decision-gate nodes are skipped. Idempotent:
1430
+ * never overwrites an explicit retryPolicy a task already declares.
1431
+ */
1432
+ function applyDefaultReadOnlyRetryPolicy(spec) {
1433
+ for (const task of spec.tasks) {
1434
+ if (task.retryPolicy !== undefined)
1435
+ continue;
1436
+ if (isSafeReadOnlyPiRetryCandidate(task)) {
1437
+ task.retryPolicy = DEFAULT_READ_ONLY_PI_RETRY_POLICY;
1438
+ }
1439
+ }
1440
+ }
675
1441
  function getTaskOrThrow(spec, id) {
676
1442
  const task = spec.tasks.find((item) => item.id === id);
677
1443
  if (!task)
@@ -758,6 +1524,8 @@ function buildReviewGatedHybridDag(standard, sources) {
758
1524
  const closeout = getTaskOrThrow(spec, "closeout-pi");
759
1525
  replaceTask(spec, cloneTask(closeout, { depends_on: ["review-gate-shell"] }));
760
1526
  spec.tasks.splice(spec.tasks.length - 1, 0, buildReviewNode(sources), buildReviewGateNode(sources));
1527
+ applySddEmbeddedEnhancements(spec, sources.sddEmbeddedSkills ?? new Set());
1528
+ applyDefaultReadOnlyRetryPolicy(spec);
761
1529
  parseDagSpec(spec);
762
1530
  assertValidDagSpec(spec);
763
1531
  return spec;
@@ -807,7 +1575,7 @@ function buildWriteSetGateNode(sources) {
807
1575
  };
808
1576
  }
809
1577
  function buildSoftVerifyNode(sources) {
810
- const implementId = implementationNodeId(resolveImplementationExecutor(sources));
1578
+ const implementId = implementationNodeId();
811
1579
  const strategy = resolveDagVerifyStrategy(sources.taskConfig);
812
1580
  const fallbackCommands = ["npm run typecheck"];
813
1581
  const commands = buildVerifyShellCommands({
@@ -841,8 +1609,8 @@ function buildSoftVerifyNode(sources) {
841
1609
  };
842
1610
  }
843
1611
  function buildProcessSupervisorNode(sources) {
844
- const implementId = implementationNodeId(resolveImplementationExecutor(sources));
845
- const repairId = repairNodeId(resolveImplementationExecutor(sources));
1612
+ const implementId = implementationNodeId();
1613
+ const repairId = repairNodeId();
846
1614
  return {
847
1615
  id: "process-supervisor-pi",
848
1616
  depends_on: ["soft-verify-shell", implementId],
@@ -886,6 +1654,7 @@ function buildProcessGateNode(sources) {
886
1654
  },
887
1655
  repairArtifactGate: {
888
1656
  fromNodeId: "process-supervisor-pi",
1657
+ repairNodeId: repairNodeId(),
889
1658
  },
890
1659
  cwd: ".",
891
1660
  timeoutMs: 60000,
@@ -893,13 +1662,12 @@ function buildProcessGateNode(sources) {
893
1662
  };
894
1663
  }
895
1664
  function buildRepairNode(sources) {
896
- const writerExecutor = resolveImplementationExecutor(sources);
897
1665
  const implement = resolveImplementPaths(sources.taskConfig);
898
1666
  return {
899
- id: repairNodeId(writerExecutor),
1667
+ id: repairNodeId(),
900
1668
  depends_on: ["process-gate-shell", "process-supervisor-pi"],
901
1669
  role: "implementer",
902
- executor: writerExecutor,
1670
+ executor: "pi",
903
1671
  toolProfile: "write",
904
1672
  complexity: isInteractiveUiTask(sources.taskConfig)
905
1673
  ? "HIGH"
@@ -921,7 +1689,7 @@ function buildRepairNode(sources) {
921
1689
  };
922
1690
  }
923
1691
  function buildHardVerifyNode(sources) {
924
- const repairId = repairNodeId(resolveImplementationExecutor(sources));
1692
+ const repairId = repairNodeId();
925
1693
  const fallbackCommands = [
926
1694
  "HARNESS_ALLOW_ACTIVE_DAG_RUNS=1 bash scripts/check-repo.sh",
927
1695
  ];
@@ -980,7 +1748,7 @@ function buildSupervisedHybridDag(standard, sources) {
980
1748
  const scoutSrc = getTaskOrThrow(standard, "scout-src");
981
1749
  const scoutTests = getTaskOrThrow(standard, "scout-tests");
982
1750
  const plan = getTaskOrThrow(standard, "plan-pi");
983
- const implement = getTaskOrThrow(standard, implementationNodeId(resolveImplementationExecutor(sources)));
1751
+ const implement = getTaskOrThrow(standard, implementationNodeId());
984
1752
  const closeout = getTaskOrThrow(standard, "closeout-pi");
985
1753
  const authorityDecision = resolveAuthoritySurfaceAudit(sources);
986
1754
  const reviewDependsOn = authorityDecision.enabled
@@ -1028,6 +1796,8 @@ function buildSupervisedHybridDag(standard, sources) {
1028
1796
  cloneTask(closeout, { depends_on: ["decision-pi"] }),
1029
1797
  ],
1030
1798
  };
1799
+ applySddEmbeddedEnhancements(spec, sources.sddEmbeddedSkills ?? new Set());
1800
+ applyDefaultReadOnlyRetryPolicy(spec);
1031
1801
  parseDagSpec(spec);
1032
1802
  assertValidDagSpec(spec);
1033
1803
  return spec;