aitable-workflow-cli 0.1.28 → 0.1.29-beta.1790736949347

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 (60) hide show
  1. package/dist/assistant/assets/index-BA6il19d.css +1 -0
  2. package/dist/assistant/assets/{index-B3LvYfRW.js → index-BMzAOp70.js} +31 -31
  3. package/dist/assistant/index.html +2 -2
  4. package/dist/chunk-BBP5RVFU.js +15 -0
  5. package/dist/cli.js +180 -73
  6. package/dist/{config-ui-server-4SVD42Z3.js → config-ui-server-UWELHUH4.js} +1 -1
  7. package/dist/web/assets/index-Bnmnydwk.css +1 -0
  8. package/dist/web/assets/index-Dbs1xWB9.js +331 -0
  9. package/dist/web/index.html +2 -2
  10. package/dist/web/install.ps1 +173 -0
  11. package/dist/web/install.sh +188 -0
  12. package/package.json +3 -3
  13. package/templates/ai-native-sdlc/.env.example +11 -0
  14. package/templates/ai-native-sdlc/aitable-workflow.config.yml +47 -0
  15. package/templates/ai-native-sdlc/coding-agents.config.json +17 -0
  16. package/templates/ai-native-sdlc/docs/plans/2026-09-15-ai-native-sdlc-dual-staff-implementation-plan.md +150 -0
  17. package/templates/ai-native-sdlc/docs/plans/2026-09-15-ai-native-sdlc-dual-staff.md +213 -0
  18. package/templates/ai-native-sdlc/docs/plans/2026-09-16-domain-role-notification-routing.md +36 -0
  19. package/templates/ai-native-sdlc/docs/plans/2026-09-16-execution-resume-and-cr-loop.md +29 -0
  20. package/templates/ai-native-sdlc/docs/plans/2026-09-16-project-handoff.md +148 -0
  21. package/templates/ai-native-sdlc/docs/plans/2026-09-17-prd-code-context-and-readable-prompts.md +37 -0
  22. package/templates/ai-native-sdlc/docs/plans/2026-09-21-plan-merge-sync-and-agent-source.md +19 -0
  23. package/templates/ai-native-sdlc/docs/plans/2026-09-21-sdlc-base-ref-and-review-notification.md +18 -0
  24. package/templates/ai-native-sdlc/docs/plans/2026-09-29-borrow-from-joe-design.md +943 -0
  25. package/templates/ai-native-sdlc/lib/artifacts.ts +126 -0
  26. package/templates/ai-native-sdlc/lib/recipe-config.ts +34 -0
  27. package/templates/ai-native-sdlc/lib/recipe-helpers.ts +595 -0
  28. package/templates/ai-native-sdlc/npmrc +1 -0
  29. package/templates/ai-native-sdlc/package.json +19 -0
  30. package/templates/ai-native-sdlc/profile/profile.yml +115 -0
  31. package/templates/ai-native-sdlc/setup/module-config.seed.json +37 -0
  32. package/templates/ai-native-sdlc/test/artifacts.test.ts +72 -0
  33. package/templates/ai-native-sdlc/test/recipe-helpers.test.ts +931 -0
  34. package/templates/ai-native-sdlc/tsconfig.json +17 -0
  35. package/templates/ai-native-sdlc/workflows/.gitkeep +3 -0
  36. package/templates/ai-native-sdlc/workflows/execution/automations/blocked-questions-dev.json +91 -0
  37. package/templates/ai-native-sdlc/workflows/execution/automations/blocked-questions.json +91 -0
  38. package/templates/ai-native-sdlc/workflows/execution/automations/cr-review-dev.json +80 -0
  39. package/templates/ai-native-sdlc/workflows/execution/automations/cr-review.json +81 -0
  40. package/templates/ai-native-sdlc/workflows/execution/automations/resume-answers.json +42 -0
  41. package/templates/ai-native-sdlc/workflows/execution/automations/spec-review-dev.json +138 -0
  42. package/templates/ai-native-sdlc/workflows/execution/recipes/coding.recipe.ts +161 -0
  43. package/templates/ai-native-sdlc/workflows/execution/recipes/prd-review.recipe.ts +200 -0
  44. package/templates/ai-native-sdlc/workflows/execution/recipes/prd.recipe.ts +163 -0
  45. package/templates/ai-native-sdlc/workflows/execution/recipes/review.recipe.ts +257 -0
  46. package/templates/ai-native-sdlc/workflows/execution/recipes/spec-review.recipe.ts +242 -0
  47. package/templates/ai-native-sdlc/workflows/execution/recipes/spec.recipe.ts +173 -0
  48. package/templates/ai-native-sdlc/workflows/execution/workflow.yml +79 -0
  49. package/templates/ai-native-sdlc/workflows/intake/automations/accept-intake.json +101 -0
  50. package/templates/ai-native-sdlc/workflows/intake/recipes/classify-module.recipe.ts +133 -0
  51. package/templates/ai-native-sdlc/workflows/intake/recipes/create-change.recipe.ts +94 -0
  52. package/templates/ai-native-sdlc/workflows/intake/recipes/mechanical-deduplicate.recipe.ts +33 -0
  53. package/templates/ai-native-sdlc/workflows/intake/workflow.yml +62 -0
  54. package/templates/ai-native-sdlc/workspaces/dev/AGENTS.md +69 -0
  55. package/templates/ai-native-sdlc/workspaces/pm/AGENTS.md +48 -0
  56. package/templates/ai-native-sdlc/workspaces/reviewer/AGENTS.md +79 -0
  57. package/dist/assistant/assets/index-CCEHOkkJ.css +0 -1
  58. package/dist/chunk-YDJTCQW4.js +0 -15
  59. package/dist/web/assets/index-DPc2WOdp.css +0 -1
  60. package/dist/web/assets/index-HTw8K38o.js +0 -332
@@ -0,0 +1,257 @@
1
+ import type { RecipeDefinition } from "aitable-workflow-core";
2
+ import { z } from "zod";
3
+ import { devStageConfig } from "../../../lib/recipe-config.js";
4
+ import { artifactName, artifactPath, resolveWorktreeDir } from "../../../lib/artifacts.js";
5
+ import {
6
+ agentQuestions,
7
+ appendHandledComments,
8
+ appendArtifact,
9
+ appendStageTransition,
10
+ blockExecution,
11
+ blocked,
12
+ changeId,
13
+ commentRepairNotice,
14
+ commitOptions,
15
+ consumeAnswersOrBlock,
16
+ crEntries,
17
+ failed,
18
+ handleCodingFailure,
19
+ isTestDemand,
20
+ isCurrentRecipe,
21
+ persistCodingSession,
22
+ questionsText,
23
+ repoPathFromUrl,
24
+ repositoryUrl,
25
+ requireRepositories,
26
+ sessionMap,
27
+ success,
28
+ testDemandInstruction,
29
+ unhandledComments,
30
+ waitForExternalProgress,
31
+ worktreeOptions,
32
+ MAX_REVIEW_ROUNDS,
33
+ parseReviewRound,
34
+ } from "../../../lib/recipe-helpers.js";
35
+
36
+ // review 是唯一需要反向闭环需求池的执行步骤,跨 workflow tracker 引用随本 recipe 就近声明。
37
+ const reviewStageConfig = devStageConfig.extend({
38
+ intake_tracker: z.object({
39
+ base_id: z.string().min(1),
40
+ table_id: z.string().min(1),
41
+ }),
42
+ });
43
+
44
+ const recipe: RecipeDefinition = {
45
+ meta: {
46
+ id: "review",
47
+ name: "评审",
48
+ role: "reviewer",
49
+ description: "Dev 跟踪既有 CR、处理评论,全部 CR 合并即完成变更",
50
+ version: "1.0.0",
51
+ requiresAgent: true,
52
+ config: reviewStageConfig,
53
+ },
54
+ async execute(ctx) {
55
+ const row = await ctx.toolkit.table.readCurrentRow();
56
+ const id = changeId(row);
57
+ const configuredRepos = requireRepositories(ctx, "dev_repositories");
58
+ const repos = isTestDemand(row) ? configuredRepos.slice(0, 1) : configuredRepos;
59
+ const intakeTarget = ctx.config.intake_tracker as
60
+ | { base_id?: string; table_id?: string }
61
+ | undefined;
62
+ if (!id || !repos.length)
63
+ return failed("评审阶段缺少变更编号或 Dev 候选仓库配置");
64
+ if (!intakeTarget?.base_id || !intakeTarget.table_id)
65
+ return failed("评审阶段缺少需求池 tracker 引用配置");
66
+ if (!isCurrentRecipe(row, ctx.stepId)) {
67
+ return blocked("当前步骤与执行 recipe 不一致,拒绝重复执行评审");
68
+ }
69
+ // 有界重试:超过最大轮次则升级人工
70
+ const currentRound = parseReviewRound(row);
71
+ if (currentRound > MAX_REVIEW_ROUNDS) {
72
+ return blocked(`评审修复已超过 ${MAX_REVIEW_ROUNDS} 轮,请人工介入`);
73
+ }
74
+ const resumed = await consumeAnswersOrBlock(ctx, row);
75
+ if (resumed.blocked) return resumed.blocked;
76
+ const existing = crEntries(row["当前CR链接"]);
77
+ if (!existing.length) return blocked("评审阶段缺少 coding 创建的代码 CR 链接");
78
+ if (existing.length) {
79
+ const unknownRepository = existing.find(
80
+ (entry) => !repos.some(
81
+ (repo) => repoPathFromUrl(repositoryUrl(repo)) === entry.repoUrl,
82
+ ),
83
+ );
84
+ if (unknownRepository) {
85
+ return failed(`评审 CR 不属于已配置的 Dev 仓库:${unknownRepository.repoUrl}`);
86
+ }
87
+ const checks = await Promise.all(
88
+ existing.map(async (entry) => {
89
+ const [comments, ready] = await Promise.all([
90
+ ctx.toolkit.codeReview.listUnresolvedComments(
91
+ entry.mrId,
92
+ repoPathFromUrl(entry.repoUrl),
93
+ ),
94
+ ctx.toolkit.codeReview.isReadyToMerge(
95
+ entry.mrId,
96
+ repoPathFromUrl(entry.repoUrl),
97
+ ),
98
+ ]);
99
+ return { entry, ready, comments };
100
+ }),
101
+ );
102
+ const repairTargets = checks.flatMap((check) => {
103
+ const comments = unhandledComments(
104
+ row["产出物"],
105
+ "review",
106
+ check.entry.mrId,
107
+ check.comments,
108
+ );
109
+ return comments.length ? [{ ...check, comments }] : [];
110
+ });
111
+ if (repairTargets.length) {
112
+ const repairRepos = repairTargets.map((target) => repos.find(
113
+ (repo) => repoPathFromUrl(repositoryUrl(repo)) === target.entry.repoUrl,
114
+ )!);
115
+ const repairWorktrees = await Promise.all(
116
+ repairRepos.map((repo) =>
117
+ ctx.toolkit.git.ensureWorktree(repo, id, id, worktreeOptions(repo)),
118
+ ),
119
+ );
120
+ const candidateWorktrees = repairWorktrees
121
+ .map((worktree, index) => `- ${repositoryUrl(repairRepos[index]!)}: ${resolveWorktreeDir(ctx.configDir, worktree.dir)}`)
122
+ .join("\n");
123
+ const repair = await ctx.toolkit.coding.run(
124
+ `## 候选业务 worktree
125
+ ${candidateWorktrees}
126
+ 公共父目录不是 Git 仓库;所有文件只能写入具体仓库 worktree。
127
+ 进入某仓库前先读取其根目录 AGENTS.md(若存在),并遵循其约定。
128
+
129
+ ${testDemandInstruction(row)}
130
+
131
+ ## 按仓库分组的未解决评论
132
+ \`\`\`json
133
+ ${JSON.stringify(repairTargets.map((target) => ({
134
+ repo: target.entry.repoUrl,
135
+ mrId: target.entry.mrId,
136
+ comments: target.comments,
137
+ })), null, 2)}
138
+ \`\`\`
139
+
140
+ ## 已消费的人工答复
141
+ ${resumed.answers}
142
+
143
+ ## 本轮任务
144
+ 逐项修复评论并运行真实测试。
145
+ 在评论所属仓库的 ${artifactPath("review", id)} 更新处理结果。`.trim(),
146
+ {
147
+ sessionId: sessionMap(row["Agent会话ID"])["review:dev"],
148
+ },
149
+ );
150
+ if (!repair.success) {
151
+ return handleCodingFailure(
152
+ ctx,
153
+ row,
154
+ "review",
155
+ "review:dev",
156
+ repair,
157
+ "CR 评论修复 Agent 未完成",
158
+ );
159
+ }
160
+ const agentSession = await persistCodingSession(ctx, row, "review:dev", repair);
161
+ const commits = await Promise.all(
162
+ repairWorktrees.map((worktree) =>
163
+ ctx.toolkit.git.commit(worktree.dir, `${id}: review fixes`, commitOptions(row)),
164
+ ),
165
+ );
166
+ let artifacts = appendArtifact(
167
+ row["产出物"],
168
+ `${artifactName("review")};评论修复commit=${commits.some((commit) => commit.committed) ? `${id}: review fixes` : "已存在"}`,
169
+ );
170
+ for (const target of repairTargets) {
171
+ artifacts = appendHandledComments(
172
+ artifacts,
173
+ "review",
174
+ target.entry.mrId,
175
+ target.comments,
176
+ );
177
+ }
178
+ const persisted = {
179
+ 产出物: artifacts,
180
+ Agent会话ID: agentSession,
181
+ 审查轮次: currentRound + 1,
182
+ };
183
+ const questions = agentQuestions(repair.response, "review");
184
+ if (questions.length) {
185
+ return blockExecution(
186
+ ctx,
187
+ "CR 评论修复仍有待确认问题",
188
+ "在人工答复中按问题编号逐项回答后自动续跑",
189
+ questionsText(questions),
190
+ persisted,
191
+ );
192
+ }
193
+ await Promise.all(
194
+ repairWorktrees.map((worktree) => ctx.toolkit.git.push(worktree.dir, id)),
195
+ );
196
+ return waitForExternalProgress(
197
+ ctx,
198
+ row,
199
+ "CR 评论已修复,等待复审",
200
+ "等待评审者关闭评论或提出新评论",
201
+ {
202
+ 评审进展: commentRepairNotice(repairTargets.map((target) => ({
203
+ repoUrl: target.entry.repoUrl,
204
+ mrUrl: target.entry.mrUrl,
205
+ commentCount: target.comments.length,
206
+ }))),
207
+ ...persisted,
208
+ },
209
+ );
210
+ }
211
+ const pending = checks.filter(
212
+ (check) => !check.ready || check.comments.length > 0,
213
+ );
214
+ if (pending.length) {
215
+ return waitForExternalProgress(
216
+ ctx,
217
+ row,
218
+ `仍有 ${pending.length} 个 CR 未可合并或存在未解决评论`,
219
+ "等待审核通过,或等待新评论被自动修复",
220
+ { 当前角色: "Dev" },
221
+ );
222
+ }
223
+ // 代码 CR 的合并由仓库流程负责;本工作流完成条件只包含评审通过和评论清零。
224
+ const demandTitle = String(row["需求"] ?? "").trim();
225
+ if (!demandTitle) return failed("变更流水缺少关联需求,无法完成需求闭环");
226
+ const demand = await ctx.toolkit.table.findRecordIn(
227
+ { baseId: intakeTarget.base_id, tableId: intakeTarget.table_id },
228
+ "需求标题",
229
+ demandTitle,
230
+ );
231
+ if (!demand)
232
+ return failed(`未在需求池找到关联需求:${demandTitle}`);
233
+ // 先完成需求池回写,避免执行记录进入 shipped 后因下游表写失败而失去重试机会。
234
+ await ctx.toolkit.table.updateRecordIn(
235
+ { baseId: intakeTarget.base_id, tableId: intakeTarget.table_id },
236
+ demand.recordId,
237
+ { 需求状态: "已完成" },
238
+ );
239
+ await ctx.toolkit.table.updateCurrentRow({
240
+ 当前阶段: "已发布",
241
+ 当前角色: "人",
242
+ 状态: "shipped",
243
+ 当前CR链接: "",
244
+ 评审进展: "",
245
+ 阻塞原因: "",
246
+ 解除条件: "",
247
+ 审计日志: appendStageTransition(
248
+ row["审计日志"],
249
+ id,
250
+ "已发布",
251
+ ),
252
+ });
253
+ return success({}, "全部代码 CR 已通过评审且无未解决评论,变更与关联需求均已完成");
254
+ }
255
+ },
256
+ };
257
+ export default recipe;
@@ -0,0 +1,242 @@
1
+ import type { RecipeDefinition } from "aitable-workflow-core";
2
+ import { devStageConfig } from "../../../lib/recipe-config.js";
3
+ import {
4
+ artifactName,
5
+ artifactPath,
6
+ resolveWorktreeDir,
7
+ stageBranchName,
8
+ } from "../../../lib/artifacts.js";
9
+ import {
10
+ agentQuestions,
11
+ appendArtifact,
12
+ appendHandledComments,
13
+ appendStageTransition,
14
+ blockExecution,
15
+ changeId,
16
+ commentRepairNotice,
17
+ commitOptions,
18
+ consumeAnswersOrBlock,
19
+ crEntries,
20
+ failed,
21
+ handleCodingFailure,
22
+ hasStageTransition,
23
+ isCurrentRecipe,
24
+ persistCodingSession,
25
+ questionsText,
26
+ repoPathFromUrl,
27
+ repositoryUrl,
28
+ requireRepositories,
29
+ sessionMap,
30
+ success,
31
+ testDemandInstruction,
32
+ unhandledComments,
33
+ waitForExternalProgress,
34
+ worktreeOptions,
35
+ MAX_REVIEW_ROUNDS,
36
+ parseReviewRound,
37
+ } from "../../../lib/recipe-helpers.js";
38
+
39
+ const recipe: RecipeDefinition = {
40
+ meta: {
41
+ id: "spec-review",
42
+ name: "技术方案评审",
43
+ role: "reviewer",
44
+ description: "Dev 跟踪技术方案 CR、处理评论,全部合并后进入编码",
45
+ version: "1.0.0",
46
+ requiresAgent: true,
47
+ config: devStageConfig,
48
+ },
49
+ async execute(ctx) {
50
+ const row = await ctx.toolkit.table.readCurrentRow();
51
+ const id = changeId(row);
52
+ const repos = requireRepositories(ctx, "dev_repositories");
53
+ if (!id || !repos.length)
54
+ return failed("技术方案评审缺少变更编号或 Dev 仓库配置");
55
+ if (!isCurrentRecipe(row, ctx.stepId))
56
+ return failed("当前步骤与技术方案评审 recipe 不一致");
57
+ if (hasStageTransition(row["审计日志"], id, "编码"))
58
+ return success({}, "恢复:技术方案已合并但步骤指针尚未推进");
59
+ // 有界重试:超过最大轮次则升级人工(放在恢复检查之后,避免误阻塞已完成阶段)
60
+ const currentRound = parseReviewRound(row);
61
+ if (currentRound > MAX_REVIEW_ROUNDS) {
62
+ return blocked(`技术方案评审修复已超过 ${MAX_REVIEW_ROUNDS} 轮,请人工介入`);
63
+ }
64
+ const resumed = await consumeAnswersOrBlock(ctx, row);
65
+ if (resumed.blocked) return resumed.blocked;
66
+ const entries = crEntries(row["当前CR链接"]);
67
+ if (!entries.length) return failed("技术方案评审缺少 spec 创建的 CR 链接");
68
+ const unknown = entries.find(
69
+ (entry) =>
70
+ !repos.some(
71
+ (repo) => repoPathFromUrl(repositoryUrl(repo)) === entry.repoUrl,
72
+ ),
73
+ );
74
+ if (unknown)
75
+ return failed(`技术方案 CR 不属于已配置的 Dev 仓库:${unknown.repoUrl}`);
76
+ const checks = await Promise.all(
77
+ entries.map(async (entry) => ({
78
+ entry,
79
+ comments: await ctx.toolkit.codeReview.listUnresolvedComments(
80
+ entry.mrId,
81
+ entry.repoUrl,
82
+ ),
83
+ merged: await ctx.toolkit.codeReview.isMerged(entry.mrId, entry.repoUrl),
84
+ })),
85
+ );
86
+ const repairs = checks.flatMap((check) => {
87
+ const comments = unhandledComments(
88
+ row["产出物"],
89
+ "spec-review",
90
+ check.entry.mrId,
91
+ check.comments,
92
+ );
93
+ return comments.length ? [{ ...check, comments }] : [];
94
+ });
95
+ if (repairs.length) {
96
+ const branch = stageBranchName("spec", id);
97
+ const repairRepos = repairs.map(
98
+ (repair) =>
99
+ repos.find(
100
+ (repo) =>
101
+ repoPathFromUrl(repositoryUrl(repo)) === repair.entry.repoUrl,
102
+ )!,
103
+ );
104
+ const worktrees = await Promise.all(
105
+ repairRepos.map((repo) =>
106
+ ctx.toolkit.git.ensureWorktree(
107
+ repo,
108
+ branch,
109
+ branch,
110
+ worktreeOptions(repo, id),
111
+ ),
112
+ ),
113
+ );
114
+ const candidateWorktrees = worktrees
115
+ .map(
116
+ (worktree, index) =>
117
+ `- ${repositoryUrl(repairRepos[index]!)}: ${resolveWorktreeDir(ctx.configDir, worktree.dir)}`,
118
+ )
119
+ .join("\n");
120
+ const repair = await ctx.toolkit.coding.run(
121
+ `## 候选技术方案 worktree
122
+ ${candidateWorktrees}
123
+ 公共父目录不是 Git 仓库;所有文件只能写入具体仓库 worktree。
124
+ 进入某仓库前先读取其根目录 AGENTS.md(若存在),并遵循其仓库约定。
125
+
126
+ ${testDemandInstruction(row)}
127
+
128
+ ## 按仓库分组的未解决评论
129
+ \`\`\`json
130
+ ${JSON.stringify(
131
+ repairs.map((repair) => ({
132
+ repo: repair.entry.repoUrl,
133
+ mrId: repair.entry.mrId,
134
+ comments: repair.comments,
135
+ })),
136
+ null,
137
+ 2,
138
+ )}
139
+ \`\`\`
140
+
141
+ ## 已消费的人工答复
142
+ ${resumed.answers}
143
+
144
+ ## 本轮任务
145
+ 逐项修复评论,更新 ${artifactPath("spec", id)},并运行适用校验。`.trim(),
146
+ { sessionId: sessionMap(row["Agent会话ID"])["spec-review:dev"] },
147
+ );
148
+ if (!repair.success) {
149
+ return handleCodingFailure(
150
+ ctx,
151
+ row,
152
+ "spec-review",
153
+ "spec-review:dev",
154
+ repair,
155
+ "技术方案评论修复 Agent 未完成",
156
+ );
157
+ }
158
+ const session = await persistCodingSession(ctx, row, "spec-review:dev", repair);
159
+ const commits = await Promise.all(
160
+ worktrees.map((worktree) =>
161
+ ctx.toolkit.git.commit(
162
+ worktree.dir,
163
+ `${id}: spec review fixes`,
164
+ commitOptions(row),
165
+ ),
166
+ ),
167
+ );
168
+ let artifacts = appendArtifact(
169
+ row["产出物"],
170
+ `${artifactName("spec")};评论修复commit=${commits.some((commit) => commit.committed) ? `${id}: spec review fixes` : "已存在"}`,
171
+ );
172
+ for (const repairTarget of repairs) {
173
+ artifacts = appendHandledComments(
174
+ artifacts,
175
+ "spec-review",
176
+ repairTarget.entry.mrId,
177
+ repairTarget.comments,
178
+ );
179
+ }
180
+ const persisted = { 产出物: artifacts, Agent会话ID: session, 审查轮次: currentRound + 1 };
181
+ const questions = agentQuestions(repair.response, "spec-review");
182
+ if (questions.length) {
183
+ return blockExecution(
184
+ ctx,
185
+ "技术方案评论修复仍有待确认问题",
186
+ "在人工答复中按问题编号逐项回答后自动续跑",
187
+ questionsText(questions),
188
+ persisted,
189
+ );
190
+ }
191
+ await Promise.all(
192
+ worktrees.map((worktree) => ctx.toolkit.git.push(worktree.dir, branch)),
193
+ );
194
+ return waitForExternalProgress(
195
+ ctx,
196
+ row,
197
+ "技术方案评论已修复,等待复审",
198
+ "等待评审者关闭评论或提出新评论",
199
+ {
200
+ 评审进展: commentRepairNotice(repairs.map((repairTarget) => ({
201
+ repoUrl: repairTarget.entry.repoUrl,
202
+ mrUrl: repairTarget.entry.mrUrl,
203
+ commentCount: repairTarget.comments.length,
204
+ }))),
205
+ ...persisted,
206
+ },
207
+ );
208
+ }
209
+ if (checks.some((check) => !check.merged)) {
210
+ return waitForExternalProgress(
211
+ ctx,
212
+ row,
213
+ "技术方案 CR 尚未全部合并",
214
+ "请合并全部技术方案 CR;合并事实将自动进入编码",
215
+ { 当前角色: "Dev" },
216
+ );
217
+ }
218
+ await Promise.all(
219
+ entries.map((entry) => {
220
+ const repository = repos.find(
221
+ (repo) => repoPathFromUrl(repositoryUrl(repo)) === entry.repoUrl,
222
+ );
223
+ if (!repository) throw new Error(`技术方案 CR 仓库配置已失效:${entry.repoUrl}`);
224
+ return ctx.toolkit.git.syncWorktree(repository, id, id);
225
+ }),
226
+ );
227
+ await ctx.toolkit.table.updateCurrentRow({
228
+ 当前阶段: "编码",
229
+ 当前角色: "Dev",
230
+ 状态: "active",
231
+ 当前CR链接: "",
232
+ 评审进展: "",
233
+ 审计日志: appendStageTransition(row["审计日志"], id, "编码"),
234
+ AI问题: "",
235
+ 阻塞原因: "",
236
+ 解除条件: "",
237
+ });
238
+ return success({}, "全部技术方案 CR 已合并,进入编码");
239
+ },
240
+ };
241
+
242
+ export default recipe;
@@ -0,0 +1,173 @@
1
+ import type { RecipeDefinition } from "aitable-workflow-core";
2
+ import { planStageConfig } from "../../../lib/recipe-config.js";
3
+ import {
4
+ artifactAbsPath,
5
+ artifactName,
6
+ artifactPath,
7
+ resolveWorktreeDir,
8
+ stageBranchName,
9
+ } from "../../../lib/artifacts.js";
10
+ import {
11
+ agentQuestions,
12
+ appendArtifact,
13
+ appendStageTransition,
14
+ blockExecution,
15
+ blocked,
16
+ changeId,
17
+ changeTitle,
18
+ commitOptions,
19
+ consumeAnswersOrBlock,
20
+ failed,
21
+ handleCodingFailure,
22
+ hasStageTransition,
23
+ isTestDemand,
24
+ isCurrentRecipe,
25
+ persistCodingSession,
26
+ questionsText,
27
+ recordValue,
28
+ repositoryUrl,
29
+ repoPathFromUrl,
30
+ formatCrEntries,
31
+ requireRepositories,
32
+ requireRepository,
33
+ sessionMap,
34
+ success,
35
+ testDemandInstruction,
36
+ worktreeOptions,
37
+ } from "../../../lib/recipe-helpers.js";
38
+
39
+ const recipe: RecipeDefinition = {
40
+ meta: {
41
+ id: "spec",
42
+ name: "技术方案",
43
+ role: "dev",
44
+ description: "Dev 基于已通过的 PRD 生成版本化技术方案",
45
+ version: "1.0.0",
46
+ requiresAgent: true,
47
+ config: planStageConfig,
48
+ },
49
+ async execute(ctx) {
50
+ const row = await ctx.toolkit.table.readCurrentRow();
51
+ const id = changeId(row);
52
+ const configuredRepos = requireRepositories(ctx, "dev_repositories");
53
+ const repos = isTestDemand(row) ? configuredRepos.slice(0, 1) : configuredRepos;
54
+ const pmRepository = requireRepository(ctx, "pm_repository");
55
+ if (!id || !repos.length || !pmRepository)
56
+ return failed("技术方案阶段缺少变更编号、PM 仓库或 Dev 候选仓库配置");
57
+ if (!isCurrentRecipe(row, ctx.stepId)) {
58
+ return blocked("当前步骤与执行 recipe 不一致,拒绝生成技术方案工件");
59
+ }
60
+ if (hasStageTransition(row["审计日志"], id, "技术方案评审")) {
61
+ return success({}, "恢复:技术方案 CR 已创建但步骤指针尚未推进");
62
+ }
63
+ const resumed = await consumeAnswersOrBlock(ctx, row);
64
+ if (resumed.blocked) return resumed.blocked;
65
+ const [pmWorktree, ...requirementWorktrees] = await Promise.all([
66
+ ctx.toolkit.git.ensureWorktree(
67
+ pmRepository,
68
+ id,
69
+ id,
70
+ worktreeOptions(pmRepository),
71
+ ),
72
+ ...repos.map((repo) =>
73
+ ctx.toolkit.git.ensureWorktree(repo, id, id, worktreeOptions(repo)),
74
+ ),
75
+ ]);
76
+ await Promise.all(requirementWorktrees.map((worktree) => ctx.toolkit.git.push(worktree.dir, id)));
77
+ const specBranch = stageBranchName("spec", id);
78
+ const specWorktrees = await Promise.all(
79
+ repos.map((repo) =>
80
+ ctx.toolkit.git.ensureWorktree(repo, specBranch, specBranch, worktreeOptions(repo, id)),
81
+ ),
82
+ );
83
+ const candidateWorktrees = specWorktrees
84
+ .map((worktree, index) => `- ${repositoryUrl(repos[index]!)}: ${resolveWorktreeDir(ctx.configDir, worktree.dir)}`)
85
+ .join("\n");
86
+ const result = await ctx.toolkit.coding.run(
87
+ `## 已合并 PRD(只读)
88
+ PM worktree:${resolveWorktreeDir(ctx.configDir, pmWorktree.dir)}
89
+ 先读取:${artifactAbsPath("prd", resolveWorktreeDir(ctx.configDir, pmWorktree.dir), id)}
90
+
91
+ ## 候选业务 worktree
92
+ ${candidateWorktrees}
93
+ 公共父目录不是 Git 仓库;所有文件只能写入具体仓库 worktree。
94
+ 进入某仓库前先读取其根目录 AGENTS.md(若存在),并遵循其约定。
95
+
96
+ ${testDemandInstruction(row)}
97
+
98
+ ## 已消费的人工答复
99
+ ${resumed.answers}
100
+
101
+ ## 需求
102
+ 标题:${recordValue(row, "需求")}
103
+ 详情:${recordValue(row, "需求详情")}
104
+
105
+ ## 本轮任务
106
+ 依据角色规程选择要改的仓库。
107
+ 在每个选中仓库的 ${artifactPath("spec", id)} 写入技术方案;未选中仓库不写。
108
+ 明确列出选中/未选中仓库及理由,并列出文件、风险和测试命令。`.trim(),
109
+ {
110
+ sessionId: sessionMap(row["Agent会话ID"])["spec:dev"],
111
+ },
112
+ );
113
+ if (!result.success) {
114
+ return handleCodingFailure(
115
+ ctx,
116
+ row,
117
+ "spec",
118
+ "spec:dev",
119
+ result,
120
+ "技术方案 Agent 未完成",
121
+ );
122
+ }
123
+ const agentSession = await persistCodingSession(ctx, row, "spec:dev", result);
124
+ const commits = await Promise.all(
125
+ specWorktrees.map((worktree) =>
126
+ ctx.toolkit.git.commit(worktree.dir, `${id}: spec`, commitOptions(row)),
127
+ ),
128
+ );
129
+ const questions = agentQuestions(result.response, "spec");
130
+ const persisted = {
131
+ 产出物: appendArtifact(
132
+ row["产出物"],
133
+ `${artifactName("spec")};commit=${commits.some((commit) => commit.committed) ? `${id}: spec` : "已存在"}`,
134
+ ),
135
+ Agent会话ID: agentSession,
136
+ };
137
+ if (questions.length) {
138
+ return blockExecution(
139
+ ctx,
140
+ "技术方案存在待确认问题",
141
+ "在人工答复中按问题编号逐项回答后自动续跑",
142
+ questionsText(questions),
143
+ persisted,
144
+ );
145
+ }
146
+ const diffs = await Promise.all(specWorktrees.map((worktree) => ctx.toolkit.git.diff(worktree.dir, id)));
147
+ const changed = specWorktrees.filter((_, index) => diffs[index]!.files.length > 0);
148
+ if (!changed.length) return failed("技术方案完成后没有相对需求分支的已提交改动,无法创建技术方案 CR");
149
+ const entries = await Promise.all(changed.map(async (worktree) => {
150
+ const repository = repos[specWorktrees.indexOf(worktree)]!;
151
+ const repoUrl = repositoryUrl(repository);
152
+ await ctx.toolkit.git.push(worktree.dir, specBranch);
153
+ const mr = await ctx.toolkit.codeReview.createMR(
154
+ repoPathFromUrl(repoUrl), specBranch, id, `${changeTitle(row, id)} · 技术方案`,
155
+ `请评审 ${artifactPath("spec", id)};合并后才会进入编码。`,
156
+ );
157
+ return { repoUrl, mrId: mr.mrId, mrUrl: mr.mrUrl };
158
+ }));
159
+ await ctx.toolkit.table.updateCurrentRow({
160
+ 当前阶段: "技术方案评审",
161
+ 当前角色: "Dev",
162
+ 状态: "active",
163
+ 当前CR链接: formatCrEntries(entries),
164
+ 审计日志: appendStageTransition(row["审计日志"], id, "技术方案评审"),
165
+ AI问题: "",
166
+ 阻塞原因: "",
167
+ 解除条件: "",
168
+ ...persisted,
169
+ });
170
+ return success({}, "技术方案工件已提交并创建技术方案 CR");
171
+ },
172
+ };
173
+ export default recipe;