@tea-agent/loop-agent 0.13.0-alpha.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/AGENTS.md +4 -0
  2. package/CHANGELOG.md +57 -53
  3. package/README.md +12 -3
  4. package/dist/application/dag/generate-task-dag.js +28 -58
  5. package/dist/application/evaluation/candidate-hash.js +75 -0
  6. package/dist/application/evaluation/candidate.js +52 -0
  7. package/dist/application/evaluation/replay.js +289 -0
  8. package/dist/application/evaluation/types.js +130 -0
  9. package/dist/cli/command-definitions.js +17 -4
  10. package/dist/cli/program.js +8 -4
  11. package/dist/commands/eval.js +235 -0
  12. package/dist/commands/init.js +131 -24
  13. package/dist/executors/pi-sdk-executor.js +38 -24
  14. package/dist/executors/shell-executor.js +226 -15
  15. package/dist/executors/shell-presets.js +20 -0
  16. package/dist/executors/shell-verification.js +7 -0
  17. package/dist/governance/manifest-types.js +1 -0
  18. package/dist/infrastructure/evaluation/candidate-store.js +439 -0
  19. package/dist/infrastructure/evaluation/store.js +40 -0
  20. package/dist/task/config-types.js +23 -0
  21. package/dist/worker/observe/routes.js +18 -3
  22. package/dist/worker/observe/spec-evidence.js +1 -1
  23. package/dist/worker/observe/static/dom.js +160 -1
  24. package/dist/worker/observe/static/state.js +14 -0
  25. package/dist/worker/observe/static/views/dag-inspector.js +35 -4
  26. package/dist/worker/observe/static/views/dag.js +9 -0
  27. package/dist/worker/observe/static/views/dashboard.js +702 -445
  28. package/dist/worker/observe/static/views/session-timeline.js +15 -1
  29. package/dist/workflows/dag/backend-test-analysis-contract.js +120 -0
  30. package/dist/workflows/dag/backend-test-case-manifest.js +503 -0
  31. package/dist/workflows/dag/backend-test-execution-contract.js +353 -0
  32. package/dist/workflows/dag/backend-test-result-contract.js +568 -0
  33. package/dist/workflows/dag/decision-envelope.js +57 -2
  34. package/dist/workflows/dag/dynamic-runtime/map.js +90 -2
  35. package/dist/workflows/dag/frontend-implementation-contract.js +240 -0
  36. package/dist/workflows/dag/frontend-project-capability.js +309 -0
  37. package/dist/workflows/dag/frontend-repair.js +341 -0
  38. package/dist/workflows/dag/frontend-risk.js +161 -0
  39. package/dist/workflows/dag/frontend-verification-trace.js +190 -0
  40. package/dist/workflows/dag/init-hybrid.js +2407 -297
  41. package/dist/workflows/dag/node-execution.js +9 -0
  42. package/dist/workflows/dag/prompt.js +9 -0
  43. package/dist/workflows/dag/repair-artifact.js +43 -3
  44. package/dist/workflows/dag/report.js +35 -1
  45. package/dist/workflows/dag/runner.js +28 -2
  46. package/dist/workflows/dag/skill-instructions.js +4 -2
  47. package/dist/workflows/dag/task-demand-routing.js +383 -0
  48. package/dist/workflows/dag/types.js +71 -13
  49. package/dist/workflows/dag/upstream-artifacts.js +1 -0
  50. package/dist/workflows/dag/validate.js +59 -1
  51. package/docs/README.md +6 -3
  52. package/docs/agent-dag-recovery-playbook.md +5 -3
  53. package/docs/agent-dag-runner.md +3 -3
  54. package/docs/architecture/README.md +3 -3
  55. package/docs/architecture/dag-execution.md +1 -1
  56. package/docs/architecture/evolution.md +13 -13
  57. package/docs/architecture/facts-and-state.md +1 -1
  58. package/docs/architecture/runtime-boundaries.md +7 -7
  59. package/docs/architecture/system-overview.md +3 -3
  60. package/docs/architecture/worker-and-feature.md +3 -3
  61. package/docs/design/README.md +124 -42
  62. package/docs/development-principles.md +4 -4
  63. package/docs/exec-plans/active/README.md +12 -11
  64. package/docs/exec-plans/completed/README.md +33 -0
  65. package/docs/feature-workflow.md +114 -39
  66. package/docs/init-surface.manifest.json +30 -3
  67. package/docs/loop-agent-harness.md +9 -8
  68. package/docs/production-readiness.md +1 -1
  69. package/docs/progress/README.md +23 -1
  70. package/docs/reports/README.md +65 -6
  71. package/docs/skills/vetted-skill-registry.md +2 -0
  72. package/docs/templates/agent-dag.schema.json +29 -1
  73. package/docs/templates/agent-dag.supervised-implementation.json +127 -8
  74. package/docs/templates/backend-test-analysis.schema.json +44 -0
  75. package/docs/templates/backend-test-case-manifest.schema.json +190 -0
  76. package/docs/templates/backend-test-dag.classify.prompt.md +75 -0
  77. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +81 -16
  78. package/docs/templates/backend-test-dag.json +311 -40
  79. package/docs/templates/backend-test-dag.retrospect.prompt.md +44 -30
  80. package/docs/templates/backend-test-dag.review-cases.prompt.md +10 -8
  81. package/docs/templates/backend-test-execution.schema.json +133 -0
  82. package/docs/templates/backend-test-result.schema.json +99 -0
  83. package/docs/templates/branch-merge-report.md +93 -0
  84. package/docs/templates/frontend-design-contract.md +9 -0
  85. package/docs/templates/frontend-eval/fixtures/failures/01-type-build-error.md +17 -0
  86. package/docs/templates/frontend-eval/fixtures/failures/02-unit-component-test-fail.md +16 -0
  87. package/docs/templates/frontend-eval/fixtures/failures/03-fixture-schema-drift.md +16 -0
  88. package/docs/templates/frontend-eval/fixtures/failures/04-missing-loading-empty-error-state.md +16 -0
  89. package/docs/templates/frontend-eval/fixtures/failures/05-forbidden-write-writeset-expansion.md +16 -0
  90. package/docs/templates/frontend-eval/fixtures/failures/06-unapproved-dependency-add.md +16 -0
  91. package/docs/templates/frontend-eval/fixtures/failures/07-mock-production-on.md +21 -0
  92. package/docs/templates/frontend-eval/fixtures/functional/01-simple-component-style.md +29 -0
  93. package/docs/templates/frontend-eval/fixtures/functional/02-form-validation.md +28 -0
  94. package/docs/templates/frontend-eval/fixtures/functional/03-list-detail-page.md +28 -0
  95. package/docs/templates/frontend-eval/fixtures/functional/04-api-mock.md +29 -0
  96. package/docs/templates/frontend-eval/fixtures/functional/05-permission-auth-gated-ui.md +27 -0
  97. package/docs/templates/frontend-eval/fixtures/functional/06-ssr-server-client-boundary.md +28 -0
  98. package/docs/templates/frontend-eval/fixtures/functional/07-shared-public-component-api.md +28 -0
  99. package/docs/templates/frontend-eval/fixtures/functional/08-pure-local-no-remote.md +27 -0
  100. package/docs/templates/frontend-eval/metrics.md +138 -0
  101. package/docs/templates/frontend-eval/smoke-targets.md +53 -0
  102. package/docs/templates/frontend-implementation-contract.schema.json +27 -0
  103. package/docs/templates/frontend-task-constraints.md +10 -0
  104. package/docs/templates/frontend-task-requirement.md +9 -0
  105. package/docs/templates/frontend-test-dag.generate-cases.prompt.md +5 -0
  106. package/docs/templates/frontend-test-dag.json +23 -0
  107. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +3 -0
  108. package/docs/templates/frontend-test-dag.retrospect.prompt.md +3 -0
  109. package/docs/templates/frontend-test-dag.review-cases.prompt.md +3 -0
  110. package/docs/templates/frontend-test-dag.review-execution.prompt.md +3 -0
  111. package/docs/templates/knowledge-graph-bootstrap-dag.json +1 -1
  112. package/docs/templates/knowledge-sync-dag.json +1 -0
  113. package/docs/verification-matrix.md +4 -1
  114. package/examples/decision-gate-agent-dag.json +4 -4
  115. package/examples/hybrid-loop-agent-dag.json +1 -1
  116. package/package.json +2 -2
  117. package/scripts/kb-bootstrap-init-skeleton.sh +2 -1
  118. package/scripts/kb-graph-incremental-prepare.mjs +19 -5
  119. package/scripts/kb-graph-promote.mjs +12 -1
  120. package/skills/ai-engineering-context/SKILL.md +2 -2
  121. package/skills/analyze-product-dependencies/SKILL.md +67 -0
  122. package/skills/analyze-product-dependencies/agents/openai.yaml +4 -0
  123. package/skills/analyze-product-dependencies/references/api-documentation-schema.md +30 -0
  124. package/skills/analyze-product-dependencies/references/dependency-analysis-schema.md +28 -0
  125. package/skills/analyze-product-dependencies/references/example.md +76 -0
  126. package/skills/analyze-product-dependencies/references/forward-test-cases.md +35 -0
  127. package/skills/analyze-product-dependencies/references/input-contract.md +11 -0
  128. package/skills/analyze-product-dependencies/references/scouting-rules.md +61 -0
  129. package/skills/analyze-product-dependencies/scripts/test-validators.mjs +267 -0
  130. package/skills/analyze-product-dependencies/scripts/validate-api-documentation.mjs +101 -0
  131. package/skills/analyze-product-dependencies/scripts/validate-dependency-analysis.mjs +142 -0
  132. package/skills/analyze-product-dependencies/scripts/validate-product-requirement-input.mjs +76 -0
  133. package/skills/analyze-product-dependencies/scripts/validation-helpers.mjs +146 -0
  134. package/skills/analyze-product-requirements/SKILL.md +90 -0
  135. package/skills/analyze-product-requirements/agents/openai.yaml +4 -0
  136. package/skills/analyze-product-requirements/references/acceptance-criteria.md +91 -0
  137. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +56 -0
  138. package/skills/analyze-product-requirements/references/example.md +86 -0
  139. package/skills/analyze-product-requirements/references/forward-test-cases.md +66 -0
  140. package/skills/analyze-product-requirements/references/product-analysis-schema.md +32 -0
  141. package/skills/analyze-product-requirements/references/product-requirement-schema.md +33 -0
  142. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +35 -0
  143. package/skills/analyze-product-requirements/scripts/test-validators.mjs +193 -0
  144. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +69 -0
  145. package/skills/analyze-product-requirements/scripts/validate-product-requirement.mjs +97 -0
  146. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +98 -0
  147. package/skills/analyze-product-requirements/scripts/validation-helpers.mjs +156 -0
  148. package/skills/browser-tools/SKILL.md +196 -0
  149. package/skills/browser-tools/browser-content.js +103 -0
  150. package/skills/browser-tools/browser-cookies.js +35 -0
  151. package/skills/browser-tools/browser-eval.js +53 -0
  152. package/skills/browser-tools/browser-hn-scraper.js +108 -0
  153. package/skills/browser-tools/browser-nav.js +44 -0
  154. package/skills/browser-tools/browser-pick.js +162 -0
  155. package/skills/browser-tools/browser-screenshot.js +34 -0
  156. package/skills/browser-tools/browser-start.js +86 -0
  157. package/skills/browser-tools/package-lock.json +2556 -0
  158. package/skills/browser-tools/package.json +19 -0
  159. package/skills/frontend-design-review/SKILL.md +6 -1
  160. package/skills/frontend-design-review/references/review-checklist.md +25 -4
  161. package/skills/frontend-implementation/SKILL.md +25 -30
  162. package/skills/frontend-implementation/references/code-standards.md +20 -22
  163. package/skills/frontend-implementation/references/node-contracts.md +17 -53
  164. package/skills/frontend-review/SKILL.md +10 -4
  165. package/skills/frontend-review/references/review-findings.md +8 -3
  166. package/skills/frontend-verification/SKILL.md +22 -9
  167. package/skills/frontend-verification/references/verification-checklist.md +17 -5
  168. package/skills/grill-with-docs/SKILL.md +5 -5
  169. package/skills/grill-with-docs/adr-format.md +3 -3
  170. package/skills/init-capability-evolution/SKILL.md +5 -5
  171. package/skills/loop-agent/SKILL.md +5 -5
  172. package/skills/loop-agent/references/README.md +3 -3
  173. package/skills/loop-agent/references/command-reference.md +98 -24
  174. package/skills/loop-agent/references/docs-converge.md +15 -15
  175. package/skills/loop-agent/references/harness-policy.md +2 -2
  176. package/skills/loop-agent/references/hybrid-dag.md +32 -22
  177. package/skills/loop-agent/references/multi-worktree.md +1 -1
  178. package/skills/loop-agent/references/orchestrator-and-interventions.md +8 -8
  179. package/skills/loop-agent/references/task-workflow.md +1 -1
  180. package/skills/loop-agent/references/verification-and-failure-handling.md +6 -4
  181. package/skills/playwright-cli/SKILL.md +420 -0
  182. package/skills/playwright-cli/references/element-attributes.md +23 -0
  183. package/skills/playwright-cli/references/playwright-tests.md +39 -0
  184. package/skills/playwright-cli/references/request-mocking.md +87 -0
  185. package/skills/playwright-cli/references/running-code.md +241 -0
  186. package/skills/playwright-cli/references/session-management.md +225 -0
  187. package/skills/playwright-cli/references/storage-state.md +275 -0
  188. package/skills/playwright-cli/references/test-generation.md +433 -0
  189. package/skills/playwright-cli/references/tracing.md +139 -0
  190. package/skills/playwright-cli/references/video-recording.md +143 -0
  191. package/skills/playwright-cli-case-generator/SKILL.md +74 -0
  192. package/skills/requesting-code-review/SKILL.md +1 -1
  193. package/skills/systematic-debugging/CREATION-LOG.md +3 -3
  194. package/skills/systematic-debugging/SKILL.md +1 -1
  195. package/skills/systematic-debugging/test-academic.md +1 -1
  196. package/skills/systematic-debugging/test-pressure-1.md +1 -1
  197. package/skills/systematic-debugging/test-pressure-2.md +1 -1
  198. package/skills/systematic-debugging/test-pressure-3.md +1 -1
  199. package/skills/verification-before-completion/SKILL.md +1 -1
@@ -0,0 +1,289 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { reportDagUseCase } from "../dag/report-dag.js";
5
+ import { readReplaySpec, writeReplayArtifacts, } from "../../infrastructure/evaluation/store.js";
6
+ function sha256(content) {
7
+ return createHash("sha256").update(content).digest("hex");
8
+ }
9
+ function pairKey(input) {
10
+ return `${input.split}\u0000${input.taskRef}\u0000${input.seed}`;
11
+ }
12
+ async function verifyEvidenceHash(input) {
13
+ const content = await readFile(input.filePath);
14
+ const actual = sha256(content);
15
+ if (actual !== input.expected) {
16
+ throw new Error(`${input.label} hash mismatch: expected ${input.expected}, got ${actual}`);
17
+ }
18
+ }
19
+ function sumOptional(values) {
20
+ if (values.some((value) => value === undefined)) {
21
+ return { value: null, missing: true };
22
+ }
23
+ return {
24
+ value: values.reduce((sum, value) => sum + (value ?? 0), 0),
25
+ missing: false,
26
+ };
27
+ }
28
+ function verifyPassed(input) {
29
+ if (input.status !== "finished")
30
+ return false;
31
+ const verificationNodes = input.nodes.filter((node) => node.executor === "shell" ||
32
+ node.nodeId.includes("verify") ||
33
+ node.nodeId.includes("gate"));
34
+ return (verificationNodes.length > 0 &&
35
+ verificationNodes.every((node) => node.status === "FINISHED" &&
36
+ (!node.failureCategory || node.failureCategory === "success")));
37
+ }
38
+ function metricsForRun(run) {
39
+ const tokens = sumOptional(run.nodes.map((node) => node.tokensUsed));
40
+ const duration = sumOptional(run.nodes.map((node) => node.durationMs));
41
+ const missingFields = [];
42
+ if (tokens.missing)
43
+ missingFields.push("tokens");
44
+ if (duration.missing)
45
+ missingFields.push("durationMs");
46
+ return {
47
+ metrics: {
48
+ verifyPassed: verifyPassed(run),
49
+ tokens: tokens.value,
50
+ durationMs: duration.value,
51
+ executorCalls: run.nodes.length,
52
+ repairPasses: run.convergence?.currentPass ?? 0,
53
+ },
54
+ missingFields,
55
+ };
56
+ }
57
+ function compareRows(incumbent, challenger) {
58
+ const reasons = [];
59
+ let verdict;
60
+ if (incumbent.metrics.verifyPassed !== challenger.metrics.verifyPassed) {
61
+ verdict = challenger.metrics.verifyPassed
62
+ ? "challenger_win"
63
+ : "incumbent_win";
64
+ reasons.push("verification_outcome");
65
+ }
66
+ else if (!incumbent.metrics.verifyPassed) {
67
+ verdict = "tie";
68
+ reasons.push("both_failed_verification");
69
+ }
70
+ else if (incumbent.metrics.tokens === null ||
71
+ challenger.metrics.tokens === null ||
72
+ incumbent.metrics.durationMs === null ||
73
+ challenger.metrics.durationMs === null) {
74
+ verdict = "incomparable";
75
+ reasons.push("missing_cost_metrics");
76
+ }
77
+ else {
78
+ const challengerNoWorse = challenger.metrics.tokens <= incumbent.metrics.tokens &&
79
+ challenger.metrics.durationMs <= incumbent.metrics.durationMs;
80
+ const incumbentNoWorse = incumbent.metrics.tokens <= challenger.metrics.tokens &&
81
+ incumbent.metrics.durationMs <= challenger.metrics.durationMs;
82
+ if (challengerNoWorse && !incumbentNoWorse) {
83
+ verdict = "challenger_win";
84
+ reasons.push("lower_cost");
85
+ }
86
+ else if (incumbentNoWorse && !challengerNoWorse) {
87
+ verdict = "incumbent_win";
88
+ reasons.push("lower_cost");
89
+ }
90
+ else if (challengerNoWorse && incumbentNoWorse) {
91
+ verdict = "tie";
92
+ reasons.push("equal_metrics");
93
+ }
94
+ else {
95
+ verdict = "incomparable";
96
+ reasons.push("cost_tradeoff");
97
+ }
98
+ }
99
+ return {
100
+ taskRef: incumbent.taskRef,
101
+ seed: incumbent.seed,
102
+ split: incumbent.split,
103
+ incumbentRunId: incumbent.runId,
104
+ challengerRunId: challenger.runId,
105
+ verdict,
106
+ reasons,
107
+ };
108
+ }
109
+ function buildScorecard(input) {
110
+ const rows = [...input.rows].sort((left, right) => [
111
+ left.split,
112
+ left.taskRef,
113
+ String(left.seed).padStart(12, "0"),
114
+ left.candidateId,
115
+ left.runId,
116
+ ]
117
+ .join("\u0000")
118
+ .localeCompare([
119
+ right.split,
120
+ right.taskRef,
121
+ String(right.seed).padStart(12, "0"),
122
+ right.candidateId,
123
+ right.runId,
124
+ ].join("\u0000")));
125
+ const incumbentByKey = new Map(rows
126
+ .filter((row) => row.candidateId === input.incumbentCandidateId)
127
+ .map((row) => [pairKey(row), row]));
128
+ const challengerByKey = new Map(rows
129
+ .filter((row) => row.candidateId === input.challengerCandidateId)
130
+ .map((row) => [pairKey(row), row]));
131
+ const keys = [
132
+ ...new Set([...incumbentByKey.keys(), ...challengerByKey.keys()]),
133
+ ].sort();
134
+ const comparisons = [];
135
+ let unpairedEvidenceCount = 0;
136
+ for (const key of keys) {
137
+ const incumbent = incumbentByKey.get(key);
138
+ const challenger = challengerByKey.get(key);
139
+ if (!incumbent || !challenger) {
140
+ unpairedEvidenceCount +=
141
+ Number(Boolean(incumbent)) + Number(Boolean(challenger));
142
+ continue;
143
+ }
144
+ comparisons.push(compareRows(incumbent, challenger));
145
+ }
146
+ const reasons = ["replay_only"];
147
+ if (comparisons.length === 0)
148
+ reasons.push("insufficient_samples");
149
+ if (unpairedEvidenceCount > 0)
150
+ reasons.push("unpaired_evidence");
151
+ return {
152
+ schemaVersion: 1,
153
+ replayId: input.replayId,
154
+ incumbentCandidateId: input.incumbentCandidateId,
155
+ challengerCandidateId: input.challengerCandidateId,
156
+ rows,
157
+ comparisons,
158
+ aggregate: {
159
+ pairedSampleCount: comparisons.length,
160
+ incumbentWins: comparisons.filter((item) => item.verdict === "incumbent_win").length,
161
+ challengerWins: comparisons.filter((item) => item.verdict === "challenger_win").length,
162
+ ties: comparisons.filter((item) => item.verdict === "tie").length,
163
+ incomparable: comparisons.filter((item) => item.verdict === "incomparable").length,
164
+ unpairedEvidenceCount,
165
+ promotionEligible: false,
166
+ reasons,
167
+ },
168
+ };
169
+ }
170
+ export function formatReplayMarkdown(scorecard) {
171
+ const lines = [
172
+ `# Eval Replay Scorecard: ${scorecard.replayId}`,
173
+ "",
174
+ "> Replay-only evidence. This report never authorizes promotion or executes Pi/DAG work.",
175
+ "",
176
+ `- incumbent: ${scorecard.incumbentCandidateId}`,
177
+ `- challenger: ${scorecard.challengerCandidateId}`,
178
+ `- promotionEligible: ${scorecard.aggregate.promotionEligible}`,
179
+ `- reasons: ${scorecard.aggregate.reasons.join(", ")}`,
180
+ "",
181
+ "## Evidence",
182
+ "",
183
+ "| split | task | seed | candidate | run | verified | tokens | durationMs | calls | repairPasses | missing |",
184
+ "|---|---|---:|---|---|---|---:|---:|---:|---:|---|",
185
+ ];
186
+ for (const row of scorecard.rows) {
187
+ lines.push(`| ${row.split} | ${row.taskRef} | ${row.seed} | ${row.candidateId} | ${row.runId} | ${row.metrics.verifyPassed} | ${row.metrics.tokens ?? "n/a"} | ${row.metrics.durationMs ?? "n/a"} | ${row.metrics.executorCalls} | ${row.metrics.repairPasses} | ${row.missingFields.join(", ") || "none"} |`);
188
+ }
189
+ lines.push("", "## Paired Comparisons", "", "| split | task | seed | incumbent run | challenger run | verdict | reasons |", "|---|---|---:|---|---|---|---|");
190
+ for (const comparison of scorecard.comparisons) {
191
+ lines.push(`| ${comparison.split} | ${comparison.taskRef} | ${comparison.seed} | ${comparison.incumbentRunId} | ${comparison.challengerRunId} | ${comparison.verdict} | ${comparison.reasons.join(", ")} |`);
192
+ }
193
+ if (scorecard.comparisons.length === 0) {
194
+ lines.push("| - | - | - | - | - | - | no paired evidence |");
195
+ }
196
+ return `${lines.join("\n")}\n`;
197
+ }
198
+ export async function replayEvaluation(input) {
199
+ const spec = await readReplaySpec(input.repoRoot, input.specPath);
200
+ const rows = [];
201
+ for (const evidence of spec.evidence) {
202
+ const runDir = path.join(input.repoRoot, ".harness", "dag-runs", "completed", evidence.runId);
203
+ const statePath = path.join(runDir, "state.json");
204
+ const runPath = path.join(runDir, "run.json");
205
+ const stateRefPath = path
206
+ .relative(input.repoRoot, statePath)
207
+ .split(path.sep)
208
+ .join("/");
209
+ const runRefPath = path
210
+ .relative(input.repoRoot, runPath)
211
+ .split(path.sep)
212
+ .join("/");
213
+ await verifyEvidenceHash({
214
+ filePath: statePath,
215
+ expected: evidence.stateSha256,
216
+ label: "state.json",
217
+ });
218
+ await verifyEvidenceHash({
219
+ filePath: runPath,
220
+ expected: evidence.runSha256,
221
+ label: "run.json",
222
+ });
223
+ const report = await reportDagUseCase({
224
+ repoRoot: input.repoRoot,
225
+ runId: evidence.runId,
226
+ lifecycle: "completed",
227
+ failedOnly: false,
228
+ latest: false,
229
+ });
230
+ const run = report.runs[0];
231
+ if (!run) {
232
+ throw new Error(`completed DAG run not found: ${evidence.runId}`);
233
+ }
234
+ if (run.evaluationAssociation.status === "present") {
235
+ if (run.evaluationAssociation.candidateId !== evidence.candidateId) {
236
+ throw new Error(`replay evidence candidateId conflict for ${evidence.runId}: evidence=${evidence.candidateId}, run=${run.evaluationAssociation.candidateId}`);
237
+ }
238
+ if (run.evaluationAssociation.seed !== evidence.seed) {
239
+ throw new Error(`replay evidence seed conflict for ${evidence.runId}: evidence=${evidence.seed}, run=${run.evaluationAssociation.seed}`);
240
+ }
241
+ if (run.evaluationAssociation.split &&
242
+ run.evaluationAssociation.split !== evidence.split) {
243
+ throw new Error(`replay evidence split conflict for ${evidence.runId}: evidence=${evidence.split}, run=${run.evaluationAssociation.split}`);
244
+ }
245
+ if (run.evaluationAssociation.taskRef &&
246
+ run.evaluationAssociation.taskRef !== evidence.taskRef) {
247
+ throw new Error(`replay evidence taskRef conflict for ${evidence.runId}: evidence=${evidence.taskRef}, run=${run.evaluationAssociation.taskRef}`);
248
+ }
249
+ }
250
+ if (![
251
+ "finished",
252
+ "failed",
253
+ "partial_failed",
254
+ "superseded",
255
+ "abandoned",
256
+ ].includes(run.status)) {
257
+ throw new Error(`completed DAG run ${evidence.runId} is not terminal (status=${run.status})`);
258
+ }
259
+ const { metrics, missingFields } = metricsForRun(run);
260
+ rows.push({
261
+ ...evidence,
262
+ lifecycle: "completed",
263
+ runStatus: run.status,
264
+ metrics,
265
+ missingFields,
266
+ evidenceRefs: [
267
+ { path: stateRefPath, sha256: evidence.stateSha256 },
268
+ { path: runRefPath, sha256: evidence.runSha256 },
269
+ ],
270
+ });
271
+ }
272
+ const scorecard = buildScorecard({
273
+ replayId: spec.replayId,
274
+ incumbentCandidateId: spec.incumbentCandidateId,
275
+ challengerCandidateId: spec.challengerCandidateId,
276
+ rows,
277
+ });
278
+ const markdown = formatReplayMarkdown(scorecard);
279
+ if (input.writeArtifacts === false) {
280
+ return { scorecard, markdown };
281
+ }
282
+ const paths = await writeReplayArtifacts({
283
+ repoRoot: input.repoRoot,
284
+ replayId: spec.replayId,
285
+ scorecard,
286
+ markdown,
287
+ });
288
+ return { scorecard, markdown, ...paths };
289
+ }
@@ -0,0 +1,130 @@
1
+ import { z } from "zod";
2
+ export const evalSplitSchema = z.enum(["public", "private", "held_out"]);
3
+ export const replayEvidenceRefSchema = z
4
+ .object({
5
+ candidateId: z.string().min(1),
6
+ taskRef: z.string().min(1),
7
+ seed: z.number().int().nonnegative(),
8
+ split: evalSplitSchema,
9
+ runId: z
10
+ .string()
11
+ .regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, "runId must be one safe path segment"),
12
+ stateSha256: z.string().regex(/^[a-f0-9]{64}$/),
13
+ runSha256: z.string().regex(/^[a-f0-9]{64}$/),
14
+ })
15
+ .strict();
16
+ export const replaySpecSchema = z
17
+ .object({
18
+ schemaVersion: z.literal(1),
19
+ replayId: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/),
20
+ incumbentCandidateId: z.string().min(1),
21
+ challengerCandidateId: z.string().min(1),
22
+ evidence: z.array(replayEvidenceRefSchema).min(1),
23
+ })
24
+ .strict()
25
+ .superRefine((spec, ctx) => {
26
+ if (spec.incumbentCandidateId === spec.challengerCandidateId) {
27
+ ctx.addIssue({
28
+ code: z.ZodIssueCode.custom,
29
+ message: "incumbentCandidateId and challengerCandidateId must differ",
30
+ path: ["challengerCandidateId"],
31
+ });
32
+ }
33
+ const allowed = new Set([
34
+ spec.incumbentCandidateId,
35
+ spec.challengerCandidateId,
36
+ ]);
37
+ const pairKeys = new Set();
38
+ for (let i = 0; i < spec.evidence.length; i += 1) {
39
+ const evidence = spec.evidence[i];
40
+ if (!allowed.has(evidence.candidateId)) {
41
+ ctx.addIssue({
42
+ code: z.ZodIssueCode.custom,
43
+ message: "evidence candidateId must match incumbent or challenger",
44
+ path: ["evidence", i, "candidateId"],
45
+ });
46
+ }
47
+ const key = `${evidence.candidateId}\u0000${evidence.split}\u0000${evidence.taskRef}\u0000${evidence.seed}`;
48
+ if (pairKeys.has(key)) {
49
+ ctx.addIssue({
50
+ code: z.ZodIssueCode.custom,
51
+ message: "duplicate evidence for candidateId + split + taskRef + seed",
52
+ path: ["evidence", i],
53
+ });
54
+ }
55
+ pairKeys.add(key);
56
+ }
57
+ });
58
+ // --- Candidate Registry (M2 W2.1–W2.2) ---
59
+ export const candidateIdSchema = z
60
+ .string()
61
+ .regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, "candidateId must be one safe path segment");
62
+ export const candidateKindSchema = z.enum([
63
+ "prompt",
64
+ "skill",
65
+ "context_policy",
66
+ "model_routing",
67
+ "profile",
68
+ "composite",
69
+ ]);
70
+ export const candidateContentRefSchema = z
71
+ .object({
72
+ path: z
73
+ .string()
74
+ .min(1)
75
+ .refine((value) => !pathIsAbsoluteLike(value), "content ref path must be repo-relative"),
76
+ sha256: z
77
+ .string()
78
+ .regex(/^(sha256:)?[a-f0-9]{64}$/i, "sha256 must be 64 hex digits"),
79
+ })
80
+ .strict();
81
+ function pathIsAbsoluteLike(value) {
82
+ if (value.startsWith("/") || value.startsWith("\\"))
83
+ return true;
84
+ if (/^[A-Za-z]:[\\/]/.test(value))
85
+ return true;
86
+ return false;
87
+ }
88
+ export const candidateManifestInputSchema = z
89
+ .object({
90
+ schemaVersion: z.literal(1),
91
+ candidateId: candidateIdSchema,
92
+ parentCandidateId: candidateIdSchema.nullable().optional(),
93
+ candidateKind: candidateKindSchema,
94
+ createdAt: z.string().min(1),
95
+ description: z.string().optional(),
96
+ contentRefs: z.array(candidateContentRefSchema).min(1),
97
+ // optional on input; always computed/verified on register/read
98
+ bundleHash: z
99
+ .string()
100
+ .regex(/^(sha256:)?[a-f0-9]{64}$/i)
101
+ .optional(),
102
+ })
103
+ .strict();
104
+ export const candidateManifestSchema = candidateManifestInputSchema
105
+ .extend({
106
+ parentCandidateId: candidateIdSchema.nullable(),
107
+ bundleHash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
108
+ })
109
+ .strict();
110
+ export const lifecycleStateSchema = z.enum([
111
+ "proposed",
112
+ "eligible",
113
+ "experimenting",
114
+ "accepted",
115
+ "rejected",
116
+ "invalid",
117
+ "retired",
118
+ ]);
119
+ export const lifecycleEventSchema = z
120
+ .object({
121
+ schemaVersion: z.literal(1),
122
+ seq: z.number().int().positive(),
123
+ from: lifecycleStateSchema.nullable(),
124
+ to: lifecycleStateSchema,
125
+ reason: z.string().min(1),
126
+ at: z.string().min(1),
127
+ previousEventHash: z.string().regex(/^[a-f0-9]{64}$/),
128
+ eventHash: z.string().regex(/^[a-f0-9]{64}$/),
129
+ })
130
+ .strict();
@@ -1,6 +1,7 @@
1
1
  import { runDoctor } from "../commands/doctor.js";
2
2
  import { runDocsArchive } from "../commands/docs-archive.js";
3
3
  import { runDocsAudit } from "../commands/docs-audit.js";
4
+ import { runEval } from "../commands/eval.js";
4
5
  import { runCoverageAudit } from "../commands/coverage-audit.js";
5
6
  import { runExamples } from "../commands/examples.js";
6
7
  import { runCloseout } from "../commands/closeout.js";
@@ -10,7 +11,7 @@ import { runInstructions } from "../commands/instructions.js";
10
11
  import { runNewTask } from "../commands/new-task.js";
11
12
  import { runImportPrd } from "../commands/import-prd.js";
12
13
  import { runPlanList } from "../commands/plan-list.js";
13
- import { runPlanCheck, runPlanComplete, runPlanCreate } from "../commands/plan.js";
14
+ import { runPlanCheck, runPlanComplete, runPlanCreate, } from "../commands/plan.js";
14
15
  import { runPromoteRun } from "../commands/promote-run.js";
15
16
  import { runSpine } from "../commands/spine.js";
16
17
  import { runStats } from "../commands/stats.js";
@@ -76,6 +77,7 @@ const INIT_SUBCOMMANDS = [
76
77
  "update",
77
78
  ];
78
79
  const EXAMPLES_SUBCOMMANDS = ["list", "show", "copy"];
80
+ const EVAL_SUBCOMMANDS = ["replay", "report", "candidate"];
79
81
  const CLOSEOUT_SUBCOMMANDS = ["task"];
80
82
  const PLAN_SUBCOMMANDS = ["list", "create", "complete", "check"];
81
83
  const SPINE_SUBCOMMANDS = ["audit"];
@@ -194,6 +196,17 @@ export const COMMAND_DEFINITIONS = [
194
196
  await runExamples(repoRoot, [subcommand, ...rest].filter(Boolean));
195
197
  },
196
198
  },
199
+ {
200
+ name: "eval",
201
+ adapter: "required",
202
+ tier: "operator",
203
+ intent: "Replay completed DAG evidence and manage immutable Candidate Registry lifecycle without live model execution or promotion.",
204
+ usage: "eval <replay|report|candidate> ...; candidate <register|show|list|transition> [--json|--markdown]",
205
+ subcommands: [...EVAL_SUBCOMMANDS],
206
+ handler: async ({ repoRoot, subcommand, rest }) => {
207
+ await runEval(repoRoot, [subcommand, ...rest].filter((arg) => Boolean(arg)));
208
+ },
209
+ },
197
210
  {
198
211
  name: "new-task",
199
212
  adapter: "required",
@@ -286,17 +299,17 @@ export const COMMAND_DEFINITIONS = [
286
299
  if (subcommand === "create") {
287
300
  const [planId, ...titleParts] = rest;
288
301
  if (!planId)
289
- throw new Error("usage: plan create <plan-id> \"<title>\"");
302
+ throw new Error('usage: plan create <plan-id> "<title>"');
290
303
  await runPlanCreate(repoRoot, planId, titleParts.join(" "));
291
304
  return;
292
305
  }
293
306
  if (subcommand === "complete") {
294
307
  const [planId, ...summaryParts] = rest;
295
308
  if (!planId)
296
- throw new Error("usage: plan complete <plan-id> --summary \"<summary>\"");
309
+ throw new Error('usage: plan complete <plan-id> --summary "<summary>"');
297
310
  const summary = parseSummaryFlag(summaryParts);
298
311
  if (!summary)
299
- throw new Error("usage: plan complete <plan-id> --summary \"<summary>\"");
312
+ throw new Error('usage: plan complete <plan-id> --summary "<summary>"');
300
313
  await runPlanComplete(repoRoot, planId, { summary });
301
314
  return;
302
315
  }
@@ -22,6 +22,7 @@ import { runDagWorkflowValidate } from "../commands/dag-workflow-validate.js";
22
22
  import { runDelegate } from "../commands/delegate.js";
23
23
  import { runDocsArchive } from "../commands/docs-archive.js";
24
24
  import { runDocsAudit } from "../commands/docs-audit.js";
25
+ import { runEval } from "../commands/eval.js";
25
26
  import { runDoctor } from "../commands/doctor.js";
26
27
  import { runExamples } from "../commands/examples.js";
27
28
  import { runGoal } from "../commands/goal.js";
@@ -38,7 +39,7 @@ import { runImportPrd } from "../commands/import-prd.js";
38
39
  import { runPiReuseBenchmark } from "../commands/pi-reuse-benchmark.js";
39
40
  import { parsePiPromptArgs, printPiPromptUsage, runPiPrompt, } from "../commands/pi-prompt.js";
40
41
  import { runPlanList } from "../commands/plan-list.js";
41
- import { runPlanCheck, runPlanComplete, runPlanCreate } from "../commands/plan.js";
42
+ import { runPlanCheck, runPlanComplete, runPlanCreate, } from "../commands/plan.js";
42
43
  import { runPromoteRun } from "../commands/promote-run.js";
43
44
  import { runReferenceIndex } from "../commands/reference-index.js";
44
45
  import { runRunDag } from "../commands/run-dag.js";
@@ -163,17 +164,17 @@ async function runCommanderAction(ctx, command, subcommand, rest) {
163
164
  if (subcommand === "create") {
164
165
  const [planId, ...titleParts] = rest;
165
166
  if (!planId)
166
- throw new Error("usage: plan create <plan-id> \"<title>\"");
167
+ throw new Error('usage: plan create <plan-id> "<title>"');
167
168
  await runPlanCreate(ctx.repoRoot, planId, titleParts.join(" "));
168
169
  return;
169
170
  }
170
171
  if (subcommand === "complete") {
171
172
  const [planId, ...summaryParts] = rest;
172
173
  if (!planId)
173
- throw new Error("usage: plan complete <plan-id> --summary \"<summary>\"");
174
+ throw new Error('usage: plan complete <plan-id> --summary "<summary>"');
174
175
  const summary = parsePlanSummaryFlag(summaryParts);
175
176
  if (!summary)
176
- throw new Error("usage: plan complete <plan-id> --summary \"<summary>\"");
177
+ throw new Error('usage: plan complete <plan-id> --summary "<summary>"');
177
178
  await runPlanComplete(ctx.repoRoot, planId, { summary });
178
179
  return;
179
180
  }
@@ -244,6 +245,9 @@ async function runCommanderAction(ctx, command, subcommand, rest) {
244
245
  case "knowledge":
245
246
  await runKnowledge(ctx.repoRoot, compactArgs([subcommand, ...rest]));
246
247
  return;
248
+ case "eval":
249
+ await runEval(ctx.repoRoot, compactArgs([subcommand, ...rest]));
250
+ return;
247
251
  case "dag":
248
252
  await runDagAction(ctx.repoRoot, subcommand, rest);
249
253
  return;