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.
Files changed (76) hide show
  1. package/.claude/skills/sillyspec-auto/SKILL.md +8 -13
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +8 -6
  3. package/.claude/skills/sillyspec-continue/SKILL.md +3 -3
  4. package/.claude/skills/sillyspec-doctor/SKILL.md +1 -1
  5. package/.claude/skills/sillyspec-execute/SKILL.md +10 -4
  6. package/.claude/skills/sillyspec-plan/SKILL.md +1 -1
  7. package/.claude/skills/sillyspec-quick/SKILL.md +3 -1
  8. package/.claude/skills/sillyspec-verify/SKILL.md +1 -1
  9. package/.sillyspec-platform-cleaned +1 -0
  10. package/.sillyspec-platform.json +7 -0
  11. package/README.md +9 -0
  12. package/package.json +2 -2
  13. package/src/change-list.js +11 -8
  14. package/src/change-risk-profile.js +22 -0
  15. package/src/db.js +170 -161
  16. package/src/dispatch/backends/local-agent.js +152 -0
  17. package/src/dispatch/backends/sillyhub-mcp.js +242 -0
  18. package/src/dispatch/probe.js +210 -0
  19. package/src/dispatch/strategy.js +121 -0
  20. package/src/doctor-diagnostics.js +19 -25
  21. package/src/fs-atomic.js +69 -69
  22. package/src/git-helper.js +66 -0
  23. package/src/hooks/worktree-guard.js +64 -55
  24. package/src/index.js +326 -36
  25. package/src/init.js +7 -14
  26. package/src/local-detect.js +120 -110
  27. package/src/machine-interface.js +54 -19
  28. package/src/migrate.js +14 -4
  29. package/src/modules.js +16 -5
  30. package/src/progress/change-registry.js +85 -70
  31. package/src/progress/consistency-doctor.js +71 -14
  32. package/src/progress/shared.js +8 -5
  33. package/src/progress/stage-machine.js +67 -65
  34. package/src/progress/step-store.js +72 -67
  35. package/src/progress.js +448 -236
  36. package/src/quicklog.js +59 -12
  37. package/src/run/command.js +78 -39
  38. package/src/run/complete-handlers.js +1028 -889
  39. package/src/run/complete.js +132 -212
  40. package/src/run/concurrent-detect.js +132 -0
  41. package/src/run/gates.js +302 -19
  42. package/src/run/prompt.js +22 -68
  43. package/src/run/quick-audit.js +10 -9
  44. package/src/run/shared.js +722 -600
  45. package/src/run/stage.js +43 -24
  46. package/src/scan-postcheck.js +9 -31
  47. package/src/sillyhub-mcp/client.js +394 -0
  48. package/src/sillyhub-mcp/config.js +62 -0
  49. package/src/spec-dir-typo.js +71 -0
  50. package/src/stage-contract-engine.js +22 -1
  51. package/src/stage-contract-spec.js +11 -4
  52. package/src/stage-contract.js +26 -3
  53. package/src/stage-review.js +118 -10
  54. package/src/stages/archive.js +4 -6
  55. package/src/stages/brainstorm-auto.js +26 -20
  56. package/src/stages/brainstorm.js +29 -5
  57. package/src/stages/cmd-existence.js +145 -0
  58. package/src/stages/doctor.js +10 -7
  59. package/src/stages/execute.js +64 -22
  60. package/src/stages/plan-postcheck.js +152 -0
  61. package/src/stages/plan.js +16 -31
  62. package/src/stages/quick.js +25 -9
  63. package/src/stages/scan.js +53 -8
  64. package/src/stages/status.js +0 -1
  65. package/src/stages/verify.js +14 -9
  66. package/src/sync.js +433 -16
  67. package/src/task-review.js +59 -1
  68. package/src/verify-postcheck.js +938 -862
  69. package/src/version.js +19 -0
  70. package/src/worktree-apply.js +218 -93
  71. package/src/worktree-deps.js +89 -5
  72. package/src/worktree.js +213 -111
  73. package/templates/prompts/taskcard-rules.md +27 -0
  74. package/templates/prompts/verify-probes.md +3 -3
  75. package/.claude/skills/sillyspec-propose/SKILL.md +0 -87
  76. package/src/stages/propose.js +0 -181
@@ -0,0 +1,62 @@
1
+ /**
2
+ * readMcpConfig — SillyHub MCP 凭据共享 helper(design §7.2 / §6 mcp 凭据数据流)。
3
+ *
4
+ * 统一读源,供三处消费点:① client.js 构造函数(_url/_token/_configured/_endpoint)
5
+ * ② dispatch/probe.js configFingerprint(缓存 key)+ no-config 快速路径 ③ execute.js
6
+ * getDispatchMode hasConfig(派发三态判定)。
7
+ *
8
+ * 优先级:local.yaml mcp 段(mcp.url + mcp.token 两键齐全)>
9
+ * process.env.SILLYHUB_MCP_URL/TOKEN fallback;
10
+ * mcp 段缺或任一键缺才回退 env。env 也缺 → 返回 null。
11
+ *
12
+ * 读法参考 src/dispatch/probe.js readProbeTtlFromLocalYaml 的 best-effort 风格:
13
+ * join(cwd,'.sillyspec','local.yaml') → existsSync 守卫 → jsYaml.load(readFileSync) → 取 doc.mcp。
14
+ *
15
+ * 铁律(design R-07):
16
+ * - best-effort 绝不抛:文件不存在 / jsYaml.load 抛 / doc 非对象 全 try/catch 回退 env
17
+ * - 纯 fs + env 读,绝不发网络(保 probe no-config 快速路径「不发网络」零回归)
18
+ * - 不引新依赖(js-yaml 复用 dispatch/probe.js:17 已引)
19
+ */
20
+
21
+ import { existsSync, readFileSync } from 'node:fs';
22
+ import { join } from 'node:path';
23
+ import jsYaml from 'js-yaml';
24
+
25
+ /**
26
+ * 读 SillyHub MCP 凭据(local.yaml mcp 段 + env fallback,best-effort 不抛不发网络)。
27
+ * @param {string} cwd - 主仓库根(local.yaml 在 <cwd>/.sillyspec/local.yaml)
28
+ * @returns {{url: string, token: string} | null}
29
+ * 凭据齐全 → { url(尾斜杠归一), token };local.yaml mcp 段缺/缺键 + env 缺 → null
30
+ */
31
+ export function readMcpConfig(cwd) {
32
+ // 1. local.yaml mcp 段优先
33
+ try {
34
+ const p = join(cwd, '.sillyspec', 'local.yaml');
35
+ if (existsSync(p)) {
36
+ const doc = jsYaml.load(readFileSync(p, 'utf8'));
37
+ if (doc && typeof doc === 'object') {
38
+ const mcp = doc.mcp;
39
+ if (mcp && typeof mcp === 'object') {
40
+ const u = typeof mcp.url === 'string' ? mcp.url : '';
41
+ const t = typeof mcp.token === 'string' ? mcp.token : '';
42
+ // 两键齐全才视为有效源(design §7.2「缺键 → 回退 env」)
43
+ if (u && t) {
44
+ return { url: u.replace(/\/+$/, ''), token: t };
45
+ }
46
+ }
47
+ }
48
+ }
49
+ } catch {
50
+ // best-effort:文件读 / js-yaml 解析失败 → 继续回退 env
51
+ }
52
+
53
+ // 2. env fallback(兼容旧部署 / 现有测试不设 env 场景,design §9)
54
+ const eu = process.env.SILLYHUB_MCP_URL;
55
+ const et = process.env.SILLYHUB_MCP_TOKEN;
56
+ if (typeof eu === 'string' && eu && typeof et === 'string' && et) {
57
+ return { url: eu.replace(/\/+$/, ''), token: et };
58
+ }
59
+
60
+ // 3. local.yaml mcp 段缺/缺键 + env 缺 → null(调用方 _configured=false 降级不发网络)
61
+ return null;
62
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * SillySpec spec 目录拼写变体检测
3
+ *
4
+ * review.json missing 时调用:检测 SPEC_ROOT 父目录下是否存在 .sillyspec 的
5
+ * 近似拼写变体目录(如 .silyspec / .sillyspc),命中则提示「路径疑似拼错」。
6
+ *
7
+ * 触发场景:agent/用户手动创建 review.json 时把 .sillyspec 拼错(少 l / 多 k 等),
8
+ * 文件落到变体目录,CLI 按规范名找不到。给一条模糊匹配提示比静默报 missing 友好。
9
+ *
10
+ * 放独立模块(而非 stage-review.js / task-review.js 内联)的原因:两者已存在
11
+ * stage-review → task-review 的常量依赖,反向 import 会形成循环依赖。
12
+ */
13
+
14
+ import { dirname, basename, join } from 'path'
15
+ import { readdirSync } from 'fs'
16
+
17
+ /**
18
+ * 计算两字符串的 Levenshtein 编辑距离(大小写不敏感比较字符)。
19
+ * @param {string} a
20
+ * @param {string} b
21
+ * @returns {number}
22
+ */
23
+ function levenshtein(a, b) {
24
+ const m = a.length
25
+ const n = b.length
26
+ if (m === 0) return n
27
+ if (n === 0) return m
28
+ let prev = Array.from({ length: n + 1 }, (_, j) => j)
29
+ let curr = new Array(n + 1)
30
+ for (let i = 1; i <= m; i++) {
31
+ curr[0] = i
32
+ for (let j = 1; j <= n; j++) {
33
+ const cost = a[i - 1].toLowerCase() === b[j - 1].toLowerCase() ? 0 : 1
34
+ curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost)
35
+ }
36
+ ;[prev, curr] = [curr, prev]
37
+ }
38
+ return prev[n]
39
+ }
40
+
41
+ /**
42
+ * 检测 spec 目录拼写变体。
43
+ * @param {string} runtimeRoot — .runtime 目录绝对路径(= <SPEC_ROOT>/.runtime)
44
+ * @returns {{ typoDir: string, canonical: string } | null} 命中返回变体目录与规范名,否则 null
45
+ */
46
+ export function detectSpecDirTypo(runtimeRoot) {
47
+ if (!runtimeRoot) return null
48
+ // runtimeRoot = <SPEC_ROOT>/.runtime → SPEC_ROOT = dirname(runtimeRoot)
49
+ const specRoot = dirname(runtimeRoot)
50
+ const parent = dirname(specRoot)
51
+ const canonical = basename(specRoot) // 期望 '.sillyspec'
52
+ if (!canonical.startsWith('.')) return null // 非隐藏目录命名,跳过(避免误报)
53
+
54
+ let siblings = []
55
+ try {
56
+ siblings = readdirSync(parent, { withFileTypes: true })
57
+ .filter((d) => d.isDirectory() && d.name.startsWith('.') && !d.name.startsWith('..') && d.name !== canonical)
58
+ .map((d) => d.name)
59
+ } catch {
60
+ return null
61
+ }
62
+ if (siblings.length === 0) return null
63
+
64
+ // 编辑距离 ≤2 视为拼写变体(覆盖少 l / 多 k / 漏字母等常见笔误)
65
+ const typo = siblings.find((name) => {
66
+ const dist = levenshtein(name, canonical)
67
+ return dist > 0 && dist <= 2
68
+ })
69
+ if (!typo) return null
70
+ return { typoDir: join(parent, typo), canonical }
71
+ }
@@ -112,13 +112,33 @@ function formatFail(failMessage, absPath) {
112
112
  : failMessage
113
113
  }
114
114
 
115
+ /**
116
+ * 规则条件判定:condition 不满足时规则跳过(同 enabled=false)。
117
+ * 用于「某规则仅在特定上下文生效」——如 brainstorm 的 proposal/requirements/tasks 仅在 scale≠small 时必需
118
+ * (小变更 scale=small 只产 design.md,见 BRAINSTORM_RULES)。
119
+ *
120
+ * condition schema: { ctxField: '<ctx 键名>', ne|eq: <值> }
121
+ * - ne: ctx[ctxField] !== ne 时条件成立(规则适用);相等则跳过
122
+ * - eq: ctx[ctxField] === eq 时条件成立;否则跳过
123
+ * fail-safe:ctx 或 ctxField 缺失 → ne 判定为「undefined !== ne 值」成立(eq 不成立),
124
+ * 即 scale 读不到时四件套仍全要求(保守默认,走重流程),不会因 condition 误放开必填项。
125
+ */
126
+ function conditionHolds(condition, ctx) {
127
+ if (!condition) return true
128
+ const actual = ctx ? ctx[condition.ctxField] : undefined
129
+ if ('ne' in condition) return actual !== condition.ne
130
+ if ('eq' in condition) return actual === condition.eq
131
+ return true // 未知操作符,放过(不阻断),避免新操作符上线时误杀
132
+ }
133
+
115
134
  // ============ 主入口 ============
116
135
 
117
136
  /**
118
137
  * 对某 stage 的全部纯 kind 规则跑判定。custom kind / target 基缺失 / enabled=false 的规则计入 skipped。
119
138
  *
120
139
  * @param {string} stage
121
- * @param {{ changeDir?: string, docsRoot?: string, archiveDir?: string, specBase?: string }} ctx
140
+ * @param {{ changeDir?: string, docsRoot?: string, archiveDir?: string, specBase?: string, scale?: string }} ctx
141
+ * 规则 target.root 解析基目录;额外字段(如 scale)供 rule.condition 按 ctxField 判定(见 conditionHolds)
122
142
  * @param {{ readFile?: Function, readDir?: Function }} [io] 默认原生 fs;plan-postcheck 可注入 CRLF-normalizing reader
123
143
  * @returns {{ ok: boolean, errors: string[], warnings: string[], applied: string[], skipped: string[] }}
124
144
  */
@@ -132,6 +152,7 @@ export function evaluateRules(stage, ctx, io, opts = {}) {
132
152
 
133
153
  for (const rule of getRulesFor(stage, { source: opts.source })) {
134
154
  if (rule.enabled === false) { skipped.push(rule.id); continue }
155
+ if (rule.condition && !conditionHolds(rule.condition, ctx)) { skipped.push(rule.id); continue }
135
156
  if (isCustomKind(rule.kind)) { skipped.push(rule.id); continue }
136
157
  const absPath = resolveTargetPath(rule.target, ctx)
137
158
  if (absPath == null) { skipped.push(rule.id); continue }
@@ -20,6 +20,9 @@
20
20
  * failMessage 失败报错(逐字 == 现有报错文案;支持 ${path} 占位 → 引擎替换为解析后的绝对路径)
21
21
  * hint? 可选出路提示(renderStageContract 附在 spec 后)
22
22
  * enabled? 默认 true,置 false 灰度停用
23
+ * condition? 可选条件对象 { ctxField, ne|eq };不满足时规则跳过(同 enabled=false)。
24
+ * 供「仅在特定上下文生效」的规则,如 brainstorm proposal/requirements/tasks 仅 scale≠small 时必需
25
+ * (小变更只产 design.md)。判定见 stage-contract-engine.js conditionHolds(fail-safe:ctx 字段缺失时 ne 成立)。
23
26
  *
24
27
  * 纯 kind(stage-contract-engine.dispatchPure 直接判定):
25
28
  * file-exists / dir-exists / literal-any / literal-all / regex / contains-section /
@@ -67,7 +70,7 @@ const BRAINSTORM_RULES = [
67
70
  stage: 'brainstorm', source: 'validateBrainstormOutputs', severity: 'error', kind: 'file-exists',
68
71
  target: { root: 'change', path: 'design.md' },
69
72
  data: {},
70
- spec: '产出 `design.md`(四件套之一;brainstorm 四件套齐全才能完成)',
73
+ spec: '产出 `design.md`(brainstorm 必产;scale=small 时这是唯一必需产物,proposal/requirements/tasks 豁免)',
71
74
  failMessage: 'brainstorm 产物缺失: ${path}',
72
75
  },
73
76
  {
@@ -75,7 +78,9 @@ const BRAINSTORM_RULES = [
75
78
  stage: 'brainstorm', source: 'validateBrainstormOutputs', severity: 'error', kind: 'file-exists',
76
79
  target: { root: 'change', path: 'proposal.md' },
77
80
  data: {},
78
- spec: '产出 `proposal.md`(四件套之一)',
81
+ // scale=small(小变更)豁免:brainstorm 末步指引 small 只产 design.md 后转 quick,四件套不全不应阻断
82
+ condition: { ctxField: 'scale', ne: 'small' },
83
+ spec: '产出 `proposal.md`(四件套之一;scale=small 时豁免——小变更只需 design.md)',
79
84
  failMessage: 'brainstorm 产物缺失: ${path}',
80
85
  },
81
86
  {
@@ -83,7 +88,8 @@ const BRAINSTORM_RULES = [
83
88
  stage: 'brainstorm', source: 'validateBrainstormOutputs', severity: 'error', kind: 'file-exists',
84
89
  target: { root: 'change', path: 'requirements.md' },
85
90
  data: {},
86
- spec: '产出 `requirements.md`(四件套之一)',
91
+ condition: { ctxField: 'scale', ne: 'small' },
92
+ spec: '产出 `requirements.md`(四件套之一;scale=small 时豁免——小变更只需 design.md)',
87
93
  failMessage: 'brainstorm 产物缺失: ${path}',
88
94
  },
89
95
  {
@@ -91,7 +97,8 @@ const BRAINSTORM_RULES = [
91
97
  stage: 'brainstorm', source: 'validateBrainstormOutputs', severity: 'error', kind: 'file-exists',
92
98
  target: { root: 'change', path: 'tasks.md' },
93
99
  data: {},
94
- spec: '产出 `tasks.md`(四件套之一)',
100
+ condition: { ctxField: 'scale', ne: 'small' },
101
+ spec: '产出 `tasks.md`(四件套之一;scale=small 时豁免——小变更只需 design.md)',
95
102
  failMessage: 'brainstorm 产物缺失: ${path}',
96
103
  },
97
104
  // 内容章节(warning)—— 逐字对齐 validateBrainstormOutputs 内容校验段
@@ -257,15 +257,28 @@ function validateBrainstormOutputs(cwd, changeName, context = {}) {
257
257
  return { ok: false, errors: [`平台模式 specRoot 缺少 changes 目录: ${changesRoot}`], warnings: [] }
258
258
  }
259
259
  const changeDir = resolveChangeDir(cwd, changeName, specRoot)
260
+ // 读 design.md frontmatter scale(若有)传入引擎 ctx,供 BRAINSTORM_RULES 的 condition 判定:
261
+ // scale=small → proposal/requirements/tasks 三规则跳过(小变更只产 design.md,与末步 prompt 一致,
262
+ // 避免"照 Step8 small 指引只写 design.md 后 --done 必撞四件套 error墙")。
263
+ // fail-safe:design.md 不存在或无 scale → null → condition(ne 'small')成立 → 四件套全要求(保守走重流程)。
264
+ const designPath = join(changeDir, 'design.md')
265
+ let scale = null
266
+ let designContent = null
267
+ if (existsSync(designPath)) {
268
+ designContent = readFileSync(designPath, 'utf8')
269
+ const fm = designContent.match(/^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n/)
270
+ const sm = fm && fm[1].match(/^scale:[ \t]*['"]?(\w+)/m)
271
+ if (sm) scale = sm[1]
272
+ }
260
273
  // 纯 kind 规则(四件套存在性 + proposal/requirements/design 章节 + tasks 列表)由引擎消费
261
274
  // stage-contract-spec.js manifest,与报错文案 + prompt 事前契约严格同源(事前给的 == 事后查的)。
262
275
  // custom kind(lifecycle / decisions)保留下方旧逻辑,数据/文案在 Batch 3 迁入 manifest。
263
- const engineResult = evaluateRules('brainstorm', { changeDir })
276
+ const engineResult = evaluateRules('brainstorm', { changeDir, scale })
264
277
  const errors = [...engineResult.errors]
265
278
  const warnings = [...engineResult.warnings]
266
279
 
267
- if (existsSync(join(changeDir, 'design.md'))) {
268
- const content = readFileSync(join(changeDir, 'design.md'), 'utf8')
280
+ if (designContent) {
281
+ const content = designContent
269
282
 
270
283
  // lifecycle-exemption(custom kind):判定算法保留(trigger/exemption/table 三段短路),
271
284
  // data + failMessage/exemptionPassedMessage 从 stage-contract-spec.js manifest 同源。
@@ -446,6 +459,16 @@ function validateVerifyOutputs(cwd, changeName, context = {}) {
446
459
  })
447
460
  const conclusion = extractVerifyConclusion(verify)
448
461
  if (['integration-critical', 'deployment-critical'].includes(changeRiskProfile.level)) {
462
+ // 关键词误伤早期引导(坑2,FR-02):命中高危关键词且无 frontmatter risk_level 时,
463
+ // 无条件透出 frontmatter 覆盖指引(不依赖 conclusion / evidence),让 agent 早期即可
464
+ // 判断是否属误判并显式覆盖,而非撞到 evidence gate 末尾才发现出路③。遵 6417a27:不做
465
+ // body 否定语境扫描,只走 frontmatter 显式覆盖通道。
466
+ if (!changeRiskProfile.explicit) {
467
+ warnings.push(
468
+ `[${changeRiskProfile.level}] 本次变更被关键词判级(命中:${changeRiskProfile.triggers.join(', ')})。` +
469
+ `若属关键词误伤(实际未触碰 daemon/session/启动入口/跨进程),可在 design.md frontmatter 加 risk_level: <真实等级>(如 unit-sufficient)显式覆盖后重跑。`
470
+ )
471
+ }
449
472
  // 显式 risk_level 声明(design frontmatter,非关键词误判)下,PASS WITH NOTES 视为对残留项的
450
473
  // 诚实声明,不强求全量集成证据——豁免本就来自 design 的明确判断。自动关键词判级维持严格:
451
474
  // PASS WITH NOTES 仍要求证据齐全,防 agent 用 PASS WITH NOTES 绕证据门控。
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * 与 task-review.js 的关系:
5
5
  * task-review 校验 execute 每个 task 的 review.json(git 代码 diff 证据:base/head)。
6
- * stage-review 校验 brainstorm/plan/propose/execute-acceptance 的阶段级 review.json
6
+ * stage-review 校验 brainstorm/plan/execute-acceptance 的阶段级 review.json
7
7
  * (文档证据:reviewedFiles + docHash)。两者 verdict 语义一致(pass/fail/cannot_verify
8
8
  * 三态 + cannot_verify 必须带 requiredEvidence 的反逃逸规则),复用 task-review 导出的
9
9
  * VALID_VERDICTS / REVIEW_SCHEMA_VERSION 常量;task-review.js 本身不改、execute 路径零风险。
@@ -13,10 +13,12 @@
13
13
  * (平台模式落 <runtimeRoot>/stage-reviews/...,与 execute-runs 同构)
14
14
  */
15
15
 
16
- import { existsSync, readFileSync, mkdirSync, readdirSync } from 'fs'
16
+ import { existsSync, readFileSync, mkdirSync, readdirSync, writeFileSync } from 'fs'
17
17
  import { join } from 'path'
18
18
  import { createHash } from 'crypto'
19
19
  import { VALID_VERDICTS, REVIEW_SCHEMA_VERSION } from './task-review.js'
20
+ import { detectSpecDirTypo } from './spec-dir-typo.js'
21
+ import { resolveRuntimeRoot } from './run/shared.js'
20
22
 
21
23
  // 文档型 stage review 的合法 reviewType
22
24
  export const STAGE_REVIEW_TYPES = ['design', 'plan', 'proposal', 'code', 'acceptance']
@@ -24,10 +26,10 @@ export const STAGE_REVIEW_TYPES = ['design', 'plan', 'proposal', 'code', 'accept
24
26
  // checklist 每项 result 的合法值
25
27
  const CHECKLIST_RESULTS = ['pass', 'gap', 'fail']
26
28
 
27
- // stage → 主审查文档(brainstorm/execute 审 design.md;plan 审 plan.md;propose 审 proposal.md)
28
- const STAGE_MAIN_DOC = { brainstorm: 'design.md', plan: 'plan.md', propose: 'proposal.md', execute: 'design.md' }
29
+ // stage → 主审查文档(brainstorm/execute 审 design.md;plan 审 plan.md)
30
+ const STAGE_MAIN_DOC = { brainstorm: 'design.md', plan: 'plan.md', execute: 'design.md' }
29
31
  // stage → reviewType(对齐 gates.js Stage Review Gate 的映射)
30
- const STAGE_REVIEW_TYPE = { brainstorm: 'design', plan: 'plan', propose: 'proposal', execute: 'acceptance' }
32
+ const STAGE_REVIEW_TYPE = { brainstorm: 'design', plan: 'plan', execute: 'acceptance' }
31
33
 
32
34
  /**
33
35
  * 渲染 review.json 产物契约(markdown)给 review 子代理事前看 —— schema 表 + 完整 JSON 示例 +
@@ -242,7 +244,7 @@ export function generateStageReviewRunId() {
242
244
  * (同一 stage 不同变更的 review 各自独立 marker);changeName 缺失时退化为按 stage 分。
243
245
  *
244
246
  * @param {string} runtimeRoot - .runtime 绝对路径
245
- * @param {string} stage - brainstorm|plan|propose|execute
247
+ * @param {string} stage - brainstorm|plan|execute
246
248
  * @param {string} [changeName] - 变更名(防多 change 串台)
247
249
  * @returns {string} marker 文件绝对路径
248
250
  */
@@ -260,7 +262,7 @@ export function stageReviewMarkerPath(runtimeRoot, stage, changeName) {
260
262
  * stage-reviews/<stage>-review-* 目录取字典序最新(向后兼容无 marker 旧数据)。
261
263
  *
262
264
  * @param {string} runtimeRoot - .runtime 绝对路径
263
- * @param {string} stage - brainstorm|plan|propose|execute
265
+ * @param {string} stage - brainstorm|plan|execute
264
266
  * @param {string} [changeName] - 变更名(读对应 marker,防多 change 串台)
265
267
  * @returns {string|null} run id(如 'review-2026-07-16-143000'),无则 null
266
268
  */
@@ -313,7 +315,7 @@ export function getLatestStageReviewRunId(runtimeRoot, stage, changeName) {
313
315
  }
314
316
 
315
317
  /**
316
- * stage-level review 总校验(brainstorm/plan/propose/execute-acceptance 的 done gate)
318
+ * stage-level review 总校验(brainstorm/plan/execute-acceptance 的 done gate)
317
319
  *
318
320
  * 规则(与 task-review.validateTaskReviews 对称):
319
321
  * - review.json 缺失 → error 阻断
@@ -328,7 +330,7 @@ export function getLatestStageReviewRunId(runtimeRoot, stage, changeName) {
328
330
  * tier=independent 才调用。
329
331
  *
330
332
  * @param {object} opts
331
- * @param {string} opts.stage - brainstorm|plan|propose|execute
333
+ * @param {string} opts.stage - brainstorm|plan|execute
332
334
  * @param {string} opts.reviewType - design|plan|proposal|code|acceptance
333
335
  * @param {string} opts.runtimeRoot - .runtime 绝对路径(平台模式为 runtimeRoot)
334
336
  * @param {string} opts.reviewRunId - stage review run id(review-<ts>)
@@ -345,9 +347,12 @@ export function validateStageReview(opts) {
345
347
  const reviewPath = join(reviewDir, 'review.json')
346
348
 
347
349
  if (!reviewRunId || !existsSync(reviewPath)) {
350
+ const errs = [`缺少 ${stage} 阶段的 stage review.json — tier=independent 要求独立审查子代理产出(期望路径:${reviewPath})`]
351
+ const typo = detectSpecDirTypo(runtimeRoot)
352
+ if (typo) errs.push(`💡 路径疑似拼错:发现 ${typo.typoDir} 目录(应为 ${typo.canonical}),review.json 可能误存于此——检查是否把 .sillyspec 拼成了变体`)
348
353
  return {
349
354
  ok: false,
350
- errors: [`缺少 ${stage} 阶段的 stage review.json — tier=independent 要求独立审查子代理产出(期望路径:${reviewPath})`],
355
+ errors: errs,
351
356
  warnings,
352
357
  review: null,
353
358
  }
@@ -430,3 +435,106 @@ export function printStageReviewResult(result, context = {}) {
430
435
  }
431
436
  }
432
437
  }
438
+
439
+ /**
440
+ * 注册一个 stage 级 review(确定性 writer)—— 生成 run 目录 + review.json 骨架(或 adopt
441
+ * agent 草稿)+ 写 marker,治 tier=independent 时「调度者手动派独立子代理 → marker 不写 →
442
+ * gate 取错 run」的死锁。与 task 级 generateTaskReviewDrafts(task-review.js)对称。
443
+ *
444
+ * docHash 由 CLI 直接 computeDocHash 算(命令有文件访问权),消除 agent 手算 hash 易错
445
+ * (部分实现 P6.1b defer:仅本 scaffold 路径确定性,不改 agent 自写 review.json 的 hash 链路)。
446
+ *
447
+ * 纯新增,不改 validateStageReview / getLatestStageReviewRunId / renderReviewJsonContract 等
448
+ * 现有函数;self-check 只验 mechanics(schema + docHash),verdict 留给 Stage Review Gate 判。
449
+ *
450
+ * @param {object} opts
451
+ * @param {string} opts.changeName - 变更名
452
+ * @param {string} opts.stage - brainstorm|plan|execute
453
+ * @param {string} [opts.fromFile] - adopt 模式:agent 已写的 review.json(保留 verdict/checklist,重算 docHash + 规范化 reviewedFiles[0])
454
+ * @param {string} opts.cwd - 主仓 cwd(解析 specBase)
455
+ * @param {object} [opts.platformOpts] - 平台选项(specRoot 等)
456
+ * @returns {{ ok: boolean, reviewRunId: string, reviewPath: string, markerPath: string, mode: 'skeleton'|'adopted', mainDoc: string, review: object }}
457
+ * @throws {Error} changeName 空 / stage 非法 / 主文档缺失 / --from 不存在或 schema 不过
458
+ */
459
+ export function registerStageReview({ changeName, stage, fromFile, cwd, platformOpts = {} }) {
460
+ if (!changeName) throw new Error('register-stage-review: changeName 不能为空')
461
+ if (!stage || !STAGE_MAIN_DOC[stage]) {
462
+ throw new Error(`register-stage-review: stage 无效 "${stage}"(应为 brainstorm|plan|execute)`)
463
+ }
464
+
465
+ const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
466
+ const runtimeRoot = resolveRuntimeRoot(platformOpts, specBase)
467
+ const changeDir = join(specBase, 'changes', changeName)
468
+ const reviewType = STAGE_REVIEW_TYPE[stage]
469
+ const mainDoc = STAGE_MAIN_DOC[stage]
470
+ const mainDocPath = join(changeDir, mainDoc)
471
+ if (!existsSync(mainDocPath)) {
472
+ throw new Error(`register-stage-review: 主审查文档不存在 ${mainDocPath}(${stage} 审 ${mainDoc}),无法算 docHash`)
473
+ }
474
+ const docHash = computeDocHash(mainDocPath)
475
+ if (!docHash) {
476
+ throw new Error(`register-stage-review: 计算 docHash 失败(computeDocHash 返回 null):${mainDocPath}`)
477
+ }
478
+ const reviewedFiles = [`changes/${changeName}/${mainDoc}`]
479
+
480
+ let review
481
+ let mode
482
+ if (fromFile) {
483
+ // adopt 模式:保留 agent 的 verdict/checklist/reviewerNotes/requiredEvidence,仅修 mechanics
484
+ const fromAbs = existsSync(fromFile) ? fromFile : join(cwd, fromFile)
485
+ if (!existsSync(fromAbs)) {
486
+ throw new Error(`register-stage-review: --from 文件不存在 ${fromFile}(相对 cwd ${fromAbs} 也未命中)`)
487
+ }
488
+ let parsed
489
+ try {
490
+ parsed = JSON.parse(readFileSync(fromAbs, 'utf8'))
491
+ } catch (e) {
492
+ throw new Error(`register-stage-review: --from 文件 JSON 解析失败 ${fromAbs}: ${e.message}`)
493
+ }
494
+ const schemaResult = validateStageReviewSchema(parsed)
495
+ if (!schemaResult.ok) {
496
+ throw new Error(`register-stage-review: --from 文件 schema 校验失败 — ${schemaResult.errors.join('; ')}`)
497
+ }
498
+ review = { ...parsed, docHash, reviewedFiles }
499
+ mode = 'adopted'
500
+ } else {
501
+ // 骨架模式:cannot_verify 待审占位(schema 反逃逸:cannot_verify 必须带非空 requiredEvidence)
502
+ review = {
503
+ schemaVersion: REVIEW_SCHEMA_VERSION,
504
+ reviewType,
505
+ specVerdict: 'cannot_verify',
506
+ qualityVerdict: 'cannot_verify',
507
+ reviewedFiles,
508
+ docHash,
509
+ requiredEvidence: [`待独立审查子代理对照 ${mainDoc} 逐节核验(骨架由 register-stage-review 生成)`],
510
+ reviewerNotes: '骨架由 register-stage-review 生成,verdict 待独立审查子代理填写',
511
+ }
512
+ mode = 'skeleton'
513
+ }
514
+
515
+ const reviewRunId = generateStageReviewRunId()
516
+ const reviewDir = join(runtimeRoot, 'stage-reviews', `${stage}-${reviewRunId}`)
517
+ const reviewPath = join(reviewDir, 'review.json')
518
+ mkdirSync(reviewDir, { recursive: true })
519
+ writeFileSync(reviewPath, JSON.stringify(review, null, 2) + '\n')
520
+
521
+ const markerPath = stageReviewMarkerPath(runtimeRoot, stage, changeName)
522
+ if (existsSync(markerPath)) {
523
+ console.warn(`⚠️ register-stage-review: marker 已存在 ${markerPath},将被覆盖为 ${reviewRunId}`)
524
+ }
525
+ mkdirSync(runtimeRoot, { recursive: true })
526
+ writeFileSync(markerPath, reviewRunId + '\n')
527
+
528
+ // self-check(fail-closed):刚写的 review 必过 schema + docHash 真实性。只验 mechanics,
529
+ // 不判 verdict(verdict 是 agent/子代理的审查结论,even fail 也如实落盘,由 Stage Review Gate 裁决)。
530
+ const schemaRecheck = validateStageReviewSchema(review)
531
+ if (!schemaRecheck.ok) {
532
+ throw new Error(`register-stage-review: 写入后 schema 自检失败(不应发生)— ${schemaRecheck.errors.join('; ')}`)
533
+ }
534
+ const hashRecheck = verifyStageReviewDocHash(review, [specBase, changeDir, cwd])
535
+ if (!hashRecheck.ok) {
536
+ throw new Error(`register-stage-review: 写入后 docHash 自检失败(不应发生)— ${hashRecheck.errors.join('; ')}`)
537
+ }
538
+
539
+ return { ok: true, reviewRunId, reviewPath, markerPath, mode, mainDoc, review }
540
+ }
@@ -51,9 +51,7 @@ module-impact.md 第一行标题必须用中文:# 模块影响分析(Module
51
51
  needs_review:如果影响无法完全确定,标记为 true
52
52
 
53
53
  8. 未匹配到任何模块的文件归入"未匹配文件"表格
54
- 9. 生成 \`.sillyspec/changes/<change-name>/module-impact.md\`
55
- 10. 完成后运行 workflow 检查:
56
- \`node -e "import('./src/workflow.js').then(w => { /* 用 loadWorkflow 加载 archive-impact,用 runPostCheck 检查 */ })\``,
54
+ 9. 生成 \`.sillyspec/changes/<change-name>/module-impact.md\``,
57
55
  outputHint: 'module-impact.md 路径 + 影响摘要',
58
56
  optional: false
59
57
  },
@@ -128,7 +126,7 @@ module_id: <module-id>
128
126
  6. 用户拒绝时,不写入,但提示"module-impact.md 已保留,可稍后手动同步"
129
127
  7. 回填 module-impact.md 的"更新结果"表格,区分目标:
130
128
  - 目标列写 "\`_module-map.yaml: <module-id>\`" 或 "\`modules/<module-id>.md\`"
131
- 8. **同步完成后**,运行 \`sillyspec modules rebuild\` 刷新索引(如果需要),或手动更新 dependencies.md
129
+ 8. **同步完成后**,如需刷新 \`_module-map.yaml\` 索引:\`sillyspec modules rebuild --force\`(默认 dry-run 只预览不写;\`--force\` 才覆盖,但会清空 tags/entrypoints/main_symbols/depends_on/used_by 等手动维护字段——与 archive-impact 的「人工备注保护」约束冲突,仅当手动字段已并入骨架或可接受覆盖时用;优先手动更新 dependencies.md / 模块卡片)
132
130
 
133
131
  ### 输出
134
132
  已更新的文件路径列表 + 用户确认状态`,
@@ -157,8 +155,8 @@ module_id: <module-id>
157
155
 
158
156
  ### 操作
159
157
  1. 如果 \`.sillyspec/ROADMAP.md\` 存在,标记对应 Phase 为已完成
160
- 2. \`git add .sillyspec/changes/\` — 暂存归档结果(不要 commit,由用户通过统一提交工具处理)
161
- 3. \`git add .sillyspec/docs/\` — 暂存模块文档更新(如有)
158
+ 2. \`git add .sillyspec/changes/archive/\` — 暂存归档结果(archive/ 下仅本次归档新增,不会裹挟 changes/ 下其他活跃变更;不要 commit,由用户通过统一提交工具处理)
159
+ 3. \`git add .sillyspec/docs/<project>/modules/\` — 暂存模块文档更新(如有;精确到本次同步的模块文档,勿 add 整个 .sillyspec/docs/)
162
160
  4. 确认 sillyspec.db 中该变更已不再 active(确认归档步骤由 CLI 调用 unregisterChange)
163
161
 
164
162
  ### 输出
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * 与 brainstorm.js 的区别:
5
5
  * 1. artifact-first:直接写文件,对话只输出摘要
6
- * 2. 自动决策:基于 AC-001~AC-010 checklist,不频繁请示
6
+ * 2. 自动决策:基于 AC-001~AC-011 checklist,不频繁请示
7
7
  * 3. next-action.json:结构化产物,驱动下游推进
8
8
  * 4. 只有 blocking questions 才 wait 用户
9
9
  * 5. 步骤从 ~13 步精简为 4 步
@@ -56,7 +56,12 @@ export const definition = {
56
56
  },
57
57
  {
58
58
  name: '需求分析与方案设计',
59
- requiresWait: true,
59
+ // conditionalWait(非 requiresWait):与主模式 Step3 对齐。prompt 明确「目标明确→跳过追问,
60
+ // 直接进入方案设计」「checklist 全 ✅ → AUTO_DECIDED」——这两种场景 AI 无需追问,必须能直接 --done。
61
+ // 若用 requiresWait,CLI 硬门强制要 --answer(complete.js:223),AI 只能伪造 --answer「无需追问」
62
+ // (正是 Grill 第三病)或 --wait 一个不存在的问题。conditionalWait 让「无需追问」路径直接 --done,
63
+ // 只在真正需要用户决策(方案选择/拆分确认)时才走 --wait(multi-agent-review B1)。
64
+ conditionalWait: true,
60
65
  repeatableWait: true,
61
66
  maxWaitRounds: 5,
62
67
  waitReason: '等待用户回答需求问题',
@@ -92,7 +97,7 @@ export const definition = {
92
97
 
93
98
  #### D. 方案设计(自动决策优先)
94
99
  1. 基于需求理解和上下文,提出 1-3 种实现方案
95
- 2. **使用自动决策 checklist(AC-001~AC-010)判断是否需要用户选择:**
100
+ 2. **使用自动决策 checklist(AC-001~AC-011)判断是否需要用户选择:**
96
101
 
97
102
  \`\`\`
98
103
  AC-001: 未修改公共 API
@@ -105,12 +110,13 @@ AC-007: 已有项目约定可复用
105
110
  AC-008: 不影响向后兼容
106
111
  AC-009: 不涉及数据迁移
107
112
  AC-010: 单模块范围内可完成
113
+ AC-011: 不涉及业务规则/产品范围/默认行为/用户可见行为变更
108
114
  \`\`\`
109
115
 
110
116
  3. **自动决策判定:**
111
117
  - 只有一个明显合理方案 + checklist 全部 ✅ → **自动选择,标记 AUTO_DECIDED**
112
118
  - 有多个方案但影响不大(checklist 全部 ✅)→ **自动选推荐方案,标记 AUTO_DECIDED**
113
- - 有多个方案且 checklist 有 ❌(影响架构/数据/接口/权限/兼容性)→ **只有这一种情况才暂停等用户选择**
119
+ - 有多个方案且 checklist 有 ❌(影响架构/数据/接口/权限/兼容性/业务)→ **只有这一种情况才暂停等用户选择**
114
120
 
115
121
  4. 如果需要用户选择方案:
116
122
  - 调用:\`sillyspec run brainstorm --wait --reason "等待用户选择方案" --options "方案A,方案B,方案C" --output "方案对比摘要"\`
@@ -136,15 +142,16 @@ AC-010: 单模块范围内可完成
136
142
  prompt: `将设计方案写入文件(artifact-first),不回显正文。
137
143
 
138
144
  ### 操作
139
- 1. 确保变更目录存在:\`mkdir -p .sillyspec/changes/<change-name>/brainstorm\`
145
+ 1. 确保变更目录存在:\`mkdir -p .sillyspec/changes/<change-name>\`
140
146
  2. **直接将设计方案写入文件,不要先在对话中输出完整内容再写文件。**
147
+ 3. 产物写入**变更根目录**(\`.sillyspec/changes/<change-name>/\`),不要建 \`brainstorm/\` 子目录——stage validator 只认根目录,写进子目录会在 \`--done\` 校验时被判产物缺失。
141
148
 
142
149
  ### 产物文件
143
150
 
144
- #### design.md(必填,写入 \`brainstorm/design.md\`)
151
+ #### design.md(必填,写入变更根目录 \`design.md\`)
145
152
  包含:背景、设计目标、非目标、总体方案、文件变更清单、接口定义、数据模型(如涉及)、兼容策略(brownfield)、风险登记、决策追踪。
146
153
 
147
- #### decisions.md(必填,写入 \`brainstorm/decisions.md\`)
154
+ #### decisions.md(必填,写入变更根目录 \`decisions.md\`)
148
155
  记录所有决策:
149
156
  \`\`\`markdown
150
157
  ## D-001@v1: 决策短标题
@@ -160,13 +167,13 @@ AC-010: 单模块范围内可完成
160
167
  - evidence: 代码/文档路径或用户回答轮次
161
168
  \`\`\`
162
169
 
163
- #### gaps.md(必填,写入 \`brainstorm/gaps.md\`)
170
+ #### gaps.md(必填,写入变更根目录 \`gaps.md\`)
164
171
  记录已识别的缺口。status=BLOCKER 的缺口会在 plan-postcheck 中触发回退。
165
172
 
166
- #### assumptions.md(必填,写入 \`brainstorm/assumptions.md\`)
173
+ #### assumptions.md(必填,写入变更根目录 \`assumptions.md\`)
167
174
  记录隐含假设和验证方法。
168
175
 
169
- #### next-action.json(必填,写入 \`brainstorm/next-action.json\`)
176
+ #### next-action.json(必填,写入变更根目录 \`next-action.json\`)
170
177
  \`\`\`json
171
178
  {
172
179
  "status": "ready_for_plan" | "waiting_for_user",
@@ -189,12 +196,12 @@ AC-010: 单模块范围内可完成
189
196
  ### 输出(artifact-first)
190
197
  只输出摘要:
191
198
  \`\`\`
192
- 已生成设计产物(5 个文件):
193
- - brainstorm/design.md — 采用 xxx 方案,涉及 N 个文件变更
194
- - brainstorm/decisions.md — M 个决策,K 个 AUTO_DECIDED
195
- - brainstorm/gaps.md — J 个缺口,0 个 BLOCKER
196
- - brainstorm/assumptions.md — L 个假设
197
- - brainstorm/next-action.json — ready_for_plan / waiting_for_user
199
+ 已生成设计产物(5 个文件,均在变更根目录):
200
+ - design.md — 采用 xxx 方案,涉及 N 个文件变更
201
+ - decisions.md — M 个决策,K 个 AUTO_DECIDED
202
+ - gaps.md — J 个缺口,0 个 BLOCKER
203
+ - assumptions.md — L 个假设
204
+ - next-action.json — ready_for_plan / waiting_for_user
198
205
  \`\`\``,
199
206
  outputHint: '产物摘要',
200
207
  optional: false
@@ -207,13 +214,12 @@ AC-010: 单模块范围内可完成
207
214
  prompt: `生成 proposal.md / requirements.md / tasks.md,让用户确认。
208
215
 
209
216
  ### 操作
210
- 1. 基于 brainstorm/ 下的设计产物,生成规范文件(直接写文件):
217
+ 1. 基于变更根目录下的设计产物(design.md / decisions.md / gaps.md / assumptions.md / next-action.json),生成规范文件(直接写入变更根目录):
211
218
  - **proposal.md**:动机、关键问题、变更范围、不在范围内、成功标准
212
219
  - **requirements.md**:角色表 + FR 编号需求 + Given/When/Then + 非功能需求
213
220
  - **tasks.md**:任务列表(只列名称,细节在 plan 阶段展开)
214
- 2. 如果 brainstorm/decisions.md 有 AUTO_DECIDED 决策,在变更根目录也写一份 decisions.md
215
- 3. 所有规范文件头部包含 YAML frontmatter
216
- 4. \`git add .sillyspec/\` — 暂存规范文件(不要 commit)
221
+ 2. 所有规范文件头部包含 YAML frontmatter
222
+ 3. \`git add {SPEC_ROOT}/changes/<change-name>/\` — 暂存本变更的规范文件(勿用 .sillyspec/ 整目录——会裹挟其他活跃变更;不要 commit)
217
223
 
218
224
  ### 输出(摘要)
219
225
  规范文件路径列表(各一句话说明)