sillyspec 3.25.0 → 3.25.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-doctor/SKILL.md +9 -0
- package/.playwright-mcp/console-2026-07-27T15-45-50-621Z.log +1 -0
- package/.playwright-mcp/page-2026-07-27T15-45-51-459Z.yml +144 -0
- package/.playwright-mcp/prompt-final.png +0 -0
- package/.playwright-mcp/prompt-view.png +0 -0
- package/CLAUDE.md +18 -0
- package/package.json +1 -1
- package/src/change-risk-profile.js +60 -12
- package/src/check-primitives.js +65 -0
- package/src/db.js +28 -8
- package/src/hooks/worktree-guard.js +9 -2
- package/src/index.js +140 -11
- package/src/machine-interface.js +13 -7
- package/src/progress/change-registry.js +217 -0
- package/src/progress/consistency-doctor.js +259 -0
- package/src/progress/shared.js +33 -0
- package/src/progress/stage-machine.js +535 -0
- package/src/progress/step-store.js +256 -0
- package/src/progress.js +75 -1191
- package/src/quicklog.js +3 -1
- package/src/run/command.js +1088 -0
- package/src/run/complete-handlers.js +874 -0
- package/src/run/complete.js +725 -0
- package/src/run/gates.js +326 -0
- package/src/run/prompt.js +619 -0
- package/src/run/quick-audit.js +107 -0
- package/src/run/scan-profile.js +238 -0
- package/src/run/shared.js +418 -0
- package/src/run/stage.js +414 -0
- package/src/run.js +23 -4379
- package/src/stage-contract-engine.js +150 -0
- package/src/stage-contract-spec.js +517 -0
- package/src/stage-contract.js +87 -150
- package/src/stage-review.js +63 -0
- package/src/stages/brainstorm.js +3 -1
- package/src/stages/execute.js +5 -2
- package/src/stages/plan-postcheck.js +38 -27
- package/src/stages/plan.js +12 -49
- package/src/stages/propose.js +2 -1
- package/src/stages/scan.js +3 -1
- package/src/workflow.js +9 -17
- package/src/worktree-apply.js +5 -2
- package/src/worktree-deps.js +114 -32
- package/src/worktree.js +23 -11
- package/src/brainstorm-postcheck.js +0 -158
package/src/run/stage.js
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* run/stage.js(W6 Step8b 从 run.js 抽出)。
|
|
3
|
+
*
|
|
4
|
+
* `run <stage>` 默认路径的执行主干(runStage)+ 其私有 helper:
|
|
5
|
+
* - runStage:输出当前 step prompt(重 execute/scan/quick 启动期副作用——worktree 创建、
|
|
6
|
+
* executeRunId 固定、scanProfile 裁剪、quick baseline 录入、plan design 契约校验、noAI 步骤代理)
|
|
7
|
+
* - autoDetectChange:唯一变更目录时自动设置 currentChange(runStage 私有)
|
|
8
|
+
* - executePlanPostcheck:noAI planPostcheck 步骤代理(runStage 私有)
|
|
9
|
+
* - ensureDepsFreshness:execute 入口已存在 worktree 时 deps 自检重供给(runStage 私有)
|
|
10
|
+
*
|
|
11
|
+
* 安全锚:run.js 始终 barrel。runStage 由 run.js import 回来(无 test 直接 import,无需 re-export)。
|
|
12
|
+
* checkApproval(runStage + runAutoMode 共用)已先下沉 shared.js(Step8a),避免 command/stage 环依赖。
|
|
13
|
+
*
|
|
14
|
+
* 路径修正(相对 src/run/):
|
|
15
|
+
* - 动态 import './worktree.js' / './task-review.js' / './stages/plan.js' /
|
|
16
|
+
* './stages/plan-postcheck.js' / './worktree-deps.js' → '../'(src/ 下退一层)
|
|
17
|
+
* - 'child_process'(execSync)裸模块名不变
|
|
18
|
+
*/
|
|
19
|
+
import { join, dirname } from 'node:path'
|
|
20
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'
|
|
21
|
+
import { writeAtomicSync } from '../fs-atomic.js'
|
|
22
|
+
import { resolveSpecDir, resolveChangeDir, triggerSync, safeGit, parsePorcelainPath, formatWaitOptions, checkApproval, getStageSteps } from './shared.js'
|
|
23
|
+
import { computeScanProfile, applyScanProfileSteps, executeScanPreflight, executeScanPostcheck } from './scan-profile.js'
|
|
24
|
+
import { outputStep } from './prompt.js'
|
|
25
|
+
import { allocateQuicklogEntry } from '../quicklog.js'
|
|
26
|
+
import { checkTransition } from '../stage-contract.js'
|
|
27
|
+
|
|
28
|
+
export async function runStage(pm, progress, stageName, cwd, changeName, skipApproval = false, platformOpts = {}, quickOpts = {}) {
|
|
29
|
+
const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
|
|
30
|
+
// 状态转换校验
|
|
31
|
+
const prevStage = progress.currentStage || ''
|
|
32
|
+
// task-07: 提取 prevStage 的 stageData,传给 checkTransition 检测 failed_post_check 门控
|
|
33
|
+
const fromStageData = (progress.stages && prevStage && progress.stages[prevStage]) || undefined
|
|
34
|
+
const transition = checkTransition(prevStage, stageName, fromStageData ? { fromStageData } : {})
|
|
35
|
+
if (!transition.allowed) {
|
|
36
|
+
console.error(`❌ 阶段转换不允许: ${prevStage || '(起始)'} → ${stageName}`)
|
|
37
|
+
console.error(` 原因: ${transition.reason}`)
|
|
38
|
+
console.error(` 提示: 使用 --skip-approval 绕过(需明确意图)`)
|
|
39
|
+
if (!skipApproval) {
|
|
40
|
+
process.exit(1)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// execute 阶段启动前检查审批
|
|
45
|
+
if (stageName === 'execute' && !skipApproval) {
|
|
46
|
+
const approval = await checkApproval(cwd, changeName, platformOpts)
|
|
47
|
+
if (approval) {
|
|
48
|
+
if (approval.status === 'rejected') {
|
|
49
|
+
console.error(`❌ 变更 ${changeName} 的执行已被拒绝:${approval.reason || '无原因'}`)
|
|
50
|
+
process.exit(1)
|
|
51
|
+
}
|
|
52
|
+
if (approval.status === 'pending') {
|
|
53
|
+
console.log(`⏳ 变更 ${changeName} 的执行审批待处理中...`)
|
|
54
|
+
console.log(' 提示:使用 --skip-approval 跳过审批检查')
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// execute 阶段:CLI 自动创建 worktree(不等 AI agent)
|
|
60
|
+
if (stageName === 'execute' && changeName) {
|
|
61
|
+
const effectiveChange = changeName
|
|
62
|
+
const { WorktreeManager } = await import('../worktree.js')
|
|
63
|
+
const wm = new WorktreeManager({ cwd })
|
|
64
|
+
const existingMeta = wm.getMeta(effectiveChange)
|
|
65
|
+
if (existingMeta) {
|
|
66
|
+
console.log(`🔗 worktree 已存在: ${existingMeta.worktreePath} (${existingMeta.mode})`)
|
|
67
|
+
} else {
|
|
68
|
+
try {
|
|
69
|
+
const result = wm.create(effectiveChange)
|
|
70
|
+
console.log(`🔗 worktree 已创建: ${result.worktreePath} (分支: ${result.branch}, 模式: ${result.mode})`)
|
|
71
|
+
} catch (e) {
|
|
72
|
+
console.error(`❌ worktree 创建失败: ${e.message}`)
|
|
73
|
+
console.error(` 修复建议:`)
|
|
74
|
+
console.error(` 1. 运行 sillyspec worktree doctor --fix 检查并修复 worktree 状态`)
|
|
75
|
+
console.error(` 2. 或手动清理残留:git worktree prune && git branch -D sillyspec/${effectiveChange}`)
|
|
76
|
+
console.error(` 3. 必要时删除残留目录 .sillyspec/.runtime/worktrees/${effectiveChange}/ 后重试`)
|
|
77
|
+
process.exit(1)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// 入口 deps 自检(D-002):已存在 worktree(create short-circuit 不供给)时重供给
|
|
81
|
+
if (existingMeta) {
|
|
82
|
+
const execSpecBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
|
|
83
|
+
await ensureDepsFreshness(cwd, effectiveChange, execSpecBase, existingMeta)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── execute 阶段启动时固定 executeRunId(绑定变更名,避免跨变更复用) ──
|
|
88
|
+
let currentExecuteRunId = null
|
|
89
|
+
if (stageName === 'execute') {
|
|
90
|
+
const { generateExecuteRunId } = await import('../task-review.js')
|
|
91
|
+
const execSpecBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
|
|
92
|
+
const runtimeRoot = platformOpts?.runtimeRoot || join(execSpecBase, '.runtime')
|
|
93
|
+
const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
|
|
94
|
+
mkdirSync(runtimeRoot, { recursive: true })
|
|
95
|
+
// 优先读取已有的变更专属标记文件
|
|
96
|
+
try {
|
|
97
|
+
if (existsSync(runIdFile)) {
|
|
98
|
+
currentExecuteRunId = readFileSync(runIdFile, 'utf8').trim()
|
|
99
|
+
}
|
|
100
|
+
} catch {}
|
|
101
|
+
if (!currentExecuteRunId) {
|
|
102
|
+
currentExecuteRunId = generateExecuteRunId()
|
|
103
|
+
writeFileSync(runIdFile, currentExecuteRunId + '\n')
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 自动探测 currentChange
|
|
108
|
+
if (autoDetectChange(progress, cwd)) {
|
|
109
|
+
progress.lastActive = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
110
|
+
await pm._write(cwd, progress, changeName)
|
|
111
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const stageData = progress.stages[stageName]
|
|
115
|
+
if (!stageData || !stageData.steps) {
|
|
116
|
+
console.error(`❌ 阶段 ${stageName} 未初始化`)
|
|
117
|
+
process.exit(1)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 用户显式调用 sillyspec run <stage>:把它标记为当前阶段
|
|
121
|
+
if (progress.currentStage !== stageName) {
|
|
122
|
+
progress.currentStage = stageName
|
|
123
|
+
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
124
|
+
await pm._write(cwd, progress, changeName)
|
|
125
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const steps = stageData.steps
|
|
129
|
+
// ── 检查是否有 waiting step 需要先处理 ──
|
|
130
|
+
const waitingIdx = steps.findIndex(s => s.status === 'waiting')
|
|
131
|
+
if (waitingIdx !== -1) {
|
|
132
|
+
const ws = steps[waitingIdx]
|
|
133
|
+
console.error(`\n⏸️ Step ${waitingIdx + 1}/${steps.length} 正在等待用户输入:${ws.name}`)
|
|
134
|
+
if (ws.waitReason) console.error(` 原因:${ws.waitReason}`)
|
|
135
|
+
if (ws.waitOptions) console.error(` 选项:${formatWaitOptions(ws.waitOptions)}`)
|
|
136
|
+
console.error(`\n 普通运行无法跳过等待中的步骤。请先处理:`)
|
|
137
|
+
console.error(` sillyspec run ${stageName} --continue --answer "你的选择"${changeName ? ` --change ${changeName}` : ''}`)
|
|
138
|
+
process.exit(1)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let currentIdx = steps.findIndex(s => s.status !== 'completed' && s.status !== 'skipped')
|
|
142
|
+
|
|
143
|
+
// stale 步骤视为可执行(等同于 pending)
|
|
144
|
+
if (currentIdx !== -1 && steps[currentIdx].status === 'stale') {
|
|
145
|
+
steps[currentIdx].status = 'pending'
|
|
146
|
+
await pm._write(cwd, progress, changeName)
|
|
147
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── scanProfile: 根据 project 规模动态裁剪步骤 ──
|
|
151
|
+
let scanProfile = null
|
|
152
|
+
if (stageName === 'scan' && steps.length > 0 && currentIdx === 0) {
|
|
153
|
+
scanProfile = computeScanProfile(cwd, platformOpts)
|
|
154
|
+
console.log(`\n📊 Scan Profile: ${scanProfile.mode} (原因: ${scanProfile.reason})`)
|
|
155
|
+
if (scanProfile.mode !== 'deep') {
|
|
156
|
+
applyScanProfileSteps(stageData, scanProfile, cwd, platformOpts)
|
|
157
|
+
// 步骤被裁剪后 currentIdx 需要重新计算
|
|
158
|
+
currentIdx = 0
|
|
159
|
+
}
|
|
160
|
+
// 保存 profile 供后续 postcheck 使用
|
|
161
|
+
stageData.scanProfile = scanProfile
|
|
162
|
+
await pm._write(cwd, progress, changeName)
|
|
163
|
+
} else if (stageName === 'scan' && stageData.scanProfile) {
|
|
164
|
+
scanProfile = stageData.scanProfile
|
|
165
|
+
}
|
|
166
|
+
if (scanProfile) platformOpts.scanProfile = scanProfile
|
|
167
|
+
|
|
168
|
+
if (stageName === 'scan') {
|
|
169
|
+
try {
|
|
170
|
+
const gitResult = safeGit(cwd, ['rev-parse', 'HEAD'])
|
|
171
|
+
const scanGuard = {
|
|
172
|
+
name_zh: '扫描守卫',
|
|
173
|
+
sourceCommit: gitResult.value,
|
|
174
|
+
sourceCommitError: gitResult.error,
|
|
175
|
+
startedAt: new Date().toISOString(),
|
|
176
|
+
forceRescan: quickOpts?.isForceRescan || false,
|
|
177
|
+
}
|
|
178
|
+
const guardFile = join(specBase, '.runtime', 'scan-guard.json')
|
|
179
|
+
mkdirSync(dirname(guardFile), { recursive: true })
|
|
180
|
+
writeFileSync(guardFile, JSON.stringify(scanGuard, null, 2) + '\n')
|
|
181
|
+
if (scanGuard.forceRescan) {
|
|
182
|
+
console.log('🛡️ scan 覆盖保护已记录: --force-rescan 已开启')
|
|
183
|
+
} else {
|
|
184
|
+
console.log('🛡️ scan 覆盖保护已记录: existing scan docs require current source_commit/updated_at')
|
|
185
|
+
}
|
|
186
|
+
} catch (e) {
|
|
187
|
+
console.warn(`⚠️ scan 覆盖保护记录失败: ${e.message}`)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (currentIdx === -1) {
|
|
192
|
+
// 所有步骤已 completed/skipped,但阶段未盖到 completed —— 这是上次最后一步完成后、
|
|
193
|
+
// stage 升级事务未提交的崩溃中间态(正常完成会被上方 stageStatus==='completed' 守卫拦下)。
|
|
194
|
+
// 旧逻辑无条件清空 steps 为 pending 会丢掉已完成的进度且不可恢复。改为走正式
|
|
195
|
+
// completeStage:产物齐则补盖完成戳,不齐则给出 actionable 提示,永不静默清空步骤。
|
|
196
|
+
console.log(`\nℹ️ ${stageName} 所有步骤已完成,但阶段未标记完成(上次可能中断)。尝试补盖完成戳…`)
|
|
197
|
+
await pm.completeStage(cwd, stageName, changeName)
|
|
198
|
+
const after = await pm.read(cwd, changeName)
|
|
199
|
+
if (after?.stages?.[stageName]?.status === 'completed') {
|
|
200
|
+
console.log(` ✅ 已补盖完成戳。下一步: sillyspec run <下一阶段>,或 sillyspec run ${stageName} --status 查看。`)
|
|
201
|
+
process.exit(0)
|
|
202
|
+
}
|
|
203
|
+
// completeStage 已打印产物校验失败的明细;这里只补充恢复指引
|
|
204
|
+
console.error(`\n ⚠️ 已保留步骤进度(未清空)。修复产物后重跑,或:`)
|
|
205
|
+
console.error(` - 重新开始: sillyspec run ${stageName} --reset`)
|
|
206
|
+
console.error(` - 强制补盖: sillyspec progress complete-stage ${stageName}${changeName ? ` --change ${changeName}` : ''} --force`)
|
|
207
|
+
process.exit(1)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// quick 阶段:记录 baselineFiles + 分配 ql-ID(CLI 接管 QUICKLOG 写入)
|
|
211
|
+
// 幂等判据是 session guard.json 文件(跨进程可靠),不是 progress.quickGuard
|
|
212
|
+
// (D-003@v1:顶层 quickGuard 不跨进程持久化;agent 在 step 间用 `run quick` 取下一步
|
|
213
|
+
// prompt 时每个新进程都进此块,按文件判幂等才不会重复分配 ql-ID / 重复写条目)。
|
|
214
|
+
if (stageName === 'quick') {
|
|
215
|
+
const sessionGuardDir = join(specBase, '.runtime', 'quick-sessions', changeName)
|
|
216
|
+
const guardFile = join(sessionGuardDir, 'guard.json')
|
|
217
|
+
let existingGuard = null
|
|
218
|
+
try {
|
|
219
|
+
if (existsSync(guardFile)) existingGuard = JSON.parse(readFileSync(guardFile, 'utf8'))
|
|
220
|
+
} catch {}
|
|
221
|
+
if (existingGuard) {
|
|
222
|
+
// 跨进程重入:复用已分配的 ql-ID,跳过 baseline 重捕与分配(幂等)
|
|
223
|
+
progress.quickGuard = existingGuard
|
|
224
|
+
} else {
|
|
225
|
+
try {
|
|
226
|
+
const { execSync } = await import('child_process')
|
|
227
|
+
const gitStatus = execSync('git status --porcelain', { cwd, encoding: 'utf8', timeout: 10000 })
|
|
228
|
+
// 记录全部预存脏文件(含 untracked + .sillyspec/ 路径)。quick 会话期间自身写入的元数据
|
|
229
|
+
// (quicklog/.runtime/modules/_module-map 等)由 auditQuickCompletion 的 isQuickMetadata 精确豁免,
|
|
230
|
+
// 不需要这里粗放过滤 .sillyspec/——旧过滤致预存 untracked .sillyspec/changes/ 不进 baseline,
|
|
231
|
+
// 却在 audit 被当「危险(.sillyspec/)+新增」误判永久 blocked(ql-20260713-002-7628 修复)。
|
|
232
|
+
const baselineFiles = gitStatus
|
|
233
|
+
.split('\n').filter(Boolean)
|
|
234
|
+
.map(line => parsePorcelainPath(line))
|
|
235
|
+
.filter(Boolean)
|
|
236
|
+
const allowedFiles = quickOpts?.quickFiles || []
|
|
237
|
+
const allowNew = quickOpts?.isAllowNew || false
|
|
238
|
+
const forceBaseline = quickOpts?.isForceBaseline || false
|
|
239
|
+
const linkedChanges = Array.isArray(quickOpts?.linkedChanges) ? quickOpts.linkedChanges : []
|
|
240
|
+
// CLI 接管:分配 ql-ID + 写 QUICKLOG「进行中」条目 + 关联 tasks.md(持锁、当天唯一)
|
|
241
|
+
const gitUser = safeGit(cwd, ['config', 'user.name']).value || 'unknown'
|
|
242
|
+
const { qlId } = await allocateQuicklogEntry(specBase, gitUser, {
|
|
243
|
+
description: quickOpts?.taskDescription || '',
|
|
244
|
+
linkedChanges,
|
|
245
|
+
allowedFiles,
|
|
246
|
+
})
|
|
247
|
+
progress.quickGuard = {
|
|
248
|
+
sessionId: changeName,
|
|
249
|
+
name_zh: '快速任务守卫',
|
|
250
|
+
baselineCommit: safeGit(cwd, ['rev-parse', 'HEAD']).value,
|
|
251
|
+
baselineFiles,
|
|
252
|
+
allowedFiles,
|
|
253
|
+
allowNew,
|
|
254
|
+
forceBaseline,
|
|
255
|
+
linkedChanges,
|
|
256
|
+
quicklogId: qlId,
|
|
257
|
+
startedAt: new Date().toISOString(),
|
|
258
|
+
}
|
|
259
|
+
// 写入 .runtime/quick-sessions/<sessionId>/guard.json 供 worktree-guard hook 读取
|
|
260
|
+
// (D-002:按 session 存,多会话各自 guard 不互覆盖。runStage 作用域内 sessionId == changeName == quick-<uuid8>,见 §4.4/4.5)
|
|
261
|
+
mkdirSync(sessionGuardDir, { recursive: true })
|
|
262
|
+
writeAtomicSync(guardFile, JSON.stringify(progress.quickGuard, null, 2))
|
|
263
|
+
const parts = [`${baselineFiles.length} 个已有脏文件`]
|
|
264
|
+
if (allowedFiles.length > 0) parts.push(`${allowedFiles.length} 个 allowedFiles`)
|
|
265
|
+
if (allowNew) parts.push('允许新增文件')
|
|
266
|
+
console.log(`🛡️ quick 变更边界已记录: ${parts.join(', ')}`)
|
|
267
|
+
console.log(`📝 QUICKLOG 条目已创建: ${qlId}`)
|
|
268
|
+
await pm._write(cwd, progress, changeName)
|
|
269
|
+
} catch (e) {
|
|
270
|
+
console.warn(`⚠️ baseline 记录失败: ${e.message}`)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (currentIdx > 0) {
|
|
276
|
+
const completed = currentIdx
|
|
277
|
+
const total = steps.length
|
|
278
|
+
console.log(`⚠️ ${stageName} 已进行到第 ${currentIdx + 1}/${total} 步(前 ${completed} 步已完成)。`)
|
|
279
|
+
console.log(` 继续执行将从中断处恢复,用 --reset 可重新开始。\n`)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ── Brainstorm → Plan Contract:plan 启动前校验 design.md ──
|
|
283
|
+
if (stageName === 'plan' && currentIdx === 0) {
|
|
284
|
+
const changeDir = resolveChangeDir(cwd, progress, platformOpts?.specRoot || null)
|
|
285
|
+
const designPath = changeDir ? join(changeDir, 'design.md') : null
|
|
286
|
+
if (designPath && existsSync(designPath)) {
|
|
287
|
+
const { validateDesignForPlan } = await import('../stages/plan.js')
|
|
288
|
+
const designContent = readFileSync(designPath, 'utf8')
|
|
289
|
+
const designValidation = validateDesignForPlan(designContent)
|
|
290
|
+
if (!designValidation.ok) {
|
|
291
|
+
console.error(`\n❌ Brainstorm → Plan Contract 校验失败:`)
|
|
292
|
+
for (const err of designValidation.errors) console.error(` - ${err}`)
|
|
293
|
+
console.error(`\n design.md 不满足 plan 契约,请先修复后重试。`)
|
|
294
|
+
console.error(` 提示:sillyspec run brainstorm --reopen --from-step <步骤> 修订设计文档`)
|
|
295
|
+
process.exit(1)
|
|
296
|
+
}
|
|
297
|
+
if (designValidation.warnings.length > 0) {
|
|
298
|
+
console.log(`⚠️ Design contract 警告(不阻断):`)
|
|
299
|
+
for (const w of designValidation.warnings) console.log(` - ${w}`)
|
|
300
|
+
console.log()
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const defSteps = await getStageSteps(stageName, cwd, progress, platformOpts?.specRoot || null)
|
|
306
|
+
if (defSteps && defSteps[currentIdx]) {
|
|
307
|
+
// noAI 步骤自动完成(CLI-only,不需要 Agent 参与)
|
|
308
|
+
if (defSteps[currentIdx].noAI || stageData.steps[currentIdx]?.noAI) {
|
|
309
|
+
const stepName = defSteps[currentIdx].name
|
|
310
|
+
const cliAction = defSteps[currentIdx]._cliAction || stageData.steps[currentIdx]?._cliAction
|
|
311
|
+
console.log(`⚙️ Step ${currentIdx + 1}/${stageData.steps.length}: ${stepName}(CLI 自动执行,无需 Agent)`)
|
|
312
|
+
if (cliAction === 'scanPreflight') {
|
|
313
|
+
await executeScanPreflight(cwd, platformOpts, scanProfile)
|
|
314
|
+
} else if (cliAction === 'scanPostcheck') {
|
|
315
|
+
await executeScanPostcheck(cwd, platformOpts, scanProfile)
|
|
316
|
+
} else if (cliAction === 'planPostcheck') {
|
|
317
|
+
await executePlanPostcheck(cwd, platformOpts, progress)
|
|
318
|
+
}
|
|
319
|
+
stageData.steps[currentIdx].status = 'completed'
|
|
320
|
+
stageData.steps[currentIdx].completedAt = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
321
|
+
await pm._write(cwd, progress, changeName)
|
|
322
|
+
// 自动前进到下一步
|
|
323
|
+
const nextIdx = stageData.steps.findIndex(s => s.status === 'pending' || s.status === 'in-progress')
|
|
324
|
+
if (nextIdx !== -1 && defSteps[nextIdx]) {
|
|
325
|
+
console.log('')
|
|
326
|
+
await outputStep(stageName, nextIdx, defSteps, cwd, changeName, progress.project || null, platformOpts)
|
|
327
|
+
} else {
|
|
328
|
+
// 所有步骤完成
|
|
329
|
+
stageData.status = 'completed'
|
|
330
|
+
stageData.completedAt = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
331
|
+
await pm._write(cwd, progress, changeName)
|
|
332
|
+
console.log(`\n✅ ${stageName} 阶段全部完成。`)
|
|
333
|
+
}
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
await outputStep(stageName, currentIdx, defSteps, cwd, changeName, progress.project || null, platformOpts)
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* 自动探测并设置 currentChange(唯一变更目录时)
|
|
347
|
+
* @returns {boolean} 是否设置了 currentChange
|
|
348
|
+
*/
|
|
349
|
+
function autoDetectChange(progress, cwd, specDir = null) {
|
|
350
|
+
if (progress.currentChange) return false
|
|
351
|
+
const changesDir = join(specDir || resolveSpecDir(cwd), 'changes')
|
|
352
|
+
if (!existsSync(changesDir)) return false
|
|
353
|
+
const entries = readdirSync(changesDir, { withFileTypes: true })
|
|
354
|
+
.filter(e => e.isDirectory() && e.name !== 'archive')
|
|
355
|
+
if (entries.length === 1) {
|
|
356
|
+
progress.currentChange = entries[0].name
|
|
357
|
+
return true
|
|
358
|
+
}
|
|
359
|
+
return false
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Plan postcheck 的执行代理:委托给 plan-postcheck.js 模块
|
|
365
|
+
*/
|
|
366
|
+
async function executePlanPostcheck(cwd, platformOpts, progress) {
|
|
367
|
+
// resolveChangeDir 从 ./shared.js 导入(W6 Step1 抽出的纯函数,原 run.js 本地函数);历史上曾误从 ./modules.js
|
|
368
|
+
// 导入(该模块未导出此函数,得到 undefined),导致 plan-postcheck.js:388 抛
|
|
369
|
+
// "resolveChangeDir is not a function"。详见 docs/sillyspec/plan-postcheck-resolvechangedir-not-a-function.md
|
|
370
|
+
const { executePlanPostcheck: runPostcheck } = await import('../stages/plan-postcheck.js')
|
|
371
|
+
await runPostcheck({
|
|
372
|
+
cwd,
|
|
373
|
+
specRoot: platformOpts?.specRoot,
|
|
374
|
+
resolveChangeDir,
|
|
375
|
+
progress
|
|
376
|
+
})
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* execute 入口 deps 自检(D-002,change 2026-06-28-worktree-deps-provision)。
|
|
383
|
+
* 已存在 worktree(create short-circuit 不供给)时,校验 depsStatus 缺失 / node_modules
|
|
384
|
+
* 丢失(missing) / lockfile 变化(stale) → 触发 provisionDeps 重供给并写回 meta。
|
|
385
|
+
*/
|
|
386
|
+
async function ensureDepsFreshness(cwd, changeName, specBase, worktreeMeta) {
|
|
387
|
+
if (!worktreeMeta || !worktreeMeta.worktreePath) return
|
|
388
|
+
const { provisionDeps, lockfileHash } = await import('../worktree-deps.js')
|
|
389
|
+
const wtPath = worktreeMeta.worktreePath
|
|
390
|
+
const nodeModulesExists = existsSync(join(wtPath, 'node_modules'))
|
|
391
|
+
const currentHash = lockfileHash(wtPath)
|
|
392
|
+
const noStatus = !worktreeMeta.depsStatus
|
|
393
|
+
const missing = ['linked', 'installed'].includes(worktreeMeta.depsStatus) && !nodeModulesExists
|
|
394
|
+
const stale = !!(worktreeMeta.depsLockHash && currentHash && currentHash !== worktreeMeta.depsLockHash)
|
|
395
|
+
if (!noStatus && !missing && !stale) return
|
|
396
|
+
const reason = noStatus ? 'depsStatus 缺失' : (missing ? 'node_modules 丢失' : 'lockfile 变化')
|
|
397
|
+
console.log(`🔄 worktree deps 自检:${reason}(depsStatus=${worktreeMeta.depsStatus || 'unknown'}),重新供给...`)
|
|
398
|
+
let deps = {}
|
|
399
|
+
try {
|
|
400
|
+
deps = provisionDeps(wtPath, cwd, { specBase }) || {}
|
|
401
|
+
} catch (e) {
|
|
402
|
+
deps = { depsStatus: 'failed', depsError: `provisionDeps crashed: ${e.message}` }
|
|
403
|
+
}
|
|
404
|
+
try {
|
|
405
|
+
const { WorktreeManager } = await import('../worktree.js')
|
|
406
|
+
const metaPath = join(new WorktreeManager({ cwd }).getWorktreePath(changeName), 'meta.json')
|
|
407
|
+
const updated = { ...worktreeMeta, ...deps }
|
|
408
|
+
writeAtomicSync(metaPath, JSON.stringify(updated, null, 2) + '\n')
|
|
409
|
+
console.log(`✅ deps 重新供给完成:depsStatus=${deps.depsStatus}`)
|
|
410
|
+
} catch (e) {
|
|
411
|
+
console.warn(`⚠️ deps meta 写回失败:${e.message}`)
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|