@devcxl/opencode-cabbage 0.3.1 → 1.0.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 (177) hide show
  1. package/README.md +34 -19
  2. package/README.zh.md +34 -19
  3. package/assets/agents/dev-lifecycle.md +60 -41
  4. package/assets/agents/team/architect.md +36 -1
  5. package/assets/agents/team/developer.md +89 -0
  6. package/assets/agents/team/goal-verify.md +5 -1
  7. package/assets/agents/team/reviewer.md +42 -1
  8. package/assets/commands/code.md +3 -2
  9. package/assets/commands/design.md +3 -2
  10. package/assets/commands/release.md +3 -2
  11. package/assets/commands/requirements.md +3 -2
  12. package/assets/commands/review.md +3 -2
  13. package/assets/commands/setup.md +3 -2
  14. package/assets/commands/tasks.md +2 -1
  15. package/assets/prompts/bootstrap.md +9 -8
  16. package/assets/prompts/stage-contract.md +55 -0
  17. package/assets/skills/flow-code/SKILL.md +38 -140
  18. package/assets/skills/flow-design/SKILL.md +33 -74
  19. package/assets/skills/flow-release/SKILL.md +31 -51
  20. package/assets/skills/flow-requirements/SKILL.md +47 -45
  21. package/assets/skills/flow-review/SKILL.md +26 -112
  22. package/assets/skills/flow-setup/SKILL.md +40 -43
  23. package/assets/skills/flow-tasks/SKILL.md +30 -139
  24. package/assets/skills/flow-tdd/SKILL.md +30 -31
  25. package/dist/kernel/caller.d.ts +48 -0
  26. package/dist/kernel/caller.js +88 -0
  27. package/dist/kernel/caller.js.map +1 -0
  28. package/dist/kernel/context.d.ts +27 -0
  29. package/dist/kernel/context.js +73 -0
  30. package/dist/kernel/context.js.map +1 -0
  31. package/dist/kernel/flow.d.ts +166 -0
  32. package/dist/kernel/flow.js +387 -0
  33. package/dist/kernel/flow.js.map +1 -0
  34. package/dist/kernel/legacy.d.ts +19 -0
  35. package/dist/kernel/legacy.js +34 -0
  36. package/dist/kernel/legacy.js.map +1 -0
  37. package/dist/kernel/mutex.d.ts +8 -0
  38. package/dist/kernel/mutex.js +23 -0
  39. package/dist/kernel/mutex.js.map +1 -0
  40. package/dist/kernel/permission.d.ts +21 -0
  41. package/dist/kernel/permission.js +37 -0
  42. package/dist/kernel/permission.js.map +1 -0
  43. package/dist/kernel/profile.d.ts +35 -0
  44. package/dist/kernel/profile.js +144 -0
  45. package/dist/kernel/profile.js.map +1 -0
  46. package/dist/kernel/records.d.ts +87 -0
  47. package/dist/kernel/records.js +197 -0
  48. package/dist/kernel/records.js.map +1 -0
  49. package/dist/kernel/release.d.ts +104 -0
  50. package/dist/kernel/release.js +229 -0
  51. package/dist/kernel/release.js.map +1 -0
  52. package/dist/kernel/review.d.ts +34 -0
  53. package/dist/kernel/review.js +81 -0
  54. package/dist/kernel/review.js.map +1 -0
  55. package/dist/kernel/session-index.d.ts +43 -0
  56. package/dist/kernel/session-index.js +120 -0
  57. package/dist/kernel/session-index.js.map +1 -0
  58. package/dist/kernel/setup.d.ts +70 -0
  59. package/dist/kernel/setup.js +324 -0
  60. package/dist/kernel/setup.js.map +1 -0
  61. package/dist/kernel/slug.d.ts +23 -0
  62. package/dist/kernel/slug.js +67 -0
  63. package/dist/kernel/slug.js.map +1 -0
  64. package/dist/kernel/task.d.ts +208 -0
  65. package/dist/kernel/task.js +788 -0
  66. package/dist/kernel/task.js.map +1 -0
  67. package/dist/{flowrun → kernel/tdd}/adapter.d.ts +1 -1
  68. package/dist/kernel/tdd/adapter.js.map +1 -0
  69. package/dist/{flowrun → kernel/tdd}/digest.d.ts +1 -1
  70. package/dist/kernel/tdd/digest.js.map +1 -0
  71. package/dist/{flowrun → kernel/tdd}/evaluator.d.ts +1 -1
  72. package/dist/kernel/tdd/evaluator.js.map +1 -0
  73. package/dist/kernel/tdd/evidence.d.ts +42 -0
  74. package/dist/kernel/tdd/evidence.js +48 -0
  75. package/dist/kernel/tdd/evidence.js.map +1 -0
  76. package/dist/{flowrun → kernel/tdd}/state.d.ts +1 -1
  77. package/dist/kernel/tdd/state.js.map +1 -0
  78. package/dist/kernel/types.d.ts +180 -0
  79. package/dist/kernel/types.js +2 -0
  80. package/dist/kernel/types.js.map +1 -0
  81. package/dist/kernel/worktree.d.ts +45 -0
  82. package/dist/kernel/worktree.js +127 -0
  83. package/dist/kernel/worktree.js.map +1 -0
  84. package/dist/plugin/agents.d.ts +5 -17
  85. package/dist/plugin/agents.js +0 -23
  86. package/dist/plugin/agents.js.map +1 -1
  87. package/dist/plugin/flow-control.d.ts +80 -0
  88. package/dist/plugin/flow-control.js +254 -0
  89. package/dist/plugin/flow-control.js.map +1 -0
  90. package/dist/plugin/goal.d.ts +7 -46
  91. package/dist/plugin/goal.js +32 -116
  92. package/dist/plugin/goal.js.map +1 -1
  93. package/dist/plugin/prompt-lint.js +78 -5
  94. package/dist/plugin/prompt-lint.js.map +1 -1
  95. package/dist/plugin/release-control.d.ts +37 -0
  96. package/dist/plugin/release-control.js +304 -0
  97. package/dist/plugin/release-control.js.map +1 -0
  98. package/dist/plugin/server.d.ts +19 -0
  99. package/dist/plugin/server.js +147 -302
  100. package/dist/plugin/server.js.map +1 -1
  101. package/dist/plugin/setup-control.d.ts +45 -0
  102. package/dist/plugin/setup-control.js +80 -0
  103. package/dist/plugin/setup-control.js.map +1 -0
  104. package/dist/plugin/shell.d.ts +7 -34
  105. package/dist/plugin/shell.js +12 -137
  106. package/dist/plugin/shell.js.map +1 -1
  107. package/dist/plugin/skills.d.ts +1 -1
  108. package/dist/plugin/skills.js +16 -17
  109. package/dist/plugin/skills.js.map +1 -1
  110. package/dist/plugin/task-control.d.ts +90 -0
  111. package/dist/plugin/task-control.js +201 -0
  112. package/dist/plugin/task-control.js.map +1 -0
  113. package/dist/plugin/tdd-checkpoint.d.ts +118 -0
  114. package/dist/plugin/tdd-checkpoint.js +581 -0
  115. package/dist/plugin/tdd-checkpoint.js.map +1 -0
  116. package/dist/util/paths.d.ts +0 -2
  117. package/dist/util/paths.js +1 -7
  118. package/dist/util/paths.js.map +1 -1
  119. package/package.json +1 -1
  120. package/assets/agents/team/backend.md +0 -66
  121. package/assets/agents/team/frontend.md +0 -63
  122. package/assets/commands/handoff.md +0 -4
  123. package/assets/commands/test.md +0 -4
  124. package/assets/context/CONTEXT.md +0 -40
  125. package/assets/skills/flow-handoff/SKILL.md +0 -76
  126. package/assets/skills/flow-requirements/references/interview-guide.md +0 -35
  127. package/assets/skills/flow-test/SKILL.md +0 -75
  128. package/dist/flowrun/adapter.js.map +0 -1
  129. package/dist/flowrun/audit.d.ts +0 -5
  130. package/dist/flowrun/audit.js +0 -67
  131. package/dist/flowrun/audit.js.map +0 -1
  132. package/dist/flowrun/ci.d.ts +0 -64
  133. package/dist/flowrun/ci.js +0 -184
  134. package/dist/flowrun/ci.js.map +0 -1
  135. package/dist/flowrun/coverage.d.ts +0 -70
  136. package/dist/flowrun/coverage.js +0 -207
  137. package/dist/flowrun/coverage.js.map +0 -1
  138. package/dist/flowrun/digest.js.map +0 -1
  139. package/dist/flowrun/evaluator.js.map +0 -1
  140. package/dist/flowrun/gate.d.ts +0 -28
  141. package/dist/flowrun/gate.js +0 -139
  142. package/dist/flowrun/gate.js.map +0 -1
  143. package/dist/flowrun/github.d.ts +0 -24
  144. package/dist/flowrun/github.js +0 -154
  145. package/dist/flowrun/github.js.map +0 -1
  146. package/dist/flowrun/index.d.ts +0 -14
  147. package/dist/flowrun/index.js +0 -15
  148. package/dist/flowrun/index.js.map +0 -1
  149. package/dist/flowrun/merge.d.ts +0 -57
  150. package/dist/flowrun/merge.js +0 -180
  151. package/dist/flowrun/merge.js.map +0 -1
  152. package/dist/flowrun/migration.d.ts +0 -11
  153. package/dist/flowrun/migration.js +0 -276
  154. package/dist/flowrun/migration.js.map +0 -1
  155. package/dist/flowrun/resilience.d.ts +0 -18
  156. package/dist/flowrun/resilience.js +0 -80
  157. package/dist/flowrun/resilience.js.map +0 -1
  158. package/dist/flowrun/state.js.map +0 -1
  159. package/dist/flowrun/transitions.d.ts +0 -66
  160. package/dist/flowrun/transitions.js +0 -168
  161. package/dist/flowrun/transitions.js.map +0 -1
  162. package/dist/flowrun/types.d.ts +0 -356
  163. package/dist/flowrun/types.js +0 -21
  164. package/dist/flowrun/types.js.map +0 -1
  165. package/dist/flowrun/validator.d.ts +0 -19
  166. package/dist/flowrun/validator.js +0 -209
  167. package/dist/flowrun/validator.js.map +0 -1
  168. package/dist/plugin/broker.d.ts +0 -73
  169. package/dist/plugin/broker.js +0 -140
  170. package/dist/plugin/broker.js.map +0 -1
  171. package/dist/plugin/tdd-tool.d.ts +0 -62
  172. package/dist/plugin/tdd-tool.js +0 -336
  173. package/dist/plugin/tdd-tool.js.map +0 -1
  174. /package/dist/{flowrun → kernel/tdd}/adapter.js +0 -0
  175. /package/dist/{flowrun → kernel/tdd}/digest.js +0 -0
  176. /package/dist/{flowrun → kernel/tdd}/evaluator.js +0 -0
  177. /package/dist/{flowrun → kernel/tdd}/state.js +0 -0
@@ -0,0 +1,788 @@
1
+ /**
2
+ * Task 生命周期 kernel 逻辑(spec §2.3 task_control)。
3
+ * 八 op:create-task / start-task / submit-task / submit-review /
4
+ * merge-task / cancel-task / destroy-worktree / status-task。
5
+ * caller:primary 全量;developer 不允许调用 task_control(无门禁绕过面)。
6
+ * gh/git 调用走可注入 executor(测试用);records/worktree/merge 复用各自模块的 executor。
7
+ */
8
+ import { exec } from "node:child_process";
9
+ import { promisify } from "node:util";
10
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
11
+ import { join } from "node:path";
12
+ import { gh as ghCli } from "../util/gh.js";
13
+ import { escapeShellArg } from "../util/shell.js";
14
+ import { pathExists } from "../util/fs.js";
15
+ import { PLUGIN_ID } from "../util/paths.js";
16
+ import { validateTitle, branchFor, worktreeFor } from "./slug.js";
17
+ import { readProjectProfile } from "./profile.js";
18
+ import { getCompletedStages } from "./flow.js";
19
+ import { worktreeStart, destroyWorktree } from "./worktree.js";
20
+ import { createTaskRecord, readFlowRecord, readTddEvidenceComment, extractEvidenceBlock } from "./records.js";
21
+ import { captureWorkspaceBaseline } from "./tdd/evidence.js";
22
+ import { evaluateTddCompliance } from "./tdd/evaluator.js";
23
+ // merge 门禁复用 kernel/review.ts 的既有实现(spec §4.1:mergeTaskPR / checkBranchProtection)
24
+ import { mergeTaskPR, checkBranchProtection } from "./review.js";
25
+ const execAsync = promisify(exec);
26
+ // ─── 常量 ───
27
+ /** Task Record 状态标签前缀:cabbage:task:<status> */
28
+ export const TASK_STATUS_LABEL_PREFIX = "cabbage:task:";
29
+ /** 默认最大并行 Task 数(PRD R7,Profile 可覆盖但内核按此判定) */
30
+ export const MAX_PARALLEL_TASKS = 5;
31
+ /** 内核内置风险 pattern(spec §12 #12:migrations/auth/permissions/release workflow/密钥/公共 API),与 Profile riskPatterns 合并 */
32
+ export const BUILTIN_RISK_PATTERNS = [
33
+ "**/migrations/**",
34
+ "**/auth/**",
35
+ "**/permissions/**",
36
+ ".github/workflows/**",
37
+ "**/.env*",
38
+ "**/*.pem",
39
+ "**/*.key",
40
+ "src/**/api/**",
41
+ ];
42
+ let taskGhExecutor = null;
43
+ let taskGitExecutor = null;
44
+ export function setTaskGhExecutor(fn) {
45
+ taskGhExecutor = fn;
46
+ }
47
+ export function setTaskGitExecutor(fn) {
48
+ taskGitExecutor = fn;
49
+ }
50
+ export async function taskGh(args, timeout) {
51
+ if (taskGhExecutor)
52
+ return taskGhExecutor(args);
53
+ return ghCli(args, timeout);
54
+ }
55
+ export async function taskGit(args) {
56
+ if (taskGitExecutor)
57
+ return taskGitExecutor(args);
58
+ return execAsync(`git ${args}`);
59
+ }
60
+ /** 在指定目录内执行 git(worktree 内 push/status 等) */
61
+ async function gitIn(dir, args) {
62
+ return taskGit(`-C '${escapeShellArg(dir)}' ${args}`);
63
+ }
64
+ // ─── Task Record body 模板与解析(纯函数) ───
65
+ /**
66
+ * 新建 Task Record 的 body:acceptance criteria + 依赖 + 状态 checklist + Closes 约定。
67
+ * criteria 渲染为 `- [ ] <id>: <description> [<verification>]`,可被 parseCriteriaFromBody 还原。
68
+ */
69
+ export function buildTaskBody(slug, criteria, dependencies) {
70
+ const criteriaLines = criteria.length > 0
71
+ ? criteria.map(c => `- [ ] ${c.id}: ${c.description} [${c.verification}]`).join("\n")
72
+ : "- [ ] TBD";
73
+ const depLines = dependencies.length > 0 ? dependencies.map(n => `- #${n}`).join("\n") : "- none";
74
+ return [
75
+ `# Task: ${slug}`,
76
+ "",
77
+ "## Acceptance Criteria",
78
+ "",
79
+ criteriaLines,
80
+ "",
81
+ "## Dependencies",
82
+ "",
83
+ depLines,
84
+ "",
85
+ "## Status",
86
+ "",
87
+ "- [ ] running",
88
+ "- [ ] reviewing",
89
+ "- [ ] merged",
90
+ "",
91
+ "<!-- PR 约定:提交 PR 时 body 必须包含 `Closes #<issueNumber>` 以在合并时关闭本 Sub Issue -->",
92
+ "",
93
+ ].join("\n");
94
+ }
95
+ /** TDD 跨批协议:Task Record body 内 TDD 上下文块 marker(tdd_checkpoint 工具解析;spec §2.3/§4) */
96
+ export const TASK_TDD_TAG = "<!-- cabbage-task-tdd -->";
97
+ /** 构造 TDD 上下文块 JSON(policy + criteria + worktreeDir),供 tdd_checkpoint resolveTaskContext 解析 */
98
+ export function buildTaskTddBlock(policy, criteria, worktreePath) {
99
+ return `${TASK_TDD_TAG}\n${JSON.stringify({ schema: 1, policy, criteria, worktreeDir: worktreePath }, null, 2)}`;
100
+ }
101
+ /** 更新 Sub Issue body(保留既有内容,替换/追加 TDD 块) */
102
+ async function updateTaskBody(issueNumber, newBody) {
103
+ try {
104
+ const bodyArg = escapeShellArg(newBody);
105
+ await taskGh(`api repos/{owner}/{repo}/issues/${issueNumber} -X PATCH -f body=${bodyArg} --silent`);
106
+ return { ok: true };
107
+ }
108
+ catch (err) {
109
+ return { ok: false, error: String(err) };
110
+ }
111
+ }
112
+ /** 解析 create-task 传入的 acceptance_criteria JSON(字符串数组 → AcceptanceCriterion[]);非法返回 null */
113
+ export function parseAcceptanceCriteria(json) {
114
+ try {
115
+ const parsed = JSON.parse(json);
116
+ if (!Array.isArray(parsed))
117
+ return null;
118
+ const criteria = [];
119
+ for (const item of parsed) {
120
+ if (!item || typeof item !== "object")
121
+ return null;
122
+ const { id, description, verification } = item;
123
+ if (typeof id !== "string" ||
124
+ typeof description !== "string" ||
125
+ (verification !== "tdd" && verification !== "regression" && verification !== "manual")) {
126
+ return null;
127
+ }
128
+ criteria.push({ id, description, verification });
129
+ }
130
+ return criteria;
131
+ }
132
+ catch {
133
+ return null;
134
+ }
135
+ }
136
+ /** 从 Task Record body 还原 acceptance criteria(buildTaskBody 的逆操作) */
137
+ export function parseCriteriaFromBody(body) {
138
+ const criteria = [];
139
+ for (const line of body.split("\n")) {
140
+ const match = /^-\s+\[\s\]\s+(\S+):\s+(.+)\s+\[(\w+)\]$/.exec(line);
141
+ if (match && (match[3] === "tdd" || match[3] === "regression" || match[3] === "manual")) {
142
+ criteria.push({ id: match[1], description: match[2].trim(), verification: match[3] });
143
+ }
144
+ }
145
+ return criteria;
146
+ }
147
+ /** 从 Task Record body 的 Dependencies 区块解析依赖 Issue 号 */
148
+ export function parseDependenciesFromBody(body) {
149
+ const deps = [];
150
+ for (const line of body.split("\n")) {
151
+ const match = /^-\s+#(\d+)$/.exec(line.trim());
152
+ if (match)
153
+ deps.push(Number(match[1]));
154
+ }
155
+ return deps;
156
+ }
157
+ /** 解析 create-task 的 depends_on 参数:"12, 13" 或 "#12,#13" → [12, 13];空/非法输入返回 [] */
158
+ export function parseDependenciesArg(raw) {
159
+ if (!raw || raw.trim() === "")
160
+ return [];
161
+ const deps = [];
162
+ for (const part of raw.split(",")) {
163
+ const n = Number(part.trim().replace(/^#/, ""));
164
+ if (Number.isInteger(n) && n > 0)
165
+ deps.push(n);
166
+ }
167
+ return deps;
168
+ }
169
+ // ─── TDD policy 冻结(start-task) ───
170
+ /** 从 Project Profile 冻结 TDD policy:enforcement 固定 runtime(PRD R6 行为变更强制 Runtime TDD) */
171
+ export function freezeTddPolicy(profile) {
172
+ const runner = profile.testCommand
173
+ ? {
174
+ adapter: "vitest",
175
+ baseCommand: profile.testCommand,
176
+ timeoutMs: 120_000,
177
+ executionInputPatterns: [],
178
+ }
179
+ : null;
180
+ return {
181
+ mode: profile.tddDefaultMode ?? "strict",
182
+ enforcement: "runtime",
183
+ runner,
184
+ testFilePatterns: profile.testFilePatterns,
185
+ implementationFilePatterns: profile.implementationFilePatterns,
186
+ generatedArtifactPatterns: [],
187
+ exception: null,
188
+ source: { manifestPath: "AGENTS.md", revisionSha: "frozen-at-start-task" },
189
+ };
190
+ }
191
+ // ─── 风险双层判定(merge-task) ───
192
+ /** 轻量 glob 匹配(支持 **、*、?),风险 pattern 匹配专用 */
193
+ export function matchRiskPattern(filePath, pattern) {
194
+ const normalized = filePath.replace(/\\/g, "/");
195
+ let regexStr = "";
196
+ let i = 0;
197
+ while (i < pattern.length) {
198
+ if (pattern[i] === "*" && pattern[i + 1] === "*") {
199
+ i += 2;
200
+ if (i < pattern.length && pattern[i] === "/") {
201
+ regexStr += "(.*/)?";
202
+ i++;
203
+ }
204
+ else {
205
+ regexStr += ".*";
206
+ }
207
+ }
208
+ else if (pattern[i] === "*") {
209
+ regexStr += "[^/]*";
210
+ i++;
211
+ }
212
+ else if (pattern[i] === ".") {
213
+ regexStr += "\\.";
214
+ i++;
215
+ }
216
+ else if (pattern[i] === "?") {
217
+ regexStr += "[^/]";
218
+ i++;
219
+ }
220
+ else {
221
+ if ("+^$(){}[]|\\".includes(pattern[i]))
222
+ regexStr += "\\" + pattern[i];
223
+ else
224
+ regexStr += pattern[i];
225
+ i++;
226
+ }
227
+ }
228
+ return new RegExp(`^${regexStr}$`).test(normalized);
229
+ }
230
+ /**
231
+ * 风险双层判定:模型初判 + 内核按实际 diff 升级,只升不降(PRD R8 / spec §12 #12)。
232
+ * modelRisk 为 high → high;否则 diff 命中任一 risk pattern → high;否则 low。
233
+ */
234
+ export function escalateRisk(modelRisk, changedFiles, riskPatterns) {
235
+ if (modelRisk === "high")
236
+ return "high";
237
+ const hit = changedFiles.some(file => riskPatterns.some(pattern => matchRiskPattern(file, pattern)));
238
+ return hit ? "high" : "low";
239
+ }
240
+ /** 解析受控 evidence comment 为结构化 blocks;无 marker 或畸形返回 null */
241
+ export function parseEvidenceComment(body) {
242
+ const extracted = extractEvidenceBlock(body);
243
+ if (!extracted)
244
+ return null;
245
+ const blocks = [];
246
+ const sections = extracted.content.split(/^### stage: /m);
247
+ for (const section of sections) {
248
+ if (section.trim() === "")
249
+ continue;
250
+ const lines = section.split("\n");
251
+ const stage = lines[0].trim();
252
+ if (stage === "")
253
+ continue;
254
+ const fields = {};
255
+ for (const line of lines.slice(1)) {
256
+ const match = /^-\s+([a-zA-Z]+):\s*(.*)$/.exec(line.trim());
257
+ if (match)
258
+ fields[match[1]] = match[2].trim();
259
+ }
260
+ blocks.push({ stage, fields });
261
+ }
262
+ return blocks;
263
+ }
264
+ /** 把受控 comment 还原为 TddEvidence(供 evaluateTddCompliance 判定);无 marker 返回 null */
265
+ export function evidenceCommentToTddEvidence(comment) {
266
+ const blocks = parseEvidenceComment(comment.body);
267
+ if (!blocks)
268
+ return null;
269
+ const cycles = [];
270
+ let regression = { status: "pending", headSha: null, treeSha: null, reworkRevision: 0, runs: [] };
271
+ let verification = { status: "pending", headSha: null, treeSha: null, runs: [] };
272
+ let index = 0;
273
+ for (const block of blocks) {
274
+ const status = mapEvidenceStatus(block.fields.status);
275
+ if (block.stage === "red" || block.stage === "green") {
276
+ index++;
277
+ cycles.push({
278
+ cycleId: block.fields.cycle ?? `cycle-${index}`,
279
+ criterionId: block.fields.criterion ?? "c1",
280
+ reworkRevision: 0,
281
+ status: block.stage === "green" && status === "pass" ? "pass" : "failed",
282
+ startWorkspaceDigest: { algorithm: "sha256-content-v1", value: "unknown" },
283
+ testFiles: [],
284
+ redTestDigest: null,
285
+ redAttempts: [],
286
+ greenAttempts: [],
287
+ });
288
+ }
289
+ else if (block.stage === "final-regression") {
290
+ regression = { status, headSha: null, treeSha: null, reworkRevision: 0, runs: [] };
291
+ }
292
+ else if (block.stage === "final-verification") {
293
+ verification = { status, headSha: null, treeSha: null, runs: [] };
294
+ }
295
+ }
296
+ const overall = verification.status === "pass" && regression.status === "pass"
297
+ ? "pass"
298
+ : verification.status === "fail" || regression.status === "fail"
299
+ ? "fail"
300
+ : "in-progress";
301
+ return {
302
+ revision: comment.revision,
303
+ reworkRevision: 0,
304
+ status: overall,
305
+ taskStart: { status: "pass", headSha: null, treeSha: null, startedAt: null },
306
+ cycles,
307
+ regression,
308
+ verification,
309
+ alternativeValidation: [],
310
+ reworks: [],
311
+ warnings: [],
312
+ updatedAt: null,
313
+ };
314
+ }
315
+ function mapEvidenceStatus(value) {
316
+ if (value === "pass")
317
+ return "pass";
318
+ if (value === "fail")
319
+ return "fail";
320
+ return "pending";
321
+ }
322
+ export function taskStatePath(projectDir, slug) {
323
+ return join(projectDir, ".opencode", PLUGIN_ID, "task-state", `${slug}.json`);
324
+ }
325
+ export async function writeTaskState(projectDir, state) {
326
+ const target = taskStatePath(projectDir, state.slug);
327
+ const tmp = `${target}.tmp`;
328
+ await mkdir(join(projectDir, ".opencode", PLUGIN_ID, "task-state"), { recursive: true });
329
+ await writeFile(tmp, JSON.stringify(state, null, 2), "utf8");
330
+ await rename(tmp, target);
331
+ }
332
+ export async function readTaskState(projectDir, slug) {
333
+ try {
334
+ return JSON.parse(await readFile(taskStatePath(projectDir, slug), "utf8"));
335
+ }
336
+ catch {
337
+ return null;
338
+ }
339
+ }
340
+ // ─── Task Record 解析(Sub Issue → issueNumber / body) ───
341
+ /** 按 title(task slug)解析 Parent 下的 Sub Issue 号 */
342
+ async function resolveTaskIssue(parentIssueNumber, taskId) {
343
+ const { stdout } = await taskGh(`issue list --parent ${parentIssueNumber} --state all --json number,title --jq '[.[] | select(.title == "${taskId}") | .number] | first'`);
344
+ const trimmed = stdout.trim();
345
+ if (trimmed === "" || trimmed === "null")
346
+ return null;
347
+ const n = Number(trimmed);
348
+ return Number.isInteger(n) ? n : null;
349
+ }
350
+ /** 读 Task Record body(供依赖解析与 criteria 解析) */
351
+ async function readTaskBody(issueNumber) {
352
+ const { stdout } = await taskGh(`issue view ${issueNumber} --json body --jq .body`);
353
+ return stdout;
354
+ }
355
+ /** 设置 Task Record 状态标签(移除其它 cabbage:task:*,添加当前) */
356
+ export async function setTaskStatusLabel(issueNumber, status) {
357
+ try {
358
+ const { stdout } = await taskGh(`issue view ${issueNumber} --json labels --jq '[.labels[].name] | join(" ") | tostring'`);
359
+ const current = stdout.trim() === "" ? [] : stdout.trim().split(" ");
360
+ const target = `${TASK_STATUS_LABEL_PREFIX}${status}`;
361
+ const stale = current.filter(l => l.startsWith(TASK_STATUS_LABEL_PREFIX) && l !== target);
362
+ const args = [];
363
+ for (const label of stale)
364
+ args.push(`--remove-label '${label}'`);
365
+ args.push(`--add-label '${target}'`);
366
+ await taskGh(`issue edit ${issueNumber} ${args.join(" ")}`);
367
+ return { ok: true };
368
+ }
369
+ catch (err) {
370
+ return { ok: false, error: String(err) };
371
+ }
372
+ }
373
+ /** 读仓库默认分支 */
374
+ async function readDefaultBranch() {
375
+ const { stdout } = await taskGh(`repo view --json defaultBranchRef --jq .defaultBranchRef.name`);
376
+ return stdout.trim();
377
+ }
378
+ /**
379
+ * create-task(spec §2.3):validateTitle → slug → 未完成同名冲突检查(branch/worktree 已存在)→
380
+ * createTaskRecord(Sub Issue,body 含 acceptance criteria、依赖、Closes 约定)关联 Parent。
381
+ */
382
+ export async function createTask(input) {
383
+ const validated = validateTitle(input.title);
384
+ if (!validated.ok) {
385
+ return { ok: false, code: validated.code, message: validated.message };
386
+ }
387
+ const slug = validated.slug;
388
+ const worktreeDir = join(input.projectDir, worktreeFor(slug));
389
+ if (await pathExists(worktreeDir)) {
390
+ return { ok: false, code: "TASK_SLUG_CONFLICT", message: `worktree ${worktreeDir} already exists; choose a different title` };
391
+ }
392
+ if (await branchExists(branchFor(slug, "feat"))) {
393
+ return { ok: false, code: "TASK_SLUG_CONFLICT", message: `branch ${branchFor(slug, "feat")} already exists; choose a different title` };
394
+ }
395
+ let criteria = [];
396
+ if (input.acceptanceCriteriaJson !== undefined) {
397
+ const parsed = parseAcceptanceCriteria(input.acceptanceCriteriaJson);
398
+ if (parsed === null) {
399
+ return { ok: false, code: "POLICY_INVALID", message: "acceptance_criteria must be a JSON array of {id, description, verification}" };
400
+ }
401
+ criteria = parsed;
402
+ }
403
+ const body = buildTaskBody(slug, criteria, input.dependencies ?? []);
404
+ const created = await createTaskRecord({ title: slug, body, parentIssueNumber: input.parentIssueNumber });
405
+ if (!created.ok) {
406
+ return { ok: false, code: "ISSUE_CREATE_FAILED", message: created.error };
407
+ }
408
+ return { ok: true, ref: created.ref };
409
+ }
410
+ /** 本地分支是否已存在(rev-parse 成功且有输出) */
411
+ async function branchExists(branch) {
412
+ try {
413
+ const { stdout } = await taskGit(`rev-parse --verify refs/heads/${branch}`);
414
+ return stdout.trim() !== "";
415
+ }
416
+ catch {
417
+ return false;
418
+ }
419
+ }
420
+ /**
421
+ * start-task(spec §2.3):前置门禁(Planning Baseline 已合并、依赖 Task 已 merged、并行数 <5)→
422
+ * worktree-start → captureWorkspaceBaseline → freezeTddPolicy → 写 Task Record 状态。
423
+ */
424
+ export async function startTask(projectDir, parentIssueNumber, taskId) {
425
+ const issueNumber = await resolveTaskIssue(parentIssueNumber, taskId);
426
+ if (issueNumber === null) {
427
+ return { ok: false, code: "TASK_NOT_FOUND", message: `no sub issue with title "${taskId}" under parent #${parentIssueNumber}` };
428
+ }
429
+ const taskBody = await readTaskBody(issueNumber);
430
+ const profile = await readProjectProfile(projectDir);
431
+ // 门禁 1:Planning Baseline 已合并(design 阶段完成 = Planning PR 已合入)
432
+ const record = await readFlowRecord(parentIssueNumber);
433
+ if (!record.ok || !getCompletedStages(record.body).includes("design")) {
434
+ return {
435
+ ok: false,
436
+ code: "PLANNING_BASELINE_NOT_MERGED",
437
+ message: "Planning Baseline (design stage) must be merged before starting tasks",
438
+ };
439
+ }
440
+ // 门禁 2:依赖 Task 已 merged(依赖 Issue state 必须为 CLOSED)
441
+ const deps = parseDependenciesFromBody(taskBody);
442
+ for (const dep of deps) {
443
+ const state = await readIssueState(dep);
444
+ if (state !== "CLOSED") {
445
+ return { ok: false, code: "DEPENDENCY_NOT_MERGED", message: `dependency task #${dep} is not merged (state: ${state})` };
446
+ }
447
+ }
448
+ // 门禁 3:并行数 < 5(running/reviewing 标签计数)
449
+ const running = await countRunningTasks(parentIssueNumber);
450
+ if (running >= MAX_PARALLEL_TASKS) {
451
+ return {
452
+ ok: false,
453
+ code: "PARALLEL_LIMIT_REACHED",
454
+ message: `parallel task limit (${MAX_PARALLEL_TASKS}) reached (${running} running/reviewing)`,
455
+ };
456
+ }
457
+ const policy = freezeTddPolicy(profile);
458
+ const base = await readDefaultBranch();
459
+ const wt = await worktreeStart({ projectDir, slug: taskId, base });
460
+ if (!wt.ok) {
461
+ return { ok: false, code: wt.code, message: wt.message };
462
+ }
463
+ const baseline = await captureWorkspaceBaseline(wt.path, {
464
+ testFilePatterns: policy.testFilePatterns,
465
+ implementationFilePatterns: policy.implementationFilePatterns,
466
+ generatedArtifactPatterns: policy.generatedArtifactPatterns,
467
+ });
468
+ // 跨批协议:将 policy/criteria/worktree 写入 Task Record body 的 cabbage-task-tdd 块(tdd_checkpoint 解析)
469
+ const criteria = parseCriteriaFromBody(taskBody);
470
+ const tddBlock = buildTaskTddBlock(policy, criteria, wt.path);
471
+ const updatedBody = taskBody.includes(TASK_TDD_TAG)
472
+ ? taskBody
473
+ : `${taskBody}\n${tddBlock}\n`;
474
+ const bodyUpdate = await updateTaskBody(issueNumber, updatedBody);
475
+ if (!bodyUpdate.ok) {
476
+ return { ok: false, code: "TASK_TDD_BLOCK_WRITE_FAILED", message: bodyUpdate.error ?? "failed to write TDD block" };
477
+ }
478
+ await writeTaskState(projectDir, {
479
+ slug: taskId,
480
+ parentIssueNumber,
481
+ issueNumber,
482
+ branch: wt.branch,
483
+ worktreePath: wt.path,
484
+ policy,
485
+ baseline,
486
+ startedAt: new Date().toISOString(),
487
+ });
488
+ const label = await setTaskStatusLabel(issueNumber, "running");
489
+ if (!label.ok) {
490
+ return { ok: false, code: "LABEL_UPDATE_FAILED", message: label.error ?? "label update failed" };
491
+ }
492
+ return { ok: true, issueNumber, branch: wt.branch, worktreePath: wt.path, policy, baseline };
493
+ }
494
+ /** 读 Issue 状态(CLOSED/OPEN) */
495
+ async function readIssueState(issueNumber) {
496
+ const { stdout } = await taskGh(`issue view ${issueNumber} --json state --jq .state`);
497
+ return stdout.trim();
498
+ }
499
+ /** 统计 flow 下 running/reviewing 的 Task 数(并行门禁) */
500
+ async function countRunningTasks(parentIssueNumber) {
501
+ const { stdout } = await taskGh(`issue list --parent ${parentIssueNumber} --json number,labels --jq '[.[] | {number, labels: [.labels[].name]}]'`);
502
+ const issues = JSON.parse(stdout);
503
+ return issues.filter(i => i.labels.some(l => l === `${TASK_STATUS_LABEL_PREFIX}running` || l === `${TASK_STATUS_LABEL_PREFIX}reviewing`)).length;
504
+ }
505
+ /**
506
+ * submit-task(spec §4.4):TDD 硬门禁——evidence 不完整(无 comment、regression 未 pass、
507
+ * compliance 未 pass)→ 拒绝创建 PR;通过后 push 分支 → gh pr create(body 引用 Task Record +
508
+ * evidence comment 链接,含 Closes 约定)→ 标记 reviewing。
509
+ */
510
+ export async function submitTask(projectDir, parentIssueNumber, taskId) {
511
+ const issueNumber = await resolveTaskIssue(parentIssueNumber, taskId);
512
+ if (issueNumber === null) {
513
+ return { ok: false, code: "TASK_NOT_FOUND", message: `no sub issue with title "${taskId}" under parent #${parentIssueNumber}` };
514
+ }
515
+ const state = await readTaskState(projectDir, taskId);
516
+ if (!state) {
517
+ return { ok: false, code: "TASK_NOT_STARTED", message: `task "${taskId}" was not started (run start-task first)` };
518
+ }
519
+ const taskBody = await readTaskBody(issueNumber);
520
+ const criteria = parseCriteriaFromBody(taskBody);
521
+ // TDD 硬门禁(§4.4):evidence comment 存在且状态可解析;waived(豁免)放行
522
+ const comment = await readTddEvidenceComment(issueNumber);
523
+ if (!comment) {
524
+ return {
525
+ ok: false,
526
+ code: "TDD_EVIDENCE_INCOMPLETE",
527
+ message: "no TDD evidence comment found on the task record",
528
+ missing: ["evidence-comment"],
529
+ };
530
+ }
531
+ // 跨批协议(与 tdd_checkpoint 一致):先提取 evidence block 内容(不含 end marker),
532
+ // 再解析其中 JSON 状态块(cabbage-tdd-state,取最后一个);失败则回退旧 markdown 有损解析
533
+ const evidenceBlock = extractEvidenceBlock(comment.body);
534
+ const evidence = (evidenceBlock ? parseJsonStateEvidence(evidenceBlock.content) : null) ?? evidenceCommentToTddEvidence(comment);
535
+ if (!evidence || evidence.revision < 1) {
536
+ return {
537
+ ok: false,
538
+ code: "TDD_EVIDENCE_INCOMPLETE",
539
+ message: "evidence comment is malformed or has no revision",
540
+ missing: ["evidence-revision"],
541
+ };
542
+ }
543
+ // 豁免语义:not-applicable / exempt-request → waived → 放行
544
+ if (evidence.status === "waived") {
545
+ return submitPr(projectDir, state, issueNumber, comment, evidence.revision, evidence, taskId);
546
+ }
547
+ const compliance = evaluateTddCompliance(state.policy, evidence, criteria);
548
+ if (compliance.status !== "pass") {
549
+ return {
550
+ ok: false,
551
+ code: "TDD_EVIDENCE_INCOMPLETE",
552
+ message: `TDD compliance not met: ${compliance.warnings.join("; ")}`,
553
+ missing: compliance.warnings.length > 0 ? compliance.warnings : ["tdd-compliance"],
554
+ };
555
+ }
556
+ return submitPr(projectDir, state, issueNumber, comment, evidence.revision, evidence, taskId);
557
+ }
558
+ /** 解析 evidence comment 内 JSON 状态块(TDD_STATE_TAG,取最后一个),失败返回 null;schema 必须为 1 */
559
+ function parseJsonStateEvidence(body) {
560
+ const marker = "<!-- cabbage-tdd-state -->";
561
+ const idx = body.lastIndexOf(marker);
562
+ if (idx === -1)
563
+ return null;
564
+ try {
565
+ const parsed = JSON.parse(body.slice(idx + marker.length).trim());
566
+ if (parsed.schema !== 1 || !parsed?.evidence)
567
+ return null;
568
+ return { ...parsed.evidence, revision: parsed.evidence.revision ?? 1 };
569
+ }
570
+ catch {
571
+ return null;
572
+ }
573
+ }
574
+ /** 创建 PR 的公共流程(evidence 通过后) */
575
+ async function submitPr(projectDir, state, issueNumber, comment, revision, _evidence, taskId) {
576
+ // push 分支(工具内以宿主凭据执行)
577
+ try {
578
+ await gitIn(state.worktreePath, `push -u origin ${state.branch}`);
579
+ }
580
+ catch (err) {
581
+ return { ok: false, code: "PUSH_FAILED", message: String(err) };
582
+ }
583
+ // 创建 PR:body 引用 Task Record + evidence comment 链接 + Closes 约定
584
+ const base = await readDefaultBranch();
585
+ const { stdout: repoOut } = await taskGh("repo view --json nameWithOwner --jq .nameWithOwner");
586
+ const repo = repoOut.trim();
587
+ const evidenceLink = `https://github.com/${repo}/issues/${issueNumber}#issuecomment-${comment.id}`;
588
+ const prTitle = `feat: ${taskId}`;
589
+ const prBody = [`# ${taskId}`, "", `- Task Record: #${issueNumber}`, `- TDD evidence: ${evidenceLink}`, "", `Closes #${issueNumber}`, ""].join("\n");
590
+ const { stdout: prOut } = await taskGh(`pr create --title '${escapeShellArg(prTitle)}' --body '${escapeShellArg(prBody)}' --base ${base} --head ${state.branch} --json number --jq .number`);
591
+ const prNumber = Number(prOut.trim());
592
+ if (!Number.isInteger(prNumber)) {
593
+ return { ok: false, code: "PR_CREATE_FAILED", message: `invalid PR number: ${prOut}` };
594
+ }
595
+ const label = await setTaskStatusLabel(issueNumber, "reviewing");
596
+ if (!label.ok) {
597
+ return { ok: false, code: "LABEL_UPDATE_FAILED", message: label.error ?? "label update failed" };
598
+ }
599
+ return { ok: true, prNumber, evidenceRevision: revision };
600
+ }
601
+ /** submit-review(spec §2.3):primary 提交 reviewer 双轴审查结果(reviewer 本身只读) */
602
+ export async function submitReview(prNumber, verdict, comment) {
603
+ try {
604
+ if (verdict === "approve") {
605
+ await taskGh(`pr review ${prNumber} --approve`);
606
+ }
607
+ else {
608
+ const body = comment ? ` --body '${escapeShellArg(comment)}'` : "";
609
+ await taskGh(`pr review ${prNumber} --request-changes${body}`);
610
+ }
611
+ return { ok: true, prNumber, verdict };
612
+ }
613
+ catch (err) {
614
+ return { ok: false, code: "REVIEW_SUBMIT_FAILED", message: String(err) };
615
+ }
616
+ }
617
+ /**
618
+ * merge-task(spec §2.3 + PRD R8):CI checks 通过 + 分支保护 + 风险双层判定(只升不降)+
619
+ * 高风险需非作者人类 approval → mergeTaskPR(--match-head-commit)→ 关闭 Sub Issue →
620
+ * worktree 干净销毁。
621
+ */
622
+ export async function mergeTask(projectDir, parentIssueNumber, taskId, modelRisk) {
623
+ const issueNumber = await resolveTaskIssue(parentIssueNumber, taskId);
624
+ if (issueNumber === null) {
625
+ return { ok: false, code: "TASK_NOT_FOUND", message: `no sub issue with title "${taskId}" under parent #${parentIssueNumber}` };
626
+ }
627
+ const state = await readTaskState(projectDir, taskId);
628
+ if (!state) {
629
+ return { ok: false, code: "TASK_NOT_STARTED", message: `task "${taskId}" was not started (run start-task first)` };
630
+ }
631
+ // 1. 定位 open PR(head 分支)
632
+ const { stdout: prOut } = await taskGh(`pr list --head ${state.branch} --state open --json number,headRefOid,author --jq '[.[] | {number, headRefOid, author}] | first'`);
633
+ if (prOut.trim() === "" || prOut.trim() === "null") {
634
+ return { ok: false, code: "PR_NOT_FOUND", message: `no open PR for branch ${state.branch}` };
635
+ }
636
+ const pr = JSON.parse(prOut);
637
+ // 2. CI checks 全部 SUCCESS
638
+ const checks = await readPrChecks(pr.number);
639
+ const failing = checks.filter(c => c.state !== "SUCCESS");
640
+ if (failing.length > 0) {
641
+ return {
642
+ ok: false,
643
+ code: "CI_CHECKS_NOT_PASSED",
644
+ message: `CI checks not passed: ${failing.map(c => `${c.name}:${c.state}`).join(", ")}`,
645
+ };
646
+ }
647
+ // 3. 分支保护存在
648
+ const { stdout: repoOut } = await taskGh("repo view --json nameWithOwner --jq .nameWithOwner");
649
+ const [owner, repo] = repoOut.trim().split("/");
650
+ const protection = await checkBranchProtection(owner, repo);
651
+ if (!protection.exists) {
652
+ return { ok: false, code: "BRANCH_PROTECTION_REQUIRED", message: "branch protection is not enabled on the default branch" };
653
+ }
654
+ // 4. 风险双层判定:模型初判 + 内核按实际 diff 升级(只升不降)
655
+ const profile = await readProjectProfile(projectDir);
656
+ const changedFiles = await readPrChangedFiles(pr.number);
657
+ const risk = escalateRisk(modelRisk, changedFiles, [...BUILTIN_RISK_PATTERNS, ...profile.riskPatterns]);
658
+ // 5. 高风险需非作者人类 approval
659
+ if (risk === "high") {
660
+ const approved = await hasNonAuthorHumanApproval(pr.number, pr.author.login);
661
+ if (!approved) {
662
+ return {
663
+ ok: false,
664
+ code: "HIGH_RISK_APPROVAL_REQUIRED",
665
+ message: `high-risk PR requires a non-author human approval (author: ${pr.author.login})`,
666
+ };
667
+ }
668
+ }
669
+ // 6. merge(--match-head-commit 防偷换)
670
+ const merged = await mergeTaskPR(pr.number, pr.headRefOid);
671
+ if (!merged.success) {
672
+ return { ok: false, code: "MERGE_FAILED", message: merged.error ?? "merge failed" };
673
+ }
674
+ // 7. 关闭 Sub Issue + 状态 merged
675
+ await taskGh(`issue close ${issueNumber}`);
676
+ await setTaskStatusLabel(issueNumber, "merged");
677
+ // 8. worktree 干净销毁(PR 已合并且干净 → 自动);失败仅警告,不阻塞合并结果
678
+ const cleanup = await destroyWorktree({ worktreeDir: state.worktreePath, branch: state.branch, prMerged: true, userConfirmed: false });
679
+ if (!cleanup.ok && cleanup.reason === "DIRTY_WORKTREE") {
680
+ return { ok: true, prNumber: pr.number, risk, warning: "PR merged but worktree has uncommitted changes; clean it manually" };
681
+ }
682
+ return { ok: true, prNumber: pr.number, risk };
683
+ }
684
+ /** 读 PR 的 statusCheckRollup 精简列表(CheckRun 用 conclusion、StatusContext 用 state) */
685
+ async function readPrChecks(prNumber) {
686
+ const { stdout } = await taskGh(`pr view ${prNumber} --json statusCheckRollup --jq '[.[] | {name: (.name // .context // "check"), state: (.state // .conclusion // "PENDING")}]'`);
687
+ return JSON.parse(stdout);
688
+ }
689
+ /** 将原始 statusCheckRollup 映射为 {name, state}(纯函数,供测试直接验证 jq 语义) */
690
+ export function mapCheckRollup(raw) {
691
+ return raw.map(item => {
692
+ const obj = (item ?? {});
693
+ return {
694
+ name: String(obj.name ?? obj.context ?? "check"),
695
+ state: String(obj.state ?? obj.conclusion ?? "PENDING"),
696
+ };
697
+ });
698
+ }
699
+ /** 读 PR 变更文件路径列表(风险判定用) */
700
+ async function readPrChangedFiles(prNumber) {
701
+ const { stdout } = await taskGh(`pr view ${prNumber} --json files --jq '[.files[].path]'`);
702
+ return JSON.parse(stdout);
703
+ }
704
+ /** 是否存在非作者的人类 APPROVED review(高风险门禁) */
705
+ async function hasNonAuthorHumanApproval(prNumber, authorLogin) {
706
+ const { stdout } = await taskGh(`pr view ${prNumber} --json author,reviews --jq '{author: .author.login, reviews: [.reviews[] | {state: .state, login: .author.login, type: .author.type}]}'`);
707
+ const data = JSON.parse(stdout);
708
+ return data.reviews.some(r => r.state === "APPROVED" && r.login !== data.author && r.type === "User");
709
+ }
710
+ /** cancel-task:受控取消(user_confirmed)→ 关闭 Sub Issue + 状态标签 cancelled */
711
+ export async function cancelTask(projectDir, parentIssueNumber, taskId, userConfirmed) {
712
+ if (!userConfirmed) {
713
+ return {
714
+ ok: false,
715
+ code: "USER_CONFIRMATION_REQUIRED",
716
+ message: "Cancelling a task requires user confirmation (user_confirmed: true)",
717
+ };
718
+ }
719
+ const issueNumber = await resolveTaskIssue(parentIssueNumber, taskId);
720
+ if (issueNumber === null) {
721
+ return { ok: false, code: "TASK_NOT_FOUND", message: `no sub issue with title "${taskId}" under parent #${parentIssueNumber}` };
722
+ }
723
+ try {
724
+ await taskGh(`issue close ${issueNumber}`);
725
+ await setTaskStatusLabel(issueNumber, "cancelled");
726
+ return { ok: true, issueNumber };
727
+ }
728
+ catch (err) {
729
+ return { ok: false, code: "GITHUB_ERROR", message: String(err) };
730
+ }
731
+ }
732
+ /** destroy-worktree:preflight 销毁(§3.3),需 user_confirmed(除非 PR 已合并且干净) */
733
+ export async function destroyTaskWorktree(projectDir, parentIssueNumber, taskId, userConfirmed) {
734
+ const issueNumber = await resolveTaskIssue(parentIssueNumber, taskId);
735
+ if (issueNumber === null) {
736
+ return { ok: false, code: "TASK_NOT_FOUND", message: `no sub issue with title "${taskId}" under parent #${parentIssueNumber}` };
737
+ }
738
+ const state = await readTaskState(projectDir, taskId);
739
+ if (!state) {
740
+ return { ok: false, code: "TASK_NOT_STARTED", message: `task "${taskId}" was not started (run start-task first)` };
741
+ }
742
+ const prMerged = await isPrMerged(state.branch);
743
+ const result = await destroyWorktree({
744
+ worktreeDir: state.worktreePath,
745
+ branch: state.branch,
746
+ prMerged,
747
+ userConfirmed,
748
+ });
749
+ if (!result.ok) {
750
+ return { ok: false, code: result.reason, message: result.message };
751
+ }
752
+ return { ok: true, reason: result.reason };
753
+ }
754
+ /** 分支是否有已合并的 PR */
755
+ async function isPrMerged(branch) {
756
+ const { stdout } = await taskGh(`pr list --head ${branch} --state merged --json number --jq 'length'`);
757
+ return Number(stdout.trim()) > 0;
758
+ }
759
+ /** status-task:读 Task Record + PR + checks 汇总 */
760
+ export async function readTaskStatus(projectDir, parentIssueNumber, taskId) {
761
+ const issueNumber = await resolveTaskIssue(parentIssueNumber, taskId);
762
+ if (issueNumber === null) {
763
+ return { ok: false, code: "TASK_NOT_FOUND", message: `no sub issue with title "${taskId}" under parent #${parentIssueNumber}` };
764
+ }
765
+ try {
766
+ const { stdout: issueOut } = await taskGh(`issue view ${issueNumber} --json number,title,state,labels,body --jq '{number, title, state, labels: [.labels[].name], body}'`);
767
+ const issue = JSON.parse(issueOut);
768
+ const branch = `feat/${taskId}`;
769
+ const { stdout: prsOut } = await taskGh(`pr list --head ${branch} --state all --json number,state,headRefName --jq '[.[] | {number, state, headRefName}]'`);
770
+ const prs = JSON.parse(prsOut);
771
+ const pullRequests = [];
772
+ for (const pr of prs) {
773
+ const checks = await readPrChecks(pr.number);
774
+ pullRequests.push({ ...pr, checks });
775
+ }
776
+ return {
777
+ ok: true,
778
+ report: { task: { issueNumber, slug: taskId, state: issue.state, labels: issue.labels }, pullRequests },
779
+ };
780
+ }
781
+ catch (err) {
782
+ const message = String(err);
783
+ return message.includes("Could not resolve") || message.includes("not found")
784
+ ? { ok: false, code: "NOT_FOUND", message }
785
+ : { ok: false, code: "GITHUB_ERROR", message };
786
+ }
787
+ }
788
+ //# sourceMappingURL=task.js.map