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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (284) hide show
  1. package/AGENTS.md +155 -153
  2. package/CHANGELOG.md +338 -265
  3. package/README.md +345 -298
  4. package/bin/agent-worker.js +22 -22
  5. package/bin/loop-agent.js +21 -21
  6. package/dist/application/dag/generate-task-dag.js +28 -28
  7. package/dist/application/evaluation/candidate-hash.js +75 -0
  8. package/dist/application/evaluation/candidate.js +52 -0
  9. package/dist/application/evaluation/replay.js +289 -0
  10. package/dist/application/evaluation/types.js +130 -0
  11. package/dist/cli/command-definitions.js +27 -7
  12. package/dist/cli/program.js +8 -4
  13. package/dist/commands/cursor-prompt.js +6 -6
  14. package/dist/commands/eval.js +235 -0
  15. package/dist/commands/init.js +544 -506
  16. package/dist/commands/knowledge.js +129 -31
  17. package/dist/commands/loop-benchmark.js +11 -11
  18. package/dist/commands/pi-reuse-benchmark.js +16 -16
  19. package/dist/executors/pi-sdk-executor.js +38 -24
  20. package/dist/executors/shell-executor.js +34 -2
  21. package/dist/executors/shell-presets.js +20 -0
  22. package/dist/executors/shell-verification.js +7 -0
  23. package/dist/governance/manifest-types.js +4 -0
  24. package/dist/infrastructure/evaluation/candidate-store.js +435 -0
  25. package/dist/infrastructure/evaluation/store.js +40 -0
  26. package/dist/sidecars/cursor-prompt/executor.js +1 -1
  27. package/dist/task/config-types.js +28 -1
  28. package/dist/task/runtime.js +27 -27
  29. package/dist/worker/cli.js +96 -1
  30. package/dist/worker/delivery/package.js +3 -3
  31. package/dist/worker/feature/decision-loader.js +37 -6
  32. package/dist/worker/feature/next-action.js +10 -2
  33. package/dist/worker/feature/ready-plan-projection.js +81 -0
  34. package/dist/worker/feature/reducer.js +2 -1
  35. package/dist/worker/feature/review.js +19 -2
  36. package/dist/worker/feature/run.js +27 -2
  37. package/dist/worker/follow-up/approve.js +5 -2
  38. package/dist/worker/follow-up/factory.js +1 -1
  39. package/dist/worker/observability/read-model.js +246 -41
  40. package/dist/worker/observe/routes.js +173 -15
  41. package/dist/worker/observe/spec-evidence.js +281 -0
  42. package/dist/worker/observe/static/api.js +46 -27
  43. package/dist/worker/observe/static/app.js +150 -150
  44. package/dist/worker/observe/static/constants.js +148 -148
  45. package/dist/worker/observe/static/copy.js +67 -67
  46. package/dist/worker/observe/static/dag-helpers.js +172 -172
  47. package/dist/worker/observe/static/dag-layout.d.ts +31 -31
  48. package/dist/worker/observe/static/dag-layout.js +83 -83
  49. package/dist/worker/observe/static/dag-model.js +72 -72
  50. package/dist/worker/observe/static/dom.js +61 -61
  51. package/dist/worker/observe/static/format-pool.js +67 -67
  52. package/dist/worker/observe/static/format.js +292 -292
  53. package/dist/worker/observe/static/index.html +308 -308
  54. package/dist/worker/observe/static/kpi.js +94 -94
  55. package/dist/worker/observe/static/relations.js +133 -128
  56. package/dist/worker/observe/static/router.js +93 -85
  57. package/dist/worker/observe/static/run-processing.js +148 -148
  58. package/dist/worker/observe/static/shell-chrome.js +68 -68
  59. package/dist/worker/observe/static/state.js +253 -253
  60. package/dist/worker/observe/static/styles.css +1902 -1890
  61. package/dist/worker/observe/static/views/batch.js +227 -226
  62. package/dist/worker/observe/static/views/dag-graph.js +172 -172
  63. package/dist/worker/observe/static/views/dag-inspector.js +607 -477
  64. package/dist/worker/observe/static/views/dag.js +362 -362
  65. package/dist/worker/observe/static/views/dashboard.js +445 -442
  66. package/dist/worker/observe/static/views/failures.js +143 -143
  67. package/dist/worker/observe/static/views/feature.js +492 -453
  68. package/dist/worker/observe/static/views/pool.js +350 -347
  69. package/dist/worker/observe/static/views/run.js +453 -453
  70. package/dist/worker/observe/static/views/session-timeline.js +205 -205
  71. package/dist/worker/observe/static/views/shell.js +7 -7
  72. package/dist/worker/observe/static/views/task.js +314 -260
  73. package/dist/worker/observe/static/views/timeline.js +163 -163
  74. package/dist/worker/pool/doctor.js +165 -0
  75. package/dist/worker/pool/migrate-state.js +303 -0
  76. package/dist/worker/pool/run-store.js +205 -17
  77. package/dist/worker/pool/types.js +17 -1
  78. package/dist/worker/pool/validation.js +100 -15
  79. package/dist/worker/report/morning-report.js +12 -2
  80. package/dist/worker/runner/run-ready.js +41 -26
  81. package/dist/worker/task-graph/ready-planner.js +136 -0
  82. package/dist/workflows/dag/backend-test-analysis-contract.js +120 -0
  83. package/dist/workflows/dag/canvas-observer.js +275 -275
  84. package/dist/workflows/dag/convergence/controller.js +16 -8
  85. package/dist/workflows/dag/dynamic-runtime/map.js +90 -2
  86. package/dist/workflows/dag/failure-routing.js +12 -1
  87. package/dist/workflows/dag/init-hybrid.js +2404 -360
  88. package/dist/workflows/dag/node-execution.js +9 -0
  89. package/dist/workflows/dag/prompt.js +9 -0
  90. package/dist/workflows/dag/report.js +35 -1
  91. package/dist/workflows/dag/runner.js +28 -2
  92. package/dist/workflows/dag/task-demand-routing.js +383 -0
  93. package/dist/workflows/dag/types.js +51 -13
  94. package/dist/workflows/dag/upstream-artifacts.js +1 -0
  95. package/dist/workflows/dag/validate.js +59 -1
  96. package/docs/README.md +106 -104
  97. package/docs/agent-dag-recovery-playbook.md +195 -184
  98. package/docs/agent-dag-runner.md +67 -67
  99. package/docs/architecture/README.md +26 -26
  100. package/docs/architecture/dag-execution.md +140 -140
  101. package/docs/architecture/evolution.md +54 -53
  102. package/docs/architecture/facts-and-state.md +71 -58
  103. package/docs/architecture/runtime-boundaries.md +191 -191
  104. package/docs/architecture/system-overview.md +93 -93
  105. package/docs/architecture/worker-and-feature.md +85 -81
  106. package/docs/cursor-prompt-sidecar.md +36 -36
  107. package/docs/decisions/README.md +18 -15
  108. package/docs/design/README.md +167 -77
  109. package/docs/development-principles.md +73 -73
  110. package/docs/exec-plans/README.md +6 -6
  111. package/docs/exec-plans/active/README.md +15 -9
  112. package/docs/exec-plans/completed/README.md +85 -73
  113. package/docs/feature-workflow.md +389 -261
  114. package/docs/harness-methodology-debugging.md +153 -153
  115. package/docs/harness-methodology-tdd.md +130 -130
  116. package/docs/harness-methodology-verification.md +27 -27
  117. package/docs/init-surface.manifest.json +289 -280
  118. package/docs/loop-agent-harness.md +142 -130
  119. package/docs/production-readiness.md +96 -96
  120. package/docs/progress/README.md +64 -54
  121. package/docs/reports/README.md +117 -94
  122. package/docs/skills/README.md +7 -7
  123. package/docs/skills/vetted-skill-registry.md +29 -27
  124. package/docs/templates/adr.md +60 -60
  125. package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
  126. package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
  127. package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -117
  128. package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
  129. package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
  130. package/docs/templates/agent-dag-report.schema.json +473 -473
  131. package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
  132. package/docs/templates/agent-dag.base.json +190 -190
  133. package/docs/templates/agent-dag.final-verification.json +185 -185
  134. package/docs/templates/agent-dag.schema.json +411 -383
  135. package/docs/templates/agent-dag.supervised-implementation.json +501 -501
  136. package/docs/templates/backend-test-analysis.schema.json +44 -0
  137. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +202 -139
  138. package/docs/templates/backend-test-dag.json +311 -276
  139. package/docs/templates/backend-test-dag.retrospect.prompt.md +125 -125
  140. package/docs/templates/backend-test-dag.review-cases.prompt.md +81 -81
  141. package/docs/templates/exec-plan.md +64 -64
  142. package/docs/templates/feature-spec.md +53 -53
  143. package/docs/templates/frontend-design-contract.md +42 -33
  144. package/docs/templates/frontend-task-constraints.md +35 -25
  145. package/docs/templates/frontend-task-requirement.md +70 -61
  146. package/docs/templates/frontend-test-dag.generate-cases.prompt.md +5 -0
  147. package/docs/templates/frontend-test-dag.json +23 -0
  148. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +3 -0
  149. package/docs/templates/frontend-test-dag.retrospect.prompt.md +3 -0
  150. package/docs/templates/frontend-test-dag.review-cases.prompt.md +3 -0
  151. package/docs/templates/frontend-test-dag.review-execution.prompt.md +3 -0
  152. package/docs/templates/harness.schema.json +221 -221
  153. package/docs/templates/hybrid-dag.json +188 -188
  154. package/docs/templates/init-evolution-review.md +35 -35
  155. package/docs/templates/interactive-ui-round2-experiment.md +66 -66
  156. package/docs/templates/knowledge-graph-bootstrap-dag.json +118 -0
  157. package/docs/templates/knowledge-sync-dag.json +178 -0
  158. package/docs/templates/knowledge-sync-draft.schema.json +71 -0
  159. package/docs/templates/product-line/AGENTS.md +8 -8
  160. package/docs/templates/product-line/README.md +9 -9
  161. package/docs/templates/product-line/acceptance.yaml +14 -14
  162. package/docs/templates/product-line/closeout.yaml +9 -9
  163. package/docs/templates/product-line/design.md +13 -13
  164. package/docs/templates/product-line/links.md +10 -10
  165. package/docs/templates/product-line/requirement.md +17 -17
  166. package/docs/templates/product-line/task-graph.yaml +15 -15
  167. package/docs/templates/product-line/task.yaml +64 -64
  168. package/docs/templates/product-line/test-plan.md +7 -7
  169. package/docs/templates/production-readiness-checklist.md +57 -57
  170. package/docs/templates/progress-log.md +17 -17
  171. package/docs/templates/project-start-checklist.md +9 -9
  172. package/docs/templates/qa-report.md +48 -48
  173. package/docs/templates/sprint-contract.md +29 -29
  174. package/docs/templates/worker-dogfood-evidence.md +80 -80
  175. package/docs/templates/worker-dogfood-setup.md +68 -68
  176. package/docs/verification-matrix.md +70 -66
  177. package/examples/decision-gate-agent-dag.json +177 -177
  178. package/examples/example-dag.json +46 -46
  179. package/examples/hybrid-loop-agent-dag.json +189 -189
  180. package/harness.json +66 -66
  181. package/package.json +88 -46
  182. package/scripts/check-product-line-docs.sh +29 -29
  183. package/scripts/check-task-pool-root.sh +32 -32
  184. package/scripts/kb-bootstrap-init-skeleton.sh +240 -0
  185. package/scripts/kb-graph-incremental-prepare.mjs +386 -0
  186. package/scripts/kb-graph-incremental-prepare.sh +5 -0
  187. package/scripts/kb-graph-materialize.mjs +105 -0
  188. package/scripts/kb-graph-materialize.sh +4 -0
  189. package/scripts/kb-graph-promote.mjs +164 -0
  190. package/scripts/kb-graph-promote.sh +4 -0
  191. package/scripts/kb-query.mjs +554 -0
  192. package/scripts/kb-query.sh +5 -0
  193. package/skills/agent-worker/SKILL.md +39 -37
  194. package/skills/agent-worker/references/agent-worker-operator.md +60 -43
  195. package/skills/ai-engineering-context/SKILL.md +48 -48
  196. package/skills/analyze-product-dependencies/SKILL.md +67 -0
  197. package/skills/analyze-product-dependencies/agents/openai.yaml +4 -0
  198. package/skills/analyze-product-dependencies/references/api-documentation-schema.md +30 -0
  199. package/skills/analyze-product-dependencies/references/dependency-analysis-schema.md +28 -0
  200. package/skills/analyze-product-dependencies/references/example.md +76 -0
  201. package/skills/analyze-product-dependencies/references/forward-test-cases.md +35 -0
  202. package/skills/analyze-product-dependencies/references/input-contract.md +11 -0
  203. package/skills/analyze-product-dependencies/references/scouting-rules.md +61 -0
  204. package/skills/analyze-product-dependencies/scripts/test-validators.mjs +267 -0
  205. package/skills/analyze-product-dependencies/scripts/validate-api-documentation.mjs +101 -0
  206. package/skills/analyze-product-dependencies/scripts/validate-dependency-analysis.mjs +142 -0
  207. package/skills/analyze-product-dependencies/scripts/validate-product-requirement-input.mjs +76 -0
  208. package/skills/analyze-product-dependencies/scripts/validation-helpers.mjs +146 -0
  209. package/skills/analyze-product-requirements/SKILL.md +90 -0
  210. package/skills/analyze-product-requirements/agents/openai.yaml +4 -0
  211. package/skills/analyze-product-requirements/references/acceptance-criteria.md +91 -0
  212. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +56 -0
  213. package/skills/analyze-product-requirements/references/example.md +86 -0
  214. package/skills/analyze-product-requirements/references/forward-test-cases.md +66 -0
  215. package/skills/analyze-product-requirements/references/product-analysis-schema.md +32 -0
  216. package/skills/analyze-product-requirements/references/product-requirement-schema.md +33 -0
  217. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +35 -0
  218. package/skills/analyze-product-requirements/scripts/test-validators.mjs +193 -0
  219. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +69 -0
  220. package/skills/analyze-product-requirements/scripts/validate-product-requirement.mjs +97 -0
  221. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +98 -0
  222. package/skills/analyze-product-requirements/scripts/validation-helpers.mjs +156 -0
  223. package/skills/code-review-core/SKILL.md +20 -20
  224. package/skills/codebase-scout/SKILL.md +19 -19
  225. package/skills/frontend-design-review/SKILL.md +66 -59
  226. package/skills/frontend-design-review/references/review-checklist.md +58 -37
  227. package/skills/frontend-implementation/SKILL.md +47 -51
  228. package/skills/frontend-implementation/references/code-standards.md +32 -34
  229. package/skills/frontend-implementation/references/design-spec.md +46 -46
  230. package/skills/frontend-implementation/references/node-contracts.md +76 -32
  231. package/skills/frontend-review/SKILL.md +59 -53
  232. package/skills/frontend-review/references/review-findings.md +47 -42
  233. package/skills/frontend-verification/SKILL.md +53 -40
  234. package/skills/frontend-verification/references/verification-checklist.md +68 -56
  235. package/skills/grill-me/SKILL.md +10 -10
  236. package/skills/grill-with-docs/SKILL.md +88 -88
  237. package/skills/grill-with-docs/adr-format.md +47 -47
  238. package/skills/grill-with-docs/context-format.md +60 -60
  239. package/skills/init-capability-evolution/SKILL.md +70 -70
  240. package/skills/loop-agent/SKILL.md +151 -151
  241. package/skills/loop-agent/references/README.md +67 -67
  242. package/skills/loop-agent/references/command-reference.md +505 -452
  243. package/skills/loop-agent/references/docs-converge.md +126 -126
  244. package/skills/loop-agent/references/harness-policy.md +263 -263
  245. package/skills/loop-agent/references/hybrid-dag.md +238 -233
  246. package/skills/loop-agent/references/learned/README.md +21 -21
  247. package/skills/loop-agent/references/long-running-loop.md +57 -57
  248. package/skills/loop-agent/references/model-routing.md +36 -36
  249. package/skills/loop-agent/references/multi-worktree.md +54 -54
  250. package/skills/loop-agent/references/one-shot-runs.md +85 -85
  251. package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -169
  252. package/skills/loop-agent/references/pi-prompt.md +23 -23
  253. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +84 -84
  254. package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -44
  255. package/skills/loop-agent/references/task-workflow.md +89 -89
  256. package/skills/loop-agent/references/verification-and-failure-handling.md +139 -139
  257. package/skills/playwright-cli/SKILL.md +420 -0
  258. package/skills/playwright-cli/references/element-attributes.md +23 -0
  259. package/skills/playwright-cli/references/playwright-tests.md +39 -0
  260. package/skills/playwright-cli/references/request-mocking.md +87 -0
  261. package/skills/playwright-cli/references/running-code.md +241 -0
  262. package/skills/playwright-cli/references/session-management.md +225 -0
  263. package/skills/playwright-cli/references/storage-state.md +275 -0
  264. package/skills/playwright-cli/references/test-generation.md +433 -0
  265. package/skills/playwright-cli/references/tracing.md +139 -0
  266. package/skills/playwright-cli/references/video-recording.md +143 -0
  267. package/skills/playwright-cli-case-generator/SKILL.md +74 -0
  268. package/skills/requesting-code-review/SKILL.md +101 -101
  269. package/skills/requesting-code-review/code-reviewer.md +168 -168
  270. package/skills/systematic-debugging/CREATION-LOG.md +119 -119
  271. package/skills/systematic-debugging/SKILL.md +296 -296
  272. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
  273. package/skills/systematic-debugging/condition-based-waiting.md +115 -115
  274. package/skills/systematic-debugging/defense-in-depth.md +122 -122
  275. package/skills/systematic-debugging/find-polluter.sh +63 -63
  276. package/skills/systematic-debugging/root-cause-tracing.md +169 -169
  277. package/skills/systematic-debugging/test-academic.md +14 -14
  278. package/skills/systematic-debugging/test-pressure-1.md +58 -58
  279. package/skills/systematic-debugging/test-pressure-2.md +68 -68
  280. package/skills/systematic-debugging/test-pressure-3.md +69 -69
  281. package/skills/test-driven-development/SKILL.md +20 -20
  282. package/skills/using-git-worktrees/SKILL.md +215 -215
  283. package/skills/verification-before-completion/SKILL.md +154 -154
  284. package/skills/webapp-testing/SKILL.md +19 -19
@@ -1,20 +1,42 @@
1
1
  import { z } from "zod";
2
2
  import { readFile, readdir } from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { getRunsJsonlPath, getTaskPoolRoot } from "./run-store.js";
4
+ import { getRunsJsonlPath, getStatesRoot, } from "./run-store.js";
5
+ import { TASK_POOL_STATE_ERROR_CODES, } from "./types.js";
5
6
  export const taskPoolRunFactSchema = z.object({
6
- schemaVersion: z.literal(1), batchRunId: z.string().min(1), workerRunId: z.string().min(1), taskId: z.string().min(1), featureId: z.string().min(1),
7
- status: z.enum(["succeeded", "failed", "run-error"]), recordedAt: z.string().datetime(),
7
+ schemaVersion: z.literal(1),
8
+ batchRunId: z.string().min(1),
9
+ workerRunId: z.string().min(1),
10
+ taskId: z.string().min(1),
11
+ featureId: z.string().min(1),
12
+ status: z.enum(["succeeded", "failed", "run-error"]),
13
+ recordedAt: z.string().datetime(),
8
14
  }).passthrough();
9
15
  export const taskPoolStateFactSchema = z.object({
10
- taskId: z.string().min(1), status: z.enum(["Draft", "Ready", "Queued", "Running", "AgentCompleted", "VerificationRunning", "HumanReview", "Done", "Failed", "Blocked", "Abandoned"]), updatedAt: z.string().datetime(),
16
+ taskId: z.string().min(1),
17
+ status: z.enum([
18
+ "Draft",
19
+ "Ready",
20
+ "Queued",
21
+ "Running",
22
+ "AgentCompleted",
23
+ "VerificationRunning",
24
+ "HumanReview",
25
+ "Done",
26
+ "Failed",
27
+ "Blocked",
28
+ "Abandoned",
29
+ ]),
30
+ updatedAt: z.string().datetime(),
31
+ schemaVersion: z.union([z.literal(1), z.literal(2)]).optional(),
32
+ featureId: z.string().min(1).optional(),
11
33
  }).passthrough();
12
34
  export async function readValidTaskPoolRuns(repoRoot) {
13
35
  const records = [];
14
36
  const warnings = [];
15
37
  try {
16
38
  const raw = await readFile(getRunsJsonlPath(repoRoot), "utf-8");
17
- for (const [index, line] of raw.split(/\r?\n/).filter(Boolean).entries())
39
+ for (const [index, line] of raw.split(/\r?\n/).filter(Boolean).entries()) {
18
40
  try {
19
41
  const parsed = taskPoolRunFactSchema.safeParse(JSON.parse(line));
20
42
  if (parsed.success)
@@ -25,6 +47,7 @@ export async function readValidTaskPoolRuns(repoRoot) {
25
47
  catch {
26
48
  warnings.push(`Task Pool runs.jsonl line ${index + 1} is corrupt`);
27
49
  }
50
+ }
28
51
  }
29
52
  catch (error) {
30
53
  if (!isNotFound(error))
@@ -32,23 +55,80 @@ export async function readValidTaskPoolRuns(repoRoot) {
32
55
  }
33
56
  return { records, warnings };
34
57
  }
58
+ /**
59
+ * Valid state reader for morning-report and similar consumers.
60
+ * - v2 feature-scoped states: included when path/content identity match.
61
+ * - bare taskId collisions among v2: warning, do not overwrite records.
62
+ * - legacy flat states: warning with migration-required code; never treated as Ready facts.
63
+ */
35
64
  export async function readValidTaskPoolStates(repoRoot) {
36
65
  const records = {};
37
66
  const warnings = [];
38
- const stateDir = path.join(getTaskPoolRoot(repoRoot), "states");
67
+ const stateDir = getStatesRoot(repoRoot);
39
68
  try {
40
- for (const entry of await readdir(stateDir))
41
- if (entry.endsWith(".json"))
69
+ const entries = await readdir(stateDir, { withFileTypes: true });
70
+ for (const entry of entries) {
71
+ if (entry.isFile() && entry.name.endsWith(".json")) {
72
+ const entryPath = path.join(stateDir, entry.name);
42
73
  try {
43
- const parsed = taskPoolStateFactSchema.safeParse(JSON.parse(await readFile(path.join(stateDir, entry), "utf-8")));
44
- if (parsed.success && parsed.data.taskId === entry.slice(0, -5))
45
- records[parsed.data.taskId] = parsed.data;
46
- else
47
- warnings.push(`Task Pool state is semantically invalid: ${entry}`);
74
+ const parsed = taskPoolStateFactSchema.safeParse(JSON.parse(await readFile(entryPath, "utf-8")));
75
+ if (!parsed.success) {
76
+ warnings.push(`Task Pool state is semantically invalid: ${entry.name}`);
77
+ continue;
78
+ }
79
+ const expectedTaskId = entry.name.slice(0, -5);
80
+ if (parsed.data.taskId !== expectedTaskId) {
81
+ warnings.push(`Task Pool state is semantically invalid: ${entry.name}`);
82
+ continue;
83
+ }
84
+ warnings.push(`${TASK_POOL_STATE_ERROR_CODES.MIGRATION_REQUIRED}: legacy state ${entry.name}`);
48
85
  }
49
86
  catch {
50
- warnings.push(`Task Pool state is corrupt: ${entry}`);
87
+ warnings.push(`Task Pool state is corrupt: ${entry.name}`);
51
88
  }
89
+ continue;
90
+ }
91
+ if (!entry.isDirectory())
92
+ continue;
93
+ const featureId = entry.name;
94
+ const featureDir = path.join(stateDir, featureId);
95
+ let featureEntries;
96
+ try {
97
+ featureEntries = await readdir(featureDir);
98
+ }
99
+ catch {
100
+ warnings.push(`Task Pool feature state directory is unreadable: ${featureId}`);
101
+ continue;
102
+ }
103
+ for (const fileName of featureEntries) {
104
+ if (!fileName.endsWith(".json"))
105
+ continue;
106
+ const relative = `${featureId}/${fileName}`;
107
+ try {
108
+ const parsed = taskPoolStateFactSchema.safeParse(JSON.parse(await readFile(path.join(featureDir, fileName), "utf-8")));
109
+ if (!parsed.success) {
110
+ warnings.push(`Task Pool state is semantically invalid: ${relative}`);
111
+ continue;
112
+ }
113
+ const expectedTaskId = fileName.slice(0, -5);
114
+ const data = parsed.data;
115
+ if (data.taskId !== expectedTaskId ||
116
+ data.featureId !== featureId ||
117
+ data.schemaVersion !== 2) {
118
+ warnings.push(`${TASK_POOL_STATE_ERROR_CODES.PATH_MISMATCH}: ${relative}`);
119
+ continue;
120
+ }
121
+ if (records[data.taskId]) {
122
+ warnings.push(`${TASK_POOL_STATE_ERROR_CODES.IDENTITY_AMBIGUOUS}: multiple features share taskId ${data.taskId}`);
123
+ continue;
124
+ }
125
+ records[data.taskId] = data;
126
+ }
127
+ catch {
128
+ warnings.push(`Task Pool state is corrupt: ${relative}`);
129
+ }
130
+ }
131
+ }
52
132
  }
53
133
  catch (error) {
54
134
  if (!isNotFound(error))
@@ -56,4 +136,9 @@ export async function readValidTaskPoolStates(repoRoot) {
56
136
  }
57
137
  return { records, warnings };
58
138
  }
59
- function isNotFound(error) { return Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT"); }
139
+ function isNotFound(error) {
140
+ return Boolean(error &&
141
+ typeof error === "object" &&
142
+ "code" in error &&
143
+ error.code === "ENOENT");
144
+ }
@@ -23,7 +23,16 @@ export async function renderMorningReport(options) {
23
23
  ...(features.length === 0 ? ["- Status: no Feature Packet discovered", "- Next Action: add or locate a Feature Packet", "- Why: no shared Feature read model is available", "- Evidence: none"] : features.flatMap((feature) => {
24
24
  const why = feature.blockingItems[0]?.message ?? `${feature.summary.tasksSucceeded}/${feature.summary.tasksTotal} tasks; ${feature.summary.requiredAcCovered}/${feature.summary.requiredAcTotal} required AC`;
25
25
  const evidence = [feature.evidence.delivery, feature.evidence.closeout, feature.evidence.morningReport, feature.evidence.observeSnapshot, ...feature.blockingItems.flatMap((item) => item.evidence)].find(Boolean) ?? "none";
26
- return [`### ${feature.featureId}`, "", `- Status: ${feature.status}`, `- Next Action: ${feature.nextAction?.command ?? feature.nextAction?.label ?? "none"}`, `- Why: ${why}`, `- Evidence: ${evidence}`, ""];
26
+ const selected = feature.planning?.selected[0];
27
+ const deferred = feature.planning?.deferred ?? [];
28
+ const blocked = feature.planning?.blocked ?? [];
29
+ const deferredText = deferred.length > 0
30
+ ? deferred.map((item) => `${item.taskId} (${item.reasonCode})`).join(", ")
31
+ : "none";
32
+ const blockedText = blocked.length > 0
33
+ ? blocked.map((item) => `${item.taskId} (${item.reasonCode}${item.blockedBy.length ? `; blockedBy ${item.blockedBy.join(",")}` : ""})`).join(", ")
34
+ : "none";
35
+ return [`### ${feature.featureId}`, "", `- Status: ${feature.status}`, `- Next Task: ${selected ? `${selected.taskId} (${selected.priority})` : "none"}`, `- Deferred: ${deferredText}`, `- Blocked: ${blockedText}`, `- Next Action: ${feature.nextAction?.command ?? feature.nextAction?.label ?? "none"}`, `- Why: ${why}`, `- Evidence: ${evidence}`, ""];
27
36
  })),
28
37
  "## Summary",
29
38
  "",
@@ -40,7 +49,8 @@ export async function renderMorningReport(options) {
40
49
  ];
41
50
  for (const run of runs) {
42
51
  const followUp = await followUpSummary(run, options.repoRoot);
43
- lines.push(`| ${run.taskId} | ${run.status} | ${run.workerRunId} | ${run.failure?.category ?? "-"} | ${followUp ?? run.failure?.derivedFollowUpTaskId ?? "-"} | ${await artifactSummary(run)} |`);
52
+ const identity = `${run.featureId}/${run.taskId}`;
53
+ lines.push(`| ${identity} | ${run.status} | ${run.workerRunId} | ${run.failure?.category ?? "-"} | ${followUp ?? run.failure?.derivedFollowUpTaskId ?? "-"} | ${await artifactSummary(run)} |`);
44
54
  }
45
55
  if (followUps > 0) {
46
56
  lines.push("", "## Human Actions", "");
@@ -4,10 +4,10 @@ import path from "node:path";
4
4
  import YAML from "yaml";
5
5
  import { controllerIdentitiesMatch, controllerIdentityExpectationFailure, resolveControllerIdentity, } from "../loop-agent/loop-agent-client.js";
6
6
  import { deriveFailureRoute, deriveFailureRouteFromError, } from "../pool/failure-routing.js";
7
- import { findRunByWorkerRunId, getTaskPoolRoot, readAllTaskPoolStates, recordTaskPoolRun, writeTaskPoolState, } from "../pool/run-store.js";
7
+ import { findRunByWorkerRunId, getTaskPoolRoot, readFeatureTaskPoolStates, recordTaskPoolRun, writeTaskPoolState, } from "../pool/run-store.js";
8
8
  import { runTaskSpec, } from "../run-task/run-task.js";
9
9
  import { formatDuration, noopProgressReporter, } from "../progress-reporter.js";
10
- import { computeReadyQueue } from "../task-graph/ready-queue.js";
10
+ import { planReadyTasks } from "../task-graph/ready-planner.js";
11
11
  import { taskGraphSpecSchema } from "../task-graph/task-graph-schema.js";
12
12
  import { taskSpecSchema } from "../task-spec/schema.js";
13
13
  import { preflightTargetRepo } from "../preflight.js";
@@ -34,15 +34,26 @@ export async function runReadyTasks(options) {
34
34
  const startedAt = now.toISOString();
35
35
  const batchRunId = options.batchRunId ?? buildBatchRunId(now);
36
36
  const graph = await loadTaskGraph(options.featureDir);
37
- const states = await readAllTaskPoolStates(options.repoRoot);
38
- const readyTaskIds = computeReadyQueue(graph, toGraphState(states)).filter((taskId) => isPoolReady(states[taskId]));
39
- const limitedTaskIds = readyTaskIds.slice(0, options.limit ?? readyTaskIds.length);
37
+ const featureId = graph.feature_id;
38
+ const states = await readFeatureTaskPoolStates(options.repoRoot, featureId);
39
+ const taskSpecs = await loadFeatureTaskSpecs(options.featureDir, graph);
40
+ const plan = planReadyTasks({
41
+ featureId,
42
+ graph,
43
+ taskSpecs,
44
+ states,
45
+ selectionLimit: options.limit ?? graph.nodes.length,
46
+ });
47
+ const limitedTaskIds = plan.selected.map((candidate) => candidate.taskId);
40
48
  const tasks = [];
41
49
  const runner = options.runTask ?? runTaskSpec;
42
50
  const progress = options.progress ?? noopProgressReporter;
43
51
  const total = limitedTaskIds.length;
44
52
  let index = 0;
45
53
  const batchStartedAt = Date.now();
54
+ const readyPlanPath = path.join(getTaskPoolRoot(options.repoRoot), "artifacts", batchRunId, "ready-plan.json");
55
+ await mkdir(path.dirname(readyPlanPath), { recursive: true });
56
+ await writeFile(readyPlanPath, `${JSON.stringify(plan, null, 2)}\n`, "utf-8");
46
57
  emit(progress, {
47
58
  type: "readyQueue.computed",
48
59
  source: "worker",
@@ -61,7 +72,10 @@ export async function runReadyTasks(options) {
61
72
  index += 1;
62
73
  const node = graph.nodes.find((candidate) => candidate.id === taskId);
63
74
  const taskSpecPath = path.join(options.featureDir, "tasks", node?.task ?? `${taskId}.yaml`);
64
- const taskSpec = await loadTaskSpec(taskSpecPath);
75
+ const taskSpec = taskSpecs.get(taskId) ?? await loadTaskSpec(taskSpecPath);
76
+ if (taskSpec.feature_id !== featureId) {
77
+ throw new Error(`task ${taskId} feature_id ${taskSpec.feature_id} does not match graph feature_id ${featureId}`);
78
+ }
65
79
  const retryOfWorkerRunId = states[taskId]?.retryOfWorkerRunId;
66
80
  let workerRunId = retryOfWorkerRunId
67
81
  ? buildRetryWorkerRunId(taskId, retryOfWorkerRunId, now)
@@ -69,14 +83,14 @@ export async function runReadyTasks(options) {
69
83
  buildStableWorkerRunId(taskId, taskSpec, now, controllerIdentity);
70
84
  if (workerRunId) {
71
85
  let existing = await findRunByWorkerRunId(options.repoRoot, workerRunId);
72
- if (existing && !controllerRunCanBeReused(existing, controllerIdentity)) {
86
+ if (existing && !runCanBeReused(existing, featureId, taskId, controllerIdentity)) {
73
87
  workerRunId = buildControllerScopedWorkerRunId(workerRunId, controllerIdentity);
74
88
  existing = await findRunByWorkerRunId(options.repoRoot, workerRunId);
75
- if (existing && !controllerRunCanBeReused(existing, controllerIdentity)) {
76
- throw new Error(`workerRunId collision has incompatible controller identity: ${workerRunId}`);
89
+ if (existing && !runCanBeReused(existing, featureId, taskId, controllerIdentity)) {
90
+ throw new Error(`workerRunId collision has incompatible feature/controller identity: ${workerRunId}`);
77
91
  }
78
92
  }
79
- if (existing) {
93
+ if (existing && runCanBeReused(existing, featureId, taskId, controllerIdentity)) {
80
94
  progress.task(`task ${index}/${total} ${taskId}: reuse existing run ${workerRunId}`);
81
95
  emit(progress, {
82
96
  type: "task.reused",
@@ -112,6 +126,8 @@ export async function runReadyTasks(options) {
112
126
  let result;
113
127
  try {
114
128
  await writeTaskPoolState(options.repoRoot, {
129
+ schemaVersion: 2,
130
+ featureId,
115
131
  taskId,
116
132
  status: "Running",
117
133
  updatedAt: new Date().toISOString(),
@@ -277,6 +293,12 @@ export async function runReadyTasks(options) {
277
293
  summary,
278
294
  tasks,
279
295
  batchRunPath,
296
+ readyPlanPath,
297
+ selectedTaskIds: limitedTaskIds,
298
+ deferredTaskIds: plan.deferred.map((candidate) => candidate.taskId),
299
+ orderingPolicy: plan.orderingPolicy,
300
+ selectionLimit: plan.selectionLimit,
301
+ effectiveConcurrency: plan.effectiveConcurrency,
280
302
  ...(controllerIdentity ? { controllerIdentity } : {}),
281
303
  };
282
304
  await mkdir(path.dirname(batchRunPath), { recursive: true });
@@ -345,7 +367,9 @@ function buildControllerScopedWorkerRunId(baseWorkerRunId, controllerIdentity) {
345
367
  .slice(0, 10);
346
368
  return `${baseWorkerRunId}-controller-${hash}`;
347
369
  }
348
- function controllerRunCanBeReused(existing, controllerIdentity) {
370
+ function runCanBeReused(existing, featureId, taskId, controllerIdentity) {
371
+ if (existing.featureId !== featureId || existing.taskId !== taskId)
372
+ return false;
349
373
  if (!existing.controllerIdentity && !controllerIdentity)
350
374
  return true;
351
375
  return controllerIdentitiesMatch(existing.controllerIdentity, controllerIdentity);
@@ -370,22 +394,13 @@ async function loadTaskGraph(featureDir) {
370
394
  async function loadTaskSpec(taskSpecPath) {
371
395
  return taskSpecSchema.parse(YAML.parse(await readFile(taskSpecPath, "utf-8")));
372
396
  }
373
- function toGraphState(states) {
374
- const graphState = {};
375
- for (const [taskId, state] of Object.entries(states)) {
376
- if (state.status === "Done")
377
- graphState[taskId] = "completed";
378
- if (state.status === "Running" || state.status === "Queued")
379
- graphState[taskId] = "running";
380
- if (state.status === "Failed")
381
- graphState[taskId] = "failed";
382
- if (state.status === "Blocked")
383
- graphState[taskId] = "blocked";
397
+ async function loadFeatureTaskSpecs(featureDir, graph) {
398
+ const specs = new Map();
399
+ for (const node of graph.nodes) {
400
+ const taskSpec = await loadTaskSpec(path.join(featureDir, "tasks", node.task ?? `${node.id}.yaml`));
401
+ specs.set(node.id, taskSpec);
384
402
  }
385
- return graphState;
386
- }
387
- function isPoolReady(state) {
388
- return !state || state.status === "Ready";
403
+ return specs;
389
404
  }
390
405
  function summarize(tasks) {
391
406
  return {
@@ -0,0 +1,136 @@
1
+ const OWN_STATUS_REASON = {
2
+ Draft: "task-draft",
3
+ Queued: "task-queued",
4
+ Running: "task-running",
5
+ AgentCompleted: "task-agent-completed",
6
+ VerificationRunning: "task-verification-running",
7
+ HumanReview: "task-human-review",
8
+ Done: "task-done",
9
+ Failed: "task-failed",
10
+ Blocked: "task-blocked",
11
+ Abandoned: "task-abandoned",
12
+ };
13
+ /** Contract severity: failed > blocked > not-completed > missing-state. */
14
+ function dependencyReason(unmet) {
15
+ if (unmet.some((item) => item.state === "Failed"))
16
+ return "dependency-failed";
17
+ if (unmet.some((item) => item.state === "Blocked"))
18
+ return "dependency-blocked";
19
+ if (unmet.some((item) => item.state !== undefined))
20
+ return "dependency-not-completed";
21
+ return "dependency-missing-state";
22
+ }
23
+ function ownReason(status) {
24
+ if (status === "Ready")
25
+ return undefined;
26
+ return OWN_STATUS_REASON[status] ?? "task-state-invalid";
27
+ }
28
+ export function planReadyTasks(input) {
29
+ if (!Number.isInteger(input.selectionLimit) || input.selectionLimit < 1) {
30
+ throw new Error("selectionLimit must be a positive integer");
31
+ }
32
+ const eligible = [];
33
+ const blocked = [];
34
+ for (const [graphIndex, node] of input.graph.nodes.entries()) {
35
+ const spec = input.taskSpecs.get(node.id);
36
+ if (!spec) {
37
+ blocked.push({
38
+ featureId: input.featureId,
39
+ taskId: node.id,
40
+ reasonCode: "task-spec-missing",
41
+ reason: "TaskSpec is missing",
42
+ blockedBy: [],
43
+ });
44
+ continue;
45
+ }
46
+ if (spec.id !== node.id || spec.type !== node.type || !same(spec.depends_on, node.depends_on)) {
47
+ blocked.push({
48
+ featureId: input.featureId,
49
+ taskId: node.id,
50
+ priority: spec.priority,
51
+ reasonCode: "task-spec-mismatch",
52
+ reason: "TaskSpec does not match task graph",
53
+ blockedBy: [],
54
+ });
55
+ continue;
56
+ }
57
+ const state = input.states[node.id];
58
+ if (state) {
59
+ const own = ownReason(state.status);
60
+ if (own) {
61
+ blocked.push({
62
+ featureId: input.featureId,
63
+ taskId: node.id,
64
+ priority: spec.priority,
65
+ reasonCode: own,
66
+ reason: own === "task-state-invalid"
67
+ ? `task status is invalid: ${String(state.status)}`
68
+ : `task status is ${state.status}`,
69
+ blockedBy: [],
70
+ currentStatus: state.status,
71
+ });
72
+ continue;
73
+ }
74
+ }
75
+ const deps = node.depends_on.map((id) => ({ id, state: input.states[id]?.status }));
76
+ const unmet = deps.filter(({ state: depState }) => depState !== "Done");
77
+ if (unmet.length) {
78
+ blocked.push({
79
+ featureId: input.featureId,
80
+ taskId: node.id,
81
+ priority: spec.priority,
82
+ reasonCode: dependencyReason(unmet),
83
+ reason: "dependencies are not completed",
84
+ blockedBy: unmet.map((item) => item.id),
85
+ });
86
+ continue;
87
+ }
88
+ eligible.push({
89
+ featureId: input.featureId,
90
+ taskId: node.id,
91
+ title: spec.title,
92
+ type: spec.type,
93
+ priority: spec.priority,
94
+ riskLevel: spec.risk_level,
95
+ graphIndex,
96
+ reason: "dependencies-satisfied",
97
+ });
98
+ }
99
+ eligible.sort((a, b) => priority(a.priority) - priority(b.priority) ||
100
+ a.graphIndex - b.graphIndex ||
101
+ ascii(a.taskId, b.taskId));
102
+ const selected = eligible.slice(0, input.selectionLimit);
103
+ const deferred = eligible.slice(input.selectionLimit).map((candidate) => ({
104
+ ...candidate,
105
+ reasonCode: "selection-limit",
106
+ selectedAhead: selected.map((item) => item.taskId),
107
+ }));
108
+ return {
109
+ schemaVersion: 1,
110
+ featureId: input.featureId,
111
+ executionMode: "serial",
112
+ effectiveConcurrency: 1,
113
+ selectionLimit: input.selectionLimit,
114
+ orderingPolicy: ["priority:P0>P1>P2>P3", "graph-order", "task-id"],
115
+ eligible,
116
+ blocked,
117
+ selected,
118
+ deferred,
119
+ summary: {
120
+ total: input.graph.nodes.length,
121
+ eligible: eligible.length,
122
+ blocked: blocked.length,
123
+ selected: selected.length,
124
+ deferred: deferred.length,
125
+ },
126
+ };
127
+ }
128
+ function priority(value) {
129
+ return { P0: 0, P1: 1, P2: 2, P3: 3 }[value];
130
+ }
131
+ function ascii(a, b) {
132
+ return a < b ? -1 : a > b ? 1 : 0;
133
+ }
134
+ function same(a, b) {
135
+ return a.length === b.length && a.every((value, index) => value === b[index]);
136
+ }
@@ -0,0 +1,120 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { z } from "zod";
5
+ import { writeDagRunJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
6
+ export const BACKEND_TEST_ANALYSIS_SCHEMA_ID = "backend-test-analysis-v1";
7
+ const sourceRefSchema = z.string().min(1);
8
+ const fieldSchema = z.object({
9
+ name: z.string().min(1),
10
+ type: z.string().min(1).optional(),
11
+ required: z.boolean().optional(),
12
+ description: z.string().optional(),
13
+ }).strict();
14
+ const errorCaseSchema = z.object({
15
+ status: z.number().int().min(400).max(599).optional(),
16
+ code: z.string().min(1).optional(),
17
+ messageField: z.string().min(1).optional(),
18
+ description: z.string().min(1),
19
+ }).strict();
20
+ export const backendTestAnalysisContractSchema = z.object({
21
+ schemaVersion: z.literal(1),
22
+ sourceBinding: z.object({
23
+ taskId: z.string().min(1),
24
+ requirementPath: z.string().min(1),
25
+ requirementSha256: z.string().regex(/^[a-f0-9]{64}$/),
26
+ referencePaths: z.array(z.string().min(1)),
27
+ requirementIds: z.array(z.string().regex(/^(?:REQ|BR|AC)-[A-Z0-9]+(?:-[A-Z0-9]+)*$/)),
28
+ }).strict(),
29
+ acceptanceCriteria: z.array(z.object({
30
+ id: z.string().regex(/^AC-[A-Z0-9]+(?:-[A-Z0-9]+)*$/),
31
+ text: z.string().min(1),
32
+ sourceRef: sourceRefSchema,
33
+ }).strict()),
34
+ endpoints: z.array(z.object({
35
+ id: z.string().min(1),
36
+ method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]),
37
+ path: z.string().startsWith("/"),
38
+ requestFields: z.array(fieldSchema),
39
+ responseFields: z.array(fieldSchema),
40
+ successStatuses: z.array(z.number().int().min(100).max(399)),
41
+ errorCases: z.array(errorCaseSchema),
42
+ }).strict()),
43
+ dataModels: z.array(z.object({ id: z.string().min(1), description: z.string().min(1), sourceRef: sourceRefSchema }).strict()),
44
+ businessRules: z.array(z.object({ id: z.string().min(1), text: z.string().min(1), sourceRef: sourceRefSchema }).strict()),
45
+ stateTransitions: z.array(z.object({ from: z.string().min(1), to: z.string().min(1), trigger: z.string().min(1), sourceRef: sourceRefSchema }).strict()),
46
+ boundaryConstraints: z.array(z.object({ field: z.string().min(1), constraint: z.string().min(1), sourceRef: sourceRefSchema }).strict()),
47
+ externalDependencies: z.array(z.object({ name: z.string().min(1), description: z.string().min(1), sourceRef: sourceRefSchema.optional() }).strict()),
48
+ risks: z.array(z.object({ description: z.string().min(1), sourceRef: sourceRefSchema.optional() }).strict()),
49
+ evidenceGaps: z.array(z.object({ description: z.string().min(1), requirementId: z.string().optional(), sourceRef: sourceRefSchema.optional() }).strict()),
50
+ }).strict();
51
+ const SECRET_KEY = /(?:password|passwd|secret|token|api[_-]?key|private[_-]?key|credential|authorization)/i;
52
+ const SECRET_VALUE = /(?:-----BEGIN [A-Z ]*PRIVATE KEY-----|\b(?:sk|ghp|github_pat|xox[baprs]|AKIA)[-_A-Za-z0-9]{12,}\b)/;
53
+ function secretIssues(value, at = "$", issues = []) {
54
+ if (typeof value === "string" && SECRET_VALUE.test(value))
55
+ issues.push(`${at}: secret-like value is forbidden`);
56
+ if (Array.isArray(value))
57
+ value.forEach((item, index) => secretIssues(item, `${at}[${index}]`, issues));
58
+ else if (value && typeof value === "object") {
59
+ for (const [key, child] of Object.entries(value)) {
60
+ if (SECRET_KEY.test(key))
61
+ issues.push(`${at}.${key}: secret-shaped field name is forbidden`);
62
+ secretIssues(child, `${at}.${key}`, issues);
63
+ }
64
+ }
65
+ return issues;
66
+ }
67
+ export function extractStrictJsonObject(text) {
68
+ const trimmed = text.trim();
69
+ if (trimmed.startsWith("{") && trimmed.endsWith("}"))
70
+ return JSON.parse(trimmed);
71
+ const blocks = [...trimmed.matchAll(/```json\s*\n([\s\S]*?)\n```/gi)];
72
+ if (blocks.length !== 1 || trimmed.replace(blocks[0][0], "").trim()) {
73
+ throw new Error("analysis output must be one pure JSON object or one fenced json block with no trailing text");
74
+ }
75
+ return JSON.parse(blocks[0][1]);
76
+ }
77
+ function assertSourceBinding(contract, binding) {
78
+ const requirement = binding.sources.find((source) => source.kind === "requirement");
79
+ const references = binding.sources.filter((source) => source.kind === "reference").map((source) => source.path).sort();
80
+ const actualReferences = [...contract.sourceBinding.referencePaths].sort();
81
+ if (!requirement || contract.sourceBinding.taskId !== binding.taskId || contract.sourceBinding.requirementPath !== requirement.path || contract.sourceBinding.requirementSha256 !== requirement.sha256) {
82
+ throw new Error("analysis source binding does not match DAG requirement source");
83
+ }
84
+ if (JSON.stringify(actualReferences) !== JSON.stringify(references))
85
+ throw new Error("analysis referencePaths do not match DAG source binding");
86
+ if (JSON.stringify(contract.sourceBinding.requirementIds) !== JSON.stringify(binding.requirementIds))
87
+ throw new Error("analysis requirementIds do not match DAG source binding");
88
+ const explicitAc = binding.requirementIds.filter((id) => id.startsWith("AC-"));
89
+ const contractAc = contract.acceptanceCriteria.map((item) => item.id);
90
+ for (const id of explicitAc)
91
+ if (!contractAc.includes(id) && !contract.evidenceGaps.some((gap) => gap.requirementId === id))
92
+ throw new Error(`analysis contract does not cover explicit acceptance criterion ${id}`);
93
+ }
94
+ export async function materializeBackendTestAnalysisContract(input) {
95
+ if (!input.sourceBinding)
96
+ throw new Error("backend-test analysis gate requires DAG sourceBinding");
97
+ if (!/^[a-z0-9][a-z0-9._-]*\.json$/.test(input.artifactName) || !/^[a-z0-9][a-z0-9._-]*$/.test(input.outputDir))
98
+ throw new Error("unsafe structured artifact path");
99
+ const nodePath = path.join(input.runDir, `${input.fromNodeId}.json`);
100
+ const record = JSON.parse(await readFile(nodePath, "utf8"));
101
+ const raw = record.assistantText?.trim() || record.stdout?.trim() || "";
102
+ let parsed;
103
+ try {
104
+ parsed = extractStrictJsonObject(raw);
105
+ }
106
+ catch (error) {
107
+ throw new Error(`invalid-output: ${error instanceof Error ? error.message : String(error)}`);
108
+ }
109
+ const secrets = secretIssues(parsed);
110
+ if (secrets.length)
111
+ throw new Error(`invalid-output: ${secrets.join("; ")}`);
112
+ const result = backendTestAnalysisContractSchema.safeParse(parsed);
113
+ if (!result.success)
114
+ throw new Error(`invalid-output: ${result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")}`);
115
+ assertSourceBinding(result.data, input.sourceBinding);
116
+ const relativePath = path.posix.join(input.outputDir, input.artifactName);
117
+ const artifactPath = await writeDagRunJsonArtifact(input.runDir, relativePath, result.data);
118
+ const normalized = `${JSON.stringify(result.data, null, 2)}\n`;
119
+ return { path: artifactPath, sha256: createHash("sha256").update(normalized).digest("hex"), schemaId: BACKEND_TEST_ANALYSIS_SCHEMA_ID };
120
+ }