sillyspec 3.25.1 → 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.
Files changed (39) hide show
  1. package/.claude/skills/sillyspec-doctor/SKILL.md +9 -0
  2. package/.playwright-mcp/console-2026-07-27T15-45-50-621Z.log +1 -0
  3. package/.playwright-mcp/page-2026-07-27T15-45-51-459Z.yml +144 -0
  4. package/.playwright-mcp/prompt-final.png +0 -0
  5. package/.playwright-mcp/prompt-view.png +0 -0
  6. package/CLAUDE.md +18 -0
  7. package/package.json +1 -1
  8. package/src/change-risk-profile.js +60 -12
  9. package/src/check-primitives.js +65 -0
  10. package/src/index.js +135 -6
  11. package/src/progress/change-registry.js +217 -0
  12. package/src/progress/consistency-doctor.js +259 -0
  13. package/src/progress/shared.js +33 -0
  14. package/src/progress/stage-machine.js +535 -0
  15. package/src/progress/step-store.js +256 -0
  16. package/src/progress.js +59 -1191
  17. package/src/quicklog.js +3 -1
  18. package/src/run/command.js +127 -50
  19. package/src/run/complete-handlers.js +2 -1
  20. package/src/run/complete.js +725 -695
  21. package/src/run/gates.js +326 -321
  22. package/src/run/prompt.js +619 -604
  23. package/src/run/quick-audit.js +107 -102
  24. package/src/run/shared.js +43 -0
  25. package/src/stage-contract-engine.js +150 -0
  26. package/src/stage-contract-spec.js +517 -0
  27. package/src/stage-contract.js +83 -148
  28. package/src/stage-review.js +63 -0
  29. package/src/stages/brainstorm.js +3 -1
  30. package/src/stages/execute.js +5 -2
  31. package/src/stages/plan-postcheck.js +38 -27
  32. package/src/stages/plan.js +12 -49
  33. package/src/stages/propose.js +2 -1
  34. package/src/stages/scan.js +3 -1
  35. package/src/workflow.js +9 -17
  36. package/src/worktree-apply.js +5 -2
  37. package/src/worktree-deps.js +114 -32
  38. package/src/worktree.js +6 -3
  39. package/src/brainstorm-postcheck.js +0 -158
@@ -51,9 +51,18 @@ sillyspec progress validate # 校验并修复
51
51
  sillyspec worktree doctor [--fix] # worktree 健康检查 + 修复
52
52
  sillyspec doctor --align-execute-progress --change <name> # 按 plan.md 声明对齐 execute 派生戳(dry-run,只报告将补哪些 step)
53
53
  sillyspec doctor --align-execute-progress --change <name> --confirm # 实际落盘:补 step 戳 + 置 execute stage status=completed
54
+ sillyspec doctor --cleanup-remnant # 扫描并报告 0 字节空占位 db(dry-run)
55
+ sillyspec doctor --cleanup-remnant --confirm # 实际删除空占位 db(仅删 0 字节,不动有内容的 db)
56
+ sillyspec doctor --dump-db --path <db 路径> # dump 指定 db 内容到文件(取证,查 stages/steps/changes 表)
57
+ sillyspec doctor --json # 结构化诊断 + 落盘 .sillyspec/.runtime/doctor-diagnosis.json
58
+ sillyspec runtime list # 枚举 .sillyspec/.runtime/ 下所有运行时产物(只读,看手上有哪些证据/状态文件)
54
59
  ```
55
60
 
56
61
  > `--align-execute-progress` 仅当 `plan.md` 所有 task checkbox 全勾时才对齐 execute 阶段进度戳。典型用于 worktree 已 cleanup(终态)但 execute 派生戳未盖上的死锁。默认 dry-run,加 `--confirm` 才写盘。doctor 信任 `plan.md` 声明、不复核代码,verify 阶段兜底。`--change` 缺省时按单活跃变更自动兜底。
62
+ >
63
+ > `--cleanup-remnant` 清理 init/平台切换残留的 0 字节空 db(占位但无数据)。默认 dry-run 只报告,`--confirm` 才删。绝不删有内容的 db。
64
+ >
65
+ > `runtime list` 是只读枚举:列出 `.sillyspec/.runtime/` 下的 db、doctor-diagnosis.json、workflow-runs/、gate-status.json、user-inputs.md 等产物及用途。多会话/压缩后想知道「我手上有哪些证据文件」时用。加 `--json` 程序化读取。
57
66
 
58
67
  ## 铁律
59
68
 
@@ -0,0 +1 @@
1
+ [ 809ms] [ERROR] Failed to load resource: the server responded with a status of 404 (File not found) @ http://localhost:8848/favicon.ico:0
@@ -0,0 +1,144 @@
1
+ - generic [active] [ref=e1]:
2
+ - banner [ref=e2]:
3
+ - generic [ref=e3]:
4
+ - generic [ref=e4]:
5
+ - text: Silly
6
+ - emphasis [ref=e5]: Spec
7
+ - generic [ref=e6]: Prompt Dossier
8
+ - generic [ref=e7]:
9
+ - text: ⌕
10
+ - textbox "搜索步骤名 / prompt 内容 / 占位符…" [ref=e8]
11
+ - generic [ref=e9]:
12
+ - generic [ref=e10]: 12 stages · 71 steps · 87k
13
+ - button "◐" [ref=e11] [cursor=pointer]
14
+ - generic [ref=e87]:
15
+ - navigation [ref=e88]:
16
+ - generic [ref=e89]:
17
+ - generic [ref=e90]:
18
+ - text: 主流程
19
+ - generic [ref=e91]: "4"
20
+ - generic [ref=e92] [cursor=pointer]:
21
+ - generic [ref=e93]: "01"
22
+ - generic [ref=e94]: 头脑风暴
23
+ - generic [ref=e95]: "8"
24
+ - generic [ref=e96] [cursor=pointer]:
25
+ - generic [ref=e97]: "02"
26
+ - generic [ref=e98]: 实现计划
27
+ - generic [ref=e99]: "5"
28
+ - generic [ref=e100] [cursor=pointer]:
29
+ - generic [ref=e101]: "03"
30
+ - generic [ref=e102]: 波次执行
31
+ - generic [ref=e103]: "12"
32
+ - generic [ref=e104] [cursor=pointer]:
33
+ - generic [ref=e105]: "04"
34
+ - generic [ref=e106]: 验证
35
+ - generic [ref=e107]: "7"
36
+ - generic [ref=e108]:
37
+ - generic [ref=e109]:
38
+ - text: 辅助
39
+ - generic [ref=e110]: "6"
40
+ - generic [ref=e111] [cursor=pointer]:
41
+ - generic [ref=e112]: "05"
42
+ - generic [ref=e113]: 项目扫描
43
+ - generic [ref=e114]: "11"
44
+ - generic [ref=e115] [cursor=pointer]:
45
+ - generic [ref=e116]: "06"
46
+ - generic [ref=e117]: 快速任务
47
+ - generic [ref=e118]: "3"
48
+ - generic [ref=e119] [cursor=pointer]:
49
+ - generic [ref=e120]: "07"
50
+ - generic [ref=e121]: 自由探索
51
+ - generic [ref=e122]: "1"
52
+ - generic [ref=e123] [cursor=pointer]:
53
+ - generic [ref=e124]: "08"
54
+ - generic [ref=e125]: 归档
55
+ - generic [ref=e126]: "5"
56
+ - generic [ref=e127] [cursor=pointer]:
57
+ - generic [ref=e128]: "09"
58
+ - generic [ref=e129]: 状态
59
+ - generic [ref=e130]: "3"
60
+ - generic [ref=e131] [cursor=pointer]:
61
+ - generic [ref=e132]: "10"
62
+ - generic [ref=e133]: 自检
63
+ - generic [ref=e134]: "5"
64
+ - generic [ref=e135]:
65
+ - generic [ref=e136]:
66
+ - text: 变体
67
+ - generic [ref=e137]: "1"
68
+ - generic [ref=e138] [cursor=pointer]:
69
+ - generic [ref=e139]: "11"
70
+ - generic [ref=e140]: 自动模式头脑风暴
71
+ - generic [ref=e141]: "4"
72
+ - generic [ref=e142]:
73
+ - generic [ref=e143]:
74
+ - text: 已废弃
75
+ - generic [ref=e144]: "1"
76
+ - generic [ref=e145] [cursor=pointer]:
77
+ - generic [ref=e146]: "12"
78
+ - generic [ref=e147]: propose · 已废弃
79
+ - generic [ref=e148]: "7"
80
+ - main [ref=e149]:
81
+ - generic [ref=e150]: 主流程 · STAGE 01
82
+ - heading "头脑风暴" [level=1] [ref=e151]
83
+ - paragraph [ref=e152]: 探索需求、分析技术方案、识别风险
84
+ - generic [ref=e153]:
85
+ - generic [ref=e154]: sourcesrc/stages/brainstorm.js
86
+ - generic [ref=e155]: persona资深架构师
87
+ - generic [ref=e156]: steps8
88
+ - generic [ref=e157]:
89
+ - button "全部展开" [ref=e158] [cursor=pointer]
90
+ - button "全部折叠" [ref=e159] [cursor=pointer]
91
+ - generic [ref=e160]: 8 STEPS
92
+ - generic [ref=e161]:
93
+ - article [ref=e162]:
94
+ - generic [ref=e163] [cursor=pointer]:
95
+ - generic [ref=e164]: 01 / 08
96
+ - generic [ref=e165]: 状态检查
97
+ - generic [ref=e166]: ▸
98
+ - article [ref=e167]:
99
+ - generic [ref=e168] [cursor=pointer]:
100
+ - generic [ref=e169]: 02 / 08
101
+ - generic [ref=e170]: 加载项目上下文
102
+ - generic [ref=e171]: ▸
103
+ - article [ref=e172]:
104
+ - generic [ref=e173] [cursor=pointer]:
105
+ - generic [ref=e174]: 03 / 08
106
+ - generic [ref=e175]: 对话式探索与需求澄清
107
+ - generic [ref=e176]:
108
+ - generic [ref=e177]: conditionalWait
109
+ - generic [ref=e178]: repeatable×8
110
+ - generic [ref=e179]: ▸
111
+ - article [ref=e180]:
112
+ - generic [ref=e181] [cursor=pointer]:
113
+ - generic [ref=e182]: 04 / 08
114
+ - generic [ref=e183]: 提出 2-3 种方案
115
+ - generic [ref=e184]: requiresWait
116
+ - generic [ref=e186]: ▸
117
+ - article [ref=e187]:
118
+ - generic [ref=e188] [cursor=pointer]:
119
+ - generic [ref=e189]: 05 / 08
120
+ - generic [ref=e190]: 分段展示设计
121
+ - generic [ref=e191]: requiresWait
122
+ - generic [ref=e193]: ▸
123
+ - article [ref=e194]:
124
+ - generic [ref=e195] [cursor=pointer]:
125
+ - generic [ref=e196]: 06 / 08
126
+ - generic [ref=e197]: 写设计文档并自审
127
+ - generic [ref=e198]: ▸
128
+ - article [ref=e199]:
129
+ - generic [ref=e200] [cursor=pointer]:
130
+ - generic [ref=e201]: 07 / 08
131
+ - generic [ref=e202]: Design Grill 交叉审查
132
+ - generic [ref=e203]: conditionalWait
133
+ - generic [ref=e205]: ▸
134
+ - article [ref=e206]:
135
+ - generic [ref=e207] [cursor=pointer]:
136
+ - generic [ref=e208]: 08 / 08
137
+ - generic [ref=e209]: 用户确认并生成规范文件
138
+ - generic [ref=e210]: requiresWait
139
+ - generic [ref=e212]: ▸
140
+ - contentinfo [ref=e213]:
141
+ - text: 数据机械提取自
142
+ - code [ref=e214]: src/stages/*.js
143
+ - text: · 单文件自包含 · file:// 可直接打开 · 生成命令:
144
+ - code [ref=e215]: node docs/prompt/_build-site.mjs
Binary file
Binary file
package/CLAUDE.md CHANGED
@@ -20,3 +20,21 @@
20
20
  - [ ] 归档/清理流程描述与实际代码逻辑一致
21
21
  - [ ] 数据库 Schema 描述与 `src/db.js` 一致
22
22
  - [ ] 更新文档头部 `updated_at` 时间戳
23
+
24
+ ## 提示词文档同步(docs/prompt/)
25
+ `docs/prompt/` 收录各阶段 CLI→Agent 的**逐步提示词原文**(人类可读 md 参考)。详见 `docs/prompt/README.md`。
26
+
27
+ ### 数据源关系(重要 — 避免双写漂移)
28
+ - **唯一数据源 = `src/stages/*.js`**(`definition.steps[].prompt`,CLI 运行时读这里)+ `src/run/prompt.js`(注入框架:persona / 铁律 / 占位符替换 / 完成后执行命令模板)。
29
+ - `docs/prompt/*.md` 是源码的**机械提取人类可读镜像**,由 `node docs/prompt/_extract.mjs` 一键再生(源码 → `_extracted.json` → md)。提取脚本对静态阶段直接读 `definition.steps`,对动态阶段(plan/execute)用示例输入跑 `buildPlanSteps`/`buildExecuteSteps` 得到真实 prompt。
30
+ - **禁止手改 md 里的 prompt 原文**(下次提取会被覆盖,且与源码不一致)。要改提示词,改源码后重跑脚本。
31
+
32
+ ### 触发同步的改动
33
+ - 修改 `src/stages/*.js`:`definition.steps[].prompt`、步骤名、步骤增删/重排、wait 配置、`_globalGuardrails`、title/description
34
+ - 修改 `src/run/prompt.js`:persona / 铁律 / 占位符替换 / 完成后执行命令模板
35
+ - 修改 `stage-review.js` / `review-tier.js` / `knowledge-match.js`:影响 `{REVIEW_JSON_CONTRACT}` / `{REVIEW_TIER}` / `{KNOWLEDGE_HIT_REPORT}` 等动态块
36
+
37
+ ### 同步步骤
38
+ 1. 改源码
39
+ 2. 运行 `node docs/prompt/_extract.mjs`(刷新 `docs/prompt/_extracted.json`)
40
+ 3. 按变动同步对应 `docs/prompt/<stage>.md`(prompt 正文以 `_extracted.json` 为准逐字替换)+ 必要时更新 `docs/prompt/README.md`(占位符总表 / persona 表 / CLI 注入框架)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.25.1",
3
+ "version": "3.25.2",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
@@ -251,6 +251,51 @@ export function detectRiskProfile({ designContent = '', planContent = '', change
251
251
  * 旧的 detectChangeRisk 接口保持向后兼容
252
252
  * @returns {{ level: string, triggers: string[], requiredVerification: string[] }}
253
253
  */
254
+ // ============ 门控可执行化(A:报错说人话) ============
255
+ //
256
+ // 历史教训:integration-critical / deployment-critical 门控只报「缺少真实集成证据 / 需要真实
257
+ // 启动验证证据」,agent 看不出具体缺哪一项、要写/做什么才算过,只能靠改结论文案撞墙。
258
+ // 此处把每一项 requiredVerification 的需求(写什么 / 做什么)和判级原因显式导出,
259
+ // 供 stage-contract 拼成可执行的报错信息。
260
+
261
+ /** 每项 requiredVerification 的需求描述 + 字面期望 */
262
+ export const VERIFICATION_NEEDS = {
263
+ unit_tests: {
264
+ desc: '单元测试(verify 必做;在 verify-result.md 写明测试套件与结果)',
265
+ literals: [],
266
+ },
267
+ contract_tests: {
268
+ desc: '契约测试(前后端/跨进程 API parity 对账,写明结论)',
269
+ literals: [],
270
+ },
271
+ real_daemon_backend_integration: {
272
+ desc: '真实 daemon↔backend 集成验证(非仅 mock 单测)。写「真实集成/端到端」的证据;',
273
+ literals: ['端到端', 'integration test', 'e2e test', 'daemon.*backend', '真实集成', 'runtime evidence', '运行时证据'],
274
+ },
275
+ runtime_log_evidence: {
276
+ desc: 'Runtime Evidence section + 指向真实日志/证据片段。',
277
+ literals: ['Runtime Evidence', '运行时证据', 'daemon log', '日志片段'],
278
+ },
279
+ real_startup_once: {
280
+ desc: '真实启动一次本变更触及的部署/启动入口(服务入口、CLI 主入口、守护进程等——须是本变更实际改动的那一类入口,不能拿无关进程的启动来凑数)。',
281
+ literals: ['启动.*一次', '实际.*启动', 'real startup', 'docker up', 'npm start', 'node server'],
282
+ },
283
+ terminal_state_assertion: {
284
+ desc: '终态断言(建议项,不阻断):AgentRun running→completed/failed、session/lease end 状态同步。',
285
+ literals: ['terminal state', '终态', 'completed failed', 'session end', 'lease end'],
286
+ },
287
+ }
288
+
289
+ /** 风险分级的判定来源(命中关键词来源 = design.md / plan.md 内容,非改动文件本身) */
290
+ export const RISK_LEVEL_CAUSES = {
291
+ 'deployment-critical':
292
+ 'design.md / plan.md 命中启动入口关键词(cli.ts / main.ts / server.(js|ts) / bootstrap / entrypoint)。' +
293
+ '注意:这是按 design/plan 里的措辞判定的,不一定代表你真改了启动入口;若属误判可在 design 中如实缩小范围,但不建议为绕门控而规避——危险链路该有真实启动证据。',
294
+ 'integration-critical':
295
+ 'design.md / plan.md 命中跨进程/状态机关键词(daemon / backend / session / lease / lifecycle / heartbeat 等)。',
296
+ 'contract-required': 'design.md / plan.md 命中 API contract 关键词(api / client / contract / dto)。',
297
+ }
298
+
254
299
  export function detectChangeRisk({ designContent = '', planContent = '', changedFiles = [] } = {}) {
255
300
  const triggers = []
256
301
  const combined = [designContent, planContent].join('\n')
@@ -308,6 +353,16 @@ export function checkIntegrationEvidence(verifyContent, requiredVerification) {
308
353
  const warnings = []
309
354
  const lower = verifyContent.toLowerCase()
310
355
 
356
+ // 字面证据正则从 VERIFICATION_NEEDS[k].literals 派生——与报错描述、prompt 事前契约严格同源,
357
+ // 杜绝历史上"描述说 A、正则查 B"的分叉(checkIntegrationEvidence 正则曾比 VERIFICATION_NEEDS
358
+ // 描述多匹配 集成测试/backend.*daemon/real.*integration 等)。literals 为空的 need
359
+ // (unit_tests/contract_tests)无字面校验,视为满足。
360
+ const hasEvidence = (k) => {
361
+ const n = VERIFICATION_NEEDS[k]
362
+ if (!n || !n.literals || n.literals.length === 0) return true
363
+ return new RegExp(n.literals.join('|'), 'i').test(lower)
364
+ }
365
+
311
366
  const needsIntegration = requiredVerification.includes('real_daemon_backend_integration')
312
367
  const needsLogEvidence = requiredVerification.includes('runtime_log_evidence')
313
368
  const needsTerminalState = requiredVerification.includes('terminal_state_assertion')
@@ -315,11 +370,7 @@ export function checkIntegrationEvidence(verifyContent, requiredVerification) {
315
370
 
316
371
  if (needsIntegration) {
317
372
  const hasMockOnly = /mock.*test.*passed|unit.*test.*passed/i.test(lower)
318
- const hasIntegrationEvidence =
319
- /集成测试|integration.*test|e2e.*test|端到端/i.test(lower) ||
320
- /daemon.*backend|backend.*daemon|真实.*集成|real.*integration/i.test(lower) ||
321
- /runtime.*evidence|运行时.*证据/i.test(lower)
322
-
373
+ const hasIntegrationEvidence = hasEvidence('real_daemon_backend_integration')
323
374
  if (!hasIntegrationEvidence && hasMockOnly) {
324
375
  errors.push('integration-critical 变更只提供了 mock 单测证据,缺少真实 daemon↔backend 集成验证')
325
376
  } else if (!hasIntegrationEvidence) {
@@ -328,25 +379,22 @@ export function checkIntegrationEvidence(verifyContent, requiredVerification) {
328
379
  }
329
380
 
330
381
  if (needsLogEvidence) {
331
- const hasRuntimeSection = /runtime.*evidence|运行时.*证据|daemon.*log|日志.*片段/i.test(lower)
332
- if (!hasRuntimeSection) {
382
+ if (!hasEvidence('runtime_log_evidence')) {
333
383
  errors.push('integration-critical 变更的 verify-result.md 缺少 Runtime Evidence section')
334
384
  }
335
385
  }
336
386
 
337
387
  if (needsTerminalState) {
338
- const hasTerminalState = /terminal.*state|终态|running.*completed|completed.*failed|session.*end|lease.*end/i.test(lower)
339
- if (!hasTerminalState) {
388
+ if (!hasEvidence('terminal_state_assertion')) {
340
389
  warnings.push('建议检查终态断言:AgentRun running→completed/failed、session end 状态同步')
341
390
  }
342
391
  }
343
392
 
344
393
  if (needsRealStartup) {
345
- const hasStartupEvidence = /启动.*一次|real.*startup|实际.*启动|docker.*up|npm.*start|node.*server/i.test(lower)
346
- if (!hasStartupEvidence) {
394
+ if (!hasEvidence('real_startup_once')) {
347
395
  errors.push('deployment-critical 变更需要真实启动验证证据')
348
396
  }
349
397
  }
350
398
 
351
399
  return { ok: errors.length === 0, errors, warnings }
352
- }
400
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * check-primitives.js — 共享的产物字面校验原语(纯函数)
3
+ *
4
+ * 从 workflow.js 的 checkOutput / _checkWorkflow 抽出,作为"何谓 contains_sections / min_lines /
5
+ * no_placeholder / no_empty_files"的**全仓单一语义源**。workflow 引擎与 stage-contract 引擎
6
+ * (stage-contract-engine.js)共用同一组谓词,杜绝"workflow 认 `## X` 子串、stage-contract 认别的"
7
+ * 这类判定分叉(本任务的"事前==事后"同源在原语层就成立)。
8
+ *
9
+ * 设计原则:纯文本判定,不碰 fs。调用方负责读文件 + CRLF 归一化后传入 content——
10
+ * 这样 Windows CRLF 差异在调用方收敛(plan-postcheck 已用 LF-normalizing reader),引擎拿到
11
+ * 什么判什么,不在原语层二次引入 readFileSync 差异(缺陷 windows-python-crlf-taskcard 的根因)。
12
+ */
13
+
14
+ // no_placeholder 的默认占位词。与原 workflow.js checkOutput/_checkWorkflow 逐字一致——
15
+ // 改动会破坏存量扫描文档的占位判定,属契约级常量。
16
+ const DEFAULT_PLACEHOLDER_PATTERNS = [
17
+ '待补充', 'TODO', 'TBD', '未分析', '根据项目情况', '根据实际情况', '按需填写',
18
+ ]
19
+
20
+ /** 内容非空(trim 后有字符)。用于 no_empty_files。 */
21
+ export function contentNonEmpty(content) {
22
+ return typeof content === 'string' && content.trim().length > 0
23
+ }
24
+
25
+ /** 行数(split('\n'))。用于 min_lines 的 detail 文案与判定。 */
26
+ export function lineCount(content) {
27
+ return typeof content === 'string' ? content.split('\n').length : 0
28
+ }
29
+
30
+ /** 行数 ≥ min。用于 min_lines 判定。 */
31
+ export function meetsMinLines(content, min) {
32
+ return lineCount(content) >= min
33
+ }
34
+
35
+ /**
36
+ * 返回缺失的章节(sections 中未以 `## <section>` 子串出现的)。
37
+ * 用于 contains_sections——判定与原 workflow.js 逐字一致(`## ${s}` 前缀子串匹配)。
38
+ */
39
+ export function missingSections(content, sections) {
40
+ if (typeof content !== 'string') return sections.slice()
41
+ return sections.filter(s => !content.includes(`## ${s}`))
42
+ }
43
+
44
+ /** 是否含全部章节(missingSections 为空的语法糖)。 */
45
+ export function hasAllSections(content, sections) {
46
+ return missingSections(content, sections).length === 0
47
+ }
48
+
49
+ /**
50
+ * 返回命中的占位词(patterns 中匹配"独立成行"的)。
51
+ * 用于 no_placeholder——正则 `^\s*[-*]?\s*<p>\s*$`(m flag)与原 workflow.js 逐字一致。
52
+ * patterns 为空/未传时用 DEFAULT_PLACEHOLDER_PATTERNS(等价原 `check.patterns || [defaults]`)。
53
+ */
54
+ export function placeholderLineMatches(content, patterns) {
55
+ if (typeof content !== 'string') return []
56
+ const pats = (patterns && patterns.length > 0) ? patterns : DEFAULT_PLACEHOLDER_PATTERNS
57
+ return pats.filter(p => new RegExp(`^\\s*[-*]?\\s*${p}\\s*$`, 'm').test(content))
58
+ }
59
+
60
+ /** 无占位词命中(placeholderLineMatches 为空的语法糖)。 */
61
+ export function hasNoPlaceholderLine(content, patterns) {
62
+ return placeholderLineMatches(content, patterns).length === 0
63
+ }
64
+
65
+ export { DEFAULT_PLACEHOLDER_PATTERNS }
package/src/index.js CHANGED
@@ -16,6 +16,8 @@ import { detectLocalYaml } from './local-detect.js';
16
16
 
17
17
  // ── CLI 入口 ──
18
18
 
19
+ // did-you-mean 从 ./run/shared.js 复用(命令级 + flag 级 typo 建议)。
20
+ import { didYouMean, assertSafeChangeName } from './run/shared.js'
19
21
  function printUsage() {
20
22
  console.log(`
21
23
  SillySpec CLI — 规范驱动开发工具包
@@ -73,11 +75,20 @@ SillySpec CLI — 规范驱动开发工具包
73
75
  doctor [--fix] [--stale-hours N] 健康检查 + 修复
74
76
 
75
77
  sillyspec local detect [--dir <path>] 生成本地配置 local.yaml(纯 fs 嗅探,零 token、不跑 scan)
78
+ sillyspec runtime list [--json] 枚举 .sillyspec/.runtime/ 运行时产物(只读,看手上有哪些证据/状态文件)
76
79
 
77
- sillyspec workflow check <name> [--project <p>] [--change <c>] [--json]
80
+ sillyspec workflow check <name> [--project <p>] [--change <c>] [--json] [--save]
81
+ 全局 workflow 校验(--save 归档到 .sillyspec/.runtime/workflow-runs/)
78
82
  sillyspec workflow list
79
83
  sillyspec gate <stage> --change <name> [--json] 机器门控:阶段能否标记完成(只读)
80
84
  sillyspec derive <facet> --change <name> [--json] 单项事实核验(facet: execute-evidence|verify-test|task-reviews|artifacts)
85
+
86
+ sillyspec doctor [子命令] 进度库健康检查 + 修复(顶层命令,非 worktree doctor)
87
+ (无子命令) 跑诊断,列出问题
88
+ --align-execute-progress [--change <名>] [--confirm] 基于 plan.md 对齐 execute 派生戳(默认 dry-run)
89
+ --cleanup-remnant [--confirm] 删除 0 字节空占位 db(默认 dry-run,仅 --confirm 落盘)
90
+ --dump-db --path <db 路径> dump 指定 db 内容到文件(取证用)
91
+ --json 结构化诊断 + 落盘 .sillyspec/.runtime/doctor-diagnosis.json
81
92
  sillyspec modules <rebuild | status | migrate>
82
93
  sillyspec change-rename <旧变更名> <新变更名>
83
94
  sillyspec knowledge <search --query "..." --limit N
@@ -175,6 +186,13 @@ async function main() {
175
186
  }
176
187
  }
177
188
 
189
+ // F3:--interactive(全局 flag,被上面吞进 interactive 变量)与 --non-interactive(透传到 filteredArgs)
190
+ // 语义相反,同时给是矛盾。在此层检测(command.js 看不到 --interactive)。
191
+ if (interactive && filteredArgs.includes('--non-interactive')) {
192
+ console.error('❌ --non-interactive 与 --interactive 互斥(一个禁用交互、一个强制交互),请只保留一个。');
193
+ process.exit(2);
194
+ }
195
+
178
196
  const command = filteredArgs[0];
179
197
  // 支持 sillyspec init /path/to/project 语法:如果第二个参数看起来像路径,当作 targetDir
180
198
  if (command === 'init' && filteredArgs[1] && !filteredArgs[1].startsWith('-')) {
@@ -233,9 +251,19 @@ async function main() {
233
251
  case 'show':
234
252
  pm.show(progDir, progChangeName);
235
253
  break;
236
- case 'check':
237
- await pm.checkConsistency(progDir, progChangeName);
254
+ case 'check': {
255
+ const result = await pm.checkConsistency(progDir, progChangeName);
256
+ // checkConsistency 只返回 {ok, issues, warnings},不打印——必须在此输出,
257
+ // 否则 agent 跑只读健康检查得到一片寂静,无法分辨「正常」还是「已损坏」。
258
+ if (!result || result.ok) {
259
+ console.log(`✅ 状态一致性检查通过${progChangeName ? `(变更 ${progChangeName})` : '(所有变更)'}`);
260
+ } else {
261
+ console.error(`❌ 状态一致性检查发现问题(${result.issues?.length || 0} 项):`);
262
+ for (const issue of (result.issues || [])) console.error(` - ${issue}`);
263
+ console.error(` 修复:sillyspec progress repair${progChangeName ? ` --change ${progChangeName}` : ''}(先 dry-run 看可修项,加 --apply 执行)`);
264
+ }
238
265
  break;
266
+ }
239
267
  case 'repair': {
240
268
  const repairApply = filteredArgs.includes('--apply');
241
269
  await pm.repairConsistency(progDir, { apply: repairApply, changeName: progChangeName });
@@ -610,6 +638,9 @@ SillySpec worktree — git worktree 隔离管理
610
638
  console.error('❌ 用法: sillyspec worktree create <change-name> [--base <branch>]');
611
639
  process.exit(1);
612
640
  }
641
+ // F6 路径穿越消毒:worktree 名用于分支名 + worktree 目录,含 ../ 会逃出 .sillyspec/worktrees/。
642
+ try { assertSafeChangeName(wtName, 'worktree 变更名'); }
643
+ catch (e) { console.error(`❌ ${e.message}`); process.exit(2); }
613
644
  const baseIdx = args.indexOf('--base');
614
645
  const base = baseIdx >= 0 && args[baseIdx + 1] ? args[baseIdx + 1] : undefined;
615
646
  try {
@@ -662,7 +693,14 @@ SillySpec worktree — git worktree 隔离管理
662
693
  } else if (result.merged) {
663
694
  console.log(`✅ 已通过 git merge 应用变更(baseline 漂移降级,引入合并提交)${result.mergeSummary ? ':' + result.mergeSummary : ''}`);
664
695
  } else {
665
- console.log(`✅ 已应用 ${result.changedFiles.length} 个文件变更`);
696
+ console.log(`✅ 已应用 ${result.changedFiles.length} 个文件变更:`);
697
+ for (const f of result.changedFiles) {
698
+ console.log(` ${f}`);
699
+ }
700
+ // filterDeliverableFiles 一刀切排除 .sillyspec/ + meta.json:
701
+ // 模块文档/变更文档等不会 auto-apply 到主工作区。须告知 agent,
702
+ // 否则它期望这些文件落地却找不到(memory: worktree-apply-excludes-module-docs)。
703
+ console.log(` ℹ️ 注:.sillyspec/ 下的文件(模块文档/变更文档等)按规则不自动 apply,如需请手动从 worktree 分支或 dangling commit 取(git show sillyspec/${wtName}:<path>)。`);
666
704
  }
667
705
  if (result.warnings && result.warnings.length > 0) {
668
706
  for (const w of result.warnings) {
@@ -708,7 +746,9 @@ SillySpec worktree — git worktree 隔离管理
708
746
  if (applyResult.errors.length > 0) {
709
747
  console.error('❌ apply 失败:', applyResult.errors.join('; '));
710
748
  } else {
711
- console.log(`✅ 已自动应用 ${applyResult.changedFiles.length} 个文件变更`);
749
+ console.log(`✅ 已自动应用 ${applyResult.changedFiles.length} 个文件变更:`);
750
+ for (const f of applyResult.changedFiles) console.log(` ${f}`);
751
+ console.log(` ℹ️ 注:.sillyspec/ 下的文件按规则不自动 apply,如需请手动从分支取(git show sillyspec/${wtName}:<path>)。`);
712
752
  }
713
753
  } else {
714
754
  console.log('Action: blocked');
@@ -970,6 +1010,14 @@ SillySpec platform — SillyHub 平台同步
970
1010
  console.error('❌ 用法: sillyspec change-rename <旧变更名> <新变更名>');
971
1011
  process.exit(1);
972
1012
  }
1013
+ // F6 路径穿越消毒:renameChange 会 mv changes/<old> → changes/<new>,名含 ../ 会逃出 changes/。
1014
+ try {
1015
+ assertSafeChangeName(oldName, '旧变更名');
1016
+ assertSafeChangeName(newName, '新变更名');
1017
+ } catch (e) {
1018
+ console.error(`❌ ${e.message}`);
1019
+ process.exit(2);
1020
+ }
973
1021
  const pm = new ProgressManager({ specDir: resolvePlatformSpecDir(dir, specDir) });
974
1022
  await pm.renameChange(dir, oldName, newName);
975
1023
  break;
@@ -1100,7 +1148,11 @@ SillySpec workflow — 工作流管理
1100
1148
  }
1101
1149
  process.exit(result.status === 'pass' ? 0 : 1);
1102
1150
  } else {
1151
+ const wfSubs = ['check', 'list'];
1152
+ const sug = didYouMean(wfSub, wfSubs);
1103
1153
  console.error(`❌ 未知子命令: workflow ${wfSub}`);
1154
+ if (sug) console.error(` 你是想输入「workflow ${sug}」吗?`);
1155
+ console.error(` 可用子命令:${wfSubs.join(' | ')}(运行 sillyspec workflow 查看用法)`);
1104
1156
  process.exit(1);
1105
1157
  }
1106
1158
  break;
@@ -1128,7 +1180,11 @@ SillySpec modules — 模块文档管理
1128
1180
  const { migrateModuleDocs } = await import('./modules.js');
1129
1181
  await migrateModuleDocs(dir);
1130
1182
  } else {
1183
+ const modSubs = ['rebuild', 'status', 'migrate'];
1184
+ const sug = didYouMean(modulesSub, modSubs);
1131
1185
  console.error(`❌ 未知子命令: modules ${modulesSub}`);
1186
+ if (sug) console.error(` 你是想输入「modules ${sug}」吗?`);
1187
+ console.error(` 可用子命令:${modSubs.join(' | ')}(运行 sillyspec modules 查看用法)`);
1132
1188
  process.exit(1);
1133
1189
  }
1134
1190
  break;
@@ -1180,10 +1236,83 @@ SillySpec modules — 模块文档管理
1180
1236
  console.log(` 路径: ${localYamlPath}`);
1181
1237
  break;
1182
1238
  }
1183
- default:
1239
+ case 'runtime': {
1240
+ // D2:.runtime/ 产物索引。CLI 往 .runtime/ 写各种证据/状态文件(db、doctor-diagnosis、
1241
+ // workflow-runs、gate-status、user-inputs…),却没有命令让 agent 一眼看到「我手上有哪些产物」。
1242
+ // 多会话/压缩后尤其痛:上轮写的证据文件,下轮不知去哪找。纯只读枚举,不写盘。
1243
+ const runtimeSubCmd = filteredArgs[1];
1244
+ if (runtimeSubCmd && runtimeSubCmd !== 'list') {
1245
+ const sug = didYouMean(runtimeSubCmd, ['list']);
1246
+ console.error(`❌ 未知子命令: runtime ${runtimeSubCmd}`);
1247
+ if (sug) console.error(` 你是想输入「runtime ${sug}」吗?`);
1248
+ console.error('用法: sillyspec runtime list 枚举 .sillyspec/.runtime/ 产物(只读)');
1249
+ process.exit(2);
1250
+ }
1251
+ const specRoot = resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec');
1252
+ const runtimeDir = join(specRoot, '.runtime');
1253
+ if (!existsSync(runtimeDir)) {
1254
+ if (json) { console.log(JSON.stringify({ runtimeDir, exists: false, artifacts: [] }, null, 2)); break; }
1255
+ console.log(`📭 无 .runtime/ 目录(${runtimeDir})——尚未产生任何运行时产物。`);
1256
+ break;
1257
+ }
1258
+ // 已知产物 → 用途说明。agent 据此判断每份产物能否作为证据/该读哪个。
1259
+ const KNOWN = {
1260
+ 'sillyspec.db': '进度库 SQLite(stages/steps/changes 表)',
1261
+ 'sillyspec.db.bak': '进度库备份(写前快照)',
1262
+ 'sillyspec.db.schema-version': '进度库 schema 版本标记',
1263
+ 'doctor-diagnosis.json': 'doctor --json 结构化诊断快照',
1264
+ 'doctor-dumps': 'doctor --dump-db 取证输出目录',
1265
+ 'workflow-runs': 'workflow check --save 归档目录',
1266
+ 'gate-status.json': '门控结果缓存',
1267
+ 'scan-guard.json': 'scan 覆盖保护记录',
1268
+ 'quick-sessions': 'quick 会话记录目录',
1269
+ 'quick-guard.json': 'quick baseline 守卫(旧位置)',
1270
+ 'worktrees': 'worktree meta(hooks 侧)',
1271
+ 'user-inputs.md': '步骤输出/输入历史追加日志',
1272
+ 'current-quick-run-id': '当前 quick sessionId 指针(--done 无 --change 时 fallback)',
1273
+ 'scan-runs': '平台模式 scan/workflow 取证目录',
1274
+ 'artifacts': '派生产物目录(init 预建)',
1275
+ 'contract-artifacts': 'execute endpoint 契约产物(verify 阶段读取)',
1276
+ 'history': '历史记录目录(init 预建)',
1277
+ 'logs': '日志目录(init 预建)',
1278
+ 'templates': '工作流模板目录(init 预建)',
1279
+ };
1280
+ let entries;
1281
+ try {
1282
+ entries = readdirSync(runtimeDir, { withFileTypes: true });
1283
+ } catch (e) {
1284
+ console.error(`❌ 读取 .runtime/ 失败: ${e.message}`);
1285
+ process.exit(1);
1286
+ }
1287
+ entries.sort((a, b) => a.name.localeCompare(b.name));
1288
+ if (json) {
1289
+ console.log(JSON.stringify({
1290
+ runtimeDir, exists: true,
1291
+ artifacts: entries.map(e => ({ name: e.name, type: e.isDirectory() ? 'dir' : 'file', desc: KNOWN[e.name] || null })),
1292
+ }, null, 2));
1293
+ break;
1294
+ }
1295
+ console.log(`📂 .runtime/ 产物(${runtimeDir})`);
1296
+ if (entries.length === 0) { console.log(' (空目录)'); break; }
1297
+ for (const e of entries) {
1298
+ const kind = e.isDirectory() ? '📁' : '📄';
1299
+ const desc = KNOWN[e.name] ? ` — ${KNOWN[e.name]}` : '';
1300
+ console.log(` ${kind} ${e.name}${desc}`);
1301
+ }
1302
+ const unknown = entries.filter(e => !KNOWN[e.name]);
1303
+ if (unknown.length > 0) {
1304
+ console.log(`\n (${unknown.length} 项未登记:${unknown.map(e => e.name).join(', ')})`);
1305
+ }
1306
+ break;
1307
+ }
1308
+ default: {
1309
+ const topCommands = ['init', 'setup', 'run', 'progress', 'worktree', 'local', 'workflow', 'gate', 'derive', 'modules', 'change-rename', 'knowledge', 'platform', 'scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive', 'quick', 'explore', 'status', 'doctor', 'auto', 'runtime'];
1310
+ const suggestion = didYouMean(command, topCommands);
1184
1311
  console.error(`❌ 未知命令: ${command}`);
1312
+ if (suggestion) console.error(` 你是想输入「${suggestion}」吗?`);
1185
1313
  printUsage();
1186
1314
  process.exit(1);
1315
+ }
1187
1316
  }
1188
1317
  }
1189
1318