@gong-ym/ai-spec-auto 0.2.15 → 0.2.17

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 (46) hide show
  1. package/.agents/commands/common/spec-continue.md +1 -1
  2. package/.agents/commands/common/spec-start-review.md +1 -1
  3. package/.agents/commands/common/spec-start.md +1 -1
  4. package/.agents/commands/common/spec-update.md +2 -2
  5. package/.agents/commands/cursor/opsx-apply.md +0 -1
  6. package/.agents/commands/cursor/opsx-propose.md +0 -1
  7. package/.agents/flows/RUN_OUTPUT.md +0 -2
  8. package/.agents/flows/common/prd-to-delivery.md +0 -3
  9. package/.agents/flows/common/requirement-to-observability.md +0 -2
  10. package/.agents/orchestration/task-orchestrator-run-plan-template.md +2 -2
  11. package/.agents/registry/flows.json +5 -5
  12. package/.agents/registry/roles.json +13 -27
  13. package/.agents/roles/common/backend-implementer.md +3 -4
  14. package/.agents/roles/common/code-guardian.md +2 -3
  15. package/.agents/roles/common/frontend-implementer.md +3 -4
  16. package/.agents/roles/common/requirement-analyst.md +7 -18
  17. package/.agents/roles/common/tooling-implementer.md +3 -4
  18. package/.agents/rules/common/12-Superpowers/346/211/247/350/241/214/350/247/204/350/214/203.md +1 -1
  19. package/.agents/rules/common/14-/345/256/241/350/256/241/346/261/207/346/212/245/350/247/204/350/214/203.md +1 -2
  20. package/.agents/rules/profiles/react/01-/351/241/271/347/233/256/346/246/202/350/277/260.md +1 -1
  21. package/.agents/rules/profiles/react/05-API/350/247/204/350/214/203.md +2 -2
  22. package/.agents/rules/profiles/vue/05-API/350/247/204/350/214/203.md +2 -2
  23. package/.agents/skills/common/archive-change/SKILL.md +0 -1
  24. package/.agents/skills/common/create-proposal/SKILL.md +7 -8
  25. package/.agents/skills/common/execute-task/SKILL.md +3 -3
  26. package/.agents/skills/profiles/react/create-api/SKILL.md +2 -2
  27. package/.agents/skills/profiles/vue/create-api/SKILL.md +2 -2
  28. package/bin/archive-change.js +37 -259
  29. package/bin/cli.js +5 -0
  30. package/bin/demo-runtime-smoke.js +11 -74
  31. package/bin/execution-semantics.js +4 -17
  32. package/bin/expert-executor.js +4 -7
  33. package/bin/protocol-workflow.js +1 -1
  34. package/bin/refresh-command.js +212 -0
  35. package/bin/runtime-state.js +18 -14
  36. package/bin/task-orchestrator-runner.js +2 -2
  37. package/internal/ai-protocol-workflow.js +80 -119
  38. package/openspec/config.yaml.template +6 -15
  39. package/openspec/schemas/expert-delivery/schema.yaml +0 -11
  40. package/openspec/schemas/expert-delivery/templates/checklist.md +12 -30
  41. package/openspec/schemas/expert-delivery/templates/iterations.md +6 -22
  42. package/openspec/schemas/expert-delivery/templates/proposal.md +7 -35
  43. package/openspec/schemas/expert-delivery/templates/tasks.md +7 -20
  44. package/package.json +1 -1
  45. package/scripts/post-publish-auto-fix-check.js +5 -61
  46. package/openspec/schemas/expert-delivery/templates/design.md +0 -61
@@ -15,7 +15,7 @@ compatibility: Requires an OpenSpec workspace, local .agents/rules constraints,
15
15
  | 层 | 职责 | 产物位置 |
16
16
  |----|------|----------|
17
17
  | **本技能** | 需求前置分析 + 上下文注入 + 后置检查 | 无独立产物(分析结论注入 OpenSpec 上下文) |
18
- | **OpenSpec** | 生成 proposal.md / specs/ / design.md / tasks.md | `openspec/changes/<name>/` |
18
+ | **OpenSpec** | 生成 proposal.md / specs/ / tasks.md | `openspec/changes/<name>/` |
19
19
  | **config.yaml** | 桥接 ai-spec-auto 规范到 OpenSpec rules | `openspec/config.yaml` |
20
20
 
21
21
  ## 使用时机
@@ -49,9 +49,9 @@ compatibility: Requires an OpenSpec workspace, local .agents/rules constraints,
49
49
  |------|------|------|
50
50
  | **是否有设计稿或 UI 要求描述** | 有 / 无 | 有 → 步骤 2 触发 design-analysis;OpenSpec 的 tasks 中应包含 UI 验收任务 |
51
51
  | **是否有接口(已提供或约定)** | 有 / 无 / 未就绪 | 有 → 正常对接;无 → 可不做数据层;未就绪 → mock,见项目 Mock 数据策略 |
52
- | **交付形态** | 新页面 / 功能组件 / 能力模块 / 其它 | 决定目录结构(routes vs components)与 OpenSpec design.md 中的技术方案 |
52
+ | **交付形态** | 新页面 / 功能组件 / 能力模块 / 其它 | 决定目录结构(routes vs components)与 OpenSpec tasks.md 中的技术方案 |
53
53
  | **是否仅样式/还原类** | 是 / 否 | 是 → 重点在 design-analysis + 验收 |
54
- | **是否存在复杂交互** | 有 / 无 | 有 → 先按 `references/interaction-spec-template.md` 收口搜索、表单、弹窗、批量操作等交互说明,再写 proposal/design/tasks |
54
+ | **是否存在复杂交互** | 有 / 无 | 有 → 先按 `references/interaction-spec-template.md` 收口搜索、表单、弹窗、批量操作等交互说明,再写 proposal/tasks |
55
55
 
56
56
  ---
57
57
 
@@ -62,9 +62,9 @@ compatibility: Requires an OpenSpec workspace, local .agents/rules constraints,
62
62
  - **使用技能**:`.agents/skills/design-analysis/SKILL.md`
63
63
  - **产出**:`docs/样式还原/<名称>-UI分析清单.md`
64
64
 
65
- 分析清单应在 OpenSpec 生成提案前或同步完成,以便 OpenSpec 的 specs/、design.md、tasks.md 能引用分析结果。
65
+ 分析清单应在 OpenSpec 生成提案前或同步完成,以便 OpenSpec 的 specs/、tasks.md 能引用分析结果。
66
66
 
67
- 若页面包含搜索、表单、弹窗、批量操作、复杂状态切换等交互,先参考 `references/interaction-spec-template.md` 把交互说明整理成摘要,再写入 `proposal.md / design.md / tasks.md`,避免实现阶段自己补口径。
67
+ 若页面包含搜索、表单、弹窗、批量操作、复杂状态切换等交互,先参考 `references/interaction-spec-template.md` 把交互说明整理成摘要,再写入 `proposal.md / tasks.md`,避免实现阶段自己补口径。
68
68
 
69
69
  ---
70
70
 
@@ -81,7 +81,6 @@ openspec/changes/<change-name>/
81
81
  ├── specs/ # Delta specs(新增/修改/删除的需求)
82
82
  │ └── <domain>/
83
83
  │ └── spec.md
84
- ├── design.md # 技术设计(方案选型、组件拆分、数据结构)
85
84
  └── tasks.md # 实施任务清单
86
85
  ```
87
86
 
@@ -100,7 +99,7 @@ openspec/changes/<change-name>/
100
99
 
101
100
  OpenSpec 生成提案后,检查以下项目并按需补充:
102
101
 
103
- ### 4.1 design.md 检查
102
+ ### 4.1 tasks.md 前置检查
104
103
  - 技术方案是否遵循 `.agents/rules/` 中的架构约束
105
104
  - 涉及页面时,是否参考了 `.agents/rules/06-路由规范.md`
106
105
  - 涉及组件时,是否参考了 `.agents/rules/04-组件规范.md`
@@ -150,7 +149,7 @@ OpenSpec 生成提案且本步骤 4 检查完毕后,**必须**以独立小节
150
149
 
151
150
  适合小改动或用户明确要求「快扫清单」、不强制每步输出四步标题时,提供**可复制**提示语:
152
151
 
153
- > 请直接按 `openspec/changes/<change-name>/tasks.md` 顺序逐项实现,每完成一项将对应 `- [ ]` 改为 `- [x]`,并保证与 `design.md`、`specs/` 一致。**凡产生代码变更**,仍须按 `.agents/rules/14-审计汇报规范.md` 输出审计报告,不得因未走 execute-task 四步而省略。
152
+ > 请直接按 `openspec/changes/<change-name>/tasks.md` 顺序逐项实现,每完成一项将对应 `- [ ]` 改为 `- [x]`,并保证与 `specs/` 一致。**凡产生代码变更**,仍须按 `.agents/rules/14-审计汇报规范.md` 输出审计报告,不得因未走 execute-task 四步而省略。
154
153
 
155
154
  #### 4.5.4 可选说明
156
155
 
@@ -44,7 +44,7 @@ metadata:
44
44
  ## 前置要求
45
45
 
46
46
  1. 当前工作区必须存在 `tasks.md` 文件,且包含未完成的任务 (`- [ ]`)。
47
- 2. 必须已完整读取对应变更目录下的 `proposal.md` 或 `design.md` 获取上下文。
47
+ 2. 必须已完整读取对应变更目录下的 `proposal.md` 或 `specs/` 获取上下文。
48
48
  3. 必须已读取 `.agents/rules/12-Superpowers执行规范.md` 确认执行原则。
49
49
  4. 必须已读取 `.agents/rules/14-审计汇报规范.md` 确认审计报告格式。
50
50
 
@@ -111,7 +111,7 @@ metadata:
111
111
 
112
112
  | 维度 | 检查项 | 结论 |
113
113
  |------|--------|------|
114
- | 架构对齐 | 实现是否符合 `design.md` / `specs/` | 通过 / 偏差说明 |
114
+ | 架构对齐 | 实现是否符合 `specs/` | 通过 / 偏差说明 |
115
115
  | 代码健壮性 | 异常捕获、类型严谨、边界处理 | 通过 / 待补充项 |
116
116
  | 规范合规 | 是否符合步骤 1 加载的规范 | 通过 / 偏差说明 |
117
117
 
@@ -124,7 +124,7 @@ metadata:
124
124
 
125
125
  | 维度 | 检查项 | 结论 |
126
126
  |------|--------|------|
127
- | 架构对齐 | 符合 design.md 组件拆分方案 | 通过 |
127
+ | 架构对齐 | 符合 specs/ 组件拆分方案 | 通过 |
128
128
  | 代码健壮性 | Props 均有默认值,loading 防重复点击 | 通过 |
129
129
  | 规范合规 | 目录 PascalCase、barrel export(04-组件规范) | 通过 |
130
130
  ```
@@ -72,7 +72,7 @@ export type UpdateBannerParams = CreateBannerParams & { id: number };
72
72
 
73
73
  ```ts
74
74
  // src/api/banner.ts
75
- import { request } from '@koi-design/vix-tools';
75
+ import { request } from '@/utils/request';
76
76
  import type {
77
77
  GetBannerListParams,
78
78
  GetBannerListResult,
@@ -140,6 +140,6 @@ const loadData = async () => {
140
140
 
141
141
  - [ ] 类型在 `src/api/types/<module>.ts`,请求在 `src/api/<module>.ts`?
142
142
  - [ ] 命名符合 `getXxxApi` / `createXxxApi` / `updateXxxApi` / `deleteXxxApi`?
143
- - [ ] 使用 `import { request } from '@koi-design/vix-tools'`?
143
+ - [ ] 使用项目统一的 `request` 工具函数(如 `@/utils/request`)?
144
144
  - [ ] 未重复处理接口错误?
145
145
  - [ ] 类型与接口文档一致?
@@ -51,7 +51,7 @@ export type UpdateBannerParams = CreateBannerParams & { id: number };
51
51
 
52
52
  ```ts
53
53
  // src/api/banner.ts
54
- import { request } from '@koi-design/vix-tools';
54
+ import { request } from '@/utils/request';
55
55
  import type {
56
56
  GetBannerListParams,
57
57
  GetBannerListResult,
@@ -100,6 +100,6 @@ export function deleteBannerApi(id: number): Promise<void> {
100
100
 
101
101
  - [ ] 类型在 `src/api/types/<name>.ts`,请求在 `src/api/<name>.ts`?
102
102
  - [ ] 命名符合 `getXxxApi` / `createXxxApi` / `updateXxxApi` / `deleteXxxApi`?
103
- - [ ] 使用 `import { request } from '@koi-design/vix-tools'`?
103
+ - [ ] 使用项目统一的 `request` 工具函数(如 `@/utils/request`)?
104
104
  - [ ] 未重复处理接口错误?
105
105
  - [ ] 类型与接口文档一致?
@@ -118,132 +118,6 @@ function countTaskCompletion(tasksPath) {
118
118
  return { completed, total };
119
119
  }
120
120
 
121
- function generateArchiveSummary(archivedPath, changeId) {
122
- const proposalPath = path.join(archivedPath, 'proposal.md');
123
- const designPath = path.join(archivedPath, 'design.md');
124
- const tasksPath = path.join(archivedPath, 'tasks.md');
125
- const checklistPath = path.join(archivedPath, 'checklist.md');
126
- const specMdPath = path.join(archivedPath, 'spec.md');
127
-
128
- // 提取标题
129
- let title = changeId;
130
- if (fs.existsSync(proposalPath)) {
131
- const content = fs.readFileSync(proposalPath, 'utf8');
132
- const titleMatch = content.match(/^#\s+(.+)$/m);
133
- if (titleMatch) title = titleMatch[1].trim();
134
- }
135
-
136
- // 提取业务目标(从 proposal)
137
- let businessGoal = '';
138
- if (fs.existsSync(proposalPath)) {
139
- const content = fs.readFileSync(proposalPath, 'utf8');
140
- // 找“业务目标”或“目标”部分
141
- const goalMatch = content.match(/(?:业务目标|目标|## Goal)[^#]*?([\s\S]*?)(?=#|$)/i);
142
- if (goalMatch) businessGoal = goalMatch[1].trim().slice(0, 300);
143
- }
144
-
145
- // 提取任务完成情况
146
- const taskStats = countTaskCompletion(tasksPath);
147
-
148
- // 提取关键设计决策(从 design)
149
- let designDecisions = '';
150
- if (fs.existsSync(designPath)) {
151
- const content = fs.readFileSync(designPath, 'utf8');
152
- const decisionMatch = content.match(/(?:设计决策|关键决策|技术方案|## Decision)[^#]*?([\s\S]*?)(?=##|$)/i);
153
- if (decisionMatch) designDecisions = decisionMatch[1].trim().slice(0, 200);
154
- }
155
-
156
- // 提取验收结论(从 checklist)
157
- let acceptance = '';
158
- if (fs.existsSync(checklistPath)) {
159
- const content = fs.readFileSync(checklistPath, 'utf8');
160
- // 先匹配 ## 结论 / ## Conclusion 二级标题,再回退匹配 # 验收 / # 结论 一级标题
161
- const conclusionMatch = content.match(/## (?:结论|Conclusion)[^#]*?([\s\S]*?)(?=##?\s|$)/i)
162
- || content.match(/# (?:验收|结论)[^#]*?([\s\S]*?)(?=##?\s|$)/i);
163
- if (conclusionMatch) acceptance = conclusionMatch[1].trim().slice(0, 200);
164
- }
165
-
166
- // 提取规格(从 spec.md 或 specs/ 目录)
167
- let specSummary = '';
168
- if (fs.existsSync(specMdPath)) {
169
- specSummary = fs.readFileSync(specMdPath, 'utf8').trim().slice(0, 300);
170
- }
171
-
172
- const now = new Date().toISOString().slice(0, 10);
173
- const summary = [
174
- `# ${title}`,
175
- '',
176
- `> 归档日期:${now} | 任务完成:${taskStats.completed}/${taskStats.total}`,
177
- '',
178
- businessGoal ? `## 业务目标\n\n${businessGoal}\n` : '',
179
- specSummary ? `## 需求规格\n\n${specSummary}\n` : '',
180
- designDecisions ? `## 设计决策\n\n${designDecisions}\n` : '',
181
- acceptance ? `## 验收结论\n\n${acceptance}\n` : '',
182
- ].filter(Boolean).join('\n');
183
-
184
- const summaryPath = path.join(archivedPath, 'summary.md');
185
- fs.writeFileSync(summaryPath, summary, 'utf8');
186
- return summaryPath;
187
- }
188
-
189
- function cleanupProcessFiles(archivedPath) {
190
- const processFiles = ['proposal.md', 'design.md', 'tasks.md', 'checklist.md', 'iterations.md', 'spec.md'];
191
- const removed = [];
192
- for (const file of processFiles) {
193
- const filePath = path.join(archivedPath, file);
194
- if (fs.existsSync(filePath)) {
195
- fs.unlinkSync(filePath);
196
- removed.push(file);
197
- }
198
- }
199
- return removed;
200
- }
201
-
202
- function normalizeMarkdown(content) {
203
- return String(content || '').replace(/\r\n/g, '\n').trim();
204
- }
205
-
206
- function normalizeSpecsArtifactPath(relPath) {
207
- const value = String(relPath || '').trim();
208
- if (!value) {
209
- return null;
210
- }
211
-
212
- const normalized = value.replace(/[\\/]+$/, '');
213
- if (/[\\/]specs$/.test(normalized)) {
214
- return normalized;
215
- }
216
-
217
- const match = normalized.match(/^(.*[\\/]specs)(?:[\\/].+)?$/);
218
- return match ? match[1] : normalized;
219
- }
220
-
221
- function mergeSpecFile(targetPath, incomingContent, changeId) {
222
- if (!fs.existsSync(targetPath)) {
223
- fs.writeFileSync(targetPath, `${incomingContent.trim()}\n`, 'utf8');
224
- return 'added';
225
- }
226
-
227
- const existingContent = fs.readFileSync(targetPath, 'utf8');
228
- const normalizedExisting = normalizeMarkdown(existingContent);
229
- const normalizedIncoming = normalizeMarkdown(incomingContent);
230
-
231
- if (!normalizedIncoming || normalizedExisting === normalizedIncoming || normalizedExisting.includes(normalizedIncoming)) {
232
- return 'unchanged';
233
- }
234
-
235
- const merged = [
236
- existingContent.trimEnd(),
237
- '',
238
- `<!-- merged from change: ${changeId} -->`,
239
- '',
240
- normalizedIncoming,
241
- '',
242
- ].join('\n');
243
- fs.writeFileSync(targetPath, merged, 'utf8');
244
- return 'updated';
245
- }
246
-
247
121
  function resolveChangeId(targetDir, explicitChangeId) {
248
122
  if (explicitChangeId) {
249
123
  return explicitChangeId;
@@ -253,68 +127,28 @@ function resolveChangeId(targetDir, explicitChangeId) {
253
127
  return currentRun?.task?.change_id || currentRun?.anchor?.task?.change_id || null;
254
128
  }
255
129
 
256
- function buildArchiveDestination(baseArchiveDir, changeId, now = new Date()) {
257
- const day = now.toISOString().slice(0, 10);
258
- const baseName = `${day}-${changeId}`;
259
- let candidate = path.join(baseArchiveDir, baseName);
260
- let suffix = 1;
261
-
262
- while (fs.existsSync(candidate)) {
263
- suffix += 1;
264
- candidate = path.join(baseArchiveDir, `${baseName}-${suffix}`);
265
- }
266
-
267
- return candidate;
268
- }
269
-
270
130
  function buildAlreadyArchivedResult(targetDir, changeId) {
271
- const currentRun = readJsonIfExists(path.join(targetDir, '.ai-spec', 'current-run.json'));
272
- const proposalPath = currentRun?.artifacts?.proposal
273
- ? path.join(targetDir, currentRun.artifacts.proposal)
274
- : null;
275
- const specsPath = currentRun?.artifacts?.specs
276
- ? path.join(targetDir, normalizeSpecsArtifactPath(currentRun.artifacts.specs))
277
- : null;
278
- const designPath = currentRun?.artifacts?.design
279
- ? path.join(targetDir, currentRun.artifacts.design)
280
- : null;
281
-
282
- if (!proposalPath || !specsPath || !designPath) {
283
- return null;
284
- }
285
- if (!proposalPath.includes(`${path.sep}openspec${path.sep}changes${path.sep}archive${path.sep}`)) {
286
- return null;
287
- }
288
- if (!fs.existsSync(proposalPath) || !fs.existsSync(specsPath) || !fs.existsSync(designPath)) {
131
+ // 检查 specs 是否已存储到 openspec/specs/{changeId}.md
132
+ const specFile = path.join(targetDir, 'openspec', 'specs', `${changeId}.md`);
133
+ if (!fs.existsSync(specFile)) {
289
134
  return null;
290
135
  }
136
+ const currentRun = readJsonIfExists(path.join(targetDir, '.ai-spec', 'current-run.json'));
291
137
  if ((currentRun?.task?.change_id || currentRun?.anchor?.task?.change_id || null) !== changeId) {
292
138
  return null;
293
139
  }
294
140
 
295
- const archivedDir = path.dirname(proposalPath);
296
- const archivedRel = path.relative(targetDir, archivedDir);
297
141
  return {
298
142
  kind: 'archive-change-result',
299
143
  status: 'success',
300
144
  target: targetDir,
301
145
  change_id: changeId,
302
- archived_to: archivedRel,
303
- archived_artifacts: {
304
- proposal: path.relative(targetDir, proposalPath),
305
- specs: path.relative(targetDir, specsPath),
306
- design: path.relative(targetDir, designPath),
307
- tasks: currentRun?.artifacts?.tasks || null,
308
- checklist: currentRun?.artifacts?.checklist || null,
309
- iterations: currentRun?.artifacts?.iterations || null,
310
- },
311
- merged: {
312
- added_domains: [],
146
+ specs_stored_at: path.relative(targetDir, specFile),
147
+ archived: {
313
148
  added_specs: [],
314
- updated_specs: [],
315
- unchanged_specs: [],
149
+ updated_specs: [path.relative(targetDir, specFile)],
316
150
  },
317
- task_completion: countTaskCompletion(path.join(archivedDir, 'tasks.md')),
151
+ task_completion: { completed: 0, total: 0 },
318
152
  already_archived: true,
319
153
  };
320
154
  }
@@ -360,8 +194,10 @@ function completeRunAfterArchive(targetDir, archiveResult) {
360
194
  fromRole: currentRun.current_role || 'archive-change',
361
195
  toRole: 'archive-change',
362
196
  status: 'success',
363
- message: '归档完成,增量规范已合并并已写入归档目录。',
364
- artifactsData: archiveResult.archived_artifacts,
197
+ message: '归档完成,规范已存储到 openspec/specs/。',
198
+ artifactsData: {
199
+ specs: archiveResult.specs_stored_at,
200
+ },
365
201
  skipArtifactCheck: true,
366
202
  clearPendingGate: true,
367
203
  });
@@ -373,25 +209,6 @@ function completeRunAfterArchive(targetDir, archiveResult) {
373
209
  };
374
210
  }
375
211
 
376
- function buildArchivedAdditionalArtifacts(currentArtifacts, changeId, archivedRelBase) {
377
- const currentAdditional = Array.isArray(currentArtifacts?.additional) ? currentArtifacts.additional : [];
378
- if (currentAdditional.length === 0) {
379
- return [];
380
- }
381
-
382
- const activeBase = `openspec/changes/${changeId}`;
383
- return currentAdditional
384
- .map((item) => String(item || '').trim())
385
- .filter(Boolean)
386
- .map((item) => item.replace(/[\\/]+$/, ''))
387
- .map((item) => {
388
- if (item === activeBase || item.startsWith(`${activeBase}/`)) {
389
- return item.replace(activeBase, archivedRelBase);
390
- }
391
- return item;
392
- });
393
- }
394
-
395
212
  function archiveChange(options = {}) {
396
213
  const targetDir = path.resolve(options.target || '.');
397
214
  const changeId = resolveChangeId(targetDir, options.changeId);
@@ -413,7 +230,6 @@ function archiveChange(options = {}) {
413
230
 
414
231
  const proposalPath = path.join(changeDir, 'proposal.md');
415
232
  const tasksPath = path.join(changeDir, 'tasks.md');
416
- const designPath = path.join(changeDir, 'design.md');
417
233
  const checklistPath = path.join(changeDir, 'checklist.md');
418
234
  const iterationsPath = path.join(changeDir, 'iterations.md');
419
235
  const specsDir = path.join(changeDir, 'specs');
@@ -424,63 +240,37 @@ function archiveChange(options = {}) {
424
240
  if (!fs.existsSync(tasksPath)) {
425
241
  throw new Error(`tasks.md is missing for change ${changeId}`);
426
242
  }
427
- if (!fs.existsSync(designPath)) {
428
- throw new Error(`design.md is missing for change ${changeId}`);
429
- }
430
243
  if (!fs.existsSync(specsDir)) {
431
244
  throw new Error(`specs/ is missing for change ${changeId}`);
432
245
  }
433
246
 
434
- const targetSpecsDir = path.join(targetDir, 'openspec', 'specs');
435
- ensureDir(targetSpecsDir);
247
+ // 先统计任务完成情况(在清理之前)
248
+ const taskCompletionStats = countTaskCompletion(path.join(changeDir, 'tasks.md'));
249
+
250
+ // 合并所有 spec 文件为单个文件
251
+ const targetSpecFile = path.join(targetDir, 'openspec', 'specs', `${changeId}.md`);
252
+ ensureDir(path.dirname(targetSpecFile));
253
+
254
+ const sourceSpecFiles = listFilesRecursive(specsDir);
255
+ const specContents = sourceSpecFiles.map((f) => fs.readFileSync(f, 'utf8').trim()).filter(Boolean);
256
+ const mergedContent = specContents.join('\n\n---\n\n');
436
257
 
437
- const merged = {
438
- added_domains: [],
258
+ const archived = {
439
259
  added_specs: [],
440
260
  updated_specs: [],
441
- unchanged_specs: [],
442
261
  };
443
- const seenDomains = new Set();
444
- const sourceSpecFiles = listFilesRecursive(specsDir);
445
- for (const sourceFile of sourceSpecFiles) {
446
- const relFromSpecs = path.relative(specsDir, sourceFile);
447
- const domain = relFromSpecs.split(path.sep)[0] || null;
448
- const targetFile = path.join(targetSpecsDir, relFromSpecs);
449
- const targetDomainDir = path.dirname(targetFile);
450
- const domainRel = domain ? path.join('openspec', 'specs', domain) : null;
451
-
452
- if (domainRel && !fs.existsSync(targetDomainDir) && !seenDomains.has(domainRel)) {
453
- ensureDir(targetDomainDir);
454
- merged.added_domains.push(domainRel);
455
- seenDomains.add(domainRel);
456
- } else {
457
- ensureDir(targetDomainDir);
458
- }
459
262
 
460
- const mergeStatus = mergeSpecFile(targetFile, fs.readFileSync(sourceFile, 'utf8'), changeId);
461
- const relTargetFile = path.relative(targetDir, targetFile);
462
- if (mergeStatus === 'added') {
463
- merged.added_specs.push(relTargetFile);
464
- } else if (mergeStatus === 'updated') {
465
- merged.updated_specs.push(relTargetFile);
466
- } else {
467
- merged.unchanged_specs.push(relTargetFile);
468
- }
263
+ if (fs.existsSync(targetSpecFile)) {
264
+ fs.writeFileSync(targetSpecFile, mergedContent, 'utf8');
265
+ archived.updated_specs.push(path.relative(targetDir, targetSpecFile));
266
+ } else {
267
+ fs.writeFileSync(targetSpecFile, mergedContent, 'utf8');
268
+ archived.added_specs.push(path.relative(targetDir, targetSpecFile));
469
269
  }
470
270
 
471
- const archiveRoot = path.join(targetDir, 'openspec', 'changes', 'archive');
472
- ensureDir(archiveRoot);
473
- const archivedPath = buildArchiveDestination(archiveRoot, changeId, options.now || new Date());
474
- fs.renameSync(changeDir, archivedPath);
271
+ // 清理变更目录(不保留归档)
272
+ fs.rmSync(changeDir, { recursive: true, force: true });
475
273
 
476
- // 先统计任务完成情况(在清理之前)
477
- const taskCompletionStats = countTaskCompletion(path.join(archivedPath, 'tasks.md'));
478
-
479
- // 生成精简 summary 并清理过程文件
480
- const summaryPath = generateArchiveSummary(archivedPath, changeId);
481
- const removedFiles = cleanupProcessFiles(archivedPath);
482
-
483
- const archivedRelBase = path.relative(targetDir, archivedPath);
484
274
  const currentRun = readJsonIfExists(path.join(targetDir, '.ai-spec', 'current-run.json'));
485
275
 
486
276
  const result = {
@@ -488,17 +278,8 @@ function archiveChange(options = {}) {
488
278
  status: 'success',
489
279
  target: targetDir,
490
280
  change_id: changeId,
491
- archived_to: archivedRelBase,
492
- summary: path.relative(targetDir, summaryPath),
493
- removed_process_files: removedFiles,
494
- archived_artifacts: {
495
- summary: path.relative(targetDir, summaryPath),
496
- specs: fs.existsSync(path.join(archivedPath, 'specs'))
497
- ? path.relative(targetDir, path.join(archivedPath, 'specs'))
498
- : null,
499
- additional: buildArchivedAdditionalArtifacts(currentRun?.artifacts || null, changeId, archivedRelBase),
500
- },
501
- merged,
281
+ specs_stored_at: path.relative(targetDir, targetSpecFile),
282
+ archived,
502
283
  task_completion: taskCompletionStats,
503
284
  };
504
285
 
@@ -515,13 +296,10 @@ function printPretty(result) {
515
296
  console.log('archive-change');
516
297
  console.log(` target: ${result.target}`);
517
298
  console.log(` change_id: ${result.change_id}`);
518
- console.log(` archived_to: ${result.archived_to}`);
519
- console.log(` summary: ${result.summary}`);
520
- console.log(` cleaned: ${result.removed_process_files.join(', ') || '(none)'}`);
521
- console.log(` added_domains: ${result.merged.added_domains.length}`);
522
- console.log(` added_specs: ${result.merged.added_specs.length}`);
523
- console.log(` updated_specs: ${result.merged.updated_specs.length}`);
524
- console.log(` unchanged_specs: ${result.merged.unchanged_specs.length}`);
299
+ console.log(` specs_stored_at: ${result.specs_stored_at}`);
300
+ console.log(` added_specs: ${result.archived.added_specs.length}`);
301
+ console.log(` updated_specs: ${result.archived.updated_specs.length}`);
302
+ console.log(` task_completion: ${result.task_completion.completed}/${result.task_completion.total}`);
525
303
  if (result.runtime_transition?.state?.status) {
526
304
  console.log(` run_status: ${result.runtime_transition.state.status}`);
527
305
  }
package/bin/cli.js CHANGED
@@ -154,6 +154,11 @@ function shouldUseIntegrityCommand(args, cwd) {
154
154
  process.exit(await repairCommand.main(args.slice(1)));
155
155
  }
156
156
 
157
+ if (args[0] === 'refresh') {
158
+ const refreshCommand = require('./refresh-command');
159
+ process.exit(refreshCommand.main(args.slice(1)));
160
+ }
161
+
157
162
  if (args[0] === 'report') {
158
163
  const reportCommand = require('./report-command');
159
164
  process.exit(await reportCommand.main(args.slice(1)));