@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.
- package/.agents/commands/common/spec-continue.md +1 -1
- package/.agents/commands/common/spec-start-review.md +1 -1
- package/.agents/commands/common/spec-start.md +1 -1
- package/.agents/commands/common/spec-update.md +2 -2
- package/.agents/commands/cursor/opsx-apply.md +0 -1
- package/.agents/commands/cursor/opsx-propose.md +0 -1
- package/.agents/flows/RUN_OUTPUT.md +0 -2
- package/.agents/flows/common/prd-to-delivery.md +0 -3
- package/.agents/flows/common/requirement-to-observability.md +0 -2
- package/.agents/orchestration/task-orchestrator-run-plan-template.md +2 -2
- package/.agents/registry/flows.json +5 -5
- package/.agents/registry/roles.json +13 -27
- package/.agents/roles/common/backend-implementer.md +3 -4
- package/.agents/roles/common/code-guardian.md +2 -3
- package/.agents/roles/common/frontend-implementer.md +3 -4
- package/.agents/roles/common/requirement-analyst.md +7 -18
- package/.agents/roles/common/tooling-implementer.md +3 -4
- package/.agents/rules/common/12-Superpowers/346/211/247/350/241/214/350/247/204/350/214/203.md +1 -1
- 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
- package/.agents/rules/profiles/react/01-/351/241/271/347/233/256/346/246/202/350/277/260.md +1 -1
- package/.agents/rules/profiles/react/05-API/350/247/204/350/214/203.md +2 -2
- package/.agents/rules/profiles/vue/05-API/350/247/204/350/214/203.md +2 -2
- package/.agents/skills/common/archive-change/SKILL.md +0 -1
- package/.agents/skills/common/create-proposal/SKILL.md +7 -8
- package/.agents/skills/common/execute-task/SKILL.md +3 -3
- package/.agents/skills/profiles/react/create-api/SKILL.md +2 -2
- package/.agents/skills/profiles/vue/create-api/SKILL.md +2 -2
- package/bin/archive-change.js +37 -259
- package/bin/cli.js +5 -0
- package/bin/demo-runtime-smoke.js +11 -74
- package/bin/execution-semantics.js +4 -17
- package/bin/expert-executor.js +4 -7
- package/bin/protocol-workflow.js +1 -1
- package/bin/refresh-command.js +212 -0
- package/bin/runtime-state.js +18 -14
- package/bin/task-orchestrator-runner.js +2 -2
- package/internal/ai-protocol-workflow.js +80 -119
- package/openspec/config.yaml.template +6 -15
- package/openspec/schemas/expert-delivery/schema.yaml +0 -11
- package/openspec/schemas/expert-delivery/templates/checklist.md +12 -30
- package/openspec/schemas/expert-delivery/templates/iterations.md +6 -22
- package/openspec/schemas/expert-delivery/templates/proposal.md +7 -35
- package/openspec/schemas/expert-delivery/templates/tasks.md +7 -20
- package/package.json +1 -1
- package/scripts/post-publish-auto-fix-check.js +5 -61
- 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/ /
|
|
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
|
|
52
|
+
| **交付形态** | 新页面 / 功能组件 / 能力模块 / 其它 | 决定目录结构(routes vs components)与 OpenSpec tasks.md 中的技术方案 |
|
|
53
53
|
| **是否仅样式/还原类** | 是 / 否 | 是 → 重点在 design-analysis + 验收 |
|
|
54
|
-
| **是否存在复杂交互** | 有 / 无 | 有 → 先按 `references/interaction-spec-template.md` 收口搜索、表单、弹窗、批量操作等交互说明,再写 proposal/
|
|
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/、
|
|
65
|
+
分析清单应在 OpenSpec 生成提案前或同步完成,以便 OpenSpec 的 specs/、tasks.md 能引用分析结果。
|
|
66
66
|
|
|
67
|
-
若页面包含搜索、表单、弹窗、批量操作、复杂状态切换等交互,先参考 `references/interaction-spec-template.md` 把交互说明整理成摘要,再写入 `proposal.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
|
|
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]`,并保证与 `
|
|
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` 或 `
|
|
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
|
-
| 架构对齐 | 实现是否符合 `
|
|
114
|
+
| 架构对齐 | 实现是否符合 `specs/` | 通过 / 偏差说明 |
|
|
115
115
|
| 代码健壮性 | 异常捕获、类型严谨、边界处理 | 通过 / 待补充项 |
|
|
116
116
|
| 规范合规 | 是否符合步骤 1 加载的规范 | 通过 / 偏差说明 |
|
|
117
117
|
|
|
@@ -124,7 +124,7 @@ metadata:
|
|
|
124
124
|
|
|
125
125
|
| 维度 | 检查项 | 结论 |
|
|
126
126
|
|------|--------|------|
|
|
127
|
-
| 架构对齐 | 符合
|
|
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 '
|
|
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
|
-
- [ ]
|
|
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 '
|
|
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
|
-
- [ ]
|
|
103
|
+
- [ ] 使用项目统一的 `request` 工具函数(如 `@/utils/request`)?
|
|
104
104
|
- [ ] 未重复处理接口错误?
|
|
105
105
|
- [ ] 类型与接口文档一致?
|
package/bin/archive-change.js
CHANGED
|
@@ -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
|
-
|
|
272
|
-
const
|
|
273
|
-
|
|
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
|
-
|
|
303
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
435
|
-
|
|
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
|
|
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
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
472
|
-
|
|
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
|
-
|
|
492
|
-
|
|
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(`
|
|
519
|
-
console.log(`
|
|
520
|
-
console.log(`
|
|
521
|
-
console.log(`
|
|
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)));
|