@devcxl/opencode-cabbage 1.1.0 → 1.3.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 (142) hide show
  1. package/.codex-plugin/plugin.json +36 -0
  2. package/README.md +53 -25
  3. package/README.zh.md +23 -23
  4. package/assets/agents/dev-lifecycle.md +208 -68
  5. package/assets/agents/team/architect.md +22 -7
  6. package/assets/agents/team/developer.md +13 -6
  7. package/assets/agents/team/goal-verify.md +4 -1
  8. package/assets/agents/team/researcher.md +59 -0
  9. package/assets/agents/team/reviewer.md +34 -18
  10. package/assets/commands/setup.md +1 -1
  11. package/assets/prompts/bootstrap.md +10 -9
  12. package/assets/prompts/stage-contract.md +25 -25
  13. package/assets/skills/flow-code/SKILL.md +23 -25
  14. package/assets/skills/flow-design/SKILL.md +94 -26
  15. package/assets/skills/flow-release/SKILL.md +41 -17
  16. package/assets/skills/flow-requirements/SKILL.md +19 -13
  17. package/assets/skills/flow-research/SKILL.md +77 -0
  18. package/assets/skills/flow-review/SKILL.md +55 -25
  19. package/assets/skills/flow-setup/SKILL.md +27 -24
  20. package/assets/skills/flow-tasks/SKILL.md +178 -35
  21. package/assets/skills/flow-tdd/SKILL.md +68 -40
  22. package/codex-skills/agent-architect/SKILL.md +79 -0
  23. package/codex-skills/agent-dev-lifecycle/SKILL.md +309 -0
  24. package/codex-skills/agent-developer/SKILL.md +51 -0
  25. package/codex-skills/agent-goal-verify/SKILL.md +45 -0
  26. package/codex-skills/agent-researcher/SKILL.md +32 -0
  27. package/codex-skills/agent-reviewer/SKILL.md +109 -0
  28. package/codex-skills/flow-code/SKILL.md +71 -0
  29. package/codex-skills/flow-design/SKILL.md +146 -0
  30. package/codex-skills/flow-release/SKILL.md +90 -0
  31. package/codex-skills/flow-requirements/SKILL.md +90 -0
  32. package/codex-skills/flow-research/SKILL.md +77 -0
  33. package/codex-skills/flow-review/SKILL.md +110 -0
  34. package/codex-skills/flow-setup/SKILL.md +75 -0
  35. package/codex-skills/flow-tasks/SKILL.md +220 -0
  36. package/codex-skills/flow-tdd/SKILL.md +235 -0
  37. package/dist/hooks/session-start.d.ts +1 -0
  38. package/dist/hooks/session-start.js +72 -0
  39. package/dist/hooks/session-start.js.map +1 -0
  40. package/dist/kernel/session-index.d.ts +8 -25
  41. package/dist/kernel/session-index.js +5 -55
  42. package/dist/kernel/session-index.js.map +1 -1
  43. package/dist/plugin/bootstrap.d.ts +0 -1
  44. package/dist/plugin/bootstrap.js +0 -3
  45. package/dist/plugin/bootstrap.js.map +1 -1
  46. package/dist/plugin/goal-store.d.ts +71 -0
  47. package/dist/plugin/goal-store.js +54 -0
  48. package/dist/plugin/goal-store.js.map +1 -0
  49. package/dist/plugin/goal.d.ts +47 -13
  50. package/dist/plugin/goal.js +228 -128
  51. package/dist/plugin/goal.js.map +1 -1
  52. package/dist/plugin/prompt-lint.js +3 -11
  53. package/dist/plugin/prompt-lint.js.map +1 -1
  54. package/dist/plugin/prompts.d.ts +0 -1
  55. package/dist/plugin/prompts.js +0 -4
  56. package/dist/plugin/prompts.js.map +1 -1
  57. package/dist/plugin/server.d.ts +3 -5
  58. package/dist/plugin/server.js +247 -155
  59. package/dist/plugin/server.js.map +1 -1
  60. package/dist/plugin/skills.d.ts +7 -1
  61. package/dist/plugin/skills.js +75 -12
  62. package/dist/plugin/skills.js.map +1 -1
  63. package/dist/plugin.d.ts +0 -1
  64. package/dist/plugin.js +1 -4
  65. package/dist/plugin.js.map +1 -1
  66. package/dist/util/gh.d.ts +0 -11
  67. package/dist/util/gh.js +0 -14
  68. package/dist/util/gh.js.map +1 -1
  69. package/dist/util/paths.d.ts +0 -14
  70. package/dist/util/paths.js +0 -69
  71. package/dist/util/paths.js.map +1 -1
  72. package/hooks/hooks.json +17 -0
  73. package/package.json +12 -4
  74. package/assets/prompts/ADR-FORMAT.md +0 -45
  75. package/assets/prompts/PRD-FORMAT.md +0 -17
  76. package/assets/templates/gitignore +0 -5
  77. package/dist/kernel/caller.d.ts +0 -48
  78. package/dist/kernel/caller.js +0 -88
  79. package/dist/kernel/caller.js.map +0 -1
  80. package/dist/kernel/flow.d.ts +0 -175
  81. package/dist/kernel/flow.js +0 -467
  82. package/dist/kernel/flow.js.map +0 -1
  83. package/dist/kernel/legacy.d.ts +0 -19
  84. package/dist/kernel/legacy.js +0 -34
  85. package/dist/kernel/legacy.js.map +0 -1
  86. package/dist/kernel/records.d.ts +0 -103
  87. package/dist/kernel/records.js +0 -240
  88. package/dist/kernel/records.js.map +0 -1
  89. package/dist/kernel/release.d.ts +0 -106
  90. package/dist/kernel/release.js +0 -241
  91. package/dist/kernel/release.js.map +0 -1
  92. package/dist/kernel/review.d.ts +0 -34
  93. package/dist/kernel/review.js +0 -82
  94. package/dist/kernel/review.js.map +0 -1
  95. package/dist/kernel/setup.d.ts +0 -95
  96. package/dist/kernel/setup.js +0 -401
  97. package/dist/kernel/setup.js.map +0 -1
  98. package/dist/kernel/slug.d.ts +0 -23
  99. package/dist/kernel/slug.js +0 -67
  100. package/dist/kernel/slug.js.map +0 -1
  101. package/dist/kernel/task.d.ts +0 -213
  102. package/dist/kernel/task.js +0 -873
  103. package/dist/kernel/task.js.map +0 -1
  104. package/dist/kernel/tdd/adapter.d.ts +0 -49
  105. package/dist/kernel/tdd/adapter.js +0 -294
  106. package/dist/kernel/tdd/adapter.js.map +0 -1
  107. package/dist/kernel/tdd/digest.d.ts +0 -18
  108. package/dist/kernel/tdd/digest.js +0 -303
  109. package/dist/kernel/tdd/digest.js.map +0 -1
  110. package/dist/kernel/tdd/evaluator.d.ts +0 -38
  111. package/dist/kernel/tdd/evaluator.js +0 -185
  112. package/dist/kernel/tdd/evaluator.js.map +0 -1
  113. package/dist/kernel/tdd/evidence.d.ts +0 -42
  114. package/dist/kernel/tdd/evidence.js +0 -48
  115. package/dist/kernel/tdd/evidence.js.map +0 -1
  116. package/dist/kernel/tdd/state.d.ts +0 -79
  117. package/dist/kernel/tdd/state.js +0 -363
  118. package/dist/kernel/tdd/state.js.map +0 -1
  119. package/dist/kernel/types.d.ts +0 -180
  120. package/dist/kernel/types.js +0 -2
  121. package/dist/kernel/types.js.map +0 -1
  122. package/dist/kernel/worktree.d.ts +0 -45
  123. package/dist/kernel/worktree.js +0 -156
  124. package/dist/kernel/worktree.js.map +0 -1
  125. package/dist/plugin/flow-control.d.ts +0 -91
  126. package/dist/plugin/flow-control.js +0 -287
  127. package/dist/plugin/flow-control.js.map +0 -1
  128. package/dist/plugin/release-control.d.ts +0 -39
  129. package/dist/plugin/release-control.js +0 -343
  130. package/dist/plugin/release-control.js.map +0 -1
  131. package/dist/plugin/setup-control.d.ts +0 -59
  132. package/dist/plugin/setup-control.js +0 -111
  133. package/dist/plugin/setup-control.js.map +0 -1
  134. package/dist/plugin/task-control.d.ts +0 -90
  135. package/dist/plugin/task-control.js +0 -208
  136. package/dist/plugin/task-control.js.map +0 -1
  137. package/dist/plugin/tdd-checkpoint.d.ts +0 -128
  138. package/dist/plugin/tdd-checkpoint.js +0 -664
  139. package/dist/plugin/tdd-checkpoint.js.map +0 -1
  140. package/dist/util/shell.d.ts +0 -1
  141. package/dist/util/shell.js +0 -4
  142. package/dist/util/shell.js.map +0 -1
@@ -1,664 +0,0 @@
1
- /**
2
- * tdd_checkpoint 工具(spec §2.3 tdd_checkpoint + §4 Runtime TDD 硬门禁 + PRD R6)。
3
- *
4
- * 与旧 `plugin/tdd-tool.ts`(Phase C 占位,接受内联 evidence)不同:
5
- * - 工具亲自执行 RED/GREEN(复用 kernel/tdd/adapter.executeRedCheck),不接受内联 evidence。
6
- * - RED 三重校验:失败分类(assertion/missing-behavior)+ 实现文件相对基线未变 + 测试输入未偷换。
7
- * - GREEN:同一测试通过 + 执行输入 digest 与 RED 一致 + 实现文件有变化。
8
- * - evidence 写入 Task Record 单个受控 comment(records.appendTddEvidence,marker/revision/幂等)。
9
- * - 状态以 `<!-- cabbage-tdd-state -->` JSON 块存于 comment,恢复时取最后一个(最新)。
10
- *
11
- * caller:developer + primary(矩阵见 kernel/caller.ts);not-applicable 仅 primary。
12
- * server.ts 接线由后续批次统一处理(本批提供工厂 + registerTddCheckpoint,不注册)。
13
- */
14
- import { tool } from "@opencode-ai/plugin/tool";
15
- import { resolveProjectDir } from "../util/paths.js";
16
- import { createHash } from "node:crypto";
17
- import { execFileSync } from "node:child_process";
18
- import { resolve, sep as pathSep } from "node:path";
19
- import { requireToolCaller, CALLER_NOT_AUTHORIZED } from "../kernel/caller.js";
20
- import { executeRedCheck, executeVitest } from "../kernel/tdd/adapter.js";
21
- import { computeWorkspaceDigest, matchesAnyPattern } from "../kernel/tdd/digest.js";
22
- import { startCycle, recordRed, recordGreen, recordFinalRegression, recordFinalVerification, abandonCycle, createTaskEvidence, } from "../kernel/tdd/state.js";
23
- import { evaluateTddCompliance } from "../kernel/tdd/evaluator.js";
24
- import { buildEvidenceBlock } from "../kernel/tdd/evidence.js";
25
- import { appendTddEvidence, readTddEvidenceComment, extractEvidenceBlock } from "../kernel/records.js";
26
- import { gh as ghCli } from "../util/gh.js";
27
- let tddGhExecutor = null;
28
- export function setTddCheckpointGhExecutor(fn) {
29
- tddGhExecutor = fn;
30
- }
31
- async function gh(args) {
32
- if (tddGhExecutor)
33
- return tddGhExecutor(args);
34
- return ghCli(args);
35
- }
36
- // ─── Task Record TDD 区块 ───
37
- /** Task Record body 中承载 TDD policy/criteria/worktree 的 JSON 区块 marker */
38
- export const TASK_TDD_TAG = "<!-- cabbage-task-tdd -->";
39
- /** 从 Task Record body 解析 TDD 区块(纯函数);无 marker 或畸形返回 null */
40
- export function parseTaskTddBlock(body) {
41
- const idx = body.indexOf(TASK_TDD_TAG);
42
- if (idx === -1)
43
- return null;
44
- const jsonText = body.slice(idx + TASK_TDD_TAG.length).trim();
45
- try {
46
- const parsed = JSON.parse(jsonText);
47
- if (!parsed || typeof parsed !== "object")
48
- return null;
49
- if (!parsed.policy || !Array.isArray(parsed.criteria) || typeof parsed.worktreeDir !== "string")
50
- return null;
51
- return { policy: parsed.policy, criteria: parsed.criteria, worktreeDir: parsed.worktreeDir };
52
- }
53
- catch {
54
- return null;
55
- }
56
- }
57
- /**
58
- * 解析 Task Record 上下文:按 title 定位 Sub Issue → 读 body → 解析 TDD 区块。
59
- * 由批 9(task_control 写 Task Record)遵循此区块格式;批 15 接线时可替换为正式解析。
60
- */
61
- export async function resolveTaskContext(parentIssueNumber, taskId, projectDir) {
62
- const { stdout } = await gh(`issue list --state all --limit 100 --json number,title,parent --jq '[.[] | select(.parent.number == ${parentIssueNumber} and .title == ${JSON.stringify(taskId)}) | .number] | first'`);
63
- const issueNumber = Number(stdout.trim());
64
- if (!Number.isInteger(issueNumber) || issueNumber <= 0) {
65
- throw new Error(`TASK_NOT_FOUND: Task "${taskId}" not found under parent #${parentIssueNumber}`);
66
- }
67
- const bodyResp = await gh(`issue view ${issueNumber} --json body --jq .body`);
68
- const block = parseTaskTddBlock(bodyResp.stdout);
69
- if (!block) {
70
- throw new Error(`TASK_TDD_BLOCK_MISSING: Task Record #${issueNumber} has no ${TASK_TDD_TAG} block`);
71
- }
72
- // worktreeDir 必须为项目内相对路径(防目录穿越,与 digest.ts validateRelPath 一致)
73
- const worktreeDir = resolve(projectDir, block.worktreeDir);
74
- if (!worktreeDir.startsWith(resolve(projectDir) + pathSep) && worktreeDir !== resolve(projectDir)) {
75
- throw new Error(`TASK_TDD_BLOCK_INVALID: worktreeDir "${block.worktreeDir}" escapes project root`);
76
- }
77
- return {
78
- issueNumber,
79
- policy: block.policy,
80
- criteria: block.criteria,
81
- worktreeDir,
82
- };
83
- }
84
- // ─── 状态序列化(存于 evidence comment) ───
85
- /** evidence comment 内状态 JSON 块 marker;解析时取最后一个(最新) */
86
- export const TDD_STATE_TAG = "<!-- cabbage-tdd-state -->";
87
- export function serializeState(evidence) {
88
- return `${TDD_STATE_TAG}\n${JSON.stringify({ schema: 1, evidence })}`;
89
- }
90
- /** 从 marker 区间内容解析最新状态;无状态块或畸形返回 null */
91
- export function parseStateFromContent(content) {
92
- const idx = content.lastIndexOf(TDD_STATE_TAG);
93
- if (idx === -1)
94
- return null;
95
- const jsonText = content.slice(idx + TDD_STATE_TAG.length).trim();
96
- try {
97
- const parsed = JSON.parse(jsonText);
98
- if (!parsed || parsed.schema !== 1 || !parsed.evidence)
99
- return null;
100
- return parsed.evidence;
101
- }
102
- catch {
103
- return null;
104
- }
105
- }
106
- /** 恢复 Task Record 的 TDD evidence 状态;无受控 comment → 初始状态 */
107
- async function restoreEvidence(issueNumber) {
108
- const comment = await readTddEvidenceComment(issueNumber);
109
- if (!comment)
110
- return createTaskEvidence();
111
- const extracted = extractEvidenceBlock(comment.body);
112
- const state = extracted ? parseStateFromContent(extracted.content) : null;
113
- if (!state) {
114
- throw new Error(`EVIDENCE_STATE_CORRUPTED: Task Record #${issueNumber} evidence comment is malformed`);
115
- }
116
- return state;
117
- }
118
- /** 人读 block + 状态 JSON 追加到受控 comment(appendTddEvidence 幂等去重) */
119
- async function persistEvidence(issueNumber, evidence, input) {
120
- const block = `${buildEvidenceBlock(input)}\n${serializeState(evidence)}`;
121
- return appendTddEvidence(issueNumber, block);
122
- }
123
- // ─── digest 辅助 ───
124
- function digestEqual(a, b) {
125
- if (a === null || b === null)
126
- return a === b;
127
- return a.algorithm === b.algorithm && a.value === b.value;
128
- }
129
- /** 实现文件子集 digest:作为 cycle 基线(cycle-start 快照)与 RED/GREEN 对比对象 */
130
- async function computeImplDigest(worktreeDir, policy) {
131
- return computeWorkspaceDigest(worktreeDir, {
132
- testFilePatterns: [],
133
- implementationFilePatterns: policy.implementationFilePatterns,
134
- generatedArtifactPatterns: policy.generatedArtifactPatterns,
135
- });
136
- }
137
- /** 测试输入 digest(executionInputPatterns):防 RED/GREEN 偷换测试配置/selector */
138
- async function computeExecutionInputDigest(worktreeDir, policy) {
139
- const patterns = policy.runner?.executionInputPatterns ?? [];
140
- return computeWorkspaceDigest(worktreeDir, {
141
- testFilePatterns: patterns,
142
- implementationFilePatterns: [],
143
- generatedArtifactPatterns: [],
144
- });
145
- }
146
- // ─── 响应辅助 ───
147
- const PLACEHOLDER = { algorithm: "sha256-content-v1", value: "0".repeat(64) };
148
- function okResponse(overrides = {}) {
149
- return JSON.stringify({ ok: true, ...overrides }, null, 2);
150
- }
151
- function errorResponse(code, message) {
152
- return JSON.stringify({ ok: false, error: { code, message } }, null, 2);
153
- }
154
- function evidenceSummary(evidence) {
155
- return {
156
- revision: evidence.revision,
157
- status: evidence.status,
158
- cycles: evidence.cycles.length,
159
- regression: evidence.regression.status,
160
- verification: evidence.verification.status,
161
- warnings: evidence.warnings,
162
- };
163
- }
164
- function bumpEvidence(evidence, patch) {
165
- return { ...evidence, ...patch, revision: evidence.revision + 1, updatedAt: new Date().toISOString() };
166
- }
167
- function requireRefs(args) {
168
- const parentIssueNumber = Number(args.parent_issue_number);
169
- const taskId = args.task_id;
170
- if (!Number.isInteger(parentIssueNumber) || !taskId)
171
- return null;
172
- return { parentIssueNumber, taskId };
173
- }
174
- async function handleCycleStart(args, deps) {
175
- const cycleId = args.cycle_id;
176
- const criterionId = args.criterion_id;
177
- const testPaths = args.test_paths;
178
- const testSelector = args.test_selector;
179
- if (!cycleId || !criterionId || !testSelector || !Array.isArray(testPaths) || testPaths.length === 0) {
180
- return { ok: false, error: { code: "POLICY_INVALID", message: "cycle-start requires cycle_id, criterion_id, test_paths, test_selector" } };
181
- }
182
- const refs = requireRefs(args);
183
- if (!refs)
184
- return { ok: false, error: { code: "POLICY_INVALID", message: "parent_issue_number and task_id are required" } };
185
- const task = await resolveTaskContext(refs.parentIssueNumber, refs.taskId, deps.projectDir);
186
- const evidence = await restoreEvidence(task.issueNumber);
187
- // 基线:实现文件 digest 快照
188
- const implDigest = await computeImplDigest(task.worktreeDir, task.policy);
189
- const res = startCycle(evidence, cycleId, criterionId, testPaths, testSelector, implDigest, task.criteria, task.policy.testFilePatterns);
190
- if (!res.ok)
191
- return { ok: false, error: { code: res.error.code, message: res.error.message } };
192
- if (res.value.evidence !== evidence) {
193
- const persisted = await persistEvidence(task.issueNumber, res.value.evidence, {
194
- stage: "cycle-start",
195
- criterionId,
196
- cycleId,
197
- command: "cycle-start",
198
- testSelector,
199
- exitCode: null,
200
- failureKind: null,
201
- status: "started",
202
- workspaceDigest: implDigest,
203
- summary: `cycle ${cycleId} started (criterion ${criterionId})`,
204
- });
205
- if (!persisted.ok)
206
- return { ok: false, error: { code: "EVIDENCE_WRITE_FAILED", message: persisted.error ?? "failed to write evidence" } };
207
- }
208
- return {
209
- ok: true,
210
- cycle: { cycleId, criterionId, status: res.value.cycle.status },
211
- evidence: evidenceSummary(res.value.evidence),
212
- };
213
- }
214
- async function handleRed(args, deps) {
215
- const cycleId = args.cycle_id;
216
- const testSelector = args.test_selector;
217
- if (!cycleId || !testSelector) {
218
- return { ok: false, error: { code: "POLICY_INVALID", message: "red requires cycle_id and test_selector" } };
219
- }
220
- const refs = requireRefs(args);
221
- if (!refs)
222
- return { ok: false, error: { code: "POLICY_INVALID", message: "parent_issue_number and task_id are required" } };
223
- const task = await resolveTaskContext(refs.parentIssueNumber, refs.taskId, deps.projectDir);
224
- if (!task.policy.runner) {
225
- return { ok: false, error: { code: "RUNNER_UNSUPPORTED", message: "RED requires a configured test runner" } };
226
- }
227
- const evidence = await restoreEvidence(task.issueNumber);
228
- const cycle = evidence.cycles.find(c => c.cycleId === cycleId);
229
- if (!cycle) {
230
- return { ok: false, error: { code: "CYCLE_CONFLICT", message: `Cycle "${cycleId}" not found` } };
231
- }
232
- // 工具亲自执行
233
- const run = await executeRedCheck(task.policy.runner, testSelector, task.worktreeDir);
234
- const inputDigest = await computeExecutionInputDigest(task.worktreeDir, task.policy);
235
- const implDigest = await computeImplDigest(task.worktreeDir, task.policy);
236
- // 实现文件相对基线未变
237
- if (!digestEqual(implDigest, cycle.startWorkspaceDigest)) {
238
- return { ok: false, error: { code: "IMPL_CHANGED_IN_RED", message: "Implementation files changed since cycle baseline; RED must only add/modify tests" } };
239
- }
240
- // 测试输入未偷换(与上一次 RED attempt 一致)
241
- const lastRed = cycle.redAttempts[cycle.redAttempts.length - 1];
242
- if (lastRed && !digestEqual(inputDigest, lastRed.executionInputDigest)) {
243
- return { ok: false, error: { code: "INPUT_SWAPPED", message: "Execution input digest differs from the previous RED attempt" } };
244
- }
245
- const redEvidence = { ...run, executionInputDigest: inputDigest };
246
- const res = recordRed(evidence, cycleId, redEvidence);
247
- if (!res.ok)
248
- return { ok: false, error: { code: res.error.code, message: res.error.message } };
249
- if (res.value.evidence !== evidence) {
250
- const persisted = await persistEvidence(task.issueNumber, res.value.evidence, {
251
- stage: "red",
252
- criterionId: cycle.criterionId,
253
- cycleId,
254
- command: redEvidence.command,
255
- testSelector,
256
- exitCode: redEvidence.exitCode,
257
- failureKind: redEvidence.failureKind,
258
- status: "red",
259
- workspaceDigest: implDigest,
260
- summary: redEvidence.summary,
261
- });
262
- if (!persisted.ok)
263
- return { ok: false, error: { code: "EVIDENCE_WRITE_FAILED", message: persisted.error ?? "failed to write evidence" } };
264
- }
265
- return {
266
- ok: true,
267
- cycle: { cycleId, criterionId: cycle.criterionId, status: res.value.cycle.status },
268
- evidence: evidenceSummary(res.value.evidence),
269
- };
270
- }
271
- async function handleGreen(args, deps) {
272
- const cycleId = args.cycle_id;
273
- const testSelector = args.test_selector;
274
- if (!cycleId || !testSelector) {
275
- return { ok: false, error: { code: "POLICY_INVALID", message: "green requires cycle_id and test_selector" } };
276
- }
277
- const refs = requireRefs(args);
278
- if (!refs)
279
- return { ok: false, error: { code: "POLICY_INVALID", message: "parent_issue_number and task_id are required" } };
280
- const task = await resolveTaskContext(refs.parentIssueNumber, refs.taskId, deps.projectDir);
281
- if (!task.policy.runner) {
282
- return { ok: false, error: { code: "RUNNER_UNSUPPORTED", message: "GREEN requires a configured test runner" } };
283
- }
284
- const evidence = await restoreEvidence(task.issueNumber);
285
- const cycle = evidence.cycles.find(c => c.cycleId === cycleId);
286
- if (!cycle) {
287
- return { ok: false, error: { code: "CYCLE_CONFLICT", message: `Cycle "${cycleId}" not found` } };
288
- }
289
- // 工具亲自执行同一测试(GREEN 复用 executeRedCheck:它已封装 runner 调用 + 失败分类,
290
- // GREEN 仅需 exitCode 0 即通过;若失败则分类为 assertion 等,语义对 GREEN 同样适用)
291
- const run = await executeRedCheck(task.policy.runner, testSelector, task.worktreeDir);
292
- const inputDigest = await computeExecutionInputDigest(task.worktreeDir, task.policy);
293
- const implDigest = await computeImplDigest(task.worktreeDir, task.policy);
294
- const implChanged = !digestEqual(implDigest, cycle.startWorkspaceDigest);
295
- // 同一测试 selector(执行输入 digest 与 RED 一致,防偷换)
296
- if (cycle.redTestDigest && !digestEqual(inputDigest, cycle.redTestDigest)) {
297
- return { ok: false, error: { code: "SELECTOR_SWAPPED", message: "Execution input digest differs from RED; test selector/config must not change" } };
298
- }
299
- const greenEvidence = { ...run, executionInputDigest: inputDigest };
300
- const res = recordGreen(evidence, cycleId, greenEvidence, implChanged);
301
- if (!res.ok)
302
- return { ok: false, error: { code: res.error.code, message: res.error.message } };
303
- if (res.value.evidence !== evidence) {
304
- const persisted = await persistEvidence(task.issueNumber, res.value.evidence, {
305
- stage: "green",
306
- criterionId: cycle.criterionId,
307
- cycleId,
308
- command: greenEvidence.command,
309
- testSelector,
310
- exitCode: greenEvidence.exitCode,
311
- failureKind: greenEvidence.failureKind,
312
- status: "pass",
313
- workspaceDigest: implDigest,
314
- summary: greenEvidence.summary,
315
- });
316
- if (!persisted.ok)
317
- return { ok: false, error: { code: "EVIDENCE_WRITE_FAILED", message: persisted.error ?? "failed to write evidence" } };
318
- }
319
- return {
320
- ok: true,
321
- cycle: { cycleId, criterionId: cycle.criterionId, status: res.value.cycle.status },
322
- evidence: evidenceSummary(res.value.evidence),
323
- };
324
- }
325
- function buildRegressionRun(command, output) {
326
- const outputDigest = createHash("sha256").update(output.stdout + "\n" + output.stderr).digest("hex");
327
- const now = new Date().toISOString();
328
- return {
329
- command,
330
- testSelector: null,
331
- exitCode: output.exitCode,
332
- failureKind: output.timedOut ? "timeout" : null,
333
- testsCollected: output.testsCollected,
334
- testsFailed: output.testsFailed,
335
- startedAt: now,
336
- finishedAt: now,
337
- durationMs: 0,
338
- changedFiles: [],
339
- outputDigest: { algorithm: "sha256-output-v1", value: outputDigest },
340
- workspaceDigest: PLACEHOLDER,
341
- executionInputDigest: PLACEHOLDER,
342
- summary: output.exitCode === 0 ? "full regression passed" : `full regression failed (exit ${output.exitCode})`,
343
- };
344
- }
345
- async function handleFinalRegression(args, deps) {
346
- const refs = requireRefs(args);
347
- if (!refs)
348
- return { ok: false, error: { code: "POLICY_INVALID", message: "parent_issue_number and task_id are required" } };
349
- const task = await resolveTaskContext(refs.parentIssueNumber, refs.taskId, deps.projectDir);
350
- if (!task.policy.runner) {
351
- return { ok: false, error: { code: "RUNNER_UNSUPPORTED", message: "final-regression requires a configured test runner" } };
352
- }
353
- const baseArgs = task.policy.runner.baseCommand.trim().split(/\s+/).filter(Boolean);
354
- if (baseArgs.length === 0) {
355
- return { ok: false, error: { code: "REGRESSION_FAILED", message: "No test command configured for final regression" } };
356
- }
357
- // 工具亲自执行全量回归
358
- const output = await executeVitest(baseArgs, task.worktreeDir, task.policy.runner.timeoutMs);
359
- const run = buildRegressionRun(baseArgs.join(" "), output);
360
- const evidence = await restoreEvidence(task.issueNumber);
361
- const res = recordFinalRegression(evidence, "worktree", "worktree", [run]);
362
- if (!res.ok)
363
- return { ok: false, error: { code: res.error.code, message: res.error.message } };
364
- const status = res.value.regression.status === "pass" ? "pass" : "fail";
365
- const persisted = await persistEvidence(task.issueNumber, res.value, {
366
- stage: "final-regression",
367
- command: run.command,
368
- testSelector: null,
369
- exitCode: run.exitCode,
370
- failureKind: run.failureKind,
371
- status,
372
- workspaceDigest: null,
373
- summary: run.summary,
374
- });
375
- if (!persisted.ok)
376
- return { ok: false, error: { code: "EVIDENCE_WRITE_FAILED", message: persisted.error ?? "failed to write evidence" } };
377
- if (res.value.regression.status !== "pass") {
378
- return { ok: false, error: { code: "REGRESSION_FAILED", message: "Final regression must pass before submit" } };
379
- }
380
- return { ok: true, evidence: evidenceSummary(res.value) };
381
- }
382
- async function handleFinalVerification(args, deps) {
383
- const refs = requireRefs(args);
384
- if (!refs)
385
- return { ok: false, error: { code: "POLICY_INVALID", message: "parent_issue_number and task_id are required" } };
386
- const task = await resolveTaskContext(refs.parentIssueNumber, refs.taskId, deps.projectDir);
387
- const evidence = await restoreEvidence(task.issueNumber);
388
- // 对照 acceptance criteria 逐条核验:每个 tdd criterion 必须有 pass cycle
389
- const tddCriteria = task.criteria.filter(c => c.verification === "tdd");
390
- const uncovered = tddCriteria.filter(c => !evidence.cycles.some(cy => cy.criterionId === c.id && cy.status === "pass"));
391
- if (uncovered.length > 0) {
392
- return {
393
- ok: false,
394
- error: { code: "CRITERIA_NOT_COVERED", message: `Criteria without a passing cycle: ${uncovered.map(c => c.id).join(", ")}` },
395
- };
396
- }
397
- const res = recordFinalVerification(evidence, "worktree", "worktree", []);
398
- if (!res.ok)
399
- return { ok: false, error: { code: res.error.code, message: res.error.message } };
400
- const persisted = await persistEvidence(task.issueNumber, res.value, {
401
- stage: "final-verification",
402
- command: "final-verification",
403
- testSelector: null,
404
- exitCode: null,
405
- failureKind: null,
406
- status: "pass",
407
- workspaceDigest: null,
408
- summary: `verified ${tddCriteria.length} tdd criteria against passing cycles`,
409
- });
410
- if (!persisted.ok)
411
- return { ok: false, error: { code: "EVIDENCE_WRITE_FAILED", message: persisted.error ?? "failed to write evidence" } };
412
- return { ok: true, evidence: evidenceSummary(res.value) };
413
- }
414
- async function handleAbandonCycle(args, deps) {
415
- const cycleId = args.cycle_id;
416
- const reason = args.reason;
417
- if (!cycleId || !reason) {
418
- return { ok: false, error: { code: "POLICY_INVALID", message: "abandon-cycle requires cycle_id and reason" } };
419
- }
420
- const refs = requireRefs(args);
421
- if (!refs)
422
- return { ok: false, error: { code: "POLICY_INVALID", message: "parent_issue_number and task_id are required" } };
423
- const task = await resolveTaskContext(refs.parentIssueNumber, refs.taskId, deps.projectDir);
424
- const evidence = await restoreEvidence(task.issueNumber);
425
- const res = abandonCycle(evidence, cycleId, reason);
426
- if (!res.ok)
427
- return { ok: false, error: { code: res.error.code, message: res.error.message } };
428
- if (res.value !== evidence) {
429
- const persisted = await persistEvidence(task.issueNumber, res.value, {
430
- stage: "abandon-cycle",
431
- cycleId,
432
- command: "abandon-cycle",
433
- testSelector: null,
434
- exitCode: null,
435
- failureKind: null,
436
- status: "abandoned",
437
- workspaceDigest: null,
438
- summary: reason,
439
- });
440
- if (!persisted.ok)
441
- return { ok: false, error: { code: "EVIDENCE_WRITE_FAILED", message: persisted.error ?? "failed to write evidence" } };
442
- }
443
- return { ok: true, evidence: evidenceSummary(res.value) };
444
- }
445
- async function handleStatus(args, deps) {
446
- const refs = requireRefs(args);
447
- if (!refs)
448
- return { ok: false, error: { code: "POLICY_INVALID", message: "parent_issue_number and task_id are required" } };
449
- const task = await resolveTaskContext(refs.parentIssueNumber, refs.taskId, deps.projectDir);
450
- const evidence = await restoreEvidence(task.issueNumber);
451
- return { ok: true, evidence: evidenceSummary(evidence) };
452
- }
453
- async function handleWaiver(args, deps, kind) {
454
- if (kind === "exempt-request" && args.user_confirmed !== true) {
455
- return { ok: false, error: { code: "EXEMPT_REQUIRES_CONFIRMATION", message: "exempt-request requires user_confirmed: true" } };
456
- }
457
- const refs = requireRefs(args);
458
- if (!refs)
459
- return { ok: false, error: { code: "POLICY_INVALID", message: "parent_issue_number and task_id are required" } };
460
- const task = await resolveTaskContext(refs.parentIssueNumber, refs.taskId, deps.projectDir);
461
- if (kind === "not-applicable") {
462
- // 内核校验:worktree 相对 HEAD 的变更(剔除构建产物后)必须全部命中文档 pattern
463
- // (纯文档豁免,防代码变更绕过 TDD 门禁)
464
- const changed = filterGeneratedChanges(listWorktreeChanges(task.worktreeDir));
465
- if (!isDocumentationOnly(changed, DOCUMENTATION_PATTERNS)) {
466
- return {
467
- ok: false,
468
- error: {
469
- code: "WAIVER_DOCS_ONLY",
470
- message: `not-applicable requires a pure documentation change; non-doc files changed: ${changed.filter(f => !isDocumentationOnly([f], DOCUMENTATION_PATTERNS)).join(", ")}`,
471
- },
472
- };
473
- }
474
- }
475
- const evidence0 = await restoreEvidence(task.issueNumber);
476
- const reason = args.reason ?? (kind === "not-applicable" ? "pure documentation change" : "exempt request");
477
- const evidence = bumpEvidence(evidence0, {
478
- status: "waived",
479
- warnings: [...evidence0.warnings, `${kind}: ${reason}`],
480
- });
481
- const persisted = await persistEvidence(task.issueNumber, evidence, {
482
- stage: kind,
483
- command: kind,
484
- testSelector: null,
485
- exitCode: null,
486
- failureKind: null,
487
- status: "waived",
488
- workspaceDigest: null,
489
- summary: reason,
490
- });
491
- if (!persisted.ok)
492
- return { ok: false, error: { code: "EVIDENCE_WRITE_FAILED", message: persisted.error ?? "failed to write evidence" } };
493
- return { ok: true, evidence: evidenceSummary(evidence) };
494
- }
495
- // ─── 纯文档豁免判定(not-applicable 内核门禁) ───
496
- /** 文档类文件 pattern:markdown 系 + 通用文本 + docs 目录 */
497
- const DOCUMENTATION_PATTERNS = ["**/*.md", "**/*.markdown", "**/*.rst", "**/*.adoc", "**/*.txt", "docs/**", "README*"];
498
- /** 构建产物/依赖目录:not-applicable 判定变更时必须排除(git status 会列出未被 .gitignore 的产物) */
499
- const GENERATED_PATH_PATTERNS = [
500
- "node_modules/**",
501
- "dist/**",
502
- "coverage/**",
503
- ".next/**",
504
- "build/**",
505
- "target/**",
506
- "out/**",
507
- "**/*.tsbuildinfo",
508
- "**/*.log",
509
- ];
510
- /** 变更文件是否全部命中文档 pattern;空变更集也视为不通过(无变更不应豁免) */
511
- export function isDocumentationOnly(changedFiles, patterns) {
512
- if (changedFiles.length === 0)
513
- return false;
514
- return changedFiles.every(f => matchesAnyPattern(f, patterns));
515
- }
516
- /** 剔除构建产物类变更(git status 列出但不应参与豁免判定的文件) */
517
- export function filterGeneratedChanges(changedFiles) {
518
- return changedFiles.filter(f => !matchesAnyPattern(f, GENERATED_PATH_PATTERNS));
519
- }
520
- /** 解析 git status --porcelain 输出为变更文件路径(重命名取新路径,跳过冲突条目) */
521
- export function parsePorcelainChanges(raw) {
522
- const files = [];
523
- for (const line of raw.split("\n")) {
524
- if (line.length < 4)
525
- continue;
526
- const code = line.slice(0, 2);
527
- // 冲突条目(UU/AU/UD/AA/DD/UA/DU/!!)无确定性变更语义,跳过
528
- if (code[0] === "U" || code[1] === "U" || code.includes("!"))
529
- continue;
530
- let path = line.slice(3);
531
- const arrow = path.indexOf(" -> ");
532
- if (arrow !== -1)
533
- path = path.slice(arrow + 4);
534
- if (path !== "")
535
- files.push(path);
536
- }
537
- return files;
538
- }
539
- let tddGitExecutor = null;
540
- export function setTddCheckpointGitExecutor(fn) {
541
- tddGitExecutor = fn;
542
- }
543
- /** 列出 worktree 相对 HEAD 的变更文件(已跟踪修改/删除 + 未跟踪新增);git 失败返回空 */
544
- export function listWorktreeChanges(worktreeDir) {
545
- const run = tddGitExecutor ?? ((args, cwd) => {
546
- try {
547
- return execFileSync("git", ["-C", cwd, ...args], {
548
- encoding: "utf-8",
549
- env: { ...process.env, GIT_CONFIG_NOSYSTEM: "1" },
550
- maxBuffer: 10 * 1024 * 1024,
551
- });
552
- }
553
- catch {
554
- return "";
555
- }
556
- });
557
- const raw = run(["status", "--porcelain"], worktreeDir);
558
- return parsePorcelainChanges(raw);
559
- }
560
- // ─── 工具工厂 ───
561
- const OPS = [
562
- "cycle-start", "red", "green", "final-regression", "final-verification",
563
- "abandon-cycle", "status", "not-applicable", "exempt-request",
564
- ];
565
- export function createTddCheckpointTool(deps) {
566
- return tool({
567
- description: `Record Runtime TDD evidence for a running Task (spec §2.3 tdd_checkpoint, PRD R6).
568
-
569
- The tool executes tests itself — it never accepts inline evidence.
570
-
571
- Operations:
572
- - cycle-start: start a TDD cycle. Requires criterion_id, test_paths, test_selector. Records the implementation-file digest baseline.
573
- - red: run the focused test (must fail with assertion or missing-behavior), verify implementation files are unchanged since baseline and the execution input digest is stable, then record RED evidence.
574
- - green: run the same test (must pass), verify the execution input digest matches RED, then record GREEN evidence.
575
- - final-regression: run the full test suite (must pass).
576
- - final-verification: verify each tdd criterion has a passing cycle.
577
- - abandon-cycle: abandon a cycle with a reason.
578
- - status: read the current evidence summary.
579
- - not-applicable: pure-documentation waiver (primary only).
580
- - exempt-request: other waivers require user_confirmed.
581
-
582
- Caller: developer + primary.`,
583
- args: {
584
- op: tool.schema.enum(OPS).describe("TDD checkpoint operation"),
585
- parent_issue_number: tool.schema.number().optional().describe("Parent GitHub Issue number (Flow Record)"),
586
- task_id: tool.schema.string().optional().describe("Task Record title (Sub Issue title)"),
587
- cycle_id: tool.schema.string().optional().describe("Cycle id (for cycle-start/red/green/abandon-cycle)"),
588
- criterion_id: tool.schema.string().optional().describe("Acceptance criterion id (for cycle-start)"),
589
- test_paths: tool.schema.array(tool.schema.string()).optional().describe("Test file paths for the cycle"),
590
- test_selector: tool.schema.string().optional().describe("Focused test selector (file path)"),
591
- reason: tool.schema.string().optional().describe("Reason for abandon-cycle / waiver"),
592
- user_confirmed: tool.schema.boolean().optional().describe("Human approval (required for exempt-request)"),
593
- },
594
- async execute(args, ctx) {
595
- const op = args.op;
596
- // caller 门禁(矩阵单一来源,§2.2):developer + primary;not-applicable 仅 primary
597
- const denied = await requireToolCaller(ctx, "tdd_checkpoint", op, deps.sessionClient);
598
- if (denied)
599
- return errorResponse(CALLER_NOT_AUTHORIZED, denied);
600
- // 会话级项目目录优先(workspace root 异常会话下插件级 projectDir 可能是 `/`)
601
- const runtimeDeps = { ...deps, projectDir: resolveProjectDir(ctx.directory, deps.projectDir) };
602
- try {
603
- const resp = await dispatch(op, args, runtimeDeps);
604
- return resp.ok ? okResponse({ cycle: resp.cycle, evidence: resp.evidence }) : errorResponse(resp.error.code, resp.error.message);
605
- }
606
- catch (err) {
607
- return errorResponse("INTERNAL_ERROR", String(err));
608
- }
609
- },
610
- });
611
- }
612
- async function dispatch(op, args, deps) {
613
- switch (op) {
614
- case "cycle-start":
615
- return handleCycleStart(args, deps);
616
- case "red":
617
- return handleRed(args, deps);
618
- case "green":
619
- return handleGreen(args, deps);
620
- case "final-regression":
621
- return handleFinalRegression(args, deps);
622
- case "final-verification":
623
- return handleFinalVerification(args, deps);
624
- case "abandon-cycle":
625
- return handleAbandonCycle(args, deps);
626
- case "status":
627
- return handleStatus(args, deps);
628
- case "not-applicable":
629
- return handleWaiver(args, deps, "not-applicable");
630
- case "exempt-request":
631
- return handleWaiver(args, deps, "exempt-request");
632
- default:
633
- return { ok: false, error: { code: "POLICY_INVALID", message: `Unknown op: "${op}"` } };
634
- }
635
- }
636
- /** 独立注册辅助:把 tdd_checkpoint 挂到工具注册表(后续接线批次使用) */
637
- export function registerTddCheckpoint(registry, deps) {
638
- registry.tdd_checkpoint = createTddCheckpointTool(deps);
639
- }
640
- /**
641
- * TDD 硬门禁:evidence 必须由 kernel 亲自写入(受控 comment 存在且状态可解析),
642
- * 再经 evaluateTddCompliance 判定;runtime 下不完整 → 拒绝。
643
- */
644
- export async function checkTddSubmitGate(input) {
645
- const comment = await readTddEvidenceComment(input.issueNumber);
646
- if (!comment) {
647
- return { ok: false, code: "TDD_EVIDENCE_INCOMPLETE", message: "No TDD evidence comment found on the Task Record" };
648
- }
649
- const extracted = extractEvidenceBlock(comment.body);
650
- const evidence = extracted ? parseStateFromContent(extracted.content) : null;
651
- if (!evidence) {
652
- return { ok: false, code: "TDD_EVIDENCE_INCOMPLETE", message: "Evidence comment exists but the TDD state is missing or malformed" };
653
- }
654
- // 豁免语义:not-applicable / exempt-request 记录为 waived → 放行(文档/经批准豁免任务)
655
- if (evidence.status === "waived") {
656
- return { ok: true, status: "waived", warnings: ["TDD waived (not-applicable or user-approved exemption)"] };
657
- }
658
- const compliance = evaluateTddCompliance(input.policy, evidence, input.criteria);
659
- if (compliance.status !== "pass" && input.policy.enforcement === "runtime") {
660
- return { ok: false, code: "TDD_EVIDENCE_INCOMPLETE", message: compliance.warnings.join("; ") };
661
- }
662
- return { ok: true, status: compliance.status, warnings: compliance.warnings };
663
- }
664
- //# sourceMappingURL=tdd-checkpoint.js.map