@tea-agent/loop-agent 0.9.0 → 0.10.0-alpha.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 (183) hide show
  1. package/AGENTS.md +8 -0
  2. package/CHANGELOG.md +92 -11
  3. package/README.md +69 -5
  4. package/dist/application/dag/args.js +4 -14
  5. package/dist/application/dag/generate-task-dag.js +32 -2
  6. package/dist/application/dag/run-dag.js +1 -27
  7. package/dist/application/dag/validate-dag.js +2 -2
  8. package/dist/application/loop/run-action.js +0 -4
  9. package/dist/cli/command-definitions.js +7 -11
  10. package/dist/cli/program.js +9 -21
  11. package/dist/commands/cursor-prompt.js +42 -82
  12. package/dist/commands/dag-approve.js +36 -0
  13. package/dist/commands/dag-reconcile-run.js +118 -0
  14. package/dist/commands/delegate.js +75 -77
  15. package/dist/commands/doctor.js +0 -18
  16. package/dist/commands/init.js +60 -40
  17. package/dist/commands/instructions.js +7 -10
  18. package/dist/commands/loop.js +4 -20
  19. package/dist/executors/config-core.js +0 -51
  20. package/dist/executors/dag-pi-executor.js +1 -1
  21. package/dist/executors/dag.js +0 -1
  22. package/dist/executors/index.js +0 -2
  23. package/dist/executors/model-routing.js +9 -9
  24. package/dist/executors/shell-executor.js +1 -1
  25. package/dist/governance/checks.js +6 -3
  26. package/dist/governance/manifest-types.js +33 -2
  27. package/dist/infrastructure/harness/loop-action-store.js +0 -3
  28. package/dist/records/harvest.js +2 -23
  29. package/dist/records/one-shot-runs.js +1 -1
  30. package/dist/shared/artifacts-core.js +24 -5
  31. package/dist/shared/output-truncation.js +37 -0
  32. package/dist/shared/package-metadata.js +353 -0
  33. package/dist/{executors/cursor-executor.js → sidecars/cursor-prompt/executor.js} +2 -42
  34. package/dist/sidecars/cursor-prompt/index.js +3 -0
  35. package/dist/sidecars/cursor-prompt/stream.js +121 -0
  36. package/dist/task/config-types.js +29 -12
  37. package/dist/task/delegate.js +9 -21
  38. package/dist/task/runtime.js +1 -2
  39. package/dist/worker/cli.js +243 -0
  40. package/dist/worker/closeout/apply.js +73 -0
  41. package/dist/worker/closeout/preview.js +30 -0
  42. package/dist/worker/delivery/final-verification.js +194 -0
  43. package/dist/worker/delivery/git-transaction.js +354 -0
  44. package/dist/worker/delivery/package.js +502 -0
  45. package/dist/worker/feature/decision-loader.js +68 -0
  46. package/dist/worker/feature/discover.js +14 -0
  47. package/dist/worker/feature/next-action.js +74 -0
  48. package/dist/worker/feature/reducer.js +133 -0
  49. package/dist/worker/feature/review.js +502 -0
  50. package/dist/worker/feature/run.js +365 -0
  51. package/dist/worker/feature/types.js +1 -0
  52. package/dist/worker/follow-up/approve.js +270 -0
  53. package/dist/worker/follow-up/factory.js +234 -0
  54. package/dist/worker/follow-up/paths.js +25 -0
  55. package/dist/worker/follow-up/policy.js +26 -0
  56. package/dist/worker/follow-up/schema.js +93 -0
  57. package/dist/worker/follow-up/store.js +96 -0
  58. package/dist/worker/loop-agent/loop-agent-client.js +329 -126
  59. package/dist/worker/metrics/projector.js +139 -0
  60. package/dist/worker/observability/read-model.js +282 -15
  61. package/dist/worker/observe/paths.js +17 -5
  62. package/dist/worker/observe/static/app.js +443 -61
  63. package/dist/worker/observe/static/index.html +3 -1
  64. package/dist/worker/observe/static/styles.css +85 -18
  65. package/dist/worker/pool/run-store.js +14 -2
  66. package/dist/worker/pool/validation.js +59 -0
  67. package/dist/worker/preflight.js +49 -1
  68. package/dist/worker/report/morning-report.js +41 -6
  69. package/dist/worker/run-task/run-task.js +23 -13
  70. package/dist/worker/runner/run-ready.js +89 -11
  71. package/dist/worker/task-spec/schema.js +0 -1
  72. package/dist/workflows/dag/convergence/controller.js +1 -1
  73. package/dist/workflows/dag/executor-registry.js +0 -2
  74. package/dist/workflows/dag/governance-profile.js +10 -0
  75. package/dist/workflows/dag/init-hybrid.js +601 -26
  76. package/dist/workflows/dag/lifecycle.js +146 -0
  77. package/dist/workflows/dag/node-execution.js +64 -7
  78. package/dist/workflows/dag/prompt.js +16 -0
  79. package/dist/workflows/dag/report.js +2 -0
  80. package/dist/workflows/dag/runner.js +176 -119
  81. package/dist/workflows/dag/scheduler.js +7 -2
  82. package/dist/workflows/dag/skill-snapshot.js +527 -0
  83. package/dist/workflows/dag/types.js +45 -9
  84. package/dist/workflows/dag/validate.js +5 -8
  85. package/dist/workflows/loop/actions/dag-action.js +0 -2
  86. package/dist/workflows/loop/actions/shared.js +1 -1
  87. package/dist/workflows/loop/actions.js +14 -31
  88. package/dist/workflows/loop/benchmark.js +1 -1
  89. package/dist/workflows/loop/index.js +1 -1
  90. package/dist/workflows/loop/policy/auto-policy.js +22 -14
  91. package/dist/workflows/loop/policy/path-patterns.js +13 -0
  92. package/docs/README.md +35 -16
  93. package/docs/agent-dag-recovery-playbook.md +1 -1
  94. package/docs/architecture/README.md +26 -0
  95. package/docs/architecture/dag-execution.md +134 -0
  96. package/docs/architecture/evolution.md +52 -0
  97. package/docs/architecture/facts-and-state.md +58 -0
  98. package/docs/architecture/runtime-boundaries.md +41 -15
  99. package/docs/architecture/system-overview.md +93 -0
  100. package/docs/architecture/worker-and-feature.md +81 -0
  101. package/docs/cursor-prompt-sidecar.md +36 -0
  102. package/docs/decisions/README.md +13 -1
  103. package/docs/design/README.md +39 -13
  104. package/docs/development-principles.md +1 -1
  105. package/docs/exec-plans/active/README.md +2 -2
  106. package/docs/exec-plans/completed/README.md +14 -1
  107. package/docs/feature-workflow.md +44 -4
  108. package/docs/init-surface.manifest.json +63 -1
  109. package/docs/loop-agent-harness.md +65 -3
  110. package/docs/progress/README.md +27 -0
  111. package/docs/reports/README.md +74 -8
  112. package/docs/skills/README.md +2 -1
  113. package/docs/skills/vetted-skill-registry.md +2 -1
  114. package/docs/templates/agent-dag-report.schema.json +4 -2
  115. package/docs/templates/agent-dag.base.json +0 -5
  116. package/docs/templates/agent-dag.final-verification.json +0 -5
  117. package/docs/templates/agent-dag.schema.json +1 -2
  118. package/docs/templates/agent-dag.supervised-implementation.json +1 -6
  119. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +131 -0
  120. package/docs/templates/backend-test-dag.json +213 -0
  121. package/docs/templates/backend-test-dag.retrospect.prompt.md +128 -0
  122. package/docs/templates/backend-test-dag.review-cases.prompt.md +85 -0
  123. package/docs/templates/frontend-design-contract.md +33 -0
  124. package/docs/templates/frontend-task-constraints.md +25 -0
  125. package/docs/templates/frontend-task-requirement.md +61 -0
  126. package/docs/templates/harness.schema.json +8 -5
  127. package/docs/templates/hybrid-dag.json +1 -6
  128. package/docs/templates/init-evolution-review.md +4 -2
  129. package/docs/templates/interactive-ui-round2-experiment.md +1 -1
  130. package/docs/templates/product-line/task.yaml +0 -1
  131. package/docs/templates/worker-dogfood-evidence.md +28 -0
  132. package/docs/templates/worker-dogfood-setup.md +20 -0
  133. package/docs/verification-matrix.md +17 -0
  134. package/examples/decision-gate-agent-dag.json +87 -33
  135. package/examples/example-dag.json +0 -5
  136. package/examples/hybrid-loop-agent-dag.json +0 -5
  137. package/harness.json +6 -11
  138. package/package.json +22 -44
  139. package/scripts/check-product-line-docs.sh +10 -3
  140. package/skills/agent-worker/SKILL.md +37 -0
  141. package/skills/agent-worker/references/agent-worker-operator.md +43 -0
  142. package/skills/frontend-design-review/SKILL.md +59 -0
  143. package/skills/frontend-design-review/references/review-checklist.md +37 -0
  144. package/skills/frontend-implementation/SKILL.md +48 -0
  145. package/skills/frontend-implementation/references/code-standards.md +34 -0
  146. package/skills/frontend-implementation/references/design-spec.md +46 -0
  147. package/skills/frontend-implementation/references/node-contracts.md +32 -0
  148. package/skills/frontend-review/SKILL.md +53 -0
  149. package/skills/frontend-review/references/review-findings.md +42 -0
  150. package/skills/frontend-verification/SKILL.md +40 -0
  151. package/skills/frontend-verification/references/verification-checklist.md +56 -0
  152. package/skills/grill-me/SKILL.md +10 -0
  153. package/skills/grill-with-docs/SKILL.md +88 -0
  154. package/skills/grill-with-docs/adr-format.md +47 -0
  155. package/skills/grill-with-docs/context-format.md +60 -0
  156. package/skills/init-capability-evolution/SKILL.md +1 -0
  157. package/skills/loop-agent/SKILL.md +11 -9
  158. package/skills/loop-agent/references/command-reference.md +28 -15
  159. package/skills/loop-agent/references/docs-converge.md +126 -0
  160. package/skills/loop-agent/references/harness-policy.md +7 -7
  161. package/skills/loop-agent/references/hybrid-dag.md +13 -15
  162. package/skills/loop-agent/references/long-running-loop.md +4 -6
  163. package/skills/loop-agent/references/multi-worktree.md +6 -6
  164. package/skills/loop-agent/references/orchestrator-and-interventions.md +3 -3
  165. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +14 -11
  166. package/skills/using-git-worktrees/SKILL.md +215 -0
  167. package/dist/commands/cursor-worker.js +0 -43
  168. package/dist/cursor-worker-entry.js +0 -8
  169. package/dist/executors/cursor-artifacts.js +0 -33
  170. package/dist/executors/cursor-execution-log.js +0 -81
  171. package/dist/executors/cursor-executor-artifacts.js +0 -134
  172. package/dist/executors/cursor-run.js +0 -115
  173. package/dist/executors/cursor-tool.js +0 -94
  174. package/dist/executors/cursor-worker-client.js +0 -223
  175. package/dist/executors/cursor-worker-protocol.js +0 -18
  176. package/dist/executors/cursor-worker-server.js +0 -54
  177. package/dist/executors/cursor-worker.js +0 -3
  178. package/dist/executors/cursor.js +0 -6
  179. package/dist/executors/dag-cursor-executor.js +0 -87
  180. package/dist/workflows/loop/actions/cursor-fix.js +0 -191
  181. package/dist/workflows/loop/policy/cursor-fix-policy.js +0 -31
  182. package/docs/cursor-executor-usage.md +0 -25
  183. package/docs/dynamic-workflow-dag-engine-roadmap.md +0 -1749
@@ -0,0 +1,502 @@
1
+ import { createHash } from "node:crypto";
2
+ import { access, readFile, realpath } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import YAML from "yaml";
5
+ import { z } from "zod";
6
+ import { getRunsJsonlPath, getTaskPoolRoot, readAllTaskPoolStates, readJsonlFile } from "../pool/run-store.js";
7
+ import { acceptanceSpecSchema } from "../task-graph/acceptance-schema.js";
8
+ import { taskGraphSpecSchema } from "../task-graph/task-graph-schema.js";
9
+ import { validateFeatureTaskGraph } from "../task-graph/validate.js";
10
+ import { followUpActionCardSchema, followUpApprovalSchema, followUpDraftSchema, followUpEvidenceSchema } from "../follow-up/schema.js";
11
+ import { readFollowUpIndex, resolveRepoFile, sha256File as sha256FollowUpFile } from "../follow-up/store.js";
12
+ import { reduceFeature } from "./reducer.js";
13
+ export async function reviewFeature(input) {
14
+ const projection = await loadFeatureProjection(input);
15
+ return reduceFeature(projection);
16
+ }
17
+ export function renderFeatureReview(model) {
18
+ const reason = model.blockingItems[0]?.message ??
19
+ `${model.summary.tasksSucceeded}/${model.summary.tasksTotal} tasks completed; ${model.summary.requiredAcCovered}/${model.summary.requiredAcTotal} required AC covered.`;
20
+ const evidence = [model.evidence.morningReport, model.evidence.observeSnapshot, model.evidence.delivery, model.evidence.closeout, ...model.blockingItems.flatMap((item) => item.evidence)]
21
+ .filter((item) => Boolean(item));
22
+ return [
23
+ `Feature: ${model.featureId}`,
24
+ `结果: ${model.statusLabel}`,
25
+ `原因: ${reason}`,
26
+ `任务: ${model.summary.tasksSucceeded}/${model.summary.tasksTotal} 完成,${model.summary.tasksReady} Ready,${model.summary.tasksFailed} 失败,${model.summary.tasksBlocked} 阻塞`,
27
+ `required AC: ${model.summary.requiredAcCovered}/${model.summary.requiredAcTotal} 已覆盖;缺口 ${model.acceptanceCoverage.filter((item) => item.required && item.status !== "covered" && item.status !== "waived").map((item) => item.acId).join(", ") || "无"}`,
28
+ `风险: high=${model.riskSummary.high}, medium=${model.riskSummary.medium}, low=${model.riskSummary.low}`,
29
+ `交付/Closeout: ${model.status === "deliverable" || model.status === "closed" ? "交付证据齐备" : "尚未就绪"}`,
30
+ `下一步: ${model.nextAction?.command ?? model.nextAction?.label ?? "当前无行动"}`,
31
+ `证据: ${evidence[0] ?? "暂无"}`,
32
+ ].join("\n") + "\n";
33
+ }
34
+ async function loadFeatureProjection(input) {
35
+ const featureDir = path.resolve(input.featureDir);
36
+ const repoRoot = path.resolve(input.repoRoot);
37
+ const projectionWarnings = [];
38
+ let validation = {
39
+ ok: false,
40
+ featureId: path.basename(featureDir),
41
+ summary: { acceptanceCount: 0, nodeCount: 0, readyWithoutState: [] },
42
+ errors: [],
43
+ };
44
+ try {
45
+ validation = await validateFeatureTaskGraph(featureDir);
46
+ }
47
+ catch (error) {
48
+ validation.errors.push({
49
+ code: "feature-packet-unreadable",
50
+ message: message(error),
51
+ path: featureDir,
52
+ });
53
+ }
54
+ let acceptance = acceptanceSpecSchema.safeParse({});
55
+ let graph = taskGraphSpecSchema.safeParse({});
56
+ try {
57
+ acceptance = acceptanceSpecSchema.safeParse(YAML.parse(await readFile(path.join(featureDir, "acceptance.yaml"), "utf-8")));
58
+ graph = taskGraphSpecSchema.safeParse(YAML.parse(await readFile(path.join(featureDir, "tasks", "task-graph.yaml"), "utf-8")));
59
+ }
60
+ catch (error) {
61
+ projectionWarnings.push(`cannot read Feature Packet: ${message(error)}`);
62
+ }
63
+ if (!acceptance.success)
64
+ projectionWarnings.push("acceptance.yaml is invalid");
65
+ if (!graph.success)
66
+ projectionWarnings.push("tasks/task-graph.yaml is invalid");
67
+ let states = {};
68
+ let runs = [];
69
+ try {
70
+ states = await readAllTaskPoolStates(repoRoot);
71
+ runs = await readJsonlFile(getRunsJsonlPath(repoRoot));
72
+ }
73
+ catch (error) {
74
+ projectionWarnings.push(`Task Pool facts are corrupt: ${message(error)}`);
75
+ }
76
+ for (const [taskId, state] of Object.entries(states)) {
77
+ if (!taskPoolStateSchema.safeParse(state).success || state.taskId !== taskId) {
78
+ projectionWarnings.push(`Task Pool state is semantically invalid: ${taskId}`);
79
+ delete states[taskId];
80
+ }
81
+ }
82
+ runs = runs.filter((run) => {
83
+ const valid = taskPoolRunSchema.safeParse(run).success;
84
+ if (!valid)
85
+ projectionWarnings.push("Task Pool run record is semantically invalid");
86
+ return valid;
87
+ });
88
+ const graphNodes = graph.success ? graph.data.nodes : [];
89
+ let pendingFollowUps = [];
90
+ let actionCards = [];
91
+ let resolvedFailureTaskIds = [];
92
+ const approvedReplacements = new Map();
93
+ try {
94
+ const followUps = await readFollowUpIndex(repoRoot, validation.featureId);
95
+ pendingFollowUps = followUps.entries.filter((entry) => entry.kind === "TaskDraft" && entry.status === "Draft" && Boolean(entry.proposedTaskId)).map((entry) => ({
96
+ followUpId: entry.followUpId,
97
+ proposedTaskId: entry.proposedTaskId,
98
+ parentTaskId: entry.parentTaskId,
99
+ draftPath: entry.draftPath,
100
+ }));
101
+ for (const entry of followUps.entries.filter((candidate) => candidate.kind === "ActionCard" && candidate.status === "ActionRequired")) {
102
+ try {
103
+ const cardPath = await resolveRepoFile(repoRoot, entry.draftPath);
104
+ if (await sha256FollowUpFile(cardPath) !== entry.draftHash)
105
+ throw new Error("action card hash changed");
106
+ const card = followUpActionCardSchema.parse(YAML.parse(await readFile(cardPath, "utf-8")));
107
+ const evidencePath = await resolveRepoFile(repoRoot, path.join(path.dirname(entry.draftPath), "evidence.json"));
108
+ if (await sha256FollowUpFile(evidencePath) !== entry.evidenceHash)
109
+ throw new Error("action card evidence manifest changed");
110
+ const evidence = followUpEvidenceSchema.parse(JSON.parse(await readFile(evidencePath, "utf-8")));
111
+ if (evidence.followUpId !== entry.followUpId || evidence.evidence.map((item) => item.ref).join("\0") !== card.evidence_refs.join("\0"))
112
+ throw new Error("action card evidence lineage changed");
113
+ for (const item of evidence.evidence) {
114
+ if (await sha256FollowUpFile(await resolveRepoFile(repoRoot, item.ref)) !== item.sha256)
115
+ throw new Error(`action card evidence changed: ${item.ref}`);
116
+ }
117
+ actionCards.push({ followUpId: entry.followUpId, parentTaskId: entry.parentTaskId, draftPath: entry.draftPath, action: card.action, label: card.label, ...(card.command ? { command: card.command } : {}) });
118
+ }
119
+ catch (error) {
120
+ projectionWarnings.push(`action card ${entry.followUpId} is inconsistent: ${message(error)}`);
121
+ }
122
+ }
123
+ for (const entry of followUps.entries.filter((candidate) => candidate.status === "Approved")) {
124
+ try {
125
+ if (!entry.approvalPath)
126
+ throw new Error("approval path is missing");
127
+ const approval = followUpApprovalSchema.parse(JSON.parse(await readFile(await resolveRepoFile(repoRoot, entry.approvalPath), "utf-8")));
128
+ const draftPath = await resolveRepoFile(repoRoot, entry.draftPath);
129
+ const draft = followUpDraftSchema.parse(YAML.parse(await readFile(draftPath, "utf-8")));
130
+ if (await sha256FollowUpFile(draftPath) !== entry.draftHash || approval.draftHash !== entry.draftHash)
131
+ throw new Error("draft hash does not match approval");
132
+ if (approval.followUpId !== entry.followUpId || approval.proposedTaskId !== entry.proposedTaskId)
133
+ throw new Error("approval lineage does not match index");
134
+ const replacementNode = graphNodes.find((node) => node.id === entry.proposedTaskId && node.type === draft.proposed_graph_patch.add_node.type);
135
+ if (!replacementNode || draft.proposed_graph_patch.add_node.id !== entry.proposedTaskId)
136
+ throw new Error("approved Follow-up node is missing");
137
+ for (const rewire of draft.proposed_graph_patch.rewire_dependents) {
138
+ const dependent = graphNodes.find((node) => node.id === rewire.task_id);
139
+ if (!dependent?.depends_on.includes(entry.proposedTaskId) || dependent.depends_on.includes(entry.parentTaskId))
140
+ throw new Error(`approved dependency rewire is missing for ${rewire.task_id}`);
141
+ }
142
+ if (!states[entry.proposedTaskId])
143
+ throw new Error("approved FIX state is missing");
144
+ resolvedFailureTaskIds.push(entry.parentTaskId);
145
+ approvedReplacements.set(entry.parentTaskId, entry.proposedTaskId);
146
+ }
147
+ catch (error) {
148
+ projectionWarnings.push(`approved follow-up ${entry.followUpId} is inconsistent: ${message(error)}`);
149
+ }
150
+ }
151
+ }
152
+ catch (error) {
153
+ projectionWarnings.push(`follow-up index is corrupt: ${message(error)}`);
154
+ }
155
+ const tasks = graphNodes.map((node) => {
156
+ const rawState = states[node.id];
157
+ const stateRun = rawState?.workerRunId
158
+ ? runs.find((run) => run.workerRunId === rawState.workerRunId)
159
+ : undefined;
160
+ const state = rawState?.workerRunId && !stateRun
161
+ ? undefined
162
+ : stateRun && stateRun.featureId !== validation.featureId
163
+ ? undefined
164
+ : rawState;
165
+ if (rawState && !state) {
166
+ projectionWarnings.push(`Task Pool state for ${node.id} has missing or cross-Feature run ownership`);
167
+ }
168
+ const latest = [...runs].reverse().find((run) => run.featureId === validation.featureId && run.taskId === node.id);
169
+ return {
170
+ taskId: node.id,
171
+ type: node.type,
172
+ dependsOn: node.depends_on,
173
+ ...(state?.status ? { status: state.status } : {}),
174
+ ...(state?.workerRunId ? { workerRunId: state.workerRunId } : {}),
175
+ ...(state?.failure?.category ? { failureCategory: state.failure.category } : {}),
176
+ failureEvidence: Object.values(latest?.failureArtifacts ?? {}).filter((value) => typeof value === "string"),
177
+ };
178
+ });
179
+ const deliveryRoot = path.join(getTaskPoolRoot(repoRoot), "artifacts", "features", validation.featureId, "delivery");
180
+ const coveragePath = path.join(deliveryRoot, "acceptance-coverage.json");
181
+ const explicitCoverage = await readExplicitCoverage(coveragePath, repoRoot, validation.featureId, projectionWarnings);
182
+ const coverage = acceptance.success
183
+ ? acceptance.data.acceptance.map((item) => {
184
+ const effectiveRefs = item.verification.expected_task_refs.map((taskId) => approvedReplacements.get(taskId) ?? taskId);
185
+ const referenced = effectiveRefs.map((taskId) => states[taskId]);
186
+ const failed = effectiveRefs.filter((taskId) => ["Failed", "Blocked"].includes(states[taskId]?.status ?? ""));
187
+ const done = effectiveRefs.filter((taskId) => states[taskId]?.status === "Done");
188
+ const explicit = explicitCoverage.get(item.id);
189
+ return {
190
+ acId: item.id,
191
+ required: item.priority === "must",
192
+ expectedTaskRefs: item.verification.expected_task_refs,
193
+ status: failed.length > 0
194
+ ? "blocked"
195
+ : explicit?.status === "covered" && explicit.evidence.length > 0
196
+ ? "covered"
197
+ : explicit?.status === "waived"
198
+ ? "waived"
199
+ : done.length === referenced.length && done.length > 0
200
+ ? "partial"
201
+ : "missing",
202
+ evidence: explicit?.evidence ?? [],
203
+ blockedBy: failed,
204
+ };
205
+ })
206
+ : [];
207
+ const deliveryPath = path.join(deliveryRoot, "delivery-manifest.json");
208
+ const morningReportPath = path.join(getTaskPoolRoot(repoRoot), "reports", "morning-report.md");
209
+ const observeSnapshotPath = path.join(getTaskPoolRoot(repoRoot), "observability", "snapshot.json");
210
+ const closeoutPath = path.join(featureDir, "closeout.yaml");
211
+ let delivery;
212
+ let deliveryQaEvidencePath;
213
+ if (await exists(deliveryPath)) {
214
+ try {
215
+ const raw = deliveryManifestSchema.safeParse(JSON.parse(await readFile(deliveryPath, "utf-8")));
216
+ const hashedRefs = raw.success
217
+ ? [raw.data.qa.evidence, ...(raw.data.finalVerification ? [raw.data.finalVerification] : []), ...raw.data.tasks.flatMap((task) => task.verificationEvidence)]
218
+ : [];
219
+ const graphTaskIds = new Set(graphNodes.map((node) => node.id));
220
+ const acceptanceIds = new Set(coverage.map((item) => item.acId));
221
+ const requiredDeliveryTaskIds = tasks
222
+ .filter((task) => task.status === "Done" && isDevelopmentTask(task.type))
223
+ .map((task) => task.taskId);
224
+ const manifestTaskIds = raw.success ? new Set(raw.data.tasks.map((task) => task.taskId)) : new Set();
225
+ const tasksComplete = raw.success &&
226
+ requiredDeliveryTaskIds.every((taskId) => manifestTaskIds.has(taskId)) &&
227
+ raw.data.tasks.every((task) => graphTaskIds.has(task.taskId) && task.acceptanceRefs.every((acId) => acceptanceIds.has(acId)));
228
+ const requiredCoverage = coverage.filter((item) => item.required);
229
+ const coveredCount = requiredCoverage.filter((item) => item.status === "covered").length;
230
+ const waivedCount = requiredCoverage.filter((item) => item.status === "waived").length;
231
+ const blockedCount = requiredCoverage.filter((item) => item.status === "blocked").length;
232
+ const coverageCountsMatch = raw.success &&
233
+ raw.data.acceptanceCoverage.requiredTotal === requiredCoverage.length &&
234
+ raw.data.acceptanceCoverage.covered === coveredCount &&
235
+ raw.data.acceptanceCoverage.waived === waivedCount &&
236
+ raw.data.acceptanceCoverage.blocked === blockedCount &&
237
+ coveredCount + waivedCount === requiredCoverage.length;
238
+ if (raw.success && raw.data.featureId === validation.featureId && tasksComplete && coverageCountsMatch && raw.data.riskSummary.high === 0 && await allHashedRefsValid(hashedRefs, repoRoot)) {
239
+ delivery = {
240
+ valid: true,
241
+ path: deliveryPath,
242
+ riskSummary: raw.data.riskSummary,
243
+ };
244
+ deliveryQaEvidencePath = raw.data.qa.evidence.path;
245
+ }
246
+ else {
247
+ projectionWarnings.push("delivery manifest does not match the Feature");
248
+ }
249
+ }
250
+ catch (error) {
251
+ projectionWarnings.push(`delivery manifest is corrupt: ${message(error)}`);
252
+ }
253
+ }
254
+ let closeout;
255
+ if (await exists(closeoutPath)) {
256
+ try {
257
+ const closeoutRaw = YAML.parse(await readFile(closeoutPath, "utf-8"));
258
+ const attemptedSuccess = Boolean(closeoutRaw && typeof closeoutRaw === "object" && closeoutRaw.status === "success");
259
+ const raw = closeoutSchema.safeParse(closeoutRaw);
260
+ const deliveryRef = raw.success
261
+ ? resolveReference(raw.data.delivery_manifest_ref, repoRoot, featureDir)
262
+ : undefined;
263
+ const qaEvidenceValid = raw.success && await allRefsExist(raw.data.qa_evidence, repoRoot) &&
264
+ raw.data.qa_evidence.some((ref) => normalizeRepoRef(ref, repoRoot) === normalizeRepoRef(deliveryQaEvidencePath ?? "", repoRoot));
265
+ const deliveryHash = delivery?.valid ? await sha256File(deliveryPath) : undefined;
266
+ closeout = {
267
+ applied: attemptedSuccess,
268
+ valid: raw.success && qaEvidenceValid && delivery?.valid === true && deliveryRef === deliveryPath && raw.data.input_facts_hash === deliveryHash,
269
+ path: closeoutPath,
270
+ };
271
+ if (attemptedSuccess && !closeout.valid) {
272
+ projectionWarnings.push("applied closeout has invalid schema, evidence, delivery reference, or facts hash");
273
+ }
274
+ }
275
+ catch (error) {
276
+ projectionWarnings.push(`closeout.yaml is corrupt: ${message(error)}`);
277
+ }
278
+ }
279
+ return {
280
+ featureId: validation.featureId,
281
+ featureDir,
282
+ repoRoot,
283
+ validationOk: validation.ok,
284
+ validationErrors: validation.errors.map((error) => `${error.code}: ${error.message}`),
285
+ tasks,
286
+ acceptance: coverage,
287
+ ...(closeout ? { closeout } : {}),
288
+ ...(delivery ? { delivery } : {}),
289
+ pendingFollowUps,
290
+ actionCards,
291
+ resolvedFailureTaskIds,
292
+ ...((await exists(morningReportPath)) ? { morningReport: morningReportPath } : {}),
293
+ ...((await exists(observeSnapshotPath)) ? { observeSnapshot: observeSnapshotPath } : {}),
294
+ projectionWarnings,
295
+ };
296
+ }
297
+ async function exists(filePath) {
298
+ try {
299
+ await access(filePath);
300
+ return true;
301
+ }
302
+ catch {
303
+ return false;
304
+ }
305
+ }
306
+ function message(error) {
307
+ return error instanceof Error ? error.message : String(error);
308
+ }
309
+ const deliveryManifestSchema = z.object({
310
+ schemaVersion: z.literal(1),
311
+ featureId: z.string().min(1),
312
+ branch: z.string().min(1),
313
+ baseBranch: z.string().min(1),
314
+ baseSha: z.string().min(1),
315
+ headSha: z.string().min(1),
316
+ createdAt: z.string().datetime(),
317
+ tasks: z.array(z.object({
318
+ taskId: z.string().min(1),
319
+ workerRunId: z.string().min(1),
320
+ dagRunId: z.string().min(1),
321
+ commit: z.string().min(1),
322
+ acceptanceRefs: z.array(z.string().min(1)),
323
+ verificationEvidence: z.array(z.object({
324
+ path: z.string().min(1),
325
+ sha256: z.string().regex(/^[a-f0-9]{64}$/),
326
+ })).min(1),
327
+ })).min(1),
328
+ qa: z.object({
329
+ verdict: z.literal("passed"),
330
+ evidence: z.object({
331
+ path: z.string().min(1),
332
+ sha256: z.string().regex(/^[a-f0-9]{64}$/),
333
+ }),
334
+ }),
335
+ finalVerification: z.object({
336
+ path: z.string().min(1),
337
+ sha256: z.string().regex(/^[a-f0-9]{64}$/),
338
+ }).optional(),
339
+ acceptanceCoverage: z.object({
340
+ requiredTotal: z.number().int().nonnegative(),
341
+ covered: z.number().int().nonnegative(),
342
+ blocked: z.number().int().nonnegative(),
343
+ waived: z.number().int().nonnegative(),
344
+ }),
345
+ riskSummary: z.object({
346
+ high: z.number().int().nonnegative(),
347
+ medium: z.number().int().nonnegative(),
348
+ low: z.number().int().nonnegative(),
349
+ }),
350
+ });
351
+ const explicitCoverageSchema = z.object({
352
+ schemaVersion: z.literal(1),
353
+ featureId: z.string().min(1),
354
+ items: z.array(z.object({
355
+ acId: z.string().min(1),
356
+ status: z.enum(["covered", "partial", "blocked", "waived", "missing"]),
357
+ evidence: z.array(z.object({
358
+ path: z.string().min(1),
359
+ sha256: z.string().regex(/^[a-f0-9]{64}$/),
360
+ })).default([]),
361
+ decision: z.object({
362
+ owner: z.string().min(1),
363
+ reason: z.string().min(1),
364
+ decidedAt: z.string().datetime(),
365
+ }).optional(),
366
+ })),
367
+ });
368
+ const failureRouteSchema = z.object({
369
+ category: z.enum(["SpecUnclear", "ContractMismatch", "ProductBug", "TestBug", "EnvFailure", "FlakyTest", "RiskyChange", "DependencyFailure", "NeedsHuman", "Unknown"]),
370
+ recommendedFollowUpKind: z.string(),
371
+ derivedFollowUpTaskId: z.string(),
372
+ source: z.enum(["report-primary-failure", "report-decision", "runner-error", "fallback"]),
373
+ });
374
+ const taskPoolStateSchema = z.object({
375
+ taskId: z.string().min(1),
376
+ status: z.enum(["Draft", "Ready", "Queued", "Running", "AgentCompleted", "VerificationRunning", "HumanReview", "Done", "Failed", "Blocked", "Abandoned"]),
377
+ updatedAt: z.string().datetime(),
378
+ workerRunId: z.string().min(1).optional(),
379
+ failure: failureRouteSchema.optional(),
380
+ }).passthrough();
381
+ const taskPoolRunSchema = z.object({
382
+ schemaVersion: z.literal(1),
383
+ batchRunId: z.string().min(1),
384
+ workerRunId: z.string().min(1),
385
+ taskId: z.string().min(1),
386
+ featureId: z.string().min(1),
387
+ status: z.enum(["succeeded", "failed", "run-error"]),
388
+ recordedAt: z.string().datetime(),
389
+ failure: failureRouteSchema.optional(),
390
+ }).passthrough();
391
+ const closeoutSchema = z.object({
392
+ status: z.literal("success"),
393
+ qa_verdict: z.literal("pass"),
394
+ qa_evidence: z.array(z.string().min(1)).min(1),
395
+ delivery_manifest_ref: z.string().min(1),
396
+ owner: z.string().min(1),
397
+ appliedAt: z.string().datetime(),
398
+ input_facts_hash: z.string().regex(/^[a-f0-9]{64}$/),
399
+ });
400
+ async function readExplicitCoverage(coveragePath, repoRoot, featureId, warnings) {
401
+ const result = new Map();
402
+ if (!(await exists(coveragePath)))
403
+ return result;
404
+ try {
405
+ const parsed = explicitCoverageSchema.safeParse(JSON.parse(await readFile(coveragePath, "utf-8")));
406
+ if (!parsed.success || parsed.data.featureId !== featureId) {
407
+ warnings.push("acceptance coverage artifact is invalid");
408
+ return result;
409
+ }
410
+ for (const item of parsed.data.items) {
411
+ if (item.status !== "covered" && item.status !== "waived")
412
+ continue;
413
+ if (item.status === "waived" && !item.decision) {
414
+ warnings.push(`acceptance waiver is missing owner/reason/time for ${item.acId}`);
415
+ continue;
416
+ }
417
+ if (item.status === "covered" && item.evidence.length === 0) {
418
+ warnings.push(`acceptance evidence is missing for ${item.acId}`);
419
+ continue;
420
+ }
421
+ if (!(await allHashedRefsValid(item.evidence, repoRoot))) {
422
+ warnings.push(`acceptance evidence is missing for ${item.acId}`);
423
+ continue;
424
+ }
425
+ result.set(item.acId, {
426
+ status: item.status,
427
+ evidence: item.evidence.map((entry) => entry.path),
428
+ });
429
+ }
430
+ }
431
+ catch (error) {
432
+ warnings.push(`acceptance coverage artifact is corrupt: ${message(error)}`);
433
+ }
434
+ return result;
435
+ }
436
+ async function allRefsExist(refs, repoRoot) {
437
+ for (const ref of refs) {
438
+ const resolved = await resolveSafeRepoRef(ref, repoRoot);
439
+ if (!resolved)
440
+ return false;
441
+ if (!(await exists(resolved)))
442
+ return false;
443
+ }
444
+ return true;
445
+ }
446
+ async function allHashedRefsValid(refs, repoRoot) {
447
+ for (const ref of refs) {
448
+ const resolved = await resolveSafeRepoRef(ref.path, repoRoot);
449
+ if (!resolved)
450
+ return false;
451
+ if (!(await exists(resolved)) || await sha256File(resolved) !== ref.sha256)
452
+ return false;
453
+ }
454
+ return true;
455
+ }
456
+ async function resolveSafeRepoRef(ref, repoRoot) {
457
+ if (!ref || path.isAbsolute(ref))
458
+ return undefined;
459
+ try {
460
+ const canonicalRepo = await realpath(repoRoot);
461
+ const resolved = await realpath(path.resolve(repoRoot, ref));
462
+ const relative = path.relative(canonicalRepo, resolved);
463
+ return relative && !relative.startsWith("..") && !path.isAbsolute(relative) ? resolved : undefined;
464
+ }
465
+ catch {
466
+ return undefined;
467
+ }
468
+ }
469
+ async function sha256File(filePath) {
470
+ try {
471
+ return createHash("sha256").update(await readFile(filePath)).digest("hex");
472
+ }
473
+ catch {
474
+ return undefined;
475
+ }
476
+ }
477
+ function resolveReference(ref, repoRoot, featureDir) {
478
+ if (path.isAbsolute(ref))
479
+ return "";
480
+ const repoRelative = path.resolve(repoRoot, ref);
481
+ const featureRelative = path.resolve(featureDir, ref);
482
+ for (const candidate of [repoRelative, featureRelative]) {
483
+ const relative = path.relative(repoRoot, candidate);
484
+ if (!relative.startsWith("..") && !path.isAbsolute(relative) && candidate.endsWith(path.join("delivery", "delivery-manifest.json"))) {
485
+ return candidate;
486
+ }
487
+ }
488
+ return "";
489
+ }
490
+ function isSafeRelativeRef(ref, repoRoot) {
491
+ if (!ref || path.isAbsolute(ref))
492
+ return false;
493
+ const resolved = path.resolve(repoRoot, ref);
494
+ const relative = path.relative(repoRoot, resolved);
495
+ return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative);
496
+ }
497
+ function normalizeRepoRef(ref, repoRoot) {
498
+ return isSafeRelativeRef(ref, repoRoot) ? path.resolve(repoRoot, ref) : "";
499
+ }
500
+ function isDevelopmentTask(type) {
501
+ return !type.startsWith("qa-") && !["architecture", "review"].includes(type);
502
+ }