sillyspec 3.18.0 → 3.18.2
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/.claude/skills/sillyspec-brainstorm/SKILL.md +24 -23
- package/.claude/skills/sillyspec-execute/SKILL.md +8 -1
- package/package.json +1 -1
- package/src/db.js +4 -0
- package/src/hooks/worktree-guard.js +97 -4
- package/src/index.js +1 -1
- package/src/progress.js +41 -14
- package/src/run.js +315 -83
- package/src/stage-contract.js +249 -12
- package/src/stages/brainstorm.js +228 -8
- package/src/stages/execute.js +12 -14
- package/src/stages/index.js +0 -2
- package/src/stages/plan.js +55 -18
- package/src/stages/propose.js +30 -4
- package/src/stages/quick.js +13 -10
- package/src/stages/scan.js +12 -0
- package/src/stages/verify.js +31 -13
- package/test/platform-artifacts.test.mjs +14 -5
- package/test/platform-failure-samples.test.mjs +3 -2
- package/test/platform-recovery-chain.test.mjs +10 -9
- package/test/platform-recovery.test.mjs +13 -5
- package/test/platform-scan-p0.test.mjs +3 -0
- package/test/scan-postcheck.test.mjs +3 -2
- package/test/spec-dir.test.mjs +2 -1
- package/test/stage-contract.test.mjs +119 -6
- package/test/stage-definitions.test.mjs +2 -6
- package/test/wait-gates.test.mjs +501 -0
- package/test/worktree-guard.test.mjs +58 -0
- package/.npmrc.bak +0 -0
|
@@ -3,41 +3,42 @@ name: sillyspec:brainstorm
|
|
|
3
3
|
description: 用于正式开始开发前的需求澄清和技术方案设计。适合用户提出新功能、新模块、架构调整、复杂改造,或说"先做需求分析、输出技术方案、创建变更前先梳理、帮我设计下"。产出结构化方案,但不直接写代码。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
## 多变更说明
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
用户触发此 skill 时,使用 `sillyspec run brainstorm` 逐步执行需求探索。
|
|
8
|
+
如果项目有多个活跃变更(`.sillyspec/changes/` 下有多个目录),所有 `sillyspec run` 命令需要加 `--change <变更名>`。只有一个变更时可省略(CLI 自动检测)。
|
|
10
9
|
|
|
11
|
-
##
|
|
10
|
+
## 执行
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
2. 运行 `sillyspec run brainstorm --change <变更名>` 获取当前步骤指令
|
|
15
|
-
3. 按步骤指令执行(对话、分析需求、设计方案等)
|
|
16
|
-
4. 完成步骤后运行 `sillyspec run brainstorm --done --change <变更名> --output "步骤摘要"`
|
|
17
|
-
5. CLI 会自动输出下一步的指令,重复 3-4 直到阶段完成
|
|
12
|
+
**你必须使用 exec 工具(shell)执行以下命令,不要自己编造流程:**
|
|
18
13
|
|
|
19
|
-
|
|
14
|
+
1. 运行 `sillyspec run brainstorm` — 读取输出的步骤 prompt
|
|
15
|
+
2. 按照输出的 prompt **严格执行**,不要跳过或自行添加步骤
|
|
16
|
+
3. 步骤完成后,运行 `sillyspec run brainstorm --done --output "你的摘要"`
|
|
17
|
+
4. 重复 2-3 直到阶段完成
|
|
18
|
+
5. **禁止**在没有运行 CLI 的情况下自行决定流程
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
# 首次启动(推荐指定变更名)
|
|
23
|
-
sillyspec run brainstorm --change 2026-05-28-agent-log-streaming
|
|
20
|
+
## 特殊步骤:requiresWait
|
|
24
21
|
|
|
25
|
-
|
|
26
|
-
sillyspec run brainstorm
|
|
22
|
+
某些步骤(如"对话式探索")需要等待用户输入。AI agent 可以:
|
|
27
23
|
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
- **方式一(推荐)**:通过自己的对话工具与用户交互,完成后直接 `--done --answer "用户回答"` 一步完成
|
|
25
|
+
- **方式二**:先 `--wait` 记录等待状态,再 `--continue --answer "用户回答"`,最后 `--done`
|
|
30
26
|
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
```bash
|
|
28
|
+
# 一步完成 wait + done(AI agent 已自行与用户交互)
|
|
29
|
+
sillyspec run brainstorm --done --change <变更名> --answer "信息够了,进入方案讨论" --output "需求已澄清"
|
|
33
30
|
|
|
34
|
-
#
|
|
35
|
-
sillyspec run brainstorm --
|
|
31
|
+
# 分步完成
|
|
32
|
+
sillyspec run brainstorm --wait --change <变更名> --reason "等待用户回答" --output "探索问题"
|
|
33
|
+
sillyspec run brainstorm --continue --answer "用户回答" --change <变更名>
|
|
34
|
+
sillyspec run brainstorm --done --change <变更名> --output "需求已澄清"
|
|
36
35
|
```
|
|
37
36
|
|
|
38
37
|
## 注意
|
|
39
|
-
- 推荐指定 `--change <变更名>`(格式:`YYYY-MM-DD-<简短描述>`),不指定时自动生成
|
|
38
|
+
- 推荐指定 `--change <变更名>`(格式:`YYYY-MM-DD-<简短描述>`),不指定时自动生成
|
|
40
39
|
- 步骤 prompt 由 CLI 管理,不需要手动读取
|
|
41
40
|
- 依赖 scan 阶段完成,CLI 会自动提醒
|
|
42
|
-
- brainstorm 阶段末步会自动生成四件套(proposal.md + design.md + requirements.md + tasks.md)
|
|
43
41
|
- brainstorm 完成后,运行 `sillyspec run plan --change <变更名>` 进入实现计划
|
|
42
|
+
|
|
43
|
+
## 用户指令
|
|
44
|
+
$ARGUMENTS
|
|
@@ -11,11 +11,18 @@ description: 用于按 plan 执行代码实现。适合用户说"开始写代码
|
|
|
11
11
|
|
|
12
12
|
**你必须使用 exec 工具(shell)执行以下命令,不要自己编造流程:**
|
|
13
13
|
|
|
14
|
-
1. 运行 `sillyspec run execute` —
|
|
14
|
+
1. 运行 `sillyspec run execute` — CLI 会自动创建 worktree 隔离环境,然后输出步骤 prompt
|
|
15
15
|
2. 按照输出的 prompt **严格执行**,不要跳过或自行添加步骤
|
|
16
16
|
3. 步骤完成后,运行 `sillyspec run execute --done --output "你的摘要"`
|
|
17
17
|
4. 重复 2-3 直到阶段完成
|
|
18
18
|
5. **禁止**在没有运行 CLI 的情况下自行决定流程
|
|
19
19
|
|
|
20
|
+
## Worktree 隔离
|
|
21
|
+
|
|
22
|
+
- CLI 启动 execute 阶段时**自动创建 git worktree**,AI agent 不需要手动创建
|
|
23
|
+
- Worktree 路径在 Step 3(确认 worktree 路径)中输出,后续子代理的 cwd 必须设为该路径
|
|
24
|
+
- **禁止跳过 worktree 或在主仓库直接写代码**
|
|
25
|
+
- 如果 worktree 创建失败,CLI 会报错并退出,需要排查后再重试
|
|
26
|
+
|
|
20
27
|
## 用户指令
|
|
21
28
|
$ARGUMENTS
|
package/package.json
CHANGED
package/src/db.js
CHANGED
|
@@ -175,6 +175,10 @@ export class DB {
|
|
|
175
175
|
this._migrateAddColumn('steps', 'wait_options', 'TEXT');
|
|
176
176
|
this._migrateAddColumn('steps', 'wait_answer', 'TEXT');
|
|
177
177
|
this._migrateAddColumn('steps', 'waited_at', 'TEXT');
|
|
178
|
+
// repeatableWait support
|
|
179
|
+
this._migrateAddColumn('steps', 'wait_answers', 'TEXT'); // JSON array
|
|
180
|
+
this._migrateAddColumn('steps', 'wait_round', 'INTEGER');
|
|
181
|
+
this._migrateAddColumn('steps', 'max_wait_rounds', 'INTEGER');
|
|
178
182
|
}
|
|
179
183
|
|
|
180
184
|
/**
|
|
@@ -126,6 +126,96 @@ function isPathInside(child, parent) {
|
|
|
126
126
|
return absChild === absParent || absChild.startsWith(absParent + path.sep)
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
function toPosixPath(filePath) {
|
|
130
|
+
return filePath.replace(/\\/g, '/')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function parseFrontmatter(content) {
|
|
134
|
+
if (!content.startsWith('---\n') && !content.startsWith('---\r\n')) return {}
|
|
135
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/)
|
|
136
|
+
if (!match) return {}
|
|
137
|
+
const result = {}
|
|
138
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
139
|
+
const m = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/)
|
|
140
|
+
if (!m) continue
|
|
141
|
+
result[m[1]] = m[2].replace(/^['"]|['"]$/g, '').trim()
|
|
142
|
+
}
|
|
143
|
+
return result
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function parseTimestamp(value) {
|
|
147
|
+
if (!value) return null
|
|
148
|
+
const time = Date.parse(value)
|
|
149
|
+
return Number.isNaN(time) ? null : time
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function getScanDocInfo(filePath) {
|
|
153
|
+
const normalized = toPosixPath(path.resolve(filePath))
|
|
154
|
+
const match = normalized.match(/^(.*)\/docs\/([^/]+)\/scan\/([^/]+\.md)$/)
|
|
155
|
+
if (!match) return null
|
|
156
|
+
return {
|
|
157
|
+
specRoot: path.resolve(match[1]),
|
|
158
|
+
projectName: match[2],
|
|
159
|
+
docName: match[3],
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function readScanGuard(scanDocInfo, projectRoot) {
|
|
164
|
+
const candidates = [
|
|
165
|
+
path.join(scanDocInfo.specRoot, '.runtime', 'scan-guard.json'),
|
|
166
|
+
path.join(projectRoot, '.sillyspec', '.runtime', 'scan-guard.json'),
|
|
167
|
+
]
|
|
168
|
+
for (const p of candidates) {
|
|
169
|
+
if (!existsSync(p)) continue
|
|
170
|
+
try {
|
|
171
|
+
return JSON.parse(readFileSync(p, 'utf8'))
|
|
172
|
+
} catch {
|
|
173
|
+
return null
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return null
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function shouldBlockScanDocOverwrite(filePath, projectRoot) {
|
|
180
|
+
const scanDocInfo = getScanDocInfo(filePath)
|
|
181
|
+
if (!scanDocInfo || !existsSync(filePath)) return { blocked: false }
|
|
182
|
+
|
|
183
|
+
const guard = readScanGuard(scanDocInfo, projectRoot)
|
|
184
|
+
if (!guard || guard.forceRescan) return { blocked: false }
|
|
185
|
+
|
|
186
|
+
let frontmatter = {}
|
|
187
|
+
try {
|
|
188
|
+
frontmatter = parseFrontmatter(readFileSync(filePath, 'utf8'))
|
|
189
|
+
} catch {
|
|
190
|
+
return { blocked: false }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const relPath = toPosixPath(path.relative(projectRoot, filePath))
|
|
194
|
+
if (frontmatter.source_commit && guard.sourceCommit && frontmatter.source_commit !== guard.sourceCommit) {
|
|
195
|
+
return {
|
|
196
|
+
blocked: true,
|
|
197
|
+
reason: [
|
|
198
|
+
`scan 覆盖保护:${relPath} 的 source_commit=${frontmatter.source_commit} 与当前 scan source_commit=${guard.sourceCommit} 不一致。`,
|
|
199
|
+
'如确认要重新生成,请重新运行 scan 并添加 --force-rescan。',
|
|
200
|
+
].join('\n')
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const existingUpdatedAt = parseTimestamp(frontmatter.updated_at)
|
|
205
|
+
const scanStartedAt = parseTimestamp(guard.startedAt)
|
|
206
|
+
if (existingUpdatedAt && scanStartedAt && existingUpdatedAt > scanStartedAt) {
|
|
207
|
+
return {
|
|
208
|
+
blocked: true,
|
|
209
|
+
reason: [
|
|
210
|
+
`scan 覆盖保护:${relPath} 的 updated_at 晚于本次 scan 开始时间,可能包含手工编辑。`,
|
|
211
|
+
'如确认要覆盖,请重新运行 scan 并添加 --force-rescan。',
|
|
212
|
+
].join('\n')
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return { blocked: false }
|
|
217
|
+
}
|
|
218
|
+
|
|
129
219
|
function isInsideWorktreeStorage(filePath, cwd) {
|
|
130
220
|
const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(cwd || process.cwd(), filePath)
|
|
131
221
|
return isPathInside(absPath, resolveWorktreeDir(cwd || process.cwd()))
|
|
@@ -489,12 +579,15 @@ export function shouldBlockWrite(filePath, cwd) {
|
|
|
489
579
|
const projectRoot = findProjectRoot(callerCwd)
|
|
490
580
|
const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(callerCwd, filePath)
|
|
491
581
|
|
|
492
|
-
// 1.
|
|
493
|
-
if (!isInsideWorktreeStorage(absPath, projectRoot) && matchFileWhitelist(absPath)) return { blocked: false }
|
|
494
|
-
|
|
495
|
-
// 2. 阶段门禁(使用 fallback 读取)
|
|
582
|
+
// 1. 阶段门禁(使用 fallback 读取)
|
|
496
583
|
const stage = readCurrentStage(projectRoot) || '(none)'
|
|
497
584
|
|
|
585
|
+
const scanGuardResult = shouldBlockScanDocOverwrite(absPath, projectRoot)
|
|
586
|
+
if (scanGuardResult.blocked) return scanGuardResult
|
|
587
|
+
|
|
588
|
+
// 2. 文件门禁:文档类/配置类始终放行,但 worktree 存储区内的源码必须继续走登记校验。
|
|
589
|
+
if (!isInsideWorktreeStorage(absPath, projectRoot) && matchFileWhitelist(absPath)) return { blocked: false }
|
|
590
|
+
|
|
498
591
|
if (!['execute', 'quick'].includes(stage)) {
|
|
499
592
|
return {
|
|
500
593
|
blocked: true,
|
package/src/index.js
CHANGED
|
@@ -40,7 +40,7 @@ SillySpec CLI — 规范驱动开发工具包
|
|
|
40
40
|
auto 连续推进 brainstorm→plan→execute→verify
|
|
41
41
|
|
|
42
42
|
可选阶段:
|
|
43
|
-
scan, brainstorm,
|
|
43
|
+
scan, brainstorm, plan, execute, verify, archive
|
|
44
44
|
quick, explore, status, doctor
|
|
45
45
|
|
|
46
46
|
sillyspec progress <cmd> 进度记录(轻量,不强制顺序)
|
package/src/progress.js
CHANGED
|
@@ -12,16 +12,32 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync } from 'fs';
|
|
15
|
-
import { join, basename } from 'path';
|
|
15
|
+
import { join, basename, dirname, resolve } from 'path';
|
|
16
16
|
import { DB } from './db.js';
|
|
17
17
|
|
|
18
18
|
// 默认规范目录名(相对于 cwd)
|
|
19
19
|
const SPEC_DIR_NAME = '.sillyspec';
|
|
20
20
|
const RUNTIME_SUBDIR = '.runtime';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 向上查找含 .sillyspec 目录的祖先目录,类似 git 找 .git 的逻辑。
|
|
24
|
+
* 找到则返回 <祖先>/.sillyspec,否则 fallback 到 <cwd>/.sillyspec。
|
|
25
|
+
*/
|
|
26
|
+
export function resolveSpecDir(startDir) {
|
|
27
|
+
let dir = resolve(startDir);
|
|
28
|
+
while (true) {
|
|
29
|
+
const candidate = join(dir, SPEC_DIR_NAME);
|
|
30
|
+
if (existsSync(candidate)) return candidate;
|
|
31
|
+
const parent = dirname(dir);
|
|
32
|
+
if (parent === dir) break; // 到达根目录
|
|
33
|
+
dir = parent;
|
|
34
|
+
}
|
|
35
|
+
return join(resolve(startDir), SPEC_DIR_NAME);
|
|
36
|
+
}
|
|
21
37
|
const CHANGES_SUBDIR = 'changes';
|
|
22
38
|
const GLOBAL_FILE = 'global.json';
|
|
23
39
|
const CURRENT_VERSION = 3;
|
|
24
|
-
const VALID_STAGES = ['scan', 'brainstorm', '
|
|
40
|
+
const VALID_STAGES = ['scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive', 'quick', 'explore'];
|
|
25
41
|
const VALID_STATUSES = ['pending', 'in-progress', 'completed', 'failed', 'blocked', 'waiting'];
|
|
26
42
|
|
|
27
43
|
const STAGE_LABELS = {
|
|
@@ -62,9 +78,10 @@ export class ProgressManager {
|
|
|
62
78
|
|
|
63
79
|
// ── 路径工具 ──
|
|
64
80
|
|
|
65
|
-
/** 获取 specDir
|
|
81
|
+
/** 获取 specDir(优先自定义,否则向上查找含 .sillyspec 的目录,fallback 到 cwd/.sillyspec) */
|
|
66
82
|
_getSpecDir(cwd) {
|
|
67
|
-
|
|
83
|
+
if (this._customSpecDir) return this._customSpecDir;
|
|
84
|
+
return resolveSpecDir(cwd);
|
|
68
85
|
}
|
|
69
86
|
|
|
70
87
|
_runtimePath(cwd, ...parts) {
|
|
@@ -207,21 +224,29 @@ export class ProgressManager {
|
|
|
207
224
|
if (stageIds.length > 0) {
|
|
208
225
|
const placeholders = stageIds.map(() => '?').join(',');
|
|
209
226
|
stepRows = sqlDb.exec(
|
|
210
|
-
`SELECT stage_id, name, status, output, completed_at, ordering, wait_reason, wait_options, wait_answer, waited_at FROM steps WHERE stage_id IN (${placeholders}) ORDER BY stage_id, ordering`,
|
|
227
|
+
`SELECT stage_id, name, status, output, completed_at, ordering, wait_reason, wait_options, wait_answer, waited_at, wait_answers, wait_round, max_wait_rounds FROM steps WHERE stage_id IN (${placeholders}) ORDER BY stage_id, ordering`,
|
|
211
228
|
stageIds
|
|
212
229
|
);
|
|
213
230
|
}
|
|
214
231
|
// 按阶段分组步骤
|
|
215
232
|
const stepsByStage = {};
|
|
216
233
|
if (stepRows && stepRows.length > 0) {
|
|
217
|
-
for (const
|
|
234
|
+
for (const row of stepRows[0].values) {
|
|
235
|
+
const [stageId, name, status, output, completedAt, ordering, waitReason, waitOptions, waitAnswer, waitedAt, waitAnswersJson, waitRound, maxWaitRounds] = row;
|
|
218
236
|
if (!stepsByStage[stageId]) stepsByStage[stageId] = [];
|
|
237
|
+
let waitAnswers = null;
|
|
238
|
+
if (waitAnswersJson) {
|
|
239
|
+
try { waitAnswers = JSON.parse(waitAnswersJson); } catch {}
|
|
240
|
+
}
|
|
219
241
|
stepsByStage[stageId].push({
|
|
220
242
|
name, status, output, completedAt,
|
|
221
243
|
...(waitReason ? { waitReason } : {}),
|
|
222
244
|
...(waitOptions ? { waitOptions } : {}),
|
|
223
245
|
...(waitAnswer ? { waitAnswer } : {}),
|
|
224
246
|
...(waitedAt ? { waitedAt } : {}),
|
|
247
|
+
...(waitAnswers ? { waitAnswers } : {}),
|
|
248
|
+
...(waitRound != null ? { waitRound } : {}),
|
|
249
|
+
...(maxWaitRounds != null ? { maxWaitRounds } : {}),
|
|
225
250
|
});
|
|
226
251
|
}
|
|
227
252
|
}
|
|
@@ -257,6 +282,9 @@ export class ProgressManager {
|
|
|
257
282
|
...(s.waitOptions ? { waitOptions: s.waitOptions } : {}),
|
|
258
283
|
...(s.waitAnswer ? { waitAnswer: s.waitAnswer } : {}),
|
|
259
284
|
...(s.waitedAt ? { waitedAt: s.waitedAt } : {}),
|
|
285
|
+
...(s.waitAnswers ? { waitAnswers: s.waitAnswers } : {}),
|
|
286
|
+
...(s.waitRound != null ? { waitRound: s.waitRound } : {}),
|
|
287
|
+
...(s.maxWaitRounds != null ? { maxWaitRounds: s.maxWaitRounds } : {}),
|
|
260
288
|
}));
|
|
261
289
|
stages[stage] = {
|
|
262
290
|
status: info.status,
|
|
@@ -340,9 +368,11 @@ export class ProgressManager {
|
|
|
340
368
|
// UPSERT 步骤(先删再插,steps 表无 UNIQUE 约束)
|
|
341
369
|
sqlDb.run('DELETE FROM steps WHERE stage_id = ? AND name = ?', [stageId, step.name]);
|
|
342
370
|
sqlDb.run(
|
|
343
|
-
'INSERT INTO steps (stage_id, name, status, output, completed_at, ordering, wait_reason, wait_options, wait_answer, waited_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
|
371
|
+
'INSERT INTO steps (stage_id, name, status, output, completed_at, ordering, wait_reason, wait_options, wait_answer, waited_at, wait_answers, wait_round, max_wait_rounds) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
|
344
372
|
[stageId, step.name, step.status || 'pending', step.output || null, step.completedAt || null, i,
|
|
345
|
-
step.waitReason
|
|
373
|
+
step.waitReason ?? null, step.waitOptions ?? null, step.waitAnswer ?? null, step.waitedAt ?? null,
|
|
374
|
+
Array.isArray(step.waitAnswers) ? JSON.stringify(step.waitAnswers) : null,
|
|
375
|
+
step.waitRound ?? null, step.maxWaitRounds ?? null]
|
|
346
376
|
);
|
|
347
377
|
}
|
|
348
378
|
}
|
|
@@ -411,11 +441,8 @@ export class ProgressManager {
|
|
|
411
441
|
VALUES (?, ?, ?)`,
|
|
412
442
|
[changeName, now, now]
|
|
413
443
|
);
|
|
414
|
-
//
|
|
415
|
-
|
|
416
|
-
`UPDATE changes SET status = 'active', last_active = ? WHERE name = ? AND status = 'archived'`,
|
|
417
|
-
[now, changeName]
|
|
418
|
-
);
|
|
444
|
+
// 注意:不复活已归档的变更——归档是不可逆操作
|
|
445
|
+
// 如果变更已存在且为 archived,保持 archived 状态不变
|
|
419
446
|
});
|
|
420
447
|
}
|
|
421
448
|
|
|
@@ -634,7 +661,7 @@ export class ProgressManager {
|
|
|
634
661
|
const changeId = changeRow[0].values[0][0];
|
|
635
662
|
|
|
636
663
|
// 批量插入 9 个阶段(INSERT OR IGNORE 跳过已存在的)
|
|
637
|
-
const allStages = ['scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive', 'quick', 'explore'
|
|
664
|
+
const allStages = ['scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive', 'quick', 'explore'];
|
|
638
665
|
for (const stage of allStages) {
|
|
639
666
|
sqlDb.run(
|
|
640
667
|
`INSERT OR IGNORE INTO stages (change_id, stage, status)
|