sillyspec 3.25.9 → 3.26.0
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-auto/SKILL.md +8 -13
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +8 -6
- package/.claude/skills/sillyspec-continue/SKILL.md +3 -3
- package/.claude/skills/sillyspec-doctor/SKILL.md +1 -1
- package/.claude/skills/sillyspec-execute/SKILL.md +10 -4
- package/.claude/skills/sillyspec-plan/SKILL.md +1 -1
- package/.claude/skills/sillyspec-quick/SKILL.md +3 -1
- package/.claude/skills/sillyspec-verify/SKILL.md +1 -1
- package/.sillyspec-platform-cleaned +1 -0
- package/.sillyspec-platform.json +7 -0
- package/README.md +9 -0
- package/package.json +2 -2
- package/src/change-list.js +11 -8
- package/src/change-risk-profile.js +22 -0
- package/src/db.js +170 -161
- package/src/dispatch/backends/local-agent.js +152 -0
- package/src/dispatch/backends/sillyhub-mcp.js +242 -0
- package/src/dispatch/probe.js +210 -0
- package/src/dispatch/strategy.js +121 -0
- package/src/doctor-diagnostics.js +19 -25
- package/src/fs-atomic.js +69 -69
- package/src/git-helper.js +66 -0
- package/src/hooks/worktree-guard.js +64 -55
- package/src/index.js +326 -36
- package/src/init.js +7 -14
- package/src/local-detect.js +120 -110
- package/src/machine-interface.js +54 -19
- package/src/migrate.js +14 -4
- package/src/modules.js +16 -5
- package/src/progress/change-registry.js +85 -70
- package/src/progress/consistency-doctor.js +71 -14
- package/src/progress/shared.js +8 -5
- package/src/progress/stage-machine.js +67 -65
- package/src/progress/step-store.js +72 -67
- package/src/progress.js +448 -236
- package/src/quicklog.js +59 -12
- package/src/run/command.js +78 -39
- package/src/run/complete-handlers.js +1028 -889
- package/src/run/complete.js +132 -212
- package/src/run/concurrent-detect.js +132 -0
- package/src/run/gates.js +302 -19
- package/src/run/prompt.js +22 -68
- package/src/run/quick-audit.js +10 -9
- package/src/run/shared.js +722 -600
- package/src/run/stage.js +43 -24
- package/src/scan-postcheck.js +9 -31
- package/src/sillyhub-mcp/client.js +394 -0
- package/src/sillyhub-mcp/config.js +62 -0
- package/src/spec-dir-typo.js +71 -0
- package/src/stage-contract-engine.js +22 -1
- package/src/stage-contract-spec.js +11 -4
- package/src/stage-contract.js +26 -3
- package/src/stage-review.js +118 -10
- package/src/stages/archive.js +4 -6
- package/src/stages/brainstorm-auto.js +26 -20
- package/src/stages/brainstorm.js +29 -5
- package/src/stages/cmd-existence.js +145 -0
- package/src/stages/doctor.js +10 -7
- package/src/stages/execute.js +64 -22
- package/src/stages/plan-postcheck.js +152 -0
- package/src/stages/plan.js +16 -31
- package/src/stages/quick.js +25 -9
- package/src/stages/scan.js +53 -8
- package/src/stages/status.js +0 -1
- package/src/stages/verify.js +14 -9
- package/src/sync.js +433 -16
- package/src/task-review.js +59 -1
- package/src/verify-postcheck.js +938 -862
- package/src/version.js +19 -0
- package/src/worktree-apply.js +218 -93
- package/src/worktree-deps.js +89 -5
- package/src/worktree.js +213 -111
- package/templates/prompts/taskcard-rules.md +27 -0
- package/templates/prompts/verify-probes.md +3 -3
- package/.claude/skills/sillyspec-propose/SKILL.md +0 -87
- package/src/stages/propose.js +0 -181
package/src/run/complete.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* run/complete.js(W6 Step7b 从 run.js 抽出)。
|
|
3
3
|
*
|
|
4
|
-
* step 完成处理核心:completeStep(调度主干——WAIT/
|
|
4
|
+
* step 完成处理核心:completeStep(调度主干——WAIT/requiresWait 门控 + 各 handler 调用 +
|
|
5
5
|
* completion path 收尾 + single-step path)+ skipStep/waitStep/continueStep(W6 Step7b-2 续搬)。
|
|
6
6
|
* 本 commit(7b-1)先搬 completeStep + 2 独占 helper(validateMetadata/validateFileLocations)。
|
|
7
7
|
*
|
|
@@ -15,106 +15,19 @@
|
|
|
15
15
|
* - node: join(path) + existsSync/readdirSync/readFileSync/mkdirSync/writeFileSync/appendFileSync/statSync(fs)
|
|
16
16
|
*/
|
|
17
17
|
import { join } from 'node:path'
|
|
18
|
-
import { existsSync,
|
|
19
|
-
import {
|
|
18
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync, appendFileSync } from 'node:fs'
|
|
19
|
+
import { writeAtomicSync } from '../fs-atomic.js'
|
|
20
|
+
import { withFileLock } from '../quicklog.js'
|
|
21
|
+
import { triggerSync, WAIT_MARKER_RE, getStageSteps, formatWaitOptions, resolveRuntimeRoot } from './shared.js'
|
|
20
22
|
import { outputStep } from './prompt.js'
|
|
21
|
-
import { enforceDepsGate, enforceReviewJsonGate,
|
|
22
|
-
import { handleArchiveConfirmStep, handlePlanGeneratePlanStep, handleScanProjectListStep, handleWorkflowPostCheck, handleQuickStageCompletion, handleExecuteWaveArtifact
|
|
23
|
-
import { stageRegistry } from '../stages/index.js'
|
|
23
|
+
import { enforceDepsGate, enforceReviewJsonGate, completeStageGates, readDesignScale } from './gates.js'
|
|
24
|
+
import { handleArchiveConfirmStep, handlePlanGeneratePlanStep, handleScanProjectListStep, handleWorkflowPostCheck, handleQuickStageCompletion, handleExecuteWaveArtifact } from './complete-handlers.js'
|
|
24
25
|
import { formatExecuteSummary } from '../worktree-apply.js'
|
|
26
|
+
import { isEndToEndTaskText } from '../change-risk-profile.js'
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const cutoff = Date.now() - 10 * 60 * 1000
|
|
31
|
-
const missing = []
|
|
32
|
-
|
|
33
|
-
function walk(dir) {
|
|
34
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
35
|
-
const full = join(dir, entry.name)
|
|
36
|
-
try {
|
|
37
|
-
if (entry.isDirectory()) { walk(full); continue }
|
|
38
|
-
if (!/\.(md|yaml|yml)$/.test(entry.name)) continue
|
|
39
|
-
const mtime = statSync(full).mtimeMs
|
|
40
|
-
if (mtime < cutoff) continue
|
|
41
|
-
const content = readFileSync(full, 'utf-8')
|
|
42
|
-
if (!content.includes('author:') && !content.includes('author:')) missing.push(full)
|
|
43
|
-
if (!content.includes('created_at:') && !content.includes('created_at:')) missing.push(full)
|
|
44
|
-
} catch (e) { /* skip unreadable files */ }
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
walk(changesDir)
|
|
49
|
-
const unique = [...new Set(missing)]
|
|
50
|
-
if (unique.length > 0) {
|
|
51
|
-
console.log(`\n⚠️ 以下文件缺少 author 或 created_at 元数据:`)
|
|
52
|
-
unique.forEach(f => console.log(` - ${f.replace(cwd + '/', '')}`))
|
|
53
|
-
console.log('请在文件头部添加 author(git 用户名)和 created_at(精确到秒)')
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* 读取 design.md frontmatter 的 scale 字段(brainstorm 末步写入 'small'|'large')。
|
|
59
|
-
* completeStep 的下一步提示据此分叉:small→quick,large/读不到→plan(fail-safe 走重流程)。
|
|
60
|
-
* 只解析首个 YAML frontmatter 块,避免误读正文里的 "scale:"。
|
|
61
|
-
*/
|
|
62
|
-
function readDesignScale(specBase, changeName) {
|
|
63
|
-
if (!changeName) return null
|
|
64
|
-
const designPath = join(specBase, 'changes', changeName, 'design.md')
|
|
65
|
-
if (!existsSync(designPath)) return null
|
|
66
|
-
const text = readFileSync(designPath, 'utf8')
|
|
67
|
-
const fm = text.match(/^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n/)
|
|
68
|
-
if (!fm) return null
|
|
69
|
-
const m = fm[1].match(/^scale:[ \t]*['"]?(\w+)/m)
|
|
70
|
-
return m ? m[1] : null
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* 验证关键文件是否存在于正确的变更目录下
|
|
75
|
-
* 防止 AI 将文件写到错误的路径
|
|
76
|
-
*/
|
|
77
|
-
function validateFileLocations(cwd, stageName, progress, changeName, specBase) {
|
|
78
|
-
const effectiveChange = changeName || progress.currentChange
|
|
79
|
-
if (!effectiveChange) return
|
|
80
|
-
|
|
81
|
-
const changeDir = join(specBase, 'changes', effectiveChange)
|
|
82
|
-
if (!existsSync(changeDir)) return
|
|
83
|
-
|
|
84
|
-
// 每个阶段完成后预期存在的文件
|
|
85
|
-
const expectedFiles = {
|
|
86
|
-
brainstorm: ['design.md', 'proposal.md', 'requirements.md', 'tasks.md'],
|
|
87
|
-
plan: ['plan.md'],
|
|
88
|
-
archive: ['module-impact.md'],
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const expected = expectedFiles[stageName]
|
|
92
|
-
if (!expected) return
|
|
93
|
-
|
|
94
|
-
const missing = []
|
|
95
|
-
for (const file of expected) {
|
|
96
|
-
if (!existsSync(join(changeDir, file))) {
|
|
97
|
-
missing.push(file)
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (missing.length > 0) {
|
|
102
|
-
console.log(`\n⚠️ 文件位置验证:以下文件未在变更目录中找到`)
|
|
103
|
-
console.log(` 变更目录:${changeDir.replace(cwd + '/', '')}/`)
|
|
104
|
-
for (const f of missing) {
|
|
105
|
-
// 检查是否写到了错误的位置
|
|
106
|
-
const wrongPath = join(specBase, 'changes', 'change', effectiveChange, f)
|
|
107
|
-
if (existsSync(wrongPath)) {
|
|
108
|
-
console.log(` ❌ ${f} — 不存在,但发现了错误路径:${wrongPath.replace(cwd + '/', '')}`)
|
|
109
|
-
console.log(` 提示:应该写入 ${changeDir.replace(cwd + '/', '')}/${f}`)
|
|
110
|
-
} else {
|
|
111
|
-
console.log(` ⬜ ${f} — 未找到(该阶段可能未产出此文件)`)
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
} else {
|
|
115
|
-
console.log(`\n✅ 文件位置验证:所有 ${expected.length} 个预期文件均在变更目录中`)
|
|
116
|
-
}
|
|
117
|
-
}
|
|
28
|
+
// validateMetadata / readDesignScale / validateFileLocations 已迁至 ./gates.js(completeStageGates
|
|
29
|
+
// 共享收尾管线,消除 noAI 末步 / continueStep 完成分支绕过 gate 的 S1/S2/S3 不对称)。
|
|
30
|
+
// completeStep 阶段完成分支(task-02 接入 completeStageGates)+ brainstorm 下一步提示改 import 自 gates.js。
|
|
118
31
|
|
|
119
32
|
/**
|
|
120
33
|
* 坑1:用 --done --answer 解掉「已 waiting 的步骤」。
|
|
@@ -147,10 +60,9 @@ export function resolveWaitingStepWithAnswer(steps, doneAnswer, nowStr) {
|
|
|
147
60
|
}
|
|
148
61
|
|
|
149
62
|
export async function completeStep(pm, progress, stageName, cwd, outputText, inputText = null, options = {}) {
|
|
150
|
-
const { printNext = true, confirm = false, changeName, platformOpts = {}, nonInteractive = false,
|
|
63
|
+
const { printNext = true, confirm = false, changeName, platformOpts = {}, nonInteractive = false, isForceBaseline = false, isAllowNew = false } = options
|
|
151
64
|
const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
|
|
152
65
|
const stageData = progress.stages[stageName]
|
|
153
|
-
const scanProfile = stageData?.scanProfile || null
|
|
154
66
|
|
|
155
67
|
// ── WAIT MARKER 硬校验 ──
|
|
156
68
|
// 如果 output 包含等待标记,拒绝 --done 推进
|
|
@@ -163,12 +75,6 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
163
75
|
process.exit(1)
|
|
164
76
|
}
|
|
165
77
|
}
|
|
166
|
-
|
|
167
|
-
// scanProfile 非 deep 模式:截断 outputText 减少 token 传递
|
|
168
|
-
let effectiveOutput = outputText
|
|
169
|
-
if (scanProfile && scanProfile.mode !== 'deep' && outputText && outputText.length > 1000) {
|
|
170
|
-
effectiveOutput = outputText.slice(0, 1000) + '\n\n…[输出已截断,完整内容见 artifact]'
|
|
171
|
-
}
|
|
172
78
|
if (!stageData || !stageData.steps) {
|
|
173
79
|
console.error(`❌ 阶段 ${stageName} 未初始化`)
|
|
174
80
|
process.exit(1)
|
|
@@ -181,13 +87,29 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
181
87
|
// requiresWait 步骤时会跳过它、把 --answer 静默丢弃,步骤永久卡 WAITING、末步报「等待用户输入」。
|
|
182
88
|
// 修复:带 doneAnswer 且存在 waiting 步骤时,把首个 waiting 拉回 pending + 补 waitAnswer,主流程
|
|
183
89
|
// requiresWait 门控见 waitAnswer 已置→不阻断→正常 completed。仅 --answer 触发,普通 --done 零变化。
|
|
184
|
-
const
|
|
90
|
+
const _doneAnswer = options && options.doneAnswer
|
|
91
|
+
// ── 多 waiting 歧义守卫(对齐 continueStep 742-751 的同名保护)──
|
|
92
|
+
// resolveWaitingStepWithAnswer 只 findIndex 解「第一个」waiting;阶段内 ≥2 个 waiting 时
|
|
93
|
+
// --done --answer 会静默把 answer 填给第一个,可能答错对象。与 --continue 对齐:多 waiting 时报错列出。
|
|
94
|
+
if (_doneAnswer) {
|
|
95
|
+
const waitingIdxs = steps.map((s, i) => (s && s.status === 'waiting') ? i : -1).filter(i => i !== -1)
|
|
96
|
+
if (waitingIdxs.length > 1) {
|
|
97
|
+
console.error(`❌ 检测到 ${waitingIdxs.length} 个等待中的步骤,--done --answer 无法确定解哪一个:`)
|
|
98
|
+
for (const i of waitingIdxs) {
|
|
99
|
+
console.error(` Step ${i + 1}: ${steps[i].name}${steps[i].waitReason ? `(${steps[i].waitReason})` : ''}`)
|
|
100
|
+
}
|
|
101
|
+
console.error(` 出路:用 --continue 逐个指定恢复(每次解一个 waiting,降到 1 个后即可 --done --answer):`)
|
|
102
|
+
console.error(` sillyspec run ${stageName} --continue --from-step <序号|名称> --answer "..."${changeName ? ` --change ${changeName}` : ''}`)
|
|
103
|
+
process.exit(1)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const _resolvedWaitIdx = resolveWaitingStepWithAnswer(steps, _doneAnswer, new Date().toLocaleString('zh-CN', { hour12: false }))
|
|
185
107
|
if (_resolvedWaitIdx !== -1) {
|
|
186
108
|
currentIdx = _resolvedWaitIdx
|
|
187
109
|
console.log(`⚠️ Step "${steps[_resolvedWaitIdx].name}" 此前处于 waiting,--done --answer 已补回答并拉回待完成。`)
|
|
188
110
|
}
|
|
189
111
|
if (currentIdx === -1) {
|
|
190
|
-
console.error('
|
|
112
|
+
console.error(`没有待完成的步骤(阶段 ${stageName} 已无 pending/in-progress 步骤)。当前阶段状态:${stageData?.status ?? '未知'}。用 \`sillyspec run ${stageName} --status\` 查看进度,或 \`sillyspec progress show\` 看全局下一步。`)
|
|
191
113
|
process.exit(1)
|
|
192
114
|
}
|
|
193
115
|
|
|
@@ -257,7 +179,7 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
257
179
|
// archive「确认归档」收尾:--confirm 门控 + archiveChangeDirectory 移动 + 推荐文档校验
|
|
258
180
|
// (抽成 handleArchiveConfirmStep;缺 --confirm 时返回 early-return 对象,completeStep 透传)
|
|
259
181
|
{
|
|
260
|
-
const _archiveEarlyReturn = await handleArchiveConfirmStep({ stageName, steps, currentIdx, confirm, outputText, pm, cwd, progress, changeName, specBase })
|
|
182
|
+
const _archiveEarlyReturn = await handleArchiveConfirmStep({ stageName, steps, currentIdx, confirm, outputText, pm, cwd, progress, changeName, specBase, platformOpts })
|
|
261
183
|
if (_archiveEarlyReturn) return _archiveEarlyReturn
|
|
262
184
|
}
|
|
263
185
|
|
|
@@ -311,14 +233,14 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
311
233
|
if (hasWaiting) {
|
|
312
234
|
// 有等待中的步骤,阶段未完成
|
|
313
235
|
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
314
|
-
|
|
236
|
+
pm._write(cwd, progress, changeName)
|
|
315
237
|
const wsIdx = steps.findIndex(s => s.status === 'waiting')
|
|
316
238
|
console.log(`\n⏸️ 阶段暂停:Step ${wsIdx + 1} 等待用户输入`)
|
|
317
239
|
if (steps[wsIdx].waitReason) console.log(` 原因:${steps[wsIdx].waitReason}`)
|
|
318
240
|
return { stageCompleted: false, currentIdx, nextPendingIdx: -1 }
|
|
319
241
|
}
|
|
320
242
|
// quick 收尾(W6 Step6b 抽至 complete-handlers.js handleQuickStageCompletion)
|
|
321
|
-
await handleQuickStageCompletion({ stageName, steps, currentIdx, cwd, progress, changeName, specBase, outputText, confirm, isForceBaseline, isAllowNew, platformOpts })
|
|
243
|
+
await handleQuickStageCompletion({ stageName, steps, currentIdx, cwd, progress, changeName, specBase, outputText, confirm, isForceBaseline, isAllowNew, platformOpts, pm })
|
|
322
244
|
|
|
323
245
|
// ── reopen --done 回填(坑 brainstorm-reopen-step-state-desync)──
|
|
324
246
|
// nextPendingIdx === -1 且无 waiting,说明要进阶段完成分支。此时若存在 stale 步骤
|
|
@@ -333,53 +255,37 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
333
255
|
st.status = 'completed'
|
|
334
256
|
st.completedAt = st.completedAt || nowStr
|
|
335
257
|
}
|
|
336
|
-
|
|
258
|
+
pm._write(cwd, progress, changeName)
|
|
337
259
|
console.log(` ⚠️ 同步回填 ${staleSteps.length} 个 stale 步骤为 completed(reopen --from-step N 后 --done,方案未变)`)
|
|
338
260
|
}
|
|
339
261
|
|
|
340
262
|
stageData.status = 'completed'
|
|
341
263
|
stageData.completedAt = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
342
264
|
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
343
|
-
|
|
344
|
-
|
|
265
|
+
// persist(_write + triggerSync)移到 completeStageGates 成功之后(task-01 / review-2026-08-09 #2):
|
|
266
|
+
// gate 任一段异常/失败 → rollbackCompletionAndReturn 回 in-progress 并落盘;此处未到 _write,DB 不留假 completed。
|
|
345
267
|
|
|
346
268
|
// Append to user-inputs.md
|
|
347
269
|
if (outputText) {
|
|
348
270
|
const inputsPath = join(specBase, '.runtime', 'user-inputs.md')
|
|
349
271
|
const entry = `\n## ${new Date().toLocaleString('zh-CN',{hour12:false})} | ${changeName || '?'} | ${stageName}: ${steps[currentIdx].name}\n${inputText ? "- 输入:" + inputText + "\n" : ""}- 输出:${outputText}\n`
|
|
350
|
-
|
|
272
|
+
try {
|
|
273
|
+
appendFileSync(inputsPath, entry)
|
|
274
|
+
} catch (e) {
|
|
275
|
+
// best-effort:Windows AV/索引占用偶发 EPERM,历史日志写失败不阻断主流程(review-2026-08-09 #7,对齐 shared.js triggerSync)
|
|
276
|
+
console.warn('⚠️ user-inputs.md 追加失败(不阻断):', e.message)
|
|
277
|
+
}
|
|
351
278
|
}
|
|
352
279
|
|
|
353
|
-
//
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const actualCompleted = steps.filter(s => s.status === 'completed').length
|
|
359
|
-
const actualTotal = steps.length
|
|
280
|
+
// 阶段完成收尾共享管线(handleScan manifest + validateMetadata/FileLocations + auxiliary 重置 +
|
|
281
|
+
// runStageCompletionGates + execute worktree cleanup),消除 S1/S2/S3 三处收尾不对称(task-01 抽出)。
|
|
282
|
+
// gate 失败已 rollback,early-return 跳过下方"阶段已完成/下一步"提示(合理收紧:回滚不该打完成提示)。
|
|
283
|
+
const _stageGatesResult = await completeStageGates({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, steps, currentIdx, outputText })
|
|
284
|
+
if (_stageGatesResult) return _stageGatesResult
|
|
360
285
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
if (actualCompleted === actualTotal && actualTotal > 0) {
|
|
365
|
-
validateFileLocations(cwd, stageName, progress, changeName, specBase)
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// 辅助阶段完成后重置步骤
|
|
369
|
-
const stageDef = stageRegistry[stageName]
|
|
370
|
-
if (stageDef?.auxiliary) {
|
|
371
|
-
const freshSteps = (stageDef.steps || []).map(s => ({
|
|
372
|
-
name: s.name,
|
|
373
|
-
status: 'pending',
|
|
374
|
-
output: null,
|
|
375
|
-
completedAt: null
|
|
376
|
-
}))
|
|
377
|
-
stageData.steps = freshSteps
|
|
378
|
-
stageData.status = 'pending'
|
|
379
|
-
stageData.completedAt = null
|
|
380
|
-
if (progress.currentStage === stageName) progress.currentStage = ''
|
|
381
|
-
await pm._write(cwd, progress, changeName)
|
|
382
|
-
}
|
|
286
|
+
// gate 全过:persist completed(task-01 移后)。此处到 _write 之间若崩,DB 仍 in-progress(内存已 completed 但未落盘),下次进 CLI 读 DB 即 in-progress,不产生"假 completed"。
|
|
287
|
+
pm._write(cwd, progress, changeName)
|
|
288
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
383
289
|
|
|
384
290
|
const total = steps.length
|
|
385
291
|
console.log(`✅ ${stageName} 阶段已完成(${total}/${total} 步)`)
|
|
@@ -434,30 +340,22 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
434
340
|
}
|
|
435
341
|
}
|
|
436
342
|
|
|
437
|
-
// 阶段完成校验 — 防御性守卫:仅当所有步骤确实标记为 completed 时才跑 validator
|
|
438
|
-
if (actualCompleted === actualTotal && actualTotal > 0) {
|
|
439
|
-
const _gateEarlyReturn = await runStageCompletionGates({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, steps, currentIdx })
|
|
440
|
-
if (_gateEarlyReturn) return _gateEarlyReturn
|
|
441
|
-
} else if (actualCompleted < actualTotal) {
|
|
442
|
-
// 实际步骤未全部完成,跳过 validator(状态可能不同步)
|
|
443
|
-
console.log(`\n⚠️ 阶段校验跳过:${actualTotal} 步中仅 ${actualCompleted} 步标记为已完成,可能存在状态不同步。如确认阶段已完成,请运行 --status 确认。`)
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// execute worktree cleanup(W6 Step6c 抽至 complete-handlers.js handleExecuteWorktreeCleanup)
|
|
447
|
-
await handleExecuteWorktreeCleanup({ stageName, changeName, cwd })
|
|
448
|
-
|
|
449
343
|
return { stageCompleted: true, currentIdx, nextPendingIdx: -1 }
|
|
450
344
|
}
|
|
451
345
|
|
|
452
346
|
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
453
|
-
|
|
347
|
+
pm._write(cwd, progress, changeName)
|
|
454
348
|
triggerSync(cwd, changeName, platformOpts)
|
|
455
349
|
|
|
456
350
|
// Append to user-inputs.md
|
|
457
351
|
if (outputText) {
|
|
458
352
|
const inputsPath = join(specBase, '.runtime', 'user-inputs.md')
|
|
459
353
|
const entry = `\n## ${new Date().toLocaleString('zh-CN',{hour12:false})} | ${changeName || '?'} | ${stageName}: ${steps[currentIdx].name}\n${inputText ? "- 输入:" + inputText + "\n" : ""}- 输出:${outputText}\n`
|
|
460
|
-
|
|
354
|
+
try {
|
|
355
|
+
appendFileSync(inputsPath, entry)
|
|
356
|
+
} catch (e) {
|
|
357
|
+
console.warn('⚠️ user-inputs.md 追加失败(不阻断):', e.message)
|
|
358
|
+
}
|
|
461
359
|
}
|
|
462
360
|
|
|
463
361
|
const defSteps = await getStageSteps(stageName, cwd, progress, platformOpts?.specRoot || null)
|
|
@@ -469,6 +367,13 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
469
367
|
|
|
470
368
|
if (printNext) {
|
|
471
369
|
await outputStep(stageName, nextPendingIdx, defSteps, cwd, changeName, progress.project || null, platformOpts)
|
|
370
|
+
// task-08:底部锚定行——outputStep 渲染的长 prompt 易被 tail 截断,末尾再打一行推进位置,
|
|
371
|
+
// 让 agent 只看末几行也能知道「推进到第几步」。仅单步推进分支(阶段完成分支已有 ✅ 阶段已完成)。
|
|
372
|
+
// defSteps 越界防御:平台模式 buildPlanSteps 长度漂移时 nextPendingIdx 可能越界
|
|
373
|
+
// (参照 prompt.js:174-179 越界降级先例),越界时静默跳过,不崩。
|
|
374
|
+
if (defSteps && defSteps[nextPendingIdx]) {
|
|
375
|
+
console.log(`\n🚀 advanced to step ${nextPendingIdx + 1}/${steps.length}: ${defSteps[nextPendingIdx].name}`)
|
|
376
|
+
}
|
|
472
377
|
}
|
|
473
378
|
return { stageCompleted: false, currentIdx, nextPendingIdx }
|
|
474
379
|
}
|
|
@@ -482,6 +387,31 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
482
387
|
* @returns {{autoChecked:boolean, checkedCount:number, skippedCount:number, planTotal:number, planChecked:number}}
|
|
483
388
|
* planTotal/planChecked 在调用方于本函数之后用 readPlanCheckboxStatus 重读(勾选已落盘)。
|
|
484
389
|
*/
|
|
390
|
+
/**
|
|
391
|
+
* 读 task 卡片文本(plan.md task 行外的详细描述/allowed_paths),用于端到端 task 判定。
|
|
392
|
+
* 卡片缺失返回空串(仅靠 plan.md task 行文本判)。
|
|
393
|
+
*/
|
|
394
|
+
function readTaskCardText(changeDir, taskNum) {
|
|
395
|
+
try { return readFileSync(join(changeDir, 'tasks', `task-${taskNum}.md`), 'utf8') } catch { return '' }
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* 判定 task 是否该被 autoCheckPlanFromReviews 自动勾选。
|
|
400
|
+
* 端到端/deployment-critical task 要求 review spec+quality 双 pass(cannot_verify 不算,防批量完成
|
|
401
|
+
* 放行未真验的端到端 task);普通 task 非 fail 即可(保主 agent 直接实现模式体验,其 cannot_verify
|
|
402
|
+
* 草稿仍可批量收尾)。坑 execute-batch-complete-endtoend-checkbox。
|
|
403
|
+
* @param {{ok?:boolean, review?:{specVerdict?:string, qualityVerdict?:string}}} r readReview 结果
|
|
404
|
+
* @param {boolean} endToEnd 是否端到端/deployment-critical task
|
|
405
|
+
* @returns {boolean}
|
|
406
|
+
*/
|
|
407
|
+
export function shouldAutoCheckTask(r, endToEnd) {
|
|
408
|
+
if (!r?.ok) return false
|
|
409
|
+
const spec = r.review?.specVerdict
|
|
410
|
+
const quality = r.review?.qualityVerdict
|
|
411
|
+
if (spec === 'fail' || quality === 'fail') return false
|
|
412
|
+
if (endToEnd) return spec === 'pass' && quality === 'pass'
|
|
413
|
+
return true
|
|
414
|
+
}
|
|
485
415
|
async function autoCheckPlanFromReviews({ stageName, changeName, cwd, platformOpts }) {
|
|
486
416
|
if (stageName !== 'execute' || !changeName) {
|
|
487
417
|
return { autoChecked: false, checkedCount: 0, skippedCount: 0 }
|
|
@@ -490,31 +420,40 @@ async function autoCheckPlanFromReviews({ stageName, changeName, cwd, platformOp
|
|
|
490
420
|
const specBaseLc = platformOpts?.specRoot || join(cwd, '.sillyspec')
|
|
491
421
|
const changeDir = join(specBaseLc, 'changes', changeName)
|
|
492
422
|
const planPath = join(changeDir, 'plan.md')
|
|
493
|
-
const runtimeRoot = platformOpts
|
|
423
|
+
const runtimeRoot = resolveRuntimeRoot(platformOpts, specBaseLc)
|
|
494
424
|
const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
|
|
495
425
|
if (!existsSync(planPath) || !existsSync(runIdFile)) {
|
|
496
426
|
return { autoChecked: false, checkedCount: 0, skippedCount: 0 }
|
|
497
427
|
}
|
|
498
428
|
const executeRunId = readFileSync(runIdFile, 'utf8').trim()
|
|
499
|
-
const planContent = readFileSync(planPath, 'utf8')
|
|
500
429
|
const { readReview } = await import('../task-review.js')
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
430
|
+
// plan.md 是 agent 与 CLI 都会写的共享文件(agent 勾 checkbox、此处 autoCheck 也勾选)。
|
|
431
|
+
// 读-改-写必须整体持锁(withFileLock 串行化多进程),否则并发 execute --done / 手动勾选互相覆盖
|
|
432
|
+
// (后到者覆盖先到者);写入用 writeAtomicSync(tmp+rename 原子),防 Windows 整文件覆盖被读半截
|
|
433
|
+
// (fs-atomic.js 头注明的 reader-writer 竞态坑)。锁文件放 changeDir,与 QUICKLOG 同机制。
|
|
434
|
+
const lockPath = join(changeDir, '.plan.md.lock')
|
|
435
|
+
return await withFileLock(lockPath, async () => {
|
|
436
|
+
const planContent = readFileSync(planPath, 'utf8')
|
|
437
|
+
let checkedCount = 0
|
|
438
|
+
let skippedCount = 0
|
|
439
|
+
const updated = planContent.replace(/^(\s*[-*]\s*\[)\s(\]\s*task-\d+)/gim, (match, p1, p2) => {
|
|
440
|
+
const taskNum = match.match(/task-(\d+)/)[1].padStart(2, '0')
|
|
441
|
+
const reviewPath = join(runtimeRoot, 'execute-runs', executeRunId, 'tasks', `task-${taskNum}`, 'review.json')
|
|
442
|
+
const r = readReview(reviewPath)
|
|
443
|
+
// 端到端 task:必须 pass(cannot_verify 不算);普通 task:非 fail 即可(坑 execute-batch-complete-endtoend-checkbox)
|
|
444
|
+
const endToEnd = isEndToEndTaskText(match + ' ' + readTaskCardText(changeDir, taskNum))
|
|
445
|
+
if (shouldAutoCheckTask(r, endToEnd)) {
|
|
446
|
+
checkedCount++
|
|
447
|
+
return `${p1}x${p2}` // 勾选
|
|
448
|
+
}
|
|
449
|
+
skippedCount++
|
|
450
|
+
return match // 不勾
|
|
451
|
+
})
|
|
452
|
+
if (checkedCount > 0) {
|
|
453
|
+
writeAtomicSync(planPath, updated)
|
|
510
454
|
}
|
|
511
|
-
skippedCount
|
|
512
|
-
return match // 不勾
|
|
455
|
+
return { autoChecked: checkedCount > 0, checkedCount, skippedCount }
|
|
513
456
|
})
|
|
514
|
-
if (checkedCount > 0) {
|
|
515
|
-
writeFileSync(planPath, updated)
|
|
516
|
-
}
|
|
517
|
-
return { autoChecked: checkedCount > 0, checkedCount, skippedCount }
|
|
518
457
|
} catch {
|
|
519
458
|
return { autoChecked: false, checkedCount: 0, skippedCount: 0 }
|
|
520
459
|
}
|
|
@@ -592,7 +531,7 @@ export async function waitStep(pm, progress, stageName, cwd, outputText, waitRea
|
|
|
592
531
|
// 查找下一个 pending 或 in-progress 的步骤
|
|
593
532
|
const currentIdx = stageData.steps.findIndex(s => s.status === 'pending' || s.status === 'in-progress')
|
|
594
533
|
if (currentIdx === -1) {
|
|
595
|
-
console.error('
|
|
534
|
+
console.error(`没有可以等待的步骤(阶段 ${stageName} 已无 pending/in-progress 步骤)。当前阶段状态:${stageData?.status ?? '未知'}。用 \`sillyspec run ${stageName} --status\` 查看进度,或 \`sillyspec progress show\` 看全局下一步。`)
|
|
596
535
|
process.exit(1)
|
|
597
536
|
}
|
|
598
537
|
|
|
@@ -651,7 +590,7 @@ export async function waitStep(pm, progress, stageName, cwd, outputText, waitRea
|
|
|
651
590
|
}
|
|
652
591
|
|
|
653
592
|
progress.lastActive = now
|
|
654
|
-
|
|
593
|
+
pm._write(cwd, progress, changeName)
|
|
655
594
|
triggerSync(cwd, changeName, platformOpts)
|
|
656
595
|
|
|
657
596
|
console.log(`⏸️ Step ${currentIdx + 1}/${stageData.steps.length} 已暂停等待:${stageData.steps[currentIdx].name}`)
|
|
@@ -678,7 +617,7 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
|
|
|
678
617
|
// 查找 waiting 的步骤
|
|
679
618
|
const waitingSteps = stageData.steps.map((s, i) => ({ ...s, idx: i })).filter(s => s.status === 'waiting')
|
|
680
619
|
if (waitingSteps.length === 0) {
|
|
681
|
-
console.error('
|
|
620
|
+
console.error(`没有处于等待状态的步骤(阶段 ${stageName} 当前无 waiting 步骤,--continue 无目标)。当前阶段状态:${stageData?.status ?? '未知'}。用 \`sillyspec run ${stageName} --status\` 查看进度,或 \`sillyspec progress show\` 看全局下一步。`)
|
|
682
621
|
process.exit(1)
|
|
683
622
|
}
|
|
684
623
|
|
|
@@ -754,7 +693,7 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
|
|
|
754
693
|
}
|
|
755
694
|
|
|
756
695
|
progress.lastActive = now
|
|
757
|
-
|
|
696
|
+
pm._write(cwd, progress, changeName)
|
|
758
697
|
triggerSync(cwd, changeName, platformOpts)
|
|
759
698
|
|
|
760
699
|
console.log(`✅ Step ${currentIdx + 1}/${stageData.steps.length} 已继续:${currentStep.name}`)
|
|
@@ -763,7 +702,11 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
|
|
|
763
702
|
// Append to user-inputs.md
|
|
764
703
|
const inputsPath = join(specBase, '.runtime', 'user-inputs.md')
|
|
765
704
|
const entry = `\n## ${now} | ${changeName || '?'} | ${stageName}: ${currentStep.name} [CONTINUED]\n- 回答:${answer}\n`
|
|
766
|
-
|
|
705
|
+
try {
|
|
706
|
+
appendFileSync(inputsPath, entry)
|
|
707
|
+
} catch (e) {
|
|
708
|
+
console.warn('⚠️ user-inputs.md 追加失败(不阻断):', e.message)
|
|
709
|
+
}
|
|
767
710
|
|
|
768
711
|
// shouldReturnToCurrentStep: 回到当前步骤继续执行(repeatable=多轮探索,requiresWait=确认后执行动作)
|
|
769
712
|
if (shouldReturnToCurrentStep) {
|
|
@@ -793,37 +736,14 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
|
|
|
793
736
|
if (nextPendingIdx === -1 && nextWaitingIdx === -1) {
|
|
794
737
|
stageData.status = 'completed'
|
|
795
738
|
stageData.completedAt = now
|
|
796
|
-
|
|
739
|
+
// persist _write 移到 completeStageGates 成功之后(task-03 / review-2026-08-09 #2):gate 异常/失败 → rollback 回 in-progress 落盘,此处未到 _write,DB 不留假 completed。
|
|
740
|
+
// 阶段完成收尾共享管线(含 execute worktree cleanup),消除 continueStep 完成分支绕过 gate 的 S2(task-01 抽出)。
|
|
741
|
+
// gate 失败已 rollback,early-return 跳过下方"阶段已完成/下一步"提示(与 completeStep 同语义)。
|
|
742
|
+
const _stageGatesResult = await completeStageGates({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, steps: stageData.steps, currentIdx, outputText: null })
|
|
743
|
+
if (_stageGatesResult) return _stageGatesResult
|
|
744
|
+
// gate 全过:persist completed(task-03 移后;此处无 triggerSync)。
|
|
745
|
+
pm._write(cwd, progress, changeName)
|
|
797
746
|
console.log(`\n✅ ${stageName} 阶段已完成(${stageData.steps.length}/${stageData.steps.length} 步)`)
|
|
798
|
-
// ── execute 阶段完成时条件性清理 worktree ──
|
|
799
|
-
if (stageName === 'execute' && changeName) {
|
|
800
|
-
try {
|
|
801
|
-
const { WorktreeManager } = await import('../worktree.js');
|
|
802
|
-
const wm = new WorktreeManager({ cwd });
|
|
803
|
-
const meta = wm.getMeta(changeName);
|
|
804
|
-
if (!meta) {
|
|
805
|
-
console.log('🔗 Worktree: n/a (no meta)');
|
|
806
|
-
} else if (meta.mode === 'native-worktree') {
|
|
807
|
-
console.log('🔗 Worktree: kept (外部隔离环境)');
|
|
808
|
-
} else {
|
|
809
|
-
// in-place 模式不再短路:cleanup 现在能安全处理 in-place(只清 meta,不碰主工作区)
|
|
810
|
-
const check = wm.hasUnappliedChanges(changeName);
|
|
811
|
-
if (check.hasChanges) {
|
|
812
|
-
console.log(`🔗 Worktree: pending apply (${check.changedFiles.length} 个未应用变更)`);
|
|
813
|
-
console.log(` 下一步: sillyspec worktree apply ${changeName}`);
|
|
814
|
-
} else {
|
|
815
|
-
const cleanResult = wm.cleanup(changeName);
|
|
816
|
-
console.log(`🔗 Worktree: ${cleanResult.result}`);
|
|
817
|
-
if (cleanResult.residual?.length > 0) {
|
|
818
|
-
console.warn(` ⚠️ 清理残留: ${cleanResult.residual.join('; ')}`);
|
|
819
|
-
console.warn(` 手动处理: sillyspec worktree cleanup ${changeName} --force`);
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
} catch (e) {
|
|
824
|
-
console.warn(`🔗 Worktree: check failed — ${e.message}`);
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
747
|
// 阶段完成后明确下一步(agent 常卡:stageData completed 但不知要 run <下一阶段> 推进 currentStage)
|
|
828
748
|
const nextStageHint = { brainstorm: 'plan', plan: 'execute', execute: 'verify', verify: 'archive' }[stageName]
|
|
829
749
|
if (nextStageHint) {
|
|
@@ -884,7 +804,7 @@ export async function skipStep(pm, progress, stageName, cwd, changeName, platfor
|
|
|
884
804
|
console.error(`⏸️ Step ${wsIdx + 1} 正在等待用户输入,不能跳过。`)
|
|
885
805
|
console.error(` 请先使用 --continue --answer "..." 继续,或用 --reset 重置。`)
|
|
886
806
|
} else {
|
|
887
|
-
console.error('
|
|
807
|
+
console.error(`没有待跳过的步骤(阶段 ${stageName} 已无 pending/in-progress 步骤)。当前阶段状态:${stageData?.status ?? '未知'}。用 \`sillyspec run ${stageName} --status\` 查看进度,或 \`sillyspec progress show\` 看全局下一步。`)
|
|
888
808
|
}
|
|
889
809
|
process.exit(1)
|
|
890
810
|
}
|
|
@@ -899,7 +819,7 @@ export async function skipStep(pm, progress, stageName, cwd, changeName, platfor
|
|
|
899
819
|
steps[currentIdx].status = 'skipped'
|
|
900
820
|
steps[currentIdx].skippedAt = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
901
821
|
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
902
|
-
|
|
822
|
+
pm._write(cwd, progress, changeName)
|
|
903
823
|
triggerSync(cwd, changeName, platformOpts)
|
|
904
824
|
|
|
905
825
|
console.log(`⏭️ Step ${currentIdx + 1}/${steps.length} 已跳过:${steps[currentIdx].name}`)
|