sillyspec 3.26.8 → 3.26.9

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.
Files changed (50) hide show
  1. package/.claude/skills/sillyspec-archive/SKILL.md +1 -2
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +1 -2
  3. package/.claude/skills/sillyspec-execute/SKILL.md +10 -2
  4. package/.claude/skills/sillyspec-explore/SKILL.md +0 -1
  5. package/.claude/skills/sillyspec-plan/SKILL.md +1 -2
  6. package/.claude/skills/sillyspec-quick/SKILL.md +1 -2
  7. package/.claude/skills/sillyspec-scan/SKILL.md +2 -2
  8. package/.claude/skills/sillyspec-status/SKILL.md +0 -1
  9. package/.claude/skills/sillyspec-verify/SKILL.md +1 -2
  10. package/README.md +5 -9
  11. package/package.json +2 -2
  12. package/packages/dashboard/server/index.js +14 -8
  13. package/src/change-risk-profile.js +5 -207
  14. package/src/config-schema.js +6 -1
  15. package/src/constants.js +97 -106
  16. package/src/db-engine.js +1 -1
  17. package/src/docs-check.js +34 -5
  18. package/src/git-helper.js +2 -2
  19. package/src/hooks/worktree-guard.js +1 -1
  20. package/src/index.js +96 -13
  21. package/src/init.js +14 -3
  22. package/src/machine-interface.js +7 -4
  23. package/src/progress/stage-machine.js +3 -0
  24. package/src/quicklog.js +182 -1
  25. package/src/run/command.js +101 -3
  26. package/src/run/complete.js +9 -1
  27. package/src/run/gates.js +67 -1
  28. package/src/run/prompt.js +721 -713
  29. package/src/run/quick-audit.js +146 -137
  30. package/src/run/shared.js +1197 -1109
  31. package/src/run/stage.js +17 -1
  32. package/src/scan-diff.js +388 -0
  33. package/src/scan-staleness.js +18 -13
  34. package/src/stage-contract-spec.js +1 -1
  35. package/src/stage-review.js +1 -1
  36. package/src/stages/brainstorm.js +2 -2
  37. package/src/stages/execute.js +991 -985
  38. package/src/stages/plan.js +22 -7
  39. package/src/stages/quick.js +1 -1
  40. package/src/stages/scan.js +2 -2
  41. package/src/stages/verify.js +10 -11
  42. package/src/sync.js +12 -0
  43. package/src/task-review.js +12 -4
  44. package/src/workflow.js +8 -185
  45. package/src/worktree-apply.js +122 -0
  46. package/src/worktree.js +2 -2
  47. package/templates/claude-instruction.md +1 -1
  48. package/templates/prompts/taskcard-rules.md +1 -0
  49. package/templates/prompts/verify-probes.md +2 -2
  50. package/templates/skills/sillyspec-onboard/SKILL.md +0 -92
package/src/constants.js CHANGED
@@ -1,106 +1,97 @@
1
- /**
2
- * SillySpec 平台状态枚举
3
- *
4
- * 所有平台产物(manifest、pointer、postcheck、workflow-runs)共享这些枚举。
5
- * SillyHub 侧直接使用常量值,不需要猜字符串。
6
- */
7
-
8
- // ── scan 阶段状态 ──
9
- export const SCAN_STATUS = Object.freeze({
10
- PENDING: 'pending', // scan 未开始
11
- IN_PROGRESS: 'in_progress', // scan 进行中
12
- SUCCESS: 'success', // scan 成功(所有检查通过)
13
- COMPLETED_WITH_WARNINGS: 'completed_with_warnings', // scan 成功但有警告
14
- FAILED_POST_CHECK: 'failed_post_check', // scan 失败(post-check 不通过)
15
- })
16
-
17
- // ── 平台指针状态 ──
18
- export const POINTER_STATUS = Object.freeze({
19
- ACTIVE: 'active', // 指针活跃,任务进行中
20
- SCAN_COMPLETED: 'scan_completed', // scan 已完成
21
- STALE: 'stale', // 指针过时(完成超过 24h,建议清理)
22
- CORRUPTED: 'corrupted', // 指针损坏(缺少必要字段)
23
- })
24
-
25
- // ── workflow 检查状态 ──
26
- export const WORKFLOW_STATUS = Object.freeze({
27
- PASS: 'pass',
28
- FAIL: 'fail',
29
- SKIPPED: 'skipped',
30
- })
31
-
32
- // ── postcheck 检查严重级别 ──
33
- export const CHECK_SEVERITY = Object.freeze({
34
- FAILED: 'failed',
35
- WARNING: 'warning',
36
- PASSED: 'passed',
37
- })
38
-
39
- // ── stage 步骤状态 ──
40
- export const STEP_STATUS = Object.freeze({
41
- PENDING: 'pending',
42
- IN_PROGRESS: 'in-progress',
43
- COMPLETED: 'completed',
44
- SKIPPED: 'skipped',
45
- })
46
-
47
- // ── stage 阶段状态 ──
48
- export const STAGE_STATUS = Object.freeze({
49
- PENDING: 'pending',
50
- IN_PROGRESS: 'in_progress',
51
- COMPLETED: 'completed',
52
- FAILED_POST_CHECK: 'failed_post_check',
53
- })
54
-
55
- /**
56
- * 判断指针是否过时(完成超过 24h)
57
- */
58
- export function isPointerStale(pointer) {
59
- if (!pointer.completedAt) return false
60
- const completed = new Date(pointer.completedAt)
61
- const age = Date.now() - completed.getTime()
62
- return age > 24 * 60 * 60 * 1000
63
- }
64
-
65
- /**
66
- * 判断指针是否损坏(缺少必要字段)
67
- */
68
- export function isPointerCorrupted(pointer) {
69
- return !pointer || !pointer.specRoot || !pointer.savedAt
70
- }
71
-
72
- // ── scan 必需文档清单(stage-contract validator 与 scan-postcheck 共用单一真相源)──
73
- // 曾分两处硬编码同一份清单,加新文档需同步改两处,易分叉。现收敛到此处一处。
74
- export const SCAN_REQUIRED_DOCS = Object.freeze([
75
- 'ARCHITECTURE.md',
76
- 'CONVENTIONS.md',
77
- 'STRUCTURE.md',
78
- 'INTEGRATIONS.md',
79
- 'TESTING.md',
80
- 'CONCERNS.md',
81
- 'PROJECT.md',
82
- ])
83
-
84
- // ── scan quick profile 必需文档子集(4 份核心文档)──
85
- // quick 档(--quick 显式 或 小项目自动判定)只生成这 4 份;scan-postcheck 按 profile 取清单:
86
- // mode==='quick' → SCAN_REQUIRED_DOCS_QUICK,否则 → SCAN_REQUIRED_DOCS(7 份)。
87
- // 由 scan-postcheck.js + complete-handlers.js 透传的 scanProfile.mode 决定取哪份。
88
- export const SCAN_REQUIRED_DOCS_QUICK = Object.freeze([
89
- 'PROJECT.md',
90
- 'ARCHITECTURE.md',
91
- 'CONVENTIONS.md',
92
- 'STRUCTURE.md',
93
- ])
94
-
95
- // ── 辅助 stage 清单(stage-contract 转移规则 与 stages/index 注册表 共用单一真相源)──
96
- // 曾在 stage-contract.js 与 stages/index.js 逐字重复定义,新增/调整辅助 stage 需同步改两处
97
- // 易分叉。现收敛一处。(注意:主流程序 STAGE_ORDER/mainFlowStages/flowStages 语义不同、
98
- // 各自用途正确,不在本清单范围。)
99
- export const AUXILIARY_STAGES = Object.freeze([
100
- 'scan',
101
- 'quick',
102
- 'explore',
103
- 'archive',
104
- 'status',
105
- 'doctor',
106
- ])
1
+ /**
2
+ * SillySpec 平台状态枚举
3
+ *
4
+ * 所有平台产物(manifest、pointer、postcheck、workflow-runs)共享这些枚举。
5
+ * SillyHub 侧直接使用常量值,不需要猜字符串。
6
+ */
7
+
8
+ // ── scan 阶段状态 ──
9
+ export const SCAN_STATUS = Object.freeze({
10
+ PENDING: 'pending', // scan 未开始
11
+ IN_PROGRESS: 'in_progress', // scan 进行中
12
+ SUCCESS: 'success', // scan 成功(所有检查通过)
13
+ COMPLETED_WITH_WARNINGS: 'completed_with_warnings', // scan 成功但有警告
14
+ FAILED_POST_CHECK: 'failed_post_check', // scan 失败(post-check 不通过)
15
+ })
16
+
17
+ // ── 平台指针状态 ──
18
+ export const POINTER_STATUS = Object.freeze({
19
+ ACTIVE: 'active', // 指针活跃,任务进行中
20
+ SCAN_COMPLETED: 'scan_completed', // scan 已完成
21
+ STALE: 'stale', // 指针过时(完成超过 24h,建议清理)
22
+ CORRUPTED: 'corrupted', // 指针损坏(缺少必要字段)
23
+ })
24
+
25
+ // ── workflow 检查状态 ──
26
+ export const WORKFLOW_STATUS = Object.freeze({
27
+ PASS: 'pass',
28
+ FAIL: 'fail',
29
+ SKIPPED: 'skipped',
30
+ })
31
+
32
+ // ── postcheck 检查严重级别 ──
33
+ export const CHECK_SEVERITY = Object.freeze({
34
+ FAILED: 'failed',
35
+ WARNING: 'warning',
36
+ PASSED: 'passed',
37
+ })
38
+
39
+ /**
40
+ * 判断指针是否过时(完成超过 24h)
41
+ */
42
+ export function isPointerStale(pointer) {
43
+ if (!pointer.completedAt) return false
44
+ const completed = new Date(pointer.completedAt)
45
+ const age = Date.now() - completed.getTime()
46
+ return age > 24 * 60 * 60 * 1000
47
+ }
48
+
49
+ /**
50
+ * 判断指针是否损坏(缺少必要字段)
51
+ */
52
+ export function isPointerCorrupted(pointer) {
53
+ return !pointer || !pointer.specRoot || !pointer.savedAt
54
+ }
55
+
56
+ // ── scan 必需文档清单(stage-contract validator 与 scan-postcheck 共用单一真相源)──
57
+ // 曾分两处硬编码同一份清单,加新文档需同步改两处,易分叉。现收敛到此处一处。
58
+ export const SCAN_REQUIRED_DOCS = Object.freeze([
59
+ 'ARCHITECTURE.md',
60
+ 'CONVENTIONS.md',
61
+ 'STRUCTURE.md',
62
+ 'INTEGRATIONS.md',
63
+ 'TESTING.md',
64
+ 'CONCERNS.md',
65
+ 'PROJECT.md',
66
+ ])
67
+
68
+ // ── scan quick profile 必需文档子集(4 份核心文档)──
69
+ // quick 档(--quick 显式 或 小项目自动判定)只生成这 4 份;scan-postcheck 按 profile 取清单:
70
+ // mode==='quick' → SCAN_REQUIRED_DOCS_QUICK,否则 → SCAN_REQUIRED_DOCS(7 份)。
71
+ // 由 scan-postcheck.js + complete-handlers.js 透传的 scanProfile.mode 决定取哪份。
72
+ export const SCAN_REQUIRED_DOCS_QUICK = Object.freeze([
73
+ 'PROJECT.md',
74
+ 'ARCHITECTURE.md',
75
+ 'CONVENTIONS.md',
76
+ 'STRUCTURE.md',
77
+ ])
78
+
79
+ // ── 辅助 stage 清单(stage-contract 转移规则 与 stages/index 注册表 共用单一真相源)──
80
+ // 曾在 stage-contract.js 与 stages/index.js 逐字重复定义,新增/调整辅助 stage 需同步改两处
81
+ // 易分叉。现收敛一处。(注意:主流程序 STAGE_ORDER/mainFlowStages/flowStages 语义不同、
82
+ // 各自用途正确,不在本清单范围。)
83
+ export const AUXILIARY_STAGES = Object.freeze([
84
+ 'scan',
85
+ 'quick',
86
+ 'explore',
87
+ 'archive',
88
+ 'status',
89
+ 'doctor',
90
+ ])
91
+
92
+ // ── 只读辅助 stage 清单(查询型辅助阶段,可读短路不触碰进度库)──
93
+ // 供 command.js 判断 status/doctor 等查询型辅助阶段执行只读短路(FR-04 / D-005@v2)。
94
+ export const READONLY_AUXILIARY_STAGES = Object.freeze([
95
+ 'status',
96
+ 'doctor',
97
+ ])
package/src/db-engine.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // 封装 node:sqlite 内置模块 + 消解 better-sqlite3 → node:sqlite 三缺口:
3
3
  // pragma → exec / transaction → 手写 SAVEPOINT / pluck → Object.values 取首列
4
4
  // 供 db.js 与 doctor-diagnostics 共用(方案 B 单一换引擎点,D-002)。
5
- // node:sqlite 在 node v22.11.0+ 无需 --experimental-sqlite flag(仍发 ExperimentalWarning)。
5
+ // node:sqlite 在 node v22.13.0+ 无需 --experimental-sqlite flag(仍发 ExperimentalWarning)。
6
6
  // Author: sillyspec execute W1 task-02
7
7
  // Created: 2026-08-11
8
8
  import { DatabaseSync } from 'node:sqlite';
package/src/docs-check.js CHANGED
@@ -12,12 +12,15 @@
12
12
  * 只读校验(不修改任何被校验文件);纯 Node 内置模块零依赖(D-008:glob 手写 walker,
13
13
  * 仅「目录递归」「目录单层」「字面路径」三形态);Windows 路径归一化;兼容 CRLF/LF。
14
14
  */
15
- import { readFileSync, existsSync, readdirSync } from 'node:fs'
15
+ import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs'
16
16
  import { join } from 'node:path'
17
17
  import jsYaml from 'js-yaml'
18
18
 
19
19
  /** 提取 file.js:line / file.js:start-end 引用(.js/.mjs,反引号包裹与裸文本均命中,全文扫描 D-006) */
20
- export const REF_RE = /([A-Za-z0-9_.\-\/]+\.(?:js|mjs)):(\d+)(?:-(\d+))?/g
20
+ const REF_RE = /([A-Za-z0-9_.\-\/]+\.(?:js|mjs)):(\d+)(?:-(\d+))?/g
21
+
22
+ /** 缺省扫描范围:docs/ + .sillyspec/docs/(scan/modules 产物同是文档,失效即该暴露;2026-08-16 用户裁决改缺省,见 doc-consistency-debt.md §八) */
23
+ const DEFAULT_DOC_PATHS = ['docs/**/*.md', '.sillyspec/docs/**/*.md']
21
24
 
22
25
  /**
23
26
  * 读 local.yaml 的 docs-check 段(best-effort,绝不抛;缺文件/无段 → 全缺省)。
@@ -187,6 +190,26 @@ export function walkGlob(projectRoot, pattern, skip = []) {
187
190
  (sn.includes('*') && matchSimple(rel, sn))
188
191
  })
189
192
  const matchExt = (name, ext) => name.endsWith(ext)
193
+ // 形态 0:**/*.ext(根级递归,无 base 前缀——B11b:原被形态 2 误解析为字面目录 `**` 静默 0 命中)
194
+ let m0 = norm(pattern).match(/^\*\*\/\*(\.[A-Za-z0-9]+)$/)
195
+ if (m0) {
196
+ const [, ext] = m0
197
+ const out = []
198
+ const rec = (dir, rel, depth) => {
199
+ if (depth > 12) return
200
+ let entries
201
+ try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return }
202
+ for (const e of entries) {
203
+ if (e.name === 'node_modules' || e.name === '.git') continue
204
+ const relPath = rel ? `${rel}/${e.name}` : e.name
205
+ if (isSkipped(relPath)) continue
206
+ if (e.isDirectory()) rec(join(dir, e.name), relPath, depth + 1)
207
+ else if (matchExt(e.name, ext)) out.push(relPath)
208
+ }
209
+ }
210
+ rec(projectRoot, '', 0)
211
+ return out
212
+ }
190
213
  // 形态 1:**/*.ext
191
214
  let m = norm(pattern).match(/^(.+?)\/\*\*\/\*(\.[A-Za-z0-9]+)$/)
192
215
  if (m) {
@@ -224,7 +247,13 @@ export function walkGlob(projectRoot, pattern, skip = []) {
224
247
  }
225
248
  // 形态 3:字面路径(不含任何通配元字符才直传;含 ?、{ }、[ ] 等不支持形态落 error)
226
249
  if (!/[*?{}\[\]]/.test(pattern)) {
227
- return existsSync(join(projectRoot, pattern)) ? [norm(pattern)] : []
250
+ const abs = join(projectRoot, pattern)
251
+ if (!existsSync(abs)) return []
252
+ // B11b:目录字面量抛配置错误(原直传 readFileSync 撞 EISDIR 裸崩 exit 1;契约应 exit 2)
253
+ if (statSync(abs).isDirectory()) {
254
+ throw new DocsCheckConfigError(`路径是目录不是文档:${pattern}(文档路径应为 .md 文件或 glob 形态 **/*.ext / dir/**/*.ext / dir/*.ext)`)
255
+ }
256
+ return [norm(pattern)]
228
257
  }
229
258
  throw new DocsCheckConfigError(`不支持的 glob 形态:${pattern}(当前仅支持 dir/**/*.ext、dir/*.ext、字面路径)`)
230
259
  }
@@ -247,8 +276,8 @@ function relDisplay(absPath, projectRoot) {
247
276
  export function runDocsCheck(opts) {
248
277
  // paths/docs 显式传 null(readDocsCheckConfig 无 local.yaml 段时的回退值)须落回缺省 glob,
249
278
  // 解构默认值只挡 undefined 不挡 null——docs-check 无配置裸跑曾因此 null.flatMap 崩溃。
250
- const { projectRoot, docs = null, paths: rawPaths = ['docs/**/*.md'], skip = [], keywordAssert = true } = opts || {}
251
- const paths = Array.isArray(rawPaths) && rawPaths.length > 0 ? rawPaths : ['docs/**/*.md']
279
+ const { projectRoot, docs = null, paths: rawPaths = DEFAULT_DOC_PATHS, skip = [], keywordAssert = true } = opts || {}
280
+ const paths = Array.isArray(rawPaths) && rawPaths.length > 0 ? rawPaths : DEFAULT_DOC_PATHS
252
281
  const warnings = []
253
282
  if (keywordAssert === false) warnings.push('关键词断言已关闭(keywordAssert=false),仅做存在性校验')
254
283
  const docFiles = docs && docs.length > 0
package/src/git-helper.js CHANGED
@@ -34,7 +34,7 @@ export function safeGit(cwd, args, opts = {}) {
34
34
  // 格式化(取首行)推迟到最终返回,避免重试分支重复格式化,并保留原始 code 供重试判定。
35
35
  const attempt = (t) => {
36
36
  try {
37
- let value = execFileSync('git', fullArgs, { encoding: 'utf8', timeout: t })
37
+ let value = execFileSync('git', fullArgs, { encoding: 'utf8', timeout: t, stdio: ['ignore', 'pipe', 'pipe'] })
38
38
  if (trim) value = value.trim()
39
39
  return { value, errorObj: null }
40
40
  } catch (e) {
@@ -59,7 +59,7 @@ export function safeGit(cwd, args, opts = {}) {
59
59
  export function git(cwd, args, opts = {}) {
60
60
  const { trim = true, timeout = 5000 } = opts
61
61
  const fullArgs = ['-c', `safe.directory=${cwd}`, '-C', cwd, ...args]
62
- const value = execFileSync('git', fullArgs, { encoding: 'utf8', timeout })
62
+ const value = execFileSync('git', fullArgs, { encoding: 'utf8', timeout, stdio: ['ignore', 'pipe', 'pipe'] })
63
63
  return trim ? value.trim() : value
64
64
  }
65
65
 
@@ -748,7 +748,7 @@ export function shouldBlockWrite(filePath, cwd) {
748
748
  * @param {string} cwd - 当前工作目录
749
749
  * @returns {{ blocked: boolean, reason?: string }}
750
750
  */
751
- export function shouldBlockBash(command, cwd) {
751
+ function shouldBlockBash(command, cwd) {
752
752
  if (!command || !command.trim()) return { blocked: false }
753
753
 
754
754
  const callerCwd = cwd || process.cwd()
package/src/index.js CHANGED
@@ -8,16 +8,19 @@
8
8
  */
9
9
  import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'fs';
10
10
  import { writeAtomicSync } from './fs-atomic.js';
11
- import { join, resolve } from 'path';
11
+ import { basename, join, resolve } from 'path';
12
12
  import { execSync } from 'child_process';
13
13
  import { git } from './git-helper.js';
14
14
  import { getVersion } from './version.js';
15
- import { ProgressManager, resolvePlatformSpecDir } from './progress.js';
15
+
16
+ // E22(性能#2):progress.js(拖 db.js→node:sqlite 链 ~48ms)与 run/shared.js(拖 stages 全家
17
+ // ~30ms)原先为顶层静态 import——最轻命令(--version/help)白付 ~78ms 启动税。两条链无模块级
18
+ // 使用点(printUsage 纯文本),改在 main() 早退之后动态加载,轻路径零加载;重路径首个使用点
19
+ // 之前完成加载,行为零变化。
16
20
 
17
21
  // ── CLI 入口 ──
18
22
 
19
- // did-you-mean 从 ./run/shared.js 复用(命令级 + flag 级 typo 建议)。
20
- import { didYouMean, assertSafeChangeName, resolveSpecDir, detectWorktreeSpecDrift } from './run/shared.js'
23
+ // did-you-mean 从 ./run/shared.js 复用(命令级 + flag 级 typo 建议)。E22:同上改惰性动态加载。
21
24
  function printUsage() {
22
25
  console.log(`
23
26
  SillySpec CLI — 规范驱动开发工具包
@@ -56,6 +59,8 @@ SillySpec CLI — 规范驱动开发工具包
56
59
  --confirm 完成时确认接受变更审计
57
60
  scan: --quick | --standard | --deep 显式选择 profile(优先于规模自动判定;三档互斥)
58
61
  --force-rescan 覆盖已有 scan 文档保护
62
+ --diff [--base <commit>] [--full] [--report] scan 文档 vs 源码漂移清单(纯只读)
63
+ (子命令: sillyspec scan diff 等价 --diff)
59
64
  archive: --confirm 归档确认(必须)
60
65
  auto: --mode <模式> 显式指定流程模式
61
66
  平台: --runtime-root <path> / --workspace-id <id> / --scan-run-id <id>
@@ -159,6 +164,10 @@ async function main() {
159
164
  process.exit(0);
160
165
  }
161
166
 
167
+ // E22:重路径统一加载(轻路径 --version/help 已早退,未付此税)。
168
+ const { ProgressManager, resolvePlatformSpecDir } = await import('./progress.js');
169
+ const { didYouMean, assertSafeChangeName, resolveSpecDir, detectWorktreeSpecDrift } = await import('./run/shared.js');
170
+
162
171
  // 解析全局选项
163
172
  let json = false;
164
173
  let saveWorkflowRunFlag = false;
@@ -308,21 +317,21 @@ async function main() {
308
317
  break;
309
318
  case 'set-stage': {
310
319
  const setStageName = filteredArgs[2];
311
- if (!setStageName) { console.log('❌ 用法: sillyspec progress set-stage <stage> [--change <name>]'); break; }
320
+ if (!setStageName) { console.log('❌ 用法: sillyspec progress set-stage <stage> [--change <name>]'); process.exit(2); }
312
321
  pm.setStage(dir, setStageName, progChangeName);
313
322
  break;
314
323
  }
315
324
  case 'add-step': {
316
325
  const addStepStage = filteredArgs[2];
317
326
  const addStepName = filteredArgs[3];
318
- if (!addStepStage || !addStepName) { console.log('❌ 用法: sillyspec progress add-step <stage> <step-name> [--change <name>]'); break; }
327
+ if (!addStepStage || !addStepName) { console.log('❌ 用法: sillyspec progress add-step <stage> <step-name> [--change <name>]'); process.exit(2); }
319
328
  pm.addStep(dir, addStepStage, addStepName, progChangeName);
320
329
  break;
321
330
  }
322
331
  case 'update-step': {
323
332
  const updStepStage = filteredArgs[2];
324
333
  const updStepName = filteredArgs[3];
325
- if (!updStepStage || !updStepName) { console.log('❌ 用法: sillyspec progress update-step <stage> <step-name> --status <status> [--output <text>] [--change <name>]'); break; }
334
+ if (!updStepStage || !updStepName) { console.log('❌ 用法: sillyspec progress update-step <stage> <step-name> --status <status> [--output <text>] [--change <name>]'); process.exit(2); }
326
335
  let updStatus = null, updOutput = undefined;
327
336
  for (let ai = 0; ai < args.length; ai++) {
328
337
  if (args[ai] === '--status' && args[ai + 1]) { updStatus = args[ai + 1]; ai++; }
@@ -334,7 +343,7 @@ async function main() {
334
343
  }
335
344
  case 'complete-stage': {
336
345
  const compStageName = filteredArgs[2];
337
- if (!compStageName) { console.log('❌ 用法: sillyspec progress complete-stage <stage> [--force]'); break; }
346
+ if (!compStageName) { console.log('❌ 用法: sillyspec progress complete-stage <stage> [--force]'); process.exit(2); }
338
347
  pm.completeStage(dir, compStageName, progChangeName, { force: args.includes('--force') });
339
348
  break;
340
349
  }
@@ -357,7 +366,7 @@ async function main() {
357
366
  if (Object.keys(batchData).length === 0) {
358
367
  console.log('用法: sillyspec progress batch --total 100 --completed 73');
359
368
  console.log(' sillyspec progress batch --status');
360
- break;
369
+ process.exit(2);
361
370
  }
362
371
  pm.updateBatchProgress(dir, batchData, progChangeName);
363
372
  console.log('✅ 批量进度已更新');
@@ -366,6 +375,7 @@ async function main() {
366
375
  }
367
376
  default:
368
377
  console.log('用法: sillyspec progress <init|show|validate|reset|set-stage|add-step|update-step|complete-stage>');
378
+ process.exit(2);
369
379
  }
370
380
  break;
371
381
  }
@@ -387,7 +397,11 @@ async function main() {
387
397
  // 只传 cwd 致 --spec-dir 对 gate verify 无效。不传 --spec-dir 时 specBase 走默认
388
398
  // resolveSpecDir(cwd),向后兼容。
389
399
  const gateOpts = { cwd: dir };
390
- if (specDir) gateOpts.specBase = specDir;
400
+ // A4:平台指针 / 显式 --spec-dir / 本地 fallback 三合一解析 specBase——平台模式(指针指向外部
401
+ // specDir)下 gate 也能核验(原只传 cwd 致 runGate 的 resolveSpecDir 指向本地孤儿库,恒
402
+ // 「无法核验」exit 2)。resolvePlatformSpecDir 指针失效时抛 PointerUnreachableError,由顶层 catch 优雅 fail-closed。
403
+ const gateSpecBase = resolvePlatformSpecDir(dir, specDir);
404
+ if (gateSpecBase) gateOpts.specBase = gateSpecBase;
391
405
  // drift 锚定(坑 execute-runs-isolation,对齐 command.js execute drift 守卫 + machine-interface
392
406
  // runGate 已扩展的 specDriftAnchor 入参):gate 是顶层命令不走 runCommand/drift 守卫,worktree cwd
393
407
  // 下 execute 段 marker 读取会落副本 .runtime。仅在未显式 --spec-dir 时算(对齐守卫条件 !specDir),
@@ -442,7 +456,9 @@ async function main() {
442
456
  // derive verify-test facet 内部同样调 runVerifyTestCheck 依赖 specBase,平台模式
443
457
  // 下需透传;不传 --spec-dir 时走默认 resolveSpecDir(cwd),向后兼容。
444
458
  const deriveOpts = { cwd: dir };
445
- if (specDir) deriveOpts.specBase = specDir;
459
+ // A4:同 gate case——平台指针/--spec-dir/本地 fallback 三合一解析 specBase。
460
+ const deriveSpecBase = resolvePlatformSpecDir(dir, specDir);
461
+ if (deriveSpecBase) deriveOpts.specBase = deriveSpecBase;
446
462
  // drift 锚定(同 gate case,对齐 command.js drift 守卫 + machine-interface runDerive 的 specDriftAnchor
447
463
  // 入参):derive 顶层命令不走 drift 守卫,worktree cwd 下 task-reviews facet 的 execute-run-id marker
448
464
  // 读取会落副本。未显式 --spec-dir 时算 anchor,runDerive task-reviews 的 resolveRuntimeRoot 读主仓 marker。
@@ -629,8 +645,54 @@ async function main() {
629
645
  }
630
646
  throw e;
631
647
  }
648
+ } else if (docsSubCmd === 'gate') {
649
+ // docs gate ratchet(docs-signals-o12 cff7479 产品化,pre-push 第三道关):docs check
650
+ // 失效数 ≤ 基线放行,超基线拦。基线 .sillyspec/docs-check-baseline 首次须显式
651
+ // --init-baseline 立线(fail-closed,不悄悄合法化存量)。
652
+ // ⚠ 2026-08-16 补接线:cff7479 落地时漏了本 CLI 分支,.husky/pre-push 一直在跑
653
+ // usage 输出且 exit 0——gate 形同虚设(docs-signals 范围内文件+hook 已就位,仅此分支缺)。
654
+ // B9(2026-08-16):flag 白名单化(对齐 docs check 分支)——未知 --xxx 显式 exit 2
655
+ // (interface-contract §1.3b 宣称),并接线 --paths 透传 runDocsGate.checkOpts(原被忽略)。
656
+ const GATE_BARE_FLAGS = ['--init-baseline'];
657
+ const GATE_PAIRED_FLAGS = ['--paths'];
658
+ const rawGateArgs = filteredArgs.slice(2);
659
+ let initBaseline = false;
660
+ let cliGatePaths = null;
661
+ for (let i = 0; i < rawGateArgs.length; i++) {
662
+ const a = rawGateArgs[i];
663
+ if (a === '--paths' && rawGateArgs[i + 1] !== undefined) {
664
+ cliGatePaths = rawGateArgs[i + 1].split(',').map(s => s.trim()).filter(Boolean);
665
+ i++;
666
+ } else if (a === '--paths') {
667
+ console.error('❌ docs gate: --paths 缺值(逗号分隔 glob,如 --paths "docs/**/*.md")');
668
+ process.exit(2);
669
+ } else if (GATE_BARE_FLAGS.includes(a)) {
670
+ if (a === '--init-baseline') initBaseline = true;
671
+ } else if (a.startsWith('--')) {
672
+ console.error(`❌ docs gate: 未知 flag「${a}」。已知 flag:${[...GATE_BARE_FLAGS, ...GATE_PAIRED_FLAGS].join(' ')}(--json 为全局 flag)`);
673
+ process.exit(2);
674
+ } else {
675
+ console.error(`❌ docs gate: 多余位置参数「${a}」。用法:sillyspec docs gate [--init-baseline] [--paths <glob,...>] [--json]`);
676
+ process.exit(2);
677
+ }
678
+ }
679
+ const { runDocsGate } = await import('./docs-gate.js');
680
+ // specBase:平台模式(specDir 指针)优先,本地回退 <repo>/.sillyspec——基线文件锚定
681
+ // 进度库根(与 .runtime 同级),跨命令稳定。
682
+ const gateSpecBase = specDir || join(dir, '.sillyspec');
683
+ const g = await runDocsGate(
684
+ { projectRoot: dir, specBase: gateSpecBase, initBaseline },
685
+ cliGatePaths ? { paths: cliGatePaths } : undefined
686
+ );
687
+ if (json) {
688
+ console.log(JSON.stringify(g, null, 2));
689
+ } else {
690
+ console.log(g.message);
691
+ }
692
+ process.exit(g.exitCode);
632
693
  } else {
633
- console.log('用法: sillyspec docs migrate | sillyspec docs check [--paths <glob,...>] [--json] [--suggest] [--suggest]');
694
+ console.log('用法: sillyspec docs migrate | sillyspec docs check [--paths <glob,...>] [--json] [--suggest] | sillyspec docs gate [--init-baseline] [--json]');
695
+ process.exit(2);
634
696
  }
635
697
  break;
636
698
  }
@@ -766,7 +828,27 @@ async function main() {
766
828
  await runCommand([command, ...filteredArgs.slice(1)], doctorEffectiveDir, specDir, { json });
767
829
  break;
768
830
  }
769
- case 'scan':
831
+ case 'scan': {
832
+ // scan diff 子命令(D-001@v1:接线唯一入口 index.js,非 command.js 裸 token 静默吞)。
833
+ // 纯只读比较命令:跳过下方共享块的 triggerPullActiveChange(不触发网络 pull),
834
+ // 也不走 scan 主流程(--standard/--deep 分支不动)。解析 --base/--full/--report 后转发 scan-diff。
835
+ if (filteredArgs[1] === 'diff') {
836
+ const { runScanDiff } = await import('./scan-diff.js')
837
+ const diffBaseIdx = filteredArgs.indexOf('--base')
838
+ const diffBase = diffBaseIdx >= 0 && filteredArgs[diffBaseIdx + 1] ? filteredArgs[diffBaseIdx + 1] : null
839
+ // specBase/projectName 与 scan 主流程同口径(scan-profile.js:specBase=进度库根,projectName=仓库根 basename)
840
+ const diffEffectiveDir = specDir ? dir : resolveEffectiveDir(dir)
841
+ process.exitCode = runScanDiff({
842
+ projectRoot: diffEffectiveDir,
843
+ specBase: specDir || join(diffEffectiveDir, '.sillyspec'),
844
+ projectName: basename(diffEffectiveDir),
845
+ base: diffBase,
846
+ full: filteredArgs.includes('--full'),
847
+ report: filteredArgs.includes('--report'),
848
+ })
849
+ break
850
+ }
851
+ }
770
852
  case 'status':
771
853
  case 'quick':
772
854
  case 'explore':
@@ -774,6 +856,7 @@ async function main() {
774
856
  case 'plan':
775
857
  case 'execute':
776
858
  case 'verify':
859
+ case 'auto':
777
860
  case 'archive': {
778
861
  const { runCommand } = await import('./run.js')
779
862
  const stageArgs = [command, ...filteredArgs.slice(1)]
package/src/init.js CHANGED
@@ -470,6 +470,14 @@ export async function cmdInit(projectDir, options = {}) {
470
470
  const version = getVersion();
471
471
  const resolvedSpecDir = specDir ? resolve(specDir) : null;
472
472
 
473
+ // C14c:绿地/棕地引导——必须在 doInstall 之前捕获目录状态(init 会创建 .sillyspec/CLAUDE.md/
474
+ // .gitignore 等,事后检测会误判);棕地(已有非隐藏源码)建议 scan,绿地(空目录)建议 brainstorm。
475
+ let brownfield = false;
476
+ try {
477
+ const entries = readdirSync(projectDir, { withFileTypes: true });
478
+ brownfield = entries.some(e => !e.name.startsWith('.'));
479
+ } catch { brownfield = false; }
480
+
473
481
  // ── 交互式模式(--interactive 或 -i)──
474
482
  if (interactive && isTTY()) {
475
483
  // 欢迎画面
@@ -603,9 +611,12 @@ export async function cmdInit(projectDir, options = {}) {
603
611
  const specDisplay = resolvedSpecDir || '.sillyspec';
604
612
  console.log(` 📁 规范目录: ${chalk.cyan(specDisplay)}`);
605
613
  console.log('');
606
- console.log(' 下一步:使用 AI 技能开始工作');
607
- console.log(` OpenClaw: ${chalk.bold('/sillyspec:brainstorm')}`);
608
- console.log(` Claude Code: ${chalk.bold('/sillyspec:brainstorm')}`);
614
+ // C14c:绿地/棕地引导区分(README:73 棕地应 scan)——early 捕获的目录状态(init 前),
615
+ // 棕地有代码零上下文进 brainstorm 会迷失,建议先 scan 生成架构文档。
616
+ const nextCmd = brownfield ? '/sillyspec:scan' : '/sillyspec:brainstorm';
617
+ console.log(' 下一步:使用 AI 技能开始工作' + (brownfield ? '(检测到现有代码,建议先扫描生成架构文档)' : ''));
618
+ console.log(` OpenClaw: ${chalk.bold(nextCmd)}`);
619
+ console.log(` Claude Code: ${chalk.bold(nextCmd)}`);
609
620
  console.log('');
610
621
  console.log(chalk.dim(' 💡 增强能力:sillyspec setup(安装 MCP 工具)'));
611
622
  console.log('');
@@ -113,14 +113,16 @@ export function buildEnvelope({
113
113
  */
114
114
  export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot, specDriftAnchor, ctx = null } = {}) {
115
115
  const specRoot = specBase || resolveSpecDir(cwd);
116
- const pm = new ProgressManager();
116
+ // A4:pm 用 specRoot(而非默认 resolveSpecDir(cwd))——--spec-dir/平台模式下 specBase 是真实
117
+ // specDir,默认解析会指向本地孤儿库,与下方 planContent/changeDir 的 specRoot 两套事实源混拼。
118
+ const pm = new ProgressManager({ specDir: specRoot });
117
119
 
118
120
  try {
119
121
  // ── 只读契约守卫(D-002@v1):db 不存在时 fail-closed 返回 exit 2,不触发建库落盘 ──
120
122
  // pm.read → _ensureDB → db.init() 在 db 不存在/schema 戳落后时会 _createSchema + _save 落盘
121
123
  // (db.js:init),这会让一个声明只读的查询命令在 spec 目录凭空建 sillyspec.db,违反只读契约。
122
124
  // db 还没建 = 该变更从未跑过流程,无从核验 → 显式返回「无法核验」而非建库。
123
- const dbPath = join(resolveSpecDir(cwd), '.runtime', 'sillyspec.db');
125
+ const dbPath = join(specRoot, '.runtime', 'sillyspec.db');
124
126
  if (!existsSync(dbPath)) {
125
127
  const envelope = buildEnvelope({
126
128
  command: 'gate',
@@ -356,11 +358,12 @@ export async function runDerive(facet, changeName, { cwd, specBase, runtimeRoot,
356
358
  }
357
359
 
358
360
  const specRoot = specBase || resolveSpecDir(cwd);
359
- const pm = new ProgressManager();
361
+ // A4:同 runGate——pm 用 specRoot 统一事实源,平台/--spec-dir 模式不读本地孤儿库。
362
+ const pm = new ProgressManager({ specDir: specRoot });
360
363
 
361
364
  try {
362
365
  // ── 只读契约守卫(D-002@v1,同 runGate):db 不存在时 fail-closed 返回 exit 2,不建库落盘 ──
363
- const dbPath = join(resolveSpecDir(cwd), '.runtime', 'sillyspec.db');
366
+ const dbPath = join(specRoot, '.runtime', 'sillyspec.db');
364
367
  if (!existsSync(dbPath)) {
365
368
  const envelope = buildEnvelope({
366
369
  command: 'derive',
@@ -275,7 +275,10 @@ export class StageMachine {
275
275
  }
276
276
 
277
277
  // 找到第一个有 pending/waiting/failed 步骤的 in-progress 阶段
278
+ // C14b:排除 scan(同下方主流程推荐循环)——scan 是 auxiliary,中途未完成时恒处
279
+ // STAGE_ORDER 首位且 in-progress,会把「下一步」劫持为 scan,掩盖主流程真实待办。
278
280
  for (const s of STAGE_ORDER) {
281
+ if (s === 'scan') continue
279
282
  const sd = data.stages[s];
280
283
  if (!sd) continue;
281
284
  if (sd.status === 'in-progress' && sd.steps) {