sillyspec 3.26.5 → 3.26.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.26.5",
3
+ "version": "3.26.6",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
package/src/db.js CHANGED
@@ -93,8 +93,10 @@ export class DB {
93
93
  * - 主库缺失/为空/损坏:尝试 .bak;成功则 warn 原因后 copy 回主库使用。
94
94
  * - 都不可用:主库不存在→建全新空库;主库曾存在(空/损坏)→fail-loud 抛错(不静默建空库吞进度)。
95
95
  * node:sqlite 打开 0 字节文件不抛错:主库须 statSync 显式判空(截断信号→走回退,防静默吞进度);
96
- * .bak 作为恢复源只要存在且能打开即用(含 0 字节空库——node:sqlite 视其为合法空库,
97
- * 生产环境 .bak 由原子写改名上一完整主库得来,永不为 0 字节,0 字节态仅出现在测试构造)。
96
+ * .bak 作为恢复源只要存在且能打开即用(含 0 字节空库——node:sqlite 视其为合法空库)。
97
+ * 注:node:sqlite 提交即持久化,不再写前备份主 .bak(sql.js 时代 _save/_atomicWriteSync 已随
98
+ * 2026-08-11 迁移移除);此恢复分支是向后兼容兜底,仅对 sql.js 时代遗留的 .bak 生效,
99
+ * 全新 node:sqlite 项目不产生主 .bak)。
98
100
  */
99
101
  _openWithFallback() {
100
102
  const bakPath = `${this.dbPath}.bak`;
package/src/fs-atomic.js CHANGED
@@ -4,8 +4,8 @@
4
4
  * 用于 .runtime/*.json、pointer、guard.json 等「会被其他进程 / hook 读取」的配置文件。
5
5
  * 保证读者要么看到旧完整内容、要么看到新完整内容,不会读到半截 JSON。
6
6
  *
7
- * 注意:db.js 有自己的 _atomicWriteSync(含 .bak 备份),那是 DB 专属持久化逻辑,
8
- * 已经过稳定性验证,此处不复用、不替换——保持 db.js 零改动。
7
+ * 注意:DB 持久化由 node:sqlite 引擎承担(提交即落盘 sillyspec.db + WAL 侧车),
8
+ * 不经此处的 writeAtomicSync;sillyspec.db 不走原子写改名。
9
9
  */
10
10
  import { writeFileSync, renameSync, unlinkSync } from 'fs';
11
11
  import { dirname, basename, join } from 'path';
package/src/index.js CHANGED
@@ -1673,7 +1673,7 @@ SillySpec config — local.yaml 配置键速查
1673
1673
  // 已知产物 → 用途说明。agent 据此判断每份产物能否作为证据/该读哪个。
1674
1674
  const KNOWN = {
1675
1675
  'sillyspec.db': '进度库 SQLite(stages/steps/changes 表)',
1676
- 'sillyspec.db.bak': '进度库备份(写前快照)',
1676
+ 'sillyspec.db.bak': '进度库备份(sql.js 时代写前快照遗留,node:sqlite 不再写入,仅 _openWithFallback 向后兼容恢复用)',
1677
1677
  'sillyspec.db.schema-version': '进度库 schema 版本标记',
1678
1678
  'doctor-diagnosis.json': 'doctor --json 结构化诊断快照',
1679
1679
  'doctor-dumps': 'doctor --dump-db 取证输出目录',
@@ -3,7 +3,7 @@
3
3
  // 调 pm.read/_write/_ensureDB/_getSpecDir/readGlobal/_runtimePath/listChanges/_appendAuditLog/
4
4
  // _renderBatchProgress(persistence-core + 其他组 delegate)。组内方法互调保持 this.X(同 class)。
5
5
  // completeStage 五层(resolve/validate/force/tx/history/print)整体搬迁,不拆流水线(保行为、最低风险)。
6
- import { mkdirSync } from 'fs';
6
+ import { mkdirSync, existsSync } from 'fs';
7
7
  import { join, resolve, basename } from 'path';
8
8
  import { writeAtomicSync } from '../fs-atomic.js';
9
9
  import { runValidators } from '../stage-contract.js';
@@ -140,21 +140,27 @@ export class StageMachine {
140
140
  console.log('');
141
141
  console.log(' ═══════════════════════════════════════');
142
142
  console.log(` 项目: ${(global?.project) || basename(cwd) || '(未命名)'}`);
143
- console.log(` 活跃变更: ${changes.length} 个`);
143
+ // 当前操作目标与活跃列表区分(坑 status-change-pointer-ambiguous):多活跃时不带
144
+ // --change 的 run/progress 不隐式选任何一个(index.js --change 解析多活跃报错),明确
145
+ // 说出来,防止把残留空壳(default/quick-xxx/目录缺失记录)误当操作目标跑错 change。
146
+ console.log(` 当前操作目标: (多活跃——不带 --change 时不隐式选定,run 须显式 --change <name>)`);
147
+ console.log(` 活跃变更记录: ${changes.length} 个(下列为 DB 中存在的活跃记录,非操作目标)`);
144
148
  console.log(' ═══════════════════════════════════════');
145
149
  console.log('');
146
150
 
151
+ const changesRoot = join(this.pm._getSpecDir(cwd), 'changes');
147
152
  for (const cn of changes) {
148
153
  const data = this.pm.read(cwd, cn);
154
+ const dirMissing = !existsSync(join(changesRoot, cn));
149
155
  if (!data) {
150
- console.log(` 📂 ${cn} — (无法读取)`);
156
+ console.log(` 📂 ${cn} — (无法读取)${dirMissing ? ' ⚠️ 目录缺失(残留记录,可用 doctor 清理)' : ''}`);
151
157
  continue;
152
158
  }
153
159
  const currentStage = data.currentStage || '(无)';
154
160
  const stageLabel = STAGE_LABELS[data.currentStage] || currentStage;
155
161
  const lastActive = data.lastActive ? this._timeAgo(data.lastActive) : '未知';
156
162
 
157
- console.log(` 📂 ${cn}`);
163
+ console.log(` 📂 ${cn}${dirMissing ? ' ⚠️ 目录缺失(残留记录,可用 doctor 清理)' : ''}`);
158
164
  console.log(` 当前阶段: ${stageLabel} 最近活跃: ${lastActive}`);
159
165
  console.log('');
160
166
  }
@@ -13,8 +13,9 @@
13
13
  * - tier=self → 当前 agent 自审,CLI 放行(不要求 review.json)
14
14
  * - tier=independent → 强制独立审查子代理 + review.json,CLI 硬校验
15
15
  *
16
- * CLI 侧(run.js done gate)与 prompt 注入侧用同一个函数,保证两边判定一致——
17
- * 不信 agent 自报 tier。
16
+ * CLI 侧(run.js done gate)与 prompt 注入侧用同一个函数,保证两边判定一致。
17
+ * 判定权归 agent 的 plan_level 自主判断(CLI 只做确定性映射,见 classifyReviewTier 注释);
18
+ * 无 plan_level 的阶段才退文件数启发式。
18
19
  */
19
20
 
20
21
  import { parseFileChangeList } from './change-list.js'
@@ -28,14 +29,21 @@ export const SELF_REVIEW_FILE_THRESHOLD = 3
28
29
  /**
29
30
  * 审查分级
30
31
  *
32
+ * 判定权归 agent 的 plan_level 判定(2026-08-14 tier-plan-level 改造):plan_level 是 agent
33
+ * 在 plan step1 按 prompt 分级规则做的自主判断,tier 直接映射它——CLI 不再用文件数启发式
34
+ * 推翻 agent 判定(此前 light + 4 文件会被文件数强制 independent,两套标准打架且不透明,
35
+ * 实证:agent 判 light 自审通过,完成时 CLI 按 7 文件强制 independent review.json)。
36
+ *
31
37
  * 判定顺序(短路):
32
- * 1. plan_level === 'none' → self(极小变更)
33
- * 2. design.md 变更文件数 ≤ SELF_REVIEW_FILE_THRESHOLD → self
34
- * 3. 否则 → independent
38
+ * 1. plan_level === 'none' → self(极小变更)
39
+ * 2. plan_level === 'light' → self(agent 自主判定轻量,自审)
40
+ * 3. plan_level === 'full' → independent(full 语义即大变更需独立审查,CLI 确定性强制)
41
+ * 4. 无 plan_level(brainstorm 等阶段 plan.md 未生成)→ 退文件数启发式:
42
+ * design.md 变更文件数 ≤ SELF_REVIEW_FILE_THRESHOLD → self,否则 independent
35
43
  *
36
- * fail-safe:designPath 传了但解析不到文件清单(fileCount=0)按字面 0 处理(真无文件
37
- * 变更 = 极小);designPath 完全没传(fileCount=null,无法证明规模)→ independent
38
- * (宁严勿松)。
44
+ * fail-safe:无 plan_level 且 designPath 传了但解析不到文件清单(fileCount=0)按字面 0
45
+ * 处理(真无文件变更 = 极小);designPath 完全没传(fileCount=null,无法证明规模)→
46
+ * independent(宁严勿松)。
39
47
  *
40
48
  * @param {object} [opts]
41
49
  * @param {string} [opts.planLevel] - plan_level 分类结果(none/light/full)。brainstorm/propose 在 plan 之前,可能拿不到。
@@ -44,7 +52,13 @@ export const SELF_REVIEW_FILE_THRESHOLD = 3
44
52
  */
45
53
  export function classifyReviewTier({ planLevel, designPath } = {}) {
46
54
  if (planLevel === 'none') {
47
- return { tier: 'self', reason: `plan_level=none(极小变更,≤${SELF_REVIEW_FILE_THRESHOLD} 文件等价)`, fileCount: null }
55
+ return { tier: 'self', reason: 'plan_level=none(极小变更,agent 自主判定自审)', fileCount: null }
56
+ }
57
+ if (planLevel === 'light') {
58
+ return { tier: 'self', reason: 'plan_level=light(agent 自主判定轻量变更,自审)', fileCount: null }
59
+ }
60
+ if (planLevel === 'full') {
61
+ return { tier: 'independent', reason: 'plan_level=full(agent 自主判定大变更,独立审查防确认偏差)', fileCount: null }
48
62
  }
49
63
 
50
64
  let fileCount = null
@@ -53,13 +67,11 @@ export function classifyReviewTier({ planLevel, designPath } = {}) {
53
67
  }
54
68
 
55
69
  if (fileCount !== null && fileCount <= SELF_REVIEW_FILE_THRESHOLD) {
56
- return { tier: 'self', reason: `变更文件 ${fileCount} ≤ ${SELF_REVIEW_FILE_THRESHOLD}`, fileCount }
70
+ return { tier: 'self', reason: `无 plan_level,变更文件 ${fileCount} ≤ ${SELF_REVIEW_FILE_THRESHOLD}(启发式)`, fileCount }
57
71
  }
58
72
 
59
73
  const reason = fileCount === null
60
- ? (planLevel
61
- ? `plan_level=${planLevel},无法解析变更文件数,按正常规模处理`
62
- : '无法解析 design.md 变更文件清单,按正常规模处理')
63
- : `变更文件 ${fileCount} > ${SELF_REVIEW_FILE_THRESHOLD}`
74
+ ? '无法解析 design.md 变更文件清单,按正常规模处理(fail-safe)'
75
+ : `无 plan_level,变更文件 ${fileCount} > ${SELF_REVIEW_FILE_THRESHOLD}(启发式)`
64
76
  return { tier: 'independent', reason, fileCount }
65
77
  }
@@ -12,13 +12,17 @@
12
12
  * 依赖(completeStep 仅用已抽叶子 + 随搬 helper,零 run.js 闭包,零动态 import):
13
13
  * - shared.js: triggerSync/WAIT_MARKER_RE/getStageSteps;prompt.js: outputStep;gates.js: enforceDepsGate/runStageCompletionGates
14
14
  * - complete-handlers.js: 8 handler;stages/index.js: stageRegistry;worktree-apply.js: formatExecuteSummary
15
+ * - scan-profile.js: executeScanPreflight/executeScanPostcheck/computeScanProfile(noAI 硬门)
16
+ * - stages/plan-postcheck.js: executePlanPostcheck(noAI 硬门)
15
17
  * - node: join(path) + existsSync/readdirSync/readFileSync/mkdirSync/writeFileSync/appendFileSync/statSync(fs)
16
18
  */
17
19
  import { join } from 'node:path'
18
20
  import { existsSync, readFileSync, mkdirSync, writeFileSync, appendFileSync } from 'node:fs'
19
21
  import { writeAtomicSync } from '../fs-atomic.js'
20
22
  import { withFileLock } from '../quicklog.js'
21
- import { triggerSync, WAIT_MARKER_RE, getStageSteps, formatWaitOptions, resolveRuntimeRoot, getOrCreateMultiRepoContext } from './shared.js'
23
+ import { triggerSync, WAIT_MARKER_RE, getStageSteps, formatWaitOptions, resolveRuntimeRoot, getOrCreateMultiRepoContext, resolveChangeDir } from './shared.js'
24
+ import { executeScanPreflight, executeScanPostcheck, computeScanProfile } from './scan-profile.js'
25
+ import { executePlanPostcheck as runPlanPostcheckLib } from '../stages/plan-postcheck.js'
22
26
  import { outputStep } from './prompt.js'
23
27
  import { enforceDepsGate, enforceReviewJsonGate, completeStageGates, readDesignScale } from './gates.js'
24
28
  import { handleArchiveConfirmStep, handlePlanGeneratePlanStep, handleScanProjectListStep, handleWorkflowPostCheck, handleQuickStageCompletion, handleExecuteWaveArtifact } from './complete-handlers.js'
@@ -60,6 +64,32 @@ export function resolveWaitingStepWithAnswer(steps, doneAnswer, nowStr) {
60
64
  return waitIdx
61
65
  }
62
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
+
63
93
  export async function completeStep(pm, progress, stageName, cwd, outputText, inputText = null, options = {}) {
64
94
  const { printNext = true, confirm = false, changeName, platformOpts = {}, nonInteractive = false, isForceBaseline = false, isAllowNew = false, isAllowDelete = false } = options
65
95
  const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
@@ -118,6 +148,10 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
118
148
  const defStepsForCurrent = await getStageSteps(stageName, cwd, progress, platformOpts?.specRoot || null)
119
149
  const currentStepDef = defStepsForCurrent?.[currentIdx] || {}
120
150
  const currentStep = steps[currentIdx]
151
+ // --done --answer 解 waiting 的路径同样过单选校验(与下方 requiresWait doneAnswer 分支同规则)
152
+ if (_resolvedWaitIdx !== -1 && _doneAnswer) {
153
+ enforceWaitChoice(currentStepDef, currentStep, _doneAnswer, stageName, changeName)
154
+ }
121
155
  if (currentStepDef.requiresWait === true && !currentStep.waitAnswer) {
122
156
  // 检查 --done 是否带了 --answer:如果是,自动补全 waitAnswer 状态,一步完成
123
157
  const doneAnswer = typeof options !== 'undefined' && options.doneAnswer ? options.doneAnswer : null
@@ -134,6 +168,7 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
134
168
  currentStep.waitReason = currentStepDef.waitReason || '等待用户输入'
135
169
  console.log(`⚠️ Step "${currentStep.name}" 的确认(${currentStepDef.waitReason})已在前置步骤完成,自动跳过。`)
136
170
  } else if (doneAnswer) {
171
+ enforceWaitChoice(currentStepDef, currentStep, doneAnswer, stageName, changeName)
137
172
  currentStep.status = 'waiting'
138
173
  currentStep.waitAnswer = doneAnswer
139
174
  currentStep.waitReason = currentStepDef.waitReason || '等待用户输入'
@@ -157,6 +192,27 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
157
192
  // 提前到每次 --done 校验(而非等 Task Review Gate 在整阶段收尾才暴露,迫使用户事后批量补)。
158
193
  await enforceReviewJsonGate(stageName, cwd, changeName, steps[currentIdx], steps, currentIdx, specBase, platformOpts)
159
194
 
195
+ // ── noAI 步骤硬门(坑 noai-done-bypass):noAI 步骤的确定性校验不可被 --done 绕过 ──
196
+ // 正常路径 agent 跑 `run <stage>` 推进到 noAI step 时,runStage 自动执行 _cliAction
197
+ // (stage.js noAI 分支,不写 step output);若 agent 对 noAI step 直接 --done,此前
198
+ // completeStep 无 noAI 检测直接标 completed,executePlanPostcheck 等 CLI 确定性校验被
199
+ // 静默跳过(实证:multi-agent-platform 2026-08-13-spec-sync-visibility tasks/ 从未
200
+ // 生成但 plan 阶段 completed——step4 output 为 agent 口吻 178 字符而非 CLI 自动路径的
201
+ // 空 output)。修复:--done 落到 noAI step 时同样执行 _cliAction,校验 throw →
202
+ // completeStep 不推进(step 保持 pending)。分支对齐 stage.js 的 noAI 自动执行。
203
+ const _isNoAIStep = currentStepDef.noAI === true || currentStep?.noAI === true
204
+ if (_isNoAIStep) {
205
+ const _cliAction = currentStepDef._cliAction || currentStep?._cliAction
206
+ console.log(`⚙️ Step ${currentIdx + 1}/${steps.length}: ${steps[currentIdx].name}(noAI,--done 路径执行 CLI 校验)`)
207
+ if (_cliAction === 'scanPreflight') {
208
+ await executeScanPreflight(cwd, platformOpts, stageData.scanProfile || computeScanProfile(cwd, platformOpts))
209
+ } else if (_cliAction === 'scanPostcheck') {
210
+ await executeScanPostcheck(cwd, platformOpts, stageData.scanProfile || computeScanProfile(cwd, platformOpts))
211
+ } else if (_cliAction === 'planPostcheck') {
212
+ await runPlanPostcheckLib({ cwd, specRoot: platformOpts?.specRoot, resolveChangeDir, progress })
213
+ }
214
+ }
215
+
160
216
  steps[currentIdx].status = 'completed'
161
217
  steps[currentIdx].completedAt = new Date().toLocaleString('zh-CN',{hour12:false})
162
218
  if (outputText) {
@@ -688,6 +744,7 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
688
744
  const prevOutput = currentStep.output || ''
689
745
  const waitRound = (currentStep.waitRound || 0) + 1
690
746
  currentStep.waitRound = waitRound
747
+ enforceWaitChoice(currentStepDef, currentStep, answer, stageName, changeName)
691
748
  currentStep.waitAnswer = answer
692
749
  currentStep.waitAnswers = Array.isArray(currentStep.waitAnswers) ? currentStep.waitAnswers : []
693
750
  currentStep.waitAnswers.push({
@@ -66,6 +66,7 @@ export const definition = {
66
66
  maxWaitRounds: 5,
67
67
  waitReason: '等待用户回答需求问题',
68
68
  waitOptions: ['回答见--answer', '信息够了,进入设计'],
69
+ waitFreeAnswer: true, // 开放回答型:--answer 内容为自由文本(需求澄清回答),非封闭单选,豁免 enforceWaitChoice
69
70
  prompt: `分析需求,必要时追问,然后设计方案。
70
71
 
71
72
  ### 操作
@@ -79,6 +79,7 @@ export const definition = {
79
79
  maxWaitRounds: 8,
80
80
  waitReason: '等待用户回答需求问题或澄清',
81
81
  waitOptions: ['回答见--answer', '信息够了,进入方案讨论'],
82
+ waitFreeAnswer: true, // 开放回答型:--answer 内容为自由文本(需求澄清回答),非封闭单选,豁免 enforceWaitChoice
82
83
  prompt: `通过对话探索需求细节、分析原型(如有)、判断拆分/批量、并对歧义点做需求澄清 Grill。本步骤合并了原有的对话探索、原型分析、需求范围评估、需求澄清 Grill 四个环节——按下方 A→D 顺序按需执行。提问遵守「一次只问一个」(见 D 节追问策略):必须问的问题逐个问,能查代码/文档解决的不问;不要把多个歧义打包成一屏清单一次抛给用户(用户记不住、答不全,反而漏掉关键约束);也不要为凑数把单个问题拖成多轮。
83
84
 
84
85
  ### A. 原型/设计图分析(如用户提供)