@tea-agent/loop-agent 0.8.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 (188) hide show
  1. package/AGENTS.md +10 -0
  2. package/CHANGELOG.md +101 -1
  3. package/README.md +69 -5
  4. package/dist/application/dag/args.js +13 -16
  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 +75 -9
  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/shared/reference-context.js +48 -22
  34. package/dist/{executors/cursor-executor.js → sidecars/cursor-prompt/executor.js} +2 -42
  35. package/dist/sidecars/cursor-prompt/index.js +3 -0
  36. package/dist/sidecars/cursor-prompt/stream.js +121 -0
  37. package/dist/task/config-types.js +30 -13
  38. package/dist/task/delegate.js +9 -21
  39. package/dist/task/runtime.js +2 -3
  40. package/dist/worker/cli.js +243 -0
  41. package/dist/worker/closeout/apply.js +73 -0
  42. package/dist/worker/closeout/preview.js +30 -0
  43. package/dist/worker/delivery/final-verification.js +194 -0
  44. package/dist/worker/delivery/git-transaction.js +354 -0
  45. package/dist/worker/delivery/package.js +502 -0
  46. package/dist/worker/feature/decision-loader.js +68 -0
  47. package/dist/worker/feature/discover.js +14 -0
  48. package/dist/worker/feature/next-action.js +74 -0
  49. package/dist/worker/feature/reducer.js +133 -0
  50. package/dist/worker/feature/review.js +502 -0
  51. package/dist/worker/feature/run.js +365 -0
  52. package/dist/worker/feature/types.js +1 -0
  53. package/dist/worker/follow-up/approve.js +270 -0
  54. package/dist/worker/follow-up/factory.js +234 -0
  55. package/dist/worker/follow-up/paths.js +25 -0
  56. package/dist/worker/follow-up/policy.js +26 -0
  57. package/dist/worker/follow-up/schema.js +93 -0
  58. package/dist/worker/follow-up/store.js +96 -0
  59. package/dist/worker/loop-agent/loop-agent-client.js +345 -101
  60. package/dist/worker/metrics/projector.js +139 -0
  61. package/dist/worker/observability/read-model.js +282 -15
  62. package/dist/worker/observe/paths.js +17 -5
  63. package/dist/worker/observe/routes.js +78 -20
  64. package/dist/worker/observe/server.js +8 -6
  65. package/dist/worker/observe/static/app.js +1045 -177
  66. package/dist/worker/observe/static/index.html +70 -43
  67. package/dist/worker/observe/static/styles.css +553 -610
  68. package/dist/worker/pool/run-store.js +14 -2
  69. package/dist/worker/pool/validation.js +59 -0
  70. package/dist/worker/preflight.js +49 -1
  71. package/dist/worker/report/morning-report.js +41 -6
  72. package/dist/worker/run-task/run-task.js +23 -13
  73. package/dist/worker/runner/run-ready.js +89 -11
  74. package/dist/worker/task-spec/schema.js +0 -1
  75. package/dist/workflows/dag/convergence/controller.js +1 -1
  76. package/dist/workflows/dag/executor-registry.js +0 -2
  77. package/dist/workflows/dag/governance-profile.js +10 -0
  78. package/dist/workflows/dag/init-hybrid.js +601 -26
  79. package/dist/workflows/dag/lifecycle.js +146 -0
  80. package/dist/workflows/dag/node-execution.js +64 -7
  81. package/dist/workflows/dag/prompt.js +16 -0
  82. package/dist/workflows/dag/report.js +2 -0
  83. package/dist/workflows/dag/runner.js +176 -119
  84. package/dist/workflows/dag/scheduler.js +7 -2
  85. package/dist/workflows/dag/skill-snapshot.js +527 -0
  86. package/dist/workflows/dag/types.js +45 -9
  87. package/dist/workflows/dag/validate.js +5 -8
  88. package/dist/workflows/loop/actions/dag-action.js +0 -2
  89. package/dist/workflows/loop/actions/shared.js +1 -1
  90. package/dist/workflows/loop/actions.js +14 -31
  91. package/dist/workflows/loop/benchmark.js +1 -1
  92. package/dist/workflows/loop/index.js +1 -1
  93. package/dist/workflows/loop/policy/auto-policy.js +22 -14
  94. package/dist/workflows/loop/policy/path-patterns.js +13 -0
  95. package/docs/README.md +35 -12
  96. package/docs/agent-dag-recovery-playbook.md +1 -1
  97. package/docs/architecture/README.md +26 -0
  98. package/docs/architecture/dag-execution.md +134 -0
  99. package/docs/architecture/evolution.md +52 -0
  100. package/docs/architecture/facts-and-state.md +58 -0
  101. package/docs/architecture/runtime-boundaries.md +41 -15
  102. package/docs/architecture/system-overview.md +93 -0
  103. package/docs/architecture/worker-and-feature.md +81 -0
  104. package/docs/cursor-prompt-sidecar.md +36 -0
  105. package/docs/decisions/README.md +13 -1
  106. package/docs/design/README.md +39 -13
  107. package/docs/development-principles.md +1 -1
  108. package/docs/exec-plans/active/README.md +2 -2
  109. package/docs/exec-plans/completed/README.md +21 -0
  110. package/docs/feature-workflow.md +44 -4
  111. package/docs/init-surface.manifest.json +63 -1
  112. package/docs/loop-agent-harness.md +65 -3
  113. package/docs/progress/README.md +27 -0
  114. package/docs/reports/README.md +74 -5
  115. package/docs/skills/README.md +2 -1
  116. package/docs/skills/vetted-skill-registry.md +2 -1
  117. package/docs/templates/agent-dag-report.schema.json +4 -2
  118. package/docs/templates/agent-dag.base.json +0 -5
  119. package/docs/templates/agent-dag.final-verification.json +0 -5
  120. package/docs/templates/agent-dag.schema.json +1 -2
  121. package/docs/templates/agent-dag.supervised-implementation.json +1 -6
  122. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +131 -0
  123. package/docs/templates/backend-test-dag.json +213 -0
  124. package/docs/templates/backend-test-dag.retrospect.prompt.md +128 -0
  125. package/docs/templates/backend-test-dag.review-cases.prompt.md +85 -0
  126. package/docs/templates/frontend-design-contract.md +33 -0
  127. package/docs/templates/frontend-task-constraints.md +25 -0
  128. package/docs/templates/frontend-task-requirement.md +61 -0
  129. package/docs/templates/harness.schema.json +8 -5
  130. package/docs/templates/hybrid-dag.json +1 -6
  131. package/docs/templates/init-evolution-review.md +4 -2
  132. package/docs/templates/interactive-ui-round2-experiment.md +1 -1
  133. package/docs/templates/product-line/task.yaml +0 -1
  134. package/docs/templates/worker-dogfood-evidence.md +28 -0
  135. package/docs/templates/worker-dogfood-setup.md +20 -0
  136. package/docs/verification-matrix.md +17 -0
  137. package/examples/decision-gate-agent-dag.json +87 -33
  138. package/examples/example-dag.json +0 -5
  139. package/examples/hybrid-loop-agent-dag.json +0 -5
  140. package/harness.json +6 -11
  141. package/package.json +22 -44
  142. package/scripts/check-product-line-docs.sh +10 -3
  143. package/scripts/check-task-pool-root.sh +1 -1
  144. package/skills/agent-worker/SKILL.md +37 -0
  145. package/skills/agent-worker/references/agent-worker-operator.md +43 -0
  146. package/skills/frontend-design-review/SKILL.md +59 -0
  147. package/skills/frontend-design-review/references/review-checklist.md +37 -0
  148. package/skills/frontend-implementation/SKILL.md +48 -0
  149. package/skills/frontend-implementation/references/code-standards.md +34 -0
  150. package/skills/frontend-implementation/references/design-spec.md +46 -0
  151. package/skills/frontend-implementation/references/node-contracts.md +32 -0
  152. package/skills/frontend-review/SKILL.md +53 -0
  153. package/skills/frontend-review/references/review-findings.md +42 -0
  154. package/skills/frontend-verification/SKILL.md +40 -0
  155. package/skills/frontend-verification/references/verification-checklist.md +56 -0
  156. package/skills/grill-me/SKILL.md +10 -0
  157. package/skills/grill-with-docs/SKILL.md +88 -0
  158. package/skills/grill-with-docs/adr-format.md +47 -0
  159. package/skills/grill-with-docs/context-format.md +60 -0
  160. package/skills/init-capability-evolution/SKILL.md +1 -0
  161. package/skills/loop-agent/SKILL.md +11 -9
  162. package/skills/loop-agent/references/command-reference.md +28 -15
  163. package/skills/loop-agent/references/docs-converge.md +126 -0
  164. package/skills/loop-agent/references/harness-policy.md +7 -7
  165. package/skills/loop-agent/references/hybrid-dag.md +13 -15
  166. package/skills/loop-agent/references/long-running-loop.md +4 -6
  167. package/skills/loop-agent/references/multi-worktree.md +6 -6
  168. package/skills/loop-agent/references/orchestrator-and-interventions.md +3 -3
  169. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +14 -11
  170. package/skills/loop-agent/references/verification-and-failure-handling.md +8 -3
  171. package/skills/using-git-worktrees/SKILL.md +215 -0
  172. package/dist/commands/cursor-worker.js +0 -43
  173. package/dist/cursor-worker-entry.js +0 -8
  174. package/dist/executors/cursor-artifacts.js +0 -33
  175. package/dist/executors/cursor-execution-log.js +0 -81
  176. package/dist/executors/cursor-executor-artifacts.js +0 -134
  177. package/dist/executors/cursor-run.js +0 -115
  178. package/dist/executors/cursor-tool.js +0 -94
  179. package/dist/executors/cursor-worker-client.js +0 -223
  180. package/dist/executors/cursor-worker-protocol.js +0 -18
  181. package/dist/executors/cursor-worker-server.js +0 -54
  182. package/dist/executors/cursor-worker.js +0 -3
  183. package/dist/executors/cursor.js +0 -6
  184. package/dist/executors/dag-cursor-executor.js +0 -87
  185. package/dist/workflows/loop/actions/cursor-fix.js +0 -191
  186. package/dist/workflows/loop/policy/cursor-fix-policy.js +0 -31
  187. package/docs/cursor-executor-usage.md +0 -25
  188. package/docs/dynamic-workflow-dag-engine-roadmap.md +0 -1749
@@ -0,0 +1,502 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { execFile } from "node:child_process";
3
+ import { mkdir, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { promisify } from "node:util";
6
+ import YAML from "yaml";
7
+ import { z } from "zod";
8
+ import { controllerIdentitiesMatch, LOOP_AGENT_PACKAGE_NAME, } from "../loop-agent/loop-agent-client.js";
9
+ import { getRunsJsonlPath, getTaskPoolRoot, readAllTaskPoolStates, readJsonlFile } from "../pool/run-store.js";
10
+ import { acceptanceSpecSchema } from "../task-graph/acceptance-schema.js";
11
+ import { taskGraphSpecSchema } from "../task-graph/task-graph-schema.js";
12
+ import { validateFeatureTaskGraph } from "../task-graph/validate.js";
13
+ import { taskSpecSchema } from "../task-spec/schema.js";
14
+ import { gitTransactionRecordSchema, transactionRecordPath } from "./git-transaction.js";
15
+ const execFileAsync = promisify(execFile);
16
+ const hashedRefSchema = z.object({ path: z.string().min(1), sha256: z.string().regex(/^[a-f0-9]{64}$/) }).strict();
17
+ export const acceptanceCoverageArtifactSchema = z.object({
18
+ schemaVersion: z.literal(1),
19
+ featureId: z.string().min(1),
20
+ waiverSource: hashedRefSchema.optional(),
21
+ items: z.array(z.object({
22
+ acId: z.string().min(1), required: z.boolean(),
23
+ status: z.enum(["covered", "partial", "blocked", "waived", "missing"]),
24
+ evidence: z.array(hashedRefSchema), blockedBy: z.array(z.string()),
25
+ decision: z.object({ owner: z.string().min(1), reason: z.string().min(1), decidedAt: z.string().datetime() }).optional(),
26
+ }).strict()),
27
+ }).strict();
28
+ export const deliveryManifestSchema = z.object({
29
+ schemaVersion: z.literal(1), featureId: z.string().min(1), branch: z.string().min(1), baseBranch: z.string().min(1),
30
+ baseSha: z.string().regex(/^[a-f0-9]{40}$/), headSha: z.string().regex(/^[a-f0-9]{40}$/), createdAt: z.string().datetime(),
31
+ tasks: z.array(z.object({ taskId: z.string(), workerRunId: z.string(), dagRunId: z.string(), commit: z.string().regex(/^[a-f0-9]{40}$/), acceptanceRefs: z.array(z.string()), verificationEvidence: z.array(hashedRefSchema).min(1), changedFiles: z.array(z.string()) }).strict()),
32
+ qa: z.object({ verdict: z.literal("passed"), evidence: hashedRefSchema }).strict(),
33
+ finalVerification: hashedRefSchema,
34
+ acceptanceCoverage: z.object({ requiredTotal: z.number().int().nonnegative(), covered: z.number().int().nonnegative(), blocked: z.number().int().nonnegative(), waived: z.number().int().nonnegative() }).strict(),
35
+ riskSummary: z.object({ high: z.number().int().nonnegative(), medium: z.number().int().nonnegative(), low: z.number().int().nonnegative() }).strict(),
36
+ }).strict();
37
+ const waiverSchema = z.object({ schemaVersion: z.literal(1), featureId: z.string(), items: z.array(z.object({ acId: z.string(), owner: z.string().min(1), reason: z.string().min(1), decidedAt: z.string().datetime() }).strict()) }).strict();
38
+ const controllerIdentityEvidenceSchema = z.object({
39
+ schemaVersion: z.literal(1),
40
+ packageName: z.literal(LOOP_AGENT_PACKAGE_NAME),
41
+ binName: z.string().min(1),
42
+ requested: z.string().min(1),
43
+ entry: z.string().min(1),
44
+ realEntry: z.string().min(1),
45
+ launch: z.object({ command: z.string().min(1), argsPrefix: z.array(z.string()) }).strict(),
46
+ binarySha256: z.string().regex(/^[a-f0-9]{64}$/),
47
+ packageRoot: z.string().min(1),
48
+ packageVersion: z.string().min(1),
49
+ reportedVersion: z.string().min(1).optional(),
50
+ packageFingerprint: z.object({
51
+ algorithm: z.literal("sha256"),
52
+ scopeVersion: z.literal(1),
53
+ scope: z.tuple([
54
+ z.literal("package.json"),
55
+ z.literal("bin/**"),
56
+ z.literal("dist/**"),
57
+ z.literal("skills/**"),
58
+ ]),
59
+ value: z.string().regex(/^sha256:[a-f0-9]{64}$/),
60
+ fileCount: z.number().int().positive(),
61
+ }).strict(),
62
+ resolvedAt: z.string().datetime(),
63
+ }).strict().superRefine((identity, ctx) => {
64
+ if (identity.reportedVersion !== undefined
65
+ && identity.reportedVersion !== identity.packageVersion) {
66
+ ctx.addIssue({
67
+ code: z.ZodIssueCode.custom,
68
+ path: ["reportedVersion"],
69
+ message: "reportedVersion must match packageVersion",
70
+ });
71
+ }
72
+ });
73
+ const evidenceRunSchema = z.object({
74
+ taskId: z.string(),
75
+ workerRunId: z.string(),
76
+ recordedAt: z.string().datetime(),
77
+ controllerIdentity: controllerIdentityEvidenceSchema.optional(),
78
+ }).strict();
79
+ const qaEvidenceSchema = z.object({ schemaVersion: z.literal(1), featureId: z.string(), verdict: z.literal("passed"), acIds: z.array(z.string()).min(1), runs: z.array(evidenceRunSchema).min(1) }).strict();
80
+ const finalVerificationSchema = z.object({ schemaVersion: z.literal(1), featureId: z.string(), kind: z.literal("final-verification"), status: z.literal("passed"), headSha: z.string().regex(/^[a-f0-9]{40}$/), run: evidenceRunSchema, shellSummary: hashedRefSchema }).strict();
81
+ const workerRunEvidenceSchema = z.object({ schemaVersion: z.literal(1), status: z.literal("succeeded"), workerRunId: z.string(), businessId: z.string(), featureId: z.string(), reportDecision: z.object({ succeeded: z.literal(true) }).passthrough(), commands: z.array(z.object({ name: z.string(), result: z.object({ ok: z.literal(true) }).passthrough() }).passthrough()).min(1), controllerIdentity: controllerIdentityEvidenceSchema.optional() }).passthrough();
82
+ export async function prepareFeatureDelivery(input) {
83
+ const repoRoot = path.resolve(input.repoRoot);
84
+ const featureDir = path.resolve(input.featureDir);
85
+ const validation = await validateFeatureTaskGraph(featureDir);
86
+ const featureId = validation.featureId;
87
+ const deliveryDir = path.join(getTaskPoolRoot(repoRoot), "artifacts", "features", featureId, "delivery");
88
+ const artifacts = { manifest: path.join(deliveryDir, "delivery-manifest.json"), coverage: path.join(deliveryDir, "acceptance-coverage.json"), prDraft: path.join(deliveryDir, "PR.md") };
89
+ const blockers = validation.ok ? [] : validation.errors.map((error) => `${error.code}: ${error.message}`);
90
+ const now = input.now ?? new Date();
91
+ const record = await readTransaction(repoRoot, featureId, blockers);
92
+ if (record) {
93
+ const branch = await git(repoRoot, ["branch", "--show-current"]).catch(() => "");
94
+ const head = await git(repoRoot, ["rev-parse", "HEAD"]).catch(() => "");
95
+ const status = await gitRaw(repoRoot, ["status", "--porcelain=v1", "--untracked-files=all"]).catch(() => "git-error");
96
+ if (branch !== record.branch)
97
+ blockers.push(`current branch must be ${record.branch}`);
98
+ if (head !== record.lastCheckpoint)
99
+ blockers.push("HEAD does not match the last Feature checkpoint");
100
+ if (status.trim())
101
+ blockers.push("Git worktree must be clean before Delivery");
102
+ if (!await gitOk(repoRoot, ["merge-base", "--is-ancestor", record.baseCommit, record.lastCheckpoint]))
103
+ blockers.push("Feature checkpoint history does not descend from the recorded base");
104
+ }
105
+ const acceptance = acceptanceSpecSchema.parse(YAML.parse(await readFile(path.join(featureDir, "acceptance.yaml"), "utf-8")));
106
+ const graph = taskGraphSpecSchema.parse(YAML.parse(await readFile(path.join(featureDir, "tasks", "task-graph.yaml"), "utf-8")));
107
+ const states = await readAllTaskPoolStates(repoRoot);
108
+ const runs = await readJsonlFile(getRunsJsonlPath(repoRoot));
109
+ const taskSpecs = new Map();
110
+ for (const node of graph.nodes)
111
+ taskSpecs.set(node.id, taskSpecSchema.parse(YAML.parse(await readFile(path.join(featureDir, "tasks", node.task), "utf-8"))));
112
+ const qaFact = await canonicalQaEvidence(repoRoot, input.qaEvidencePath, featureId, runs, taskSpecs, now, blockers);
113
+ const finalFact = record ? await canonicalFinalEvidence(repoRoot, input.finalVerificationPath, featureId, runs, taskSpecs, record, now, blockers) : undefined;
114
+ if (qaFact && finalFact && qaFact.data.runs.some((entry) => entry.workerRunId === finalFact.data.run.workerRunId))
115
+ blockers.push("final verification must use a dedicated run not included in the QA aggregate");
116
+ const qaEvidence = qaFact?.ref;
117
+ const finalVerification = finalFact?.ref;
118
+ const waivers = await readWaivers(input.waiverPath, repoRoot, featureId, blockers);
119
+ const taskRunEvidence = new Map();
120
+ for (const taskId of new Set(acceptance.acceptance.flatMap((item) => item.verification.expected_task_refs))) {
121
+ const evidence = await canonicalTaskRunEvidence(repoRoot, featureId, taskId, runs, blockers);
122
+ if (evidence)
123
+ taskRunEvidence.set(taskId, evidence);
124
+ }
125
+ const coverageItems = acceptance.acceptance.map((ac) => {
126
+ const blockedBy = ac.verification.expected_task_refs.filter((taskId) => ["Failed", "Blocked"].includes(states[taskId]?.status ?? ""));
127
+ const allDone = ac.verification.expected_task_refs.every((taskId) => states[taskId]?.status === "Done");
128
+ const waiver = waivers.items.get(ac.id);
129
+ const taskEvidence = allDone ? ac.verification.expected_task_refs.map((taskId) => taskRunEvidence.get(taskId)).filter((item) => Boolean(item)) : [];
130
+ const evidence = qaEvidence && qaFact?.data.acIds.includes(ac.id) && allDone && taskEvidence.length === ac.verification.expected_task_refs.length ? [qaEvidence, ...taskEvidence] : [];
131
+ return { acId: ac.id, required: ac.priority === "must", status: waiver ? "waived" : blockedBy.length ? "blocked" : allDone && evidence.length ? "covered" : allDone ? "partial" : "missing", evidence, blockedBy, ...(waiver ? { decision: waiver } : {}) };
132
+ });
133
+ const coverage = acceptanceCoverageArtifactSchema.parse({ schemaVersion: 1, featureId, ...(waivers.ref ? { waiverSource: waivers.ref } : {}), items: coverageItems });
134
+ const deliveryTasks = [];
135
+ if (record)
136
+ for (const checkpoint of record.checkpoints) {
137
+ const spec = taskSpecs.get(checkpoint.taskId);
138
+ if (!spec || !isDevelopmentTask(spec.type))
139
+ continue;
140
+ if (!await checkpointMatches(repoRoot, record.lastCheckpoint, checkpoint, spec)) {
141
+ blockers.push(`checkpoint ${checkpoint.taskId} does not match Git history, trailers, or changed files`);
142
+ continue;
143
+ }
144
+ const run = runs.find((candidate) => candidate.workerRunId === checkpoint.workerRunId && candidate.status === "succeeded" && candidate.taskId === checkpoint.taskId);
145
+ if (!run?.runRecordPath) {
146
+ blockers.push(`successful checkpoint ${checkpoint.taskId} is missing canonical run evidence`);
147
+ continue;
148
+ }
149
+ const evidence = await canonicalTaskRunEvidence(repoRoot, featureId, checkpoint.taskId, runs, blockers, checkpoint.workerRunId);
150
+ if (!evidence)
151
+ continue;
152
+ deliveryTasks.push({ taskId: checkpoint.taskId, workerRunId: checkpoint.workerRunId, dagRunId: run.harnessTaskId ?? run.workerRunId, commit: checkpoint.commit, acceptanceRefs: spec.acceptance_refs, verificationEvidence: [evidence], changedFiles: checkpoint.changedFiles });
153
+ }
154
+ for (const [taskId, spec] of taskSpecs)
155
+ if (isDevelopmentTask(spec.type) && states[taskId]?.status === "Done" && !deliveryTasks.some((task) => task.taskId === taskId))
156
+ blockers.push(`completed development task ${taskId} has no checkpointed Delivery evidence`);
157
+ const required = coverage.items.filter((item) => item.required);
158
+ const covered = required.filter((item) => item.status === "covered").length;
159
+ const waived = required.filter((item) => item.status === "waived").length;
160
+ const blocked = required.filter((item) => item.status === "blocked").length;
161
+ if (required.some((item) => !["covered", "waived"].includes(item.status)))
162
+ blockers.push("required acceptance coverage is incomplete");
163
+ let manifest;
164
+ if (record && qaEvidence && finalVerification)
165
+ manifest = deliveryManifestSchema.parse({ schemaVersion: 1, featureId, branch: record.branch, baseBranch: record.baseBranch, baseSha: record.baseCommit, headSha: record.lastCheckpoint, createdAt: now.toISOString(), tasks: deliveryTasks, qa: { verdict: "passed", evidence: qaEvidence }, finalVerification, acceptanceCoverage: { requiredTotal: required.length, covered, blocked, waived }, riskSummary: { high: 0, medium: waived, low: 0 } });
166
+ const result = { schemaVersion: 1, status: blockers.length ? "blocked" : input.dryRun ? "ready" : "written", featureId, dryRun: input.dryRun ?? false, blockers, artifacts, ...(manifest ? { manifest } : {}) };
167
+ if (!input.dryRun && blockers.length === 0 && manifest)
168
+ await writeDeliveryArtifacts(artifacts, coverage, manifest);
169
+ return result;
170
+ }
171
+ export async function validateDeliveryPackage(input) {
172
+ const repoRoot = path.resolve(input.repoRoot);
173
+ const featureDir = path.resolve(input.featureDir);
174
+ const now = input.now ?? new Date();
175
+ const blockers = [];
176
+ const validation = await validateFeatureTaskGraph(featureDir);
177
+ const featureId = validation.featureId;
178
+ const deliveryDir = path.join(getTaskPoolRoot(repoRoot), "artifacts", "features", featureId, "delivery");
179
+ let manifest;
180
+ let coverage;
181
+ try {
182
+ manifest = deliveryManifestSchema.parse(JSON.parse(await readFile(path.join(deliveryDir, "delivery-manifest.json"), "utf-8")));
183
+ }
184
+ catch (error) {
185
+ blockers.push(`delivery manifest is invalid: ${message(error)}`);
186
+ }
187
+ try {
188
+ coverage = acceptanceCoverageArtifactSchema.parse(JSON.parse(await readFile(path.join(deliveryDir, "acceptance-coverage.json"), "utf-8")));
189
+ }
190
+ catch (error) {
191
+ blockers.push(`acceptance coverage is invalid: ${message(error)}`);
192
+ }
193
+ const record = await readTransaction(repoRoot, featureId, blockers);
194
+ if (!manifest || !coverage || !record)
195
+ return { valid: false, blockers };
196
+ if (manifest.featureId !== featureId || coverage.featureId !== featureId)
197
+ blockers.push("Delivery Feature ownership mismatch");
198
+ if (manifest.branch !== record.branch || manifest.baseBranch !== record.baseBranch || manifest.baseSha !== record.baseCommit || manifest.headSha !== record.lastCheckpoint)
199
+ blockers.push("Delivery manifest does not match the Git transaction");
200
+ const branch = await git(repoRoot, ["branch", "--show-current"]).catch(() => "");
201
+ const head = await git(repoRoot, ["rev-parse", "HEAD"]).catch(() => "");
202
+ if (branch !== record.branch || head !== record.lastCheckpoint)
203
+ blockers.push("current Git position does not match Delivery");
204
+ const rawCloseoutStatus = await gitRaw(repoRoot, ["status", "--porcelain=v1", "--untracked-files=all"]).catch(() => "git-error");
205
+ const allowedDirtyPaths = new Set((input.allowedDirtyPaths ?? []).map((item) => item.replace(/\\/g, "/")));
206
+ const closeoutStatus = rawCloseoutStatus.split("\n").filter(Boolean).filter((line) => !allowedDirtyPaths.has(line.slice(3).replace(/\\/g, "/"))).join("\n").trim();
207
+ if (closeoutStatus)
208
+ blockers.push(`Git worktree is not clean for Closeout: ${closeoutStatus}`);
209
+ const graph = taskGraphSpecSchema.parse(YAML.parse(await readFile(path.join(featureDir, "tasks", "task-graph.yaml"), "utf-8")));
210
+ const acceptance = acceptanceSpecSchema.parse(YAML.parse(await readFile(path.join(featureDir, "acceptance.yaml"), "utf-8")));
211
+ const states = await readAllTaskPoolStates(repoRoot);
212
+ const specs = new Map();
213
+ for (const node of graph.nodes)
214
+ specs.set(node.id, taskSpecSchema.parse(YAML.parse(await readFile(path.join(featureDir, "tasks", node.task), "utf-8"))));
215
+ const runs = await readJsonlFile(getRunsJsonlPath(repoRoot));
216
+ for (const task of manifest.tasks) {
217
+ const checkpoint = record.checkpoints.find((entry) => entry.taskId === task.taskId && entry.workerRunId === task.workerRunId && entry.commit === task.commit);
218
+ const spec = specs.get(task.taskId);
219
+ if (!checkpoint || !spec || JSON.stringify([...task.acceptanceRefs].sort()) !== JSON.stringify([...(spec?.acceptance_refs ?? [])].sort()) || JSON.stringify([...task.changedFiles].sort()) !== JSON.stringify([...checkpoint.changedFiles].sort()) || !await checkpointMatches(repoRoot, record.lastCheckpoint, checkpoint, spec))
220
+ blockers.push(`Delivery task ${task.taskId} does not match its checkpoint`);
221
+ const canonical = await canonicalTaskRunEvidence(repoRoot, featureId, task.taskId, runs, blockers, task.workerRunId);
222
+ if (!canonical || !task.verificationEvidence.some((ref) => ref.path === canonical.path && ref.sha256 === canonical.sha256))
223
+ blockers.push(`Delivery task evidence is not canonical: ${task.taskId}`);
224
+ }
225
+ const expectedDevTasks = [...specs.values()].filter((spec) => isDevelopmentTask(spec.type) && states[spec.id]?.status === "Done").map((spec) => spec.id).sort();
226
+ if (JSON.stringify(expectedDevTasks) !== JSON.stringify(manifest.tasks.map((task) => task.taskId).sort()))
227
+ blockers.push("Delivery manifest does not contain exactly the completed development tasks");
228
+ const qa = await canonicalQaEvidence(repoRoot, manifest.qa.evidence.path, featureId, runs, specs, now, blockers);
229
+ if (!qa || qa.ref.sha256 !== manifest.qa.evidence.sha256)
230
+ blockers.push("QA evidence hash or semantics changed");
231
+ const final = await canonicalFinalEvidence(repoRoot, manifest.finalVerification.path, featureId, runs, specs, record, now, blockers);
232
+ if (!final || final.ref.sha256 !== manifest.finalVerification.sha256)
233
+ blockers.push("final verification hash or semantics changed");
234
+ if (qa && final && qa.data.runs.some((entry) => entry.workerRunId === final.data.run.workerRunId))
235
+ blockers.push("final verification run is not dedicated");
236
+ const required = coverage.items.filter((item) => item.required);
237
+ const counts = { requiredTotal: required.length, covered: required.filter((item) => item.status === "covered").length, blocked: required.filter((item) => item.status === "blocked").length, waived: required.filter((item) => item.status === "waived").length };
238
+ if (JSON.stringify(counts) !== JSON.stringify(manifest.acceptanceCoverage) || required.some((item) => !["covered", "waived"].includes(item.status)))
239
+ blockers.push("Delivery acceptance coverage no longer matches the manifest");
240
+ const coverageById = new Map(coverage.items.map((item) => [item.acId, item]));
241
+ if (coverageById.size !== coverage.items.length || coverage.items.length !== acceptance.acceptance.length)
242
+ blockers.push("Acceptance coverage item set does not match AcceptanceSpec");
243
+ const canonicalWaivers = new Map();
244
+ if (coverage.waiverSource) {
245
+ if (!await hashedRefValid(repoRoot, coverage.waiverSource))
246
+ blockers.push("waiver source hash changed");
247
+ else
248
+ try {
249
+ const raw = waiverSchema.parse(JSON.parse(await readFile(await safeRepoPath(repoRoot, coverage.waiverSource.path), "utf-8")));
250
+ if (raw.featureId !== featureId)
251
+ throw new Error("Feature mismatch");
252
+ for (const item of raw.items)
253
+ canonicalWaivers.set(item.acId, { owner: item.owner, reason: item.reason, decidedAt: item.decidedAt });
254
+ }
255
+ catch (error) {
256
+ blockers.push(`waiver source is invalid: ${message(error)}`);
257
+ }
258
+ }
259
+ for (const ac of acceptance.acceptance) {
260
+ const item = coverageById.get(ac.id);
261
+ if (!item) {
262
+ blockers.push(`Acceptance coverage is missing ${ac.id}`);
263
+ continue;
264
+ }
265
+ if (item.required !== (ac.priority === "must"))
266
+ blockers.push(`Acceptance required flag changed: ${ac.id}`);
267
+ const blockedBy = ac.verification.expected_task_refs.filter((taskId) => ["Failed", "Blocked"].includes(states[taskId]?.status ?? ""));
268
+ const allDone = ac.verification.expected_task_refs.every((taskId) => states[taskId]?.status === "Done");
269
+ const expectedRefs = [];
270
+ for (const taskId of ac.verification.expected_task_refs) {
271
+ const ref = await canonicalTaskRunEvidence(repoRoot, featureId, taskId, runs, blockers);
272
+ if (ref)
273
+ expectedRefs.push(ref);
274
+ }
275
+ const waiver = canonicalWaivers.get(ac.id);
276
+ const waiverMatches = Boolean(waiver && item.decision && JSON.stringify(waiver) === JSON.stringify(item.decision));
277
+ const expectedStatus = waiverMatches ? "waived" : blockedBy.length ? "blocked" : allDone && expectedRefs.length === ac.verification.expected_task_refs.length && qa?.data.acIds.includes(ac.id) ? "covered" : allDone ? "partial" : "missing";
278
+ if (item.status !== expectedStatus)
279
+ blockers.push(`Acceptance coverage status is not derivable: ${ac.id}`);
280
+ if (JSON.stringify([...item.blockedBy].sort()) !== JSON.stringify([...blockedBy].sort()))
281
+ blockers.push(`Acceptance blockers changed: ${ac.id}`);
282
+ for (const ref of expectedRefs)
283
+ if (!item.evidence.some((candidate) => candidate.path === ref.path && candidate.sha256 === ref.sha256))
284
+ blockers.push(`Acceptance run evidence is missing: ${ac.id}`);
285
+ if (qa?.data.acIds.includes(ac.id) && !item.evidence.some((candidate) => candidate.path === qa.ref.path && candidate.sha256 === qa.ref.sha256))
286
+ blockers.push(`Acceptance QA evidence is missing: ${ac.id}`);
287
+ for (const ref of item.evidence)
288
+ if (!await hashedRefValid(repoRoot, ref))
289
+ blockers.push(`Acceptance evidence changed: ${item.acId}`);
290
+ }
291
+ const age = now.getTime() - new Date(manifest.createdAt).getTime();
292
+ if (age < 0 || age > 24 * 60 * 60 * 1000)
293
+ blockers.push("Delivery manifest is not fresh within 24 hours");
294
+ return { valid: blockers.length === 0, blockers, manifest };
295
+ }
296
+ function isDevelopmentTask(type) { return !type.startsWith("qa-") && !["architecture", "review"].includes(type); }
297
+ async function readTransaction(repoRoot, featureId, blockers) { try {
298
+ return gitTransactionRecordSchema.parse(JSON.parse(await readFile(transactionRecordPath(repoRoot, featureId), "utf-8")));
299
+ }
300
+ catch {
301
+ blockers.push("Feature Git transaction record is missing or invalid");
302
+ return undefined;
303
+ } }
304
+ async function readWaivers(filePath, repoRoot, featureId, blockers) {
305
+ const items = new Map();
306
+ if (!filePath)
307
+ return { items };
308
+ const ref = await hashedRepoRef(repoRoot, filePath, blockers, "waiver artifact");
309
+ if (!ref)
310
+ return { items };
311
+ try {
312
+ const parsed = waiverSchema.parse(JSON.parse(await readFile(await safeRepoPath(repoRoot, filePath), "utf-8")));
313
+ if (parsed.featureId !== featureId)
314
+ throw new Error("Feature mismatch");
315
+ for (const item of parsed.items)
316
+ items.set(item.acId, { owner: item.owner, reason: item.reason, decidedAt: item.decidedAt });
317
+ return { items, ref };
318
+ }
319
+ catch (error) {
320
+ blockers.push(`waiver artifact is invalid: ${message(error)}`);
321
+ return { items };
322
+ }
323
+ }
324
+ async function hashedRepoRef(repoRoot, ref, blockers, label) { try {
325
+ const absolute = await safeRepoPath(repoRoot, ref);
326
+ const content = await readFile(absolute);
327
+ return { path: path.relative(await realpath(repoRoot), absolute).replace(/\\/g, "/"), sha256: createHash("sha256").update(content).digest("hex") };
328
+ }
329
+ catch (error) {
330
+ blockers.push(`${label} is missing or unsafe: ${message(error)}`);
331
+ return undefined;
332
+ } }
333
+ async function safeRepoPath(repoRoot, ref) { const canonicalRepo = await realpath(repoRoot); const lexical = path.isAbsolute(ref) ? path.resolve(ref) : path.resolve(repoRoot, ref); const absolute = await realpath(lexical); const relative = path.relative(canonicalRepo, absolute); if (!relative || relative.startsWith("..") || path.isAbsolute(relative))
334
+ throw new Error("path escapes repo"); return absolute; }
335
+ async function canonicalQaEvidence(repoRoot, ref, featureId, runs, specs, now, blockers) {
336
+ const hashed = await hashedRepoRef(repoRoot, ref, blockers, "QA evidence");
337
+ if (!hashed)
338
+ return undefined;
339
+ try {
340
+ const data = qaEvidenceSchema.parse(JSON.parse(await readFile(await safeRepoPath(repoRoot, ref), "utf-8")));
341
+ if (data.featureId !== featureId)
342
+ throw new Error("Feature ownership mismatch");
343
+ for (const entry of data.runs) {
344
+ const run = assertEvidenceRun(entry, featureId, runs, specs, now, true);
345
+ await readCanonicalWorkerRun(repoRoot, run);
346
+ }
347
+ const allowedAcIds = new Set(data.runs.flatMap((entry) => specs.get(entry.taskId)?.acceptance_refs ?? []));
348
+ for (const acId of data.acIds)
349
+ if (!allowedAcIds.has(acId))
350
+ throw new Error(`QA run scope does not cover ${acId}`);
351
+ return { data, ref: hashed };
352
+ }
353
+ catch (error) {
354
+ blockers.push(`QA evidence is not canonical: ${message(error)}`);
355
+ return undefined;
356
+ }
357
+ }
358
+ async function canonicalFinalEvidence(repoRoot, ref, featureId, runs, specs, record, now, blockers) {
359
+ const hashed = await hashedRepoRef(repoRoot, ref, blockers, "final verification");
360
+ if (!hashed)
361
+ return undefined;
362
+ try {
363
+ const data = finalVerificationSchema.parse(JSON.parse(await readFile(await safeRepoPath(repoRoot, ref), "utf-8")));
364
+ if (data.featureId !== featureId)
365
+ throw new Error("Feature ownership mismatch");
366
+ const taskPoolRun = assertEvidenceRun(data.run, featureId, runs, specs, now, true);
367
+ if (data.headSha !== record.lastCheckpoint)
368
+ throw new Error("final verification is not bound to Delivery HEAD");
369
+ const latestCheckpointAt = Math.max(...record.checkpoints.map((entry) => new Date(entry.createdAt).getTime()));
370
+ if (new Date(data.run.recordedAt).getTime() < latestCheckpointAt)
371
+ throw new Error("final verification predates the last checkpoint");
372
+ const runRecord = await readCanonicalWorkerRun(repoRoot, taskPoolRun);
373
+ if (!runRecord.commands.some((command) => command.name === "run-dag"))
374
+ throw new Error("final verification run is missing a successful run-dag command");
375
+ const spec = specs.get(data.run.taskId);
376
+ const requiredCommands = spec.verify.commands.filter((command) => command.required);
377
+ if (requiredCommands.length === 0)
378
+ throw new Error("final verification TaskSpec has no required commands");
379
+ const expectedSummaryPath = path.join(".harness", "dag-runs", "completed", data.run.workerRunId, "verify-shell", "result.summary.md").replace(/\\/g, "/");
380
+ if (data.shellSummary.path !== expectedSummaryPath || !await hashedRefValid(repoRoot, data.shellSummary))
381
+ throw new Error("final verification shell summary is missing or changed");
382
+ const summary = await readFile(await safeRepoPath(repoRoot, data.shellSummary.path), "utf-8");
383
+ for (const required of requiredCommands) {
384
+ const escaped = required.command.replace(/\|/g, "\\|");
385
+ if (!summary.split(/\r?\n/).some((line) => line.includes("| true | 0 | success |") && (line.endsWith(`| ${escaped} |`) || line.endsWith(`'${escaped}' |`))))
386
+ throw new Error(`final verification command evidence is missing: ${required.id}`);
387
+ }
388
+ return { data, ref: hashed };
389
+ }
390
+ catch (error) {
391
+ blockers.push(`final verification is not canonical: ${message(error)}`);
392
+ return undefined;
393
+ }
394
+ }
395
+ function assertEvidenceRun(entry, featureId, runs, specs, now, requireQa) {
396
+ const run = runs.find((candidate) => candidate.featureId === featureId && candidate.taskId === entry.taskId && candidate.workerRunId === entry.workerRunId && candidate.status === "succeeded");
397
+ if (!run || run.recordedAt !== entry.recordedAt)
398
+ throw new Error(`successful run ownership/time mismatch: ${entry.workerRunId}`);
399
+ if (requireQa && specs.get(entry.taskId)?.type !== "qa-execute")
400
+ throw new Error(`run is not a qa-execute task: ${entry.taskId}`);
401
+ const age = now.getTime() - new Date(run.recordedAt).getTime();
402
+ if (age < 0 || age > 24 * 60 * 60 * 1000)
403
+ throw new Error(`run is not fresh within 24 hours: ${entry.workerRunId}`);
404
+ assertControllerIdentityPair(entry.controllerIdentity, run.controllerIdentity, "evidence and Task Pool run");
405
+ return run;
406
+ }
407
+ async function canonicalTaskRunEvidence(repoRoot, featureId, taskId, runs, blockers, workerRunId) {
408
+ const run = [...runs].reverse().find((candidate) => candidate.featureId === featureId && candidate.taskId === taskId && candidate.status === "succeeded" && candidate.runRecordPath && (!workerRunId || candidate.workerRunId === workerRunId));
409
+ if (!run?.runRecordPath)
410
+ return undefined;
411
+ const hashed = await hashedRepoRef(repoRoot, run.runRecordPath, blockers, `${taskId} run evidence`);
412
+ if (!hashed)
413
+ return undefined;
414
+ try {
415
+ await readCanonicalWorkerRun(repoRoot, run);
416
+ return hashed;
417
+ }
418
+ catch (error) {
419
+ blockers.push(`${taskId} run evidence is not canonical: ${message(error)}`);
420
+ return undefined;
421
+ }
422
+ }
423
+ async function readCanonicalWorkerRun(repoRoot, run) {
424
+ if (!run?.runRecordPath)
425
+ throw new Error("Task Pool run record path is missing");
426
+ const record = workerRunEvidenceSchema.parse(JSON.parse(await readFile(await safeRepoPath(repoRoot, run.runRecordPath), "utf-8")));
427
+ if (record.featureId !== run.featureId || record.businessId !== run.taskId || record.workerRunId !== run.workerRunId)
428
+ throw new Error("run record ownership mismatch");
429
+ assertControllerIdentityPair(run.controllerIdentity, record.controllerIdentity, "Task Pool run and worker run record");
430
+ return record;
431
+ }
432
+ function assertControllerIdentityPair(left, right, label) {
433
+ if (left === undefined && right === undefined)
434
+ return;
435
+ if (left === undefined || right === undefined)
436
+ throw new Error(`controller identity presence mismatch between ${label}`);
437
+ const parsedLeft = controllerIdentityEvidenceSchema.parse(left);
438
+ const parsedRight = controllerIdentityEvidenceSchema.parse(right);
439
+ if (!controllerIdentitiesMatch(parsedLeft, parsedRight))
440
+ throw new Error(`controller identity mismatch between ${label}`);
441
+ }
442
+ async function writeDeliveryArtifacts(artifacts, coverage, manifest) {
443
+ const deliveryDir = path.dirname(artifacts.manifest);
444
+ const parent = path.dirname(deliveryDir);
445
+ const staging = path.join(parent, `.delivery.${randomUUID()}.staging`);
446
+ const backup = path.join(parent, `.delivery.${randomUUID()}.backup`);
447
+ await mkdir(staging, { recursive: true });
448
+ await writeFile(path.join(staging, "acceptance-coverage.json"), `${JSON.stringify(coverage, null, 2)}\n`);
449
+ await writeFile(path.join(staging, "delivery-manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
450
+ await writeFile(path.join(staging, "PR.md"), renderPr(manifest));
451
+ let backedUp = false;
452
+ try {
453
+ try {
454
+ await rename(deliveryDir, backup);
455
+ backedUp = true;
456
+ }
457
+ catch (error) {
458
+ if (!isNotFound(error))
459
+ throw error;
460
+ }
461
+ await rename(staging, deliveryDir);
462
+ if (backedUp)
463
+ await rm(backup, { recursive: true, force: true });
464
+ }
465
+ catch (error) {
466
+ await rm(staging, { recursive: true, force: true });
467
+ if (backedUp) {
468
+ await rm(deliveryDir, { recursive: true, force: true });
469
+ await rename(backup, deliveryDir);
470
+ }
471
+ throw error;
472
+ }
473
+ }
474
+ function renderPr(manifest) { return `# Summary\n\nFeature ${manifest.featureId} local Delivery Package.\n\n## Feature/base/head\n\n- Branch: ${manifest.branch}\n- Base: ${manifest.baseBranch} @ ${manifest.baseSha}\n- Head: ${manifest.headSha}\n\n## Completed Tasks\n\n${manifest.tasks.map((task) => `- ${task.taskId}: ${task.commit} (${task.changedFiles.join(", ")})`).join("\n")}\n\n## Acceptance Coverage\n\n- Required: ${manifest.acceptanceCoverage.requiredTotal}\n- Covered: ${manifest.acceptanceCoverage.covered}\n- Waived: ${manifest.acceptanceCoverage.waived}\n- Blocked: ${manifest.acceptanceCoverage.blocked}\n\n## Verification\n\n- QA: ${manifest.qa.evidence.path}\n- Final: ${manifest.finalVerification.path}\n\n## Changed Files\n\n${[...new Set(manifest.tasks.flatMap((task) => task.changedFiles))].map((file) => `- ${file}`).join("\n")}\n\n## Risks\n\n- high=${manifest.riskSummary.high}, medium=${manifest.riskSummary.medium}, low=${manifest.riskSummary.low}\n\n## Human Review Checklist\n\n- [ ] Review diff and commits\n- [ ] Confirm required AC coverage and waivers\n- [ ] Confirm QA and final verification evidence\n- [ ] Review risks\n- [ ] Decide whether to push or create a remote PR manually\n`; }
475
+ async function git(repoRoot, args) { const result = await execFileAsync("git", args, { cwd: repoRoot, encoding: "utf8" }); return result.stdout.trim(); }
476
+ async function gitRaw(repoRoot, args) { const result = await execFileAsync("git", args, { cwd: repoRoot, encoding: "utf8" }); return result.stdout; }
477
+ async function gitOk(repoRoot, args) { try {
478
+ await git(repoRoot, args);
479
+ return true;
480
+ }
481
+ catch {
482
+ return false;
483
+ } }
484
+ async function checkpointMatches(repoRoot, head, checkpoint, spec) {
485
+ if (!await gitOk(repoRoot, ["merge-base", "--is-ancestor", checkpoint.commit, head]))
486
+ return false;
487
+ const messageText = await gitRaw(repoRoot, ["show", "-s", "--format=%B", checkpoint.commit]).catch(() => "");
488
+ for (const trailer of [`Feature: ${spec.feature_id}`, `Task: ${spec.id}`, `Acceptance: ${spec.acceptance_refs.join(", ")}`, `Worker-Run: ${checkpoint.workerRunId}`])
489
+ if (!messageText.split(/\r?\n/).includes(trailer))
490
+ return false;
491
+ const actual = (await gitRaw(repoRoot, ["diff-tree", "--no-commit-id", "--name-only", "-r", checkpoint.commit]).catch(() => "")).split(/\r?\n/).filter(Boolean).sort();
492
+ return JSON.stringify(actual) === JSON.stringify([...checkpoint.changedFiles].sort());
493
+ }
494
+ async function hashedRefValid(repoRoot, ref) { try {
495
+ const content = await readFile(await safeRepoPath(repoRoot, ref.path));
496
+ return createHash("sha256").update(content).digest("hex") === ref.sha256;
497
+ }
498
+ catch {
499
+ return false;
500
+ } }
501
+ function message(error) { return error instanceof Error ? error.message : String(error); }
502
+ function isNotFound(error) { return Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT"); }
@@ -0,0 +1,68 @@
1
+ import { readFile, readdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { getRunsJsonlPath, getTaskPoolRoot } from "../pool/run-store.js";
4
+ import { discoverFeatureDirs } from "./discover.js";
5
+ import { reviewFeature } from "./review.js";
6
+ import { taskPoolRunFactSchema, taskPoolStateFactSchema } from "../pool/validation.js";
7
+ export async function loadFeatureDecisionModels(repoRoot) {
8
+ const projectionWarnings = await auditTaskPoolFacts(repoRoot);
9
+ let features = [];
10
+ for (const featureDir of await discoverFeatureDirs(repoRoot)) {
11
+ try {
12
+ const feature = await reviewFeature({ featureDir, repoRoot });
13
+ features.push(feature.projectionWarnings.length ? degradeExisting(feature, feature.projectionWarnings) : feature);
14
+ }
15
+ catch (error) {
16
+ const warning = `${path.basename(featureDir)}: ${message(error)}`;
17
+ projectionWarnings.push(warning);
18
+ features.push(degradedFeature(path.basename(featureDir), warning));
19
+ }
20
+ }
21
+ if (projectionWarnings.length > 0) {
22
+ features = features.length ? features.map((feature) => degradeExisting(feature, projectionWarnings)) : [degradedFeature("task-pool", projectionWarnings.join("; "))];
23
+ }
24
+ features.sort((a, b) => priority(a.status) - priority(b.status) || a.featureId.localeCompare(b.featureId));
25
+ return { features, projectionWarnings };
26
+ }
27
+ function degradeExisting(feature, warnings) {
28
+ return { ...feature, status: "needs_action", statusLabel: "需处理", blockingItems: [...warnings.map((warning) => ({ type: "projection_warning", message: warning, evidence: [] })), ...feature.blockingItems], nextAction: { kind: "repair_projection", label: "修复损坏的 Feature / Task Pool facts 后重新审阅" }, projectionWarnings: [...new Set([...feature.projectionWarnings, ...warnings])] };
29
+ }
30
+ function degradedFeature(featureId, warning) { return { schemaVersion: 1, featureId, status: "needs_action", statusLabel: "需处理", summary: { tasksTotal: 0, tasksSucceeded: 0, tasksFailed: 0, tasksReady: 0, tasksBlocked: 0, requiredAcTotal: 0, requiredAcCovered: 0 }, riskSummary: { high: 1, medium: 0, low: 0 }, blockingItems: [{ type: "projection_warning", message: warning, evidence: [] }], followUps: { pending: [], actionCards: [], resolvedFailureTaskIds: [] }, tasks: [], acceptanceCoverage: [], nextAction: { kind: "repair_projection", label: "修复损坏事实后重新审阅" }, alternativeActions: [], evidence: { morningReport: null, observeSnapshot: null, delivery: null, closeout: null }, projectionWarnings: [warning] }; }
31
+ async function auditTaskPoolFacts(repoRoot) {
32
+ const warnings = [];
33
+ try {
34
+ const raw = await readFile(getRunsJsonlPath(repoRoot), "utf-8");
35
+ raw.split(/\r?\n/).filter(Boolean).forEach((line, index) => { try {
36
+ if (!taskPoolRunFactSchema.safeParse(JSON.parse(line)).success)
37
+ warnings.push(`Task Pool runs.jsonl line ${index + 1} is semantically invalid`);
38
+ }
39
+ catch {
40
+ warnings.push(`Task Pool runs.jsonl line ${index + 1} is corrupt`);
41
+ } });
42
+ }
43
+ catch (error) {
44
+ if (!isNotFound(error))
45
+ warnings.push(`Task Pool runs.jsonl is unreadable: ${message(error)}`);
46
+ }
47
+ const stateDir = path.join(getTaskPoolRoot(repoRoot), "states");
48
+ try {
49
+ for (const entry of await readdir(stateDir))
50
+ if (entry.endsWith(".json"))
51
+ try {
52
+ const parsed = taskPoolStateFactSchema.safeParse(JSON.parse(await readFile(path.join(stateDir, entry), "utf-8")));
53
+ if (!parsed.success || parsed.data.taskId !== entry.slice(0, -5))
54
+ warnings.push(`Task Pool state is semantically invalid: ${entry}`);
55
+ }
56
+ catch {
57
+ warnings.push(`Task Pool state is corrupt: ${entry}`);
58
+ }
59
+ }
60
+ catch (error) {
61
+ if (!isNotFound(error))
62
+ warnings.push(`Task Pool states are unreadable: ${message(error)}`);
63
+ }
64
+ return warnings;
65
+ }
66
+ function priority(status) { return ({ needs_action: 0, running: 1, ready: 2, awaiting_qa: 3, deliverable: 4, draft: 5, closed: 6 })[status]; }
67
+ function isNotFound(error) { return Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT"); }
68
+ function message(error) { return error instanceof Error ? error.message : String(error); }
@@ -0,0 +1,14 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ export async function discoverFeatureDirs(repoRoot) {
4
+ const found = new Set();
5
+ for (const parent of [path.join(repoRoot, "features"), path.join(repoRoot, "dogfood", "features")]) {
6
+ try {
7
+ for (const entry of await readdir(parent, { withFileTypes: true }))
8
+ if (entry.isDirectory())
9
+ found.add(path.join(parent, entry.name));
10
+ }
11
+ catch { }
12
+ }
13
+ return [...found].sort();
14
+ }