sillyspec 3.26.6 → 3.26.8
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 +2 -0
- package/.claude/skills/sillyspec-execute/SKILL.md +1 -1
- package/.claude/skills/sillyspec-knowledge/SKILL.md +1 -1
- package/.claude/skills/sillyspec-plan/SKILL.md +1 -1
- package/.husky/pre-push +9 -2
- package/bin/sillyspec.js +1 -1
- package/package.json +3 -3
- package/packages/dashboard/server/index.js +8 -1
- package/src/config-schema.js +305 -287
- package/src/docs-check.js +333 -0
- package/src/docs-debt.js +226 -0
- package/src/docs-gate.js +106 -0
- package/src/doctor-diagnostics.js +23 -2
- package/src/index.js +1880 -1750
- package/src/init.js +81 -14
- package/src/modules.js +490 -486
- package/src/progress.js +37 -13
- package/src/quicklog.js +19 -4
- package/src/run/command.js +54 -24
- package/src/run/complete-handlers.js +1254 -1128
- package/src/run/complete.js +8 -34
- package/src/run/gates.js +32 -3
- package/src/run/prompt.js +713 -633
- package/src/run/quick-audit.js +137 -123
- package/src/run/shared.js +1109 -929
- package/src/run/stage.js +8 -3
- package/src/scan-staleness.js +90 -0
- package/src/setup.js +27 -9
- package/src/sillyhub-mcp/client.js +149 -14
- package/src/stage-contract-spec.js +15 -0
- package/src/stage-contract.js +8 -5
- package/src/stages/brainstorm-auto.js +0 -1
- package/src/stages/brainstorm.js +1 -1
- package/src/stages/execute.js +985 -967
- package/src/stages/plan-postcheck.js +93 -36
- package/src/stages/verify.js +1 -1
- package/src/sync.js +60 -32
- package/src/task-review.js +38 -6
- package/src/verify-postcheck.js +22 -5
- package/src/worktree-apply.js +6 -3
- package/src/worktree-deps.js +5 -3
- package/src/worktree.js +4 -2
package/src/run/complete.js
CHANGED
|
@@ -64,32 +64,6 @@ export function resolveWaitingStepWithAnswer(steps, doneAnswer, nowStr) {
|
|
|
64
64
|
return waitIdx
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
/**
|
|
68
|
-
* wait 选项单选校验(坑 wait-choice-enforcement):定义了 waitOptions 的 requiresWait/
|
|
69
|
-
* repeatableWait 步骤,--answer 必须命中预设选项之一——防止 agent 一句话代答绕过人工
|
|
70
|
-
* 选择(方案选择类 wait 的 answer 本就该是选项本身)。开放回答型步骤(澄清追问,answer
|
|
71
|
-
* 为自由文本)在 step 定义显式声明 waitFreeAnswer: true 豁免。
|
|
72
|
-
* 校验失败打印选项清单 + exit 1(fail-loud,与 requiresWait 门同风格)。
|
|
73
|
-
*
|
|
74
|
-
* waitOptions 优先取 stepDef(运行时构建的步骤定义,与 brainstorm/plan 静态定义同源),
|
|
75
|
-
* 回退 DB step entry(--continue 路径 waiting 步骤的 waitOptions 已落盘)。
|
|
76
|
-
*/
|
|
77
|
-
function enforceWaitChoice(stepDef, stepEntry, answer, stageName, changeName) {
|
|
78
|
-
if (answer == null) return
|
|
79
|
-
const opts = Array.isArray(stepDef?.waitOptions) ? stepDef.waitOptions
|
|
80
|
-
: (Array.isArray(stepEntry?.waitOptions) ? stepEntry.waitOptions : null)
|
|
81
|
-
if (!opts || opts.length === 0) return
|
|
82
|
-
if (stepDef?.waitFreeAnswer === true) return
|
|
83
|
-
if (opts.includes(answer)) return
|
|
84
|
-
console.error(`❌ Step "${stepEntry?.name || '当前步骤'}" 是封闭单选 wait,--answer 必须命中预设选项之一(防 AI 代答绕过人工选择)。`)
|
|
85
|
-
console.error(` 你的 --answer: ${JSON.stringify(answer)}`)
|
|
86
|
-
console.error(` 可选值(复制其一): ${opts.join(' / ')}`)
|
|
87
|
-
console.error(` 自由文本回答的场景,该步骤应声明 waitFreeAnswer(当前未声明)。`)
|
|
88
|
-
if (changeName) console.error(` 重试:sillyspec run ${stageName} --done --answer "<选项>" --change ${changeName}`)
|
|
89
|
-
else console.error(` 重试:sillyspec run ${stageName} --done --answer "<选项>"`)
|
|
90
|
-
process.exit(1)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
67
|
export async function completeStep(pm, progress, stageName, cwd, outputText, inputText = null, options = {}) {
|
|
94
68
|
const { printNext = true, confirm = false, changeName, platformOpts = {}, nonInteractive = false, isForceBaseline = false, isAllowNew = false, isAllowDelete = false } = options
|
|
95
69
|
const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
|
|
@@ -148,10 +122,6 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
148
122
|
const defStepsForCurrent = await getStageSteps(stageName, cwd, progress, platformOpts?.specRoot || null)
|
|
149
123
|
const currentStepDef = defStepsForCurrent?.[currentIdx] || {}
|
|
150
124
|
const currentStep = steps[currentIdx]
|
|
151
|
-
// --done --answer 解 waiting 的路径同样过单选校验(与下方 requiresWait doneAnswer 分支同规则)
|
|
152
|
-
if (_resolvedWaitIdx !== -1 && _doneAnswer) {
|
|
153
|
-
enforceWaitChoice(currentStepDef, currentStep, _doneAnswer, stageName, changeName)
|
|
154
|
-
}
|
|
155
125
|
if (currentStepDef.requiresWait === true && !currentStep.waitAnswer) {
|
|
156
126
|
// 检查 --done 是否带了 --answer:如果是,自动补全 waitAnswer 状态,一步完成
|
|
157
127
|
const doneAnswer = typeof options !== 'undefined' && options.doneAnswer ? options.doneAnswer : null
|
|
@@ -168,7 +138,6 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
168
138
|
currentStep.waitReason = currentStepDef.waitReason || '等待用户输入'
|
|
169
139
|
console.log(`⚠️ Step "${currentStep.name}" 的确认(${currentStepDef.waitReason})已在前置步骤完成,自动跳过。`)
|
|
170
140
|
} else if (doneAnswer) {
|
|
171
|
-
enforceWaitChoice(currentStepDef, currentStep, doneAnswer, stageName, changeName)
|
|
172
141
|
currentStep.status = 'waiting'
|
|
173
142
|
currentStep.waitAnswer = doneAnswer
|
|
174
143
|
currentStep.waitReason = currentStepDef.waitReason || '等待用户输入'
|
|
@@ -508,8 +477,14 @@ async function autoCheckPlanFromReviews({ stageName, changeName, cwd, platformOp
|
|
|
508
477
|
if (!existsSync(planPath) || !existsSync(runIdFile)) {
|
|
509
478
|
return { autoChecked: false, checkedCount: 0, skippedCount: 0 }
|
|
510
479
|
}
|
|
511
|
-
const
|
|
512
|
-
const { readReview } = await import('../task-review.js')
|
|
480
|
+
const c = readFileSync(runIdFile, 'utf8').trim()
|
|
481
|
+
const { readReview, isValidExecuteRunId } = await import('../task-review.js')
|
|
482
|
+
// marker 是 agent 可写内容:格式校验防注入/穿越,非法视为缺失(跳过 autoCheck,不误勾)
|
|
483
|
+
if (c && !isValidExecuteRunId(c)) {
|
|
484
|
+
console.warn(`⚠️ execute run marker 内容非法(期望 exec-YYYY-MM-DD-HHMMSS,实得 ${JSON.stringify(c.slice(0, 60))}),跳过 review 自动勾选`)
|
|
485
|
+
return { autoChecked: false, checkedCount: 0, skippedCount: 0 }
|
|
486
|
+
}
|
|
487
|
+
const executeRunId = c
|
|
513
488
|
// plan.md 是 agent 与 CLI 都会写的共享文件(agent 勾 checkbox、此处 autoCheck 也勾选)。
|
|
514
489
|
// 读-改-写必须整体持锁(withFileLock 串行化多进程),否则并发 execute --done / 手动勾选互相覆盖
|
|
515
490
|
// (后到者覆盖先到者);写入用 writeAtomicSync(tmp+rename 原子),防 Windows 整文件覆盖被读半截
|
|
@@ -744,7 +719,6 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
|
|
|
744
719
|
const prevOutput = currentStep.output || ''
|
|
745
720
|
const waitRound = (currentStep.waitRound || 0) + 1
|
|
746
721
|
currentStep.waitRound = waitRound
|
|
747
|
-
enforceWaitChoice(currentStepDef, currentStep, answer, stageName, changeName)
|
|
748
722
|
currentStep.waitAnswer = answer
|
|
749
723
|
currentStep.waitAnswers = Array.isArray(currentStep.waitAnswers) ? currentStep.waitAnswers : []
|
|
750
724
|
currentStep.waitAnswers.push({
|
package/src/run/gates.js
CHANGED
|
@@ -115,9 +115,14 @@ export async function enforceReviewJsonGate(stageName, cwd, changeName, step, st
|
|
|
115
115
|
const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
|
|
116
116
|
const planPath = join(specBase, 'changes', changeName, 'plan.md')
|
|
117
117
|
if (!existsSync(runIdFile) || !existsSync(planPath)) return true
|
|
118
|
+
const { validateCheckedTaskReviews, resolveLatestExecuteRunIdWithTasks, isValidExecuteRunId } = await import('../task-review.js')
|
|
118
119
|
let executeRunId = readFileSync(runIdFile, 'utf8').trim()
|
|
120
|
+
// marker 是 agent 可写内容:格式校验防注入/穿越,非法视为缺失走漂移兜底重定位
|
|
121
|
+
if (executeRunId && !isValidExecuteRunId(executeRunId)) {
|
|
122
|
+
console.warn(`⚠️ execute run marker 内容非法(期望 exec-YYYY-MM-DD-HHMMSS,实得 ${JSON.stringify(executeRunId.slice(0, 60))}),改扫真实 run 目录`)
|
|
123
|
+
executeRunId = ''
|
|
124
|
+
}
|
|
119
125
|
const planContent = readFileSync(planPath, 'utf8')
|
|
120
|
-
const { validateCheckedTaskReviews, resolveLatestExecuteRunIdWithTasks } = await import('../task-review.js')
|
|
121
126
|
// marker 漂移兜底(gate-atom-a 正确修法):marker 指向的 run 缺 tasks/(generateExecuteRunId 只写
|
|
122
127
|
// marker 不建目录,漂移后新 run 不继承旧 review)时,无视 marker 改扫 execute-runs/ 取 mtime 最新
|
|
123
128
|
// 且真正含 tasks/ 的 run,用其齐备的 review.json 校验,避免误报「review.json 不存在」。注意不能用
|
|
@@ -243,6 +248,24 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
243
248
|
const { runVerifyRequiredEvidenceCheck, printVerifyRequiredEvidenceCheck } = await import('../verify-postcheck.js')
|
|
244
249
|
const evidenceCheck = runVerifyRequiredEvidenceCheck({ cwd, specBase, changeName })
|
|
245
250
|
printVerifyRequiredEvidenceCheck(evidenceCheck)
|
|
251
|
+
// ── module-impact 死信探针(blocking,债单 D-1/D-5):更新结果表 pending/待办行 → 阻断 verify ──
|
|
252
|
+
// 与 archive 移动前校验(extractPendingDocSyncRows)同一口径,把死信号从 archive 提前到 verify:
|
|
253
|
+
// agent 在 verify 阶段就须完成文档同步并回填 done/skipped,而非拖到归档被拦(修复 perf-remediation
|
|
254
|
+
// 类「verify PASS → archive 才发现 pending」的时序漏洞)。
|
|
255
|
+
const verifyChangeDir = resolveChangeDir(cwd, progress, platformOpts?.specRoot)
|
|
256
|
+
if (verifyChangeDir) {
|
|
257
|
+
const impactPath = join(verifyChangeDir, 'module-impact.md')
|
|
258
|
+
if (existsSync(impactPath)) {
|
|
259
|
+
const { extractPendingDocSyncRows } = await import('./complete-handlers.js')
|
|
260
|
+
const pendingRows = extractPendingDocSyncRows(readFileSync(impactPath, 'utf8'))
|
|
261
|
+
if (pendingRows.length > 0) {
|
|
262
|
+
console.error(`\n❌ verify 阶段被阻断:module-impact.md「更新结果」表存在 ${pendingRows.length} 个未清 pending/待办项(死信)`)
|
|
263
|
+
for (const row of pendingRows) console.error(` - ${row}`)
|
|
264
|
+
console.error(' 文档同步是 verify 的收尾义务:请完成模块文档同步并回填状态为 done/skipped(说明原因),再重新完成 verify。')
|
|
265
|
+
return rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
246
269
|
console.log('\n✅ 验证通过,下一步:sillyspec run archive')
|
|
247
270
|
}
|
|
248
271
|
|
|
@@ -333,12 +356,18 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
333
356
|
const planContent = readFileSync(planPath, 'utf8')
|
|
334
357
|
const runtimeRoot = resolveRuntimeRoot(platformOpts, effectiveSpecBase)
|
|
335
358
|
|
|
336
|
-
// execute run id
|
|
359
|
+
// execute run id:从变更专属标记文件读取(agent 可写内容,格式校验防注入/穿越)
|
|
337
360
|
const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
|
|
338
361
|
let executeRunId = ''
|
|
339
362
|
try {
|
|
340
363
|
if (existsSync(runIdFile)) {
|
|
341
|
-
|
|
364
|
+
const c = readFileSync(runIdFile, 'utf8').trim()
|
|
365
|
+
const { isValidExecuteRunId } = await import('../task-review.js')
|
|
366
|
+
if (c && !isValidExecuteRunId(c)) {
|
|
367
|
+
console.warn(`⚠️ execute run marker 内容非法(期望 exec-YYYY-MM-DD-HHMMSS,实得 ${JSON.stringify(c.slice(0, 60))}),视为缺失回退扫描`)
|
|
368
|
+
} else {
|
|
369
|
+
executeRunId = c
|
|
370
|
+
}
|
|
342
371
|
}
|
|
343
372
|
} catch {}
|
|
344
373
|
if (!executeRunId) {
|