sillyspec 3.28.0 → 3.28.3

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 (60) hide show
  1. package/.claude/skills/sillyspec-auto/SKILL.md +54 -93
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
  3. package/.claude/skills/sillyspec-continue/SKILL.md +17 -47
  4. package/.claude/skills/sillyspec-execute/SKILL.md +222 -214
  5. package/.claude/skills/sillyspec-knowledge/SKILL.md +40 -270
  6. package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
  7. package/.claude/skills/sillyspec-propose/SKILL.md +16 -87
  8. package/.claude/skills/sillyspec-resume/SKILL.md +21 -70
  9. package/.claude/skills/sillyspec-state/SKILL.md +25 -64
  10. package/.claude/skills/sillyspec-workspace/SKILL.md +33 -175
  11. package/package.json +1 -1
  12. package/src/archive-delta.js +462 -0
  13. package/src/constants.js +6 -0
  14. package/src/design-facts.js +418 -0
  15. package/src/dispatch/backends/sillyhub-mcp.js +17 -5
  16. package/src/docs-check.js +197 -34
  17. package/src/docs-migrate.js +124 -0
  18. package/src/doctor-diagnostics.js +11 -2
  19. package/src/endpoint-baseline.js +109 -0
  20. package/src/git-helper.js +31 -0
  21. package/src/index.js +381 -22
  22. package/src/modules.js +41 -12
  23. package/src/progress/change-registry.js +39 -0
  24. package/src/progress/consistency-doctor.js +124 -5
  25. package/src/progress/stage-machine.js +13 -2
  26. package/src/progress.js +5 -0
  27. package/src/quicklog.js +51 -0
  28. package/src/run/command.js +199 -5
  29. package/src/run/complete-handlers.js +60 -4
  30. package/src/run/complete.js +105 -6
  31. package/src/run/gates.js +353 -2
  32. package/src/run/progress-confirm.js +66 -0
  33. package/src/run/prompt.js +193 -15
  34. package/src/run/quick-audit.js +12 -5
  35. package/src/run/scan-profile.js +117 -7
  36. package/src/run/shared.js +22 -6
  37. package/src/run/stage.js +14 -1
  38. package/src/scan-diff.js +54 -1
  39. package/src/scan-facts.js +173 -0
  40. package/src/scan-postcheck.js +114 -3
  41. package/src/spec-sync.js +19 -2
  42. package/src/stage-review.js +62 -3
  43. package/src/stages/archive.js +5 -5
  44. package/src/stages/brainstorm-auto.js +2 -1
  45. package/src/stages/brainstorm.js +12 -20
  46. package/src/stages/execute.js +19 -21
  47. package/src/stages/plan-postcheck.js +194 -1
  48. package/src/stages/plan.js +8 -3
  49. package/src/stages/quick.js +4 -2
  50. package/src/stages/scan.js +44 -102
  51. package/src/stages/verify.js +18 -18
  52. package/src/task-review.js +200 -0
  53. package/src/taskcard.js +8 -1
  54. package/src/verify-postcheck.js +771 -29
  55. package/src/verify-probes.js +89 -10
  56. package/src/workflow.js +30 -0
  57. package/templates/agents-instruction.md +1 -1
  58. package/templates/prompts/taskcard-rules.md +5 -0
  59. package/templates/workflows/scan-docs.yaml +6 -0
  60. package/.claude/skills/verify-per-user/SKILL.md +0 -6
@@ -5,35 +5,13 @@ export const definition = {
5
5
  auxiliary: true,
6
6
  steps: [
7
7
  {
8
+ // noAI 化(2026-09-05 ①):ls 级探测不值一整轮 agent 往返——CLI 直跑(scanDetectProjects),
9
+ // 只打印建议不注册(原红线语义不变)。步骤名保持不变(存量进度库按名匹配迁移)。
8
10
  name: '探测项目结构并建议子项目',
9
- prompt: `扫描项目顶层目录结构,自动发现可能的子项目,**需用户确认后才创建 projects 配置**。
10
-
11
- ### 操作
12
- 1. 列出项目顶层目录:\`ls -d */ 2>/dev/null | grep -v node_modules | grep -v '.git' | grep -v '.sillyspec'\`
13
- 2. 对每个顶层目录,快速判断是否为独立项目(检查 package.json / pom.xml / build.gradle / pyproject.toml / go.mod 等构建文件)
14
- 3. 对每个疑似独立项目,检测技术栈:\`cat <dir>/package.json 2>/dev/null | head -5\` 或类似
15
- 4. 对比 \`{PROJECTS_ROOT}/\` 已有配置,找出未注册的子项目
16
-
17
- ### 判断标准(满足任一即为子项目)
18
- - 有独立的构建文件(package.json, pom.xml, build.gradle, pyproject.toml 等)
19
- - 有独立的源码目录结构(src/, app/, lib/ 等)
20
- - 有独立的测试目录(test/, tests/, __tests__/ 等)
21
- - 不是 .git / node_modules / .sillyspec / dist / build 等工具目录
22
-
23
- ### 输出格式
24
- 列出发现的可能子项目列表,每个含:
25
- - 目录名
26
- - 技术栈(如 Next.js + TypeScript、FastAPI + Python)
27
- - 是否已注册到 projects/
28
- - 建议:注册 / 跳过
29
-
30
- ### ⛔ 红线
31
- - **不要自动创建 projects 配置文件**,只列出建议供用户确认
32
- - **不要修改任何文件**,只做探测和报告
33
-
34
- ### 输出
35
- 子项目建议列表(含技术栈和注册状态)`,
36
- outputHint: '子项目建议列表',
11
+ noAI: true,
12
+ _cliAction: 'scanDetectProjects',
13
+ prompt: '',
14
+ outputHint: '子项目建议列表(CLI 探测输出)',
37
15
  optional: false
38
16
  },
39
17
  {
@@ -90,34 +68,34 @@ export const definition = {
90
68
  {
91
69
  name: '构建环境探测',
92
70
  perProject: true,
93
- prompt: `探测当前项目的构建环境和依赖。
71
+ prompt: `生成事实底稿(机械事实由 CLI 预咀嚼)+ 补充机器拿不到的线索。
94
72
 
95
73
  ### 操作
96
74
  对扫描列表中的每个项目重复以下操作:
97
- 1. 进入项目目录(子项目用其 path,如 \`packages/dashboard/\`)
98
- 2. \`cat package.json pom.xml build.gradle go.mod Cargo.toml requirements.txt pyproject.toml Gemfile composer.json 2>/dev/null\`
99
- 3. \`find <project-dir> -maxdepth 2 -name "*.config.*" -not -path "*/node_modules/*" -not -path "*/.git/*" | head -20 | xargs cat 2>/dev/null\`
100
- 4. 结果保存到 \`{DOCS_ROOT}/scan/_env-detect.md\`(临时文件,扫描完删除)
75
+ 1. **先跑 CLI 确定性抽取**(端点/依赖/脚本/源码规模/git 基线,零 AI token):
76
+ \`sillyspec scan facts\`(主项目;子项目加 \`--path <仓库相对路径> --project <项目名>\`)。
77
+ 产物落 \`{DOCS_ROOT}/scan/_facts.md\`——后续所有子代理共享这一份底稿,**不要重复 grep 发现这些事实**。
78
+ 2. 补充机器拿不到的线索(保持精简):README / CI 配置里的构建与测试命令、特殊环境要求、
79
+ 配置文件要点(\`cat <project-dir>/*.config.* 2>/dev/null | head -80\`),
80
+ 追加写入 \`{DOCS_ROOT}/scan/_env-detect.md\`(临时文件,扫描完删除)。
81
+
82
+ ### ⛔ 红线
83
+ - **禁止自己 grep/rg 重新发现端点、路由、依赖清单**——以 \`_facts.md\` 为准;发现与源码冲突时以底稿为准并在文档中标注差异
84
+ - 只读探测,不修改任何源文件
101
85
 
102
86
  ### 输出
103
- 每个项目的环境探测结果摘要`,
104
- outputHint: '环境探测摘要',
87
+ 每个项目的 _facts.md 状态(类型/依赖数/端点数)+ 补充线索摘要`,
88
+ outputHint: '_facts.md 状态 + 环境探测补充摘要',
105
89
  optional: false
106
90
  },
107
91
  {
92
+ // noAI 化(2026-09-05 ①):existsSync×7 的机械检查——CLI 一次遍历全部已注册项目(scanResumeCheck)。
93
+ // 不再 perProject(单次 CLI 调用覆盖多项目,注册表步数不变仍为 11)。
108
94
  name: '断点续扫检测',
109
- perProject: true,
110
- prompt: `检测当前项目已有扫描文档,列出缺失的。
111
-
112
- ### 操作
113
- 对扫描列表中的每个项目分别执行:
114
- 1. 检查 7 份文档是否存在:ARCHITECTURE、STRUCTURE、CONVENTIONS、INTEGRATIONS、TESTING、CONCERNS、PROJECT
115
- 路径:\`{DOCS_ROOT}/scan/<DOC>.md\`
116
- 2. 列出已有 ✅ 和缺失 ⬜
117
-
118
- ### 输出
119
- 每个项目的已有/缺失文档列表`,
120
- outputHint: '断点续扫状态',
95
+ noAI: true,
96
+ _cliAction: 'scanResumeCheck',
97
+ prompt: '',
98
+ outputHint: '各项目已有/缺失文档清单(CLI 输出)',
121
99
  optional: false
122
100
  },
123
101
  {
@@ -134,8 +112,8 @@ export const definition = {
134
112
  a. 将 \`<project>\` 替换为实际项目名,得到该项目的目标文件路径
135
113
  b. 为每个角色启动独立子代理(可并行),每个子代理负责 1-2 份文档
136
114
  c. 子代理的搜索范围限定在该项目目录内(子项目如 \`packages/dashboard/\`,不要搜索主项目源码)
137
- d. 子代理直接用 grep/rg 搜索源码并写入文件,结果不回传到你的上下文
138
- e. 等待该项目所有子代理完成后,验证文件是否生成且非空
115
+ d. 子代理在**事实底稿覆盖范围内的机械事实**(端点/依赖/规模)直接消费注入的 \`_facts.md\`,不重新 grep 发现;底稿之外的架构理解仍用 grep/rg 搜索源码并写入文件,结果不回传到你的上下文
116
+ e. 等待该项目所有子代理完成后,直接进入下一项目——产物存在性/引用有效性由第 3 步 workflow check 与 postcheck **机器校验**,不要自己读回文件核对
139
117
  f. 该项目完成后,继续下一个项目
140
118
  3. 所有项目完成后,运行以下命令检查产物:
141
119
  \`sillyspec workflow check scan-docs --project <project>\`(CLI 子命令,任意项目根可用)
@@ -144,19 +122,9 @@ export const definition = {
144
122
 
145
123
  ### 覆盖保护
146
124
  - **scan_depth: quick 的浅层文档允许覆盖升级**:读取旧 frontmatter 时,若旧文档含 scan_depth: quick(由 --quick 快速接入生成的浅层版本),即使其 source_commit 与当前 HEAD 一致、updated_at 未变,也允许覆盖重写——深度扫描的目的就是把它升级为完整文档。
147
- - 生成每份 scan 文档时,frontmatter 必须包含:
148
- \`\`\`yaml
149
- ---
150
- author: <git 用户名(git config user.name 的输出,如 qinyi)>
151
- created_at: <精确到秒的时间,如 2026-07-27 10:30:00>
152
- source_commit: <git-head-short>
153
- updated_at: <now-iso-datetime>
154
- generator: sillyspec-scan
155
- ---
156
- \`\`\`
157
- (\`<git-head-short>\` / \`<now-iso-datetime>\` 两占位符由 CLI 注入 prompt 时自动替换为当前 HEAD 短哈希与时间——照 prompt 里已替换的真实值抄写,勿自跑 git rev-parse;漏 header 事后 \`sillyspec scan-fix-headers\` 一键补 author/created_at)
125
+ - **frontmatter 由 CLI 自动写入(2026-09-05 ②):子代理不写 frontmatter、不跑 git 取值**。文档正文第 1 行直接写中文标题;终检步骤(scanFinalize)的 CLI 会按需注入 author / created_at / source_commit / updated_at / generator,只补缺不覆盖。
158
126
  - 覆盖已有 scan 文档前先读取旧 frontmatter;如果旧文档的 \`source_commit\` 与当前 HEAD 不一致,或旧文档 \`updated_at\` 晚于本次 scan 开始时间,不要覆盖。
159
- - 如果用户明确传入 \`--force-rescan\`,允许覆盖,但仍需写入新的 \`source_commit\` 和 \`updated_at\`。
127
+ - 如果用户明确传入 \`--force-rescan\`,允许覆盖(新 frontmatter 仍由 CLI 注入)。
160
128
 
161
129
  ### 子代理上下文注入
162
130
  启动每个子代理前,将以下信息拼入子代理 prompt:
@@ -164,9 +132,10 @@ export const definition = {
164
132
  - 目标文件路径(从 workflow YAML 中 \`<project>\` 替换后的路径)
165
133
  - 检查要求(从 workflow YAML 中该角色的 checks)
166
134
  - 断点续扫步骤列出的缺失文档列表
167
- - 环境探测结果摘要(如有 _env-detect.md,直接贴入)
135
+ - **事实底稿 \`{DOCS_ROOT}/scan/_facts.md\` 全文直接贴入**(CLI 确定性抽取的端点/依赖/规模清单——子代理**禁止重新 grep 发现**这些机械事实,只做解读与成文;INTEGRATIONS/ARCHITECTURE 中的端点与集成条目必须与底稿一致,并直接使用底稿里的 file:line 引用)
136
+ - 环境探测补充摘要(如有 _env-detect.md,直接贴入)
168
137
  - **⚠️ 必须强调:子代理必须用 write 工具将文件写入磁盘**
169
- - **文件标题用中文**(sillyhub 平台解析识别用):frontmatter 必须在文件最前(第 1 行 --- 起),frontmatter 结束的 --- 之后空一行,再写 # 中文名(English)作为标题。严禁把 # 标题放到 frontmatter 之前(否则 frontmatter 不在头部、「检查产物完整性」步的 frontmatter 检查会报缺 author/created_at)。各文档标准标题格式:
138
+ - **文件标题用中文**(sillyhub 平台解析识别用):正文第 1 行写 # 中文名(English)作为标题(CLI 会把 frontmatter 自动插到标题之前,你不用管)。各文档标准标题格式:
170
139
  - STRUCTURE.md = # 目录结构(Structure)
171
140
  - CONVENTIONS.md = # 代码约定(Conventions)
172
141
  - ARCHITECTURE.md = # 架构(Architecture)
@@ -174,6 +143,11 @@ export const definition = {
174
143
  - CONCERNS.md = # 关注点(Concerns)
175
144
  - INTEGRATIONS.md = # 集成(Integrations)
176
145
  - PROJECT.md = # 项目(Project)
146
+ - **事实引用纪律**:子代理写入文档的关键事实(模块职责、技术栈组件、外部集成、核心命令)必须带反引号包裹的 path:line 引用(如 \`src/db.js:42\`),且引用必须真实——CLI postcheck 会逐条核验 file:line,失效引用产生 warning。**禁止猜行号**:不确定时只写文件路径不写行号;无法确证的结论标「待确认」,不写成事实。
147
+
148
+ ### 收敛规则(防打转)
149
+ - \`sillyspec workflow check scan-docs\` 报告失败项后,**只修复报告点名的角色/文件对应的问题**,不要重写未失败的文档
150
+ - 每轮修复后重跑检查,跟踪失败项数量:连续两轮失败项数量没有下降 → 停止重试,在 --output 里如实报告未解决的失败项与已尝试的修复,等待人工决策。**禁止为通过检查而编造内容或降低文档质量**
177
151
 
178
152
  ### 完成后
179
153
  列出每个项目的 7 份文档状态:
@@ -562,46 +536,14 @@ INDEX.md 维护索引,格式(每行:关键词1|关键词2 → [条目名](
562
536
  optional: false
563
537
  },
564
538
  {
539
+ // noAI 化(2026-09-05 ①②):原 40 行自检 prompt 的职责全部机器化——
540
+ // 文档存在性/header/路径污染/API 错误信号 → runScanPostCheck;frontmatter → stampScanDocHeaders;
541
+ // _env-detect 清理 + 非平台 git add → CLI 直做。failed_post_check 时 throw 保持 step pending。
565
542
  name: '自检和提交',
566
- perProject: true,
567
- prompt: `验证当前项目的扫描完整性,清理并提交。
568
-
569
- ### 操作
570
- 对扫描列表中的每个项目分别执行:
571
- 1. 检查 7 份 scan 文档是否全部生成(\`{DOCS_ROOT}/scan/\`)
572
- 2. 检查模块文档状态(\`{DOCS_ROOT}/modules/\`)
573
- 3. 自检门控:ARCHITECTURE(技术栈+Schema摘要)、CONVENTIONS(隐形规则+代码风格)、STRUCTURE(目录结构)、INTEGRATIONS(外部依赖)、TESTING(测试现状)、CONCERNS(技术债务)、PROJECT(项目概览)
574
- 4. 检查 flows/ 和 glossary.md 是否已生成(如有)
575
- 5. 清理:\`rm -f {DOCS_ROOT}/scan/_env-detect.md\`
576
- 6. 如果非平台模式:\`git add {DOCS_ROOT}/ {KNOWLEDGE_ROOT}/\` — 暂存本次扫描产物(scan 文档 / 模块文档 / flows / glossary / knowledge;勿用 .sillyspec/ 整目录——会裹挟 changes/ 下其他活跃变更;不要 commit,由用户通过统一提交工具处理)。如果平台模式:跳过 git add(specRoot 不在 sourceRoot 的 git repo 内)。
577
-
578
- ### ⛔ 路径合规检查(平台模式下必须执行)
579
- 7. 确认所有文档都写入 \`{DOCS_ROOT}/\`(spec-root 下),**而非源码目录下的 .sillyspec/**
580
- 8. 检查是否出现 tool_use_error 或 API Error 未恢复
581
- 9. 检查 7 份文档 header 是否包含 author 和 created_at
582
- 10. 复查 detect 核验结果——commands 键已由 detect 核验 scripts 存在性,agent 无需重复核验或标 unavailable,仅确认 local.yaml 与 detect 产出一致
583
-
584
- ### ⛔ API 错误处理
585
- - 遇到 API Error 529(服务过载)或 rate_limit 时,**停止当前操作并报告**,不要自动重试
586
- - 遇到 tool_use_error 时,记录错误信息并跳过该文件/操作,继续处理下一项
587
- - 如果连续 3 次操作失败,输出失败摘要并停止
588
-
589
- ### ⛔ 最终状态判定
590
- 如果出现以下**任意**情况,最终状态**不能**写"全部通过",只能写 \`completed_with_warnings\` 或 \`failed_post_check\`:
591
- - 源码目录下存在 docs(路径合规检查失败)
592
- - source_commit 为 null
593
- - Write 工具出现过失败
594
- - API Error 529 或 rate_limit
595
- - fallback / retry / skipped validation
596
- - 文档引用不存在的文件或模块
597
- - 文档内容包含 .sillyspec/ 等工具目录的扫描结果
598
-
599
- ### 输出
600
- 每个项目的扫描完整性报告(必须包含路径合规检查结果和最终状态)
601
-
602
- ### 注意
603
- - ❌ 修改代码 / 编造路径 / 读源码全文`,
604
- outputHint: '自检报告',
543
+ noAI: true,
544
+ _cliAction: 'scanFinalize',
545
+ prompt: '',
546
+ outputHint: 'CLI 终检报告(frontmatter 注入 + postcheck + 清理 + 暂存)',
605
547
  optional: false
606
548
  }
607
549
  ]
@@ -45,19 +45,12 @@ export const definition = {
45
45
  {
46
46
  name: '进度确认',
47
47
  migratedFrom: ['状态检查'],
48
- prompt: `> 💡 先说清楚:\`sillyspec run verify\`(不带 --done)**只下发执行指令,不会替你跑测试/构建**——真正的测试由 CLI 在最后 --done 时统一执行(local.yaml 的 commands.test,同步对账可能耗时较长);「运行测试和质量扫描」那步**不需要你重复手动跑全量**(避免与 CLI 对账重复耗时),只做 lint/静态检查 + 可选快速冒烟。别以为敲了 run verify 就自动验证了。
49
-
50
- 检查当前进度,确认可以执行 verify。
51
-
52
- > ⚠️ 本步用 \`sillyspec progress show\` 查**流程进度**(推进工作流用),不要用 \`sillyspec status\`(那是**项目级快照**,只读、不推进流程,是另一条命令)。
53
-
54
- ### 操作
55
- 1. 运行 \`sillyspec progress show\`
56
- 2. 确认 currentStage 为 "verify"
57
-
58
- ### 输出
59
- 当前进度摘要`,
60
- outputHint: '进度摘要',
48
+ // noAI 化(2026-09-07):唯一非常量信息(「CLI 在 --done 统一跑测试」)改由 progressConfirm
49
+ // console 提示;快照复述与阶段检查已机械化。步骤名不变(存量进度库按名匹配迁移)。
50
+ noAI: true,
51
+ _cliAction: 'progressConfirm',
52
+ prompt: '',
53
+ outputHint: '进度摘要(CLI 快照输出)',
61
54
  optional: false
62
55
  },
63
56
  {
@@ -70,7 +63,8 @@ export const definition = {
70
63
  - 如果存在 P0/P1 unresolved/blocking 决策,验证结论不能为 PASS
71
64
  - 如果发现 superseded 决策被下游引用,标记为 ⚠️ stale decision reference
72
65
  3. 加载项目信息:\`cat {SPEC_ROOT}/projects/*.yaml 2>/dev/null\`
73
- 4. 加载本地配置:\`cat {SPEC_ROOT}/local.yaml 2>/dev/null\`(构建命令、测试命令、lint 命令等)若 local.yaml 不存在,先 \`sillyspec local detect\` 生成骨架再读取
66
+ 4. 构建命令(CLI 自 local.yaml 注入,勿再读文件;未配置时按注入说明跑 local detect):
67
+ {LOCAL_COMMANDS}
74
68
  5. 加载代码规范:\`cat {SPEC_ROOT}/docs/<project>/scan/CONVENTIONS.md 2>/dev/null\`
75
69
  - 测试现状:\`cat {SPEC_ROOT}/docs/<project>/scan/TESTING.md 2>/dev/null\`(了解既有测试约定与覆盖范围,验收时对照)
76
70
  - 技术债清单:\`cat {SPEC_ROOT}/docs/<project>/scan/CONCERNS.md 2>/dev/null\`(🔴/🟡 区域;本次变更若触碰须在 verify-result.md 标注)
@@ -104,6 +98,9 @@ export const definition = {
104
98
  name: '逐项检查任务',
105
99
  prompt: `对照 tasks.md(任务注册表唯一真相)检查每个任务完成状态。勾选由 execute 双路写入(agent 按 review gate 手动勾 + CLI autoCheckPlanFromReviews 机器勾选器按 review.json 自动勾),本阶段只读对照、不改勾。
106
100
 
101
+ ### 勾选状态(CLI 注入,勿手数)
102
+ {TASKS_CHECKBOX}
103
+
107
104
  ### 操作
108
105
  对每个 checkbox:
109
106
  1. 检查相关文件是否存在
@@ -173,7 +170,8 @@ export const definition = {
173
170
  prompt: `运行代码质量扫描(测试实测统一由 CLI 对账执行,本步不重复手动跑全量)。
174
171
 
175
172
  ### 操作
176
- 1. 读取 \`{SPEC_ROOT}/local.yaml\` 获取构建、测试和 lint 命令若 local.yaml 不存在,先 \`sillyspec local detect\` 生成骨架再读取
173
+ 1. 构建命令(CLI 自 local.yaml 注入,勿再读文件;未配置时按注入说明跑 local detect):
174
+ {LOCAL_COMMANDS}
177
175
  2. **不要手动重复跑 commands.test**——CLI 会在最终 --done 时统一执行一次(按变更命中模块子集),本步再跑 = 与 CLI 对账重复耗时(实测 198s×2)。如为提前发现实现问题,可对变更模块做**针对性快速冒烟**(可选,非必需):
178
176
  - Maven:\`mvn test -pl <变更模块> -am\`(仅编译变更模块及其依赖)
179
177
  - Gradle:\`./gradlew :<模块>:test\`
@@ -214,9 +212,11 @@ export const definition = {
214
212
  你只需:在 verify-result.md 的「变更风险等级」section 如实记录变更性质;若变更涉及 daemon/backend 跨进程、session/lease/lifecycle 状态机、或部署启动路径,在「Runtime Evidence」section 提供真实集成证据(启动命令、daemon↔backend 调用与日志关键片段、终态断言)。
215
213
  - **集成证据是自报告、CLI 不独立运行时核验**:「Runtime Evidence」由你如实填写,CLI 只校验其**字面存在**(是否含关键词),**不会替你启动 daemon、打真实请求或跑迁移**——它是否名副其实取决于你是否实跑过。务必实跑后据实填写,不得凭堆关键词通过门控。(测试套件对账另算:commands.test 由 CLI 真实执行,那块谎报无效。)
216
214
 
217
- 3. **生成 verify-result.md 骨架(勿从零手写)**:先跑 \`sillyspec verify-probes --change <change-name> --init\`——一条命令生成九章节骨架(已存在不覆盖),其中**探针结果章节已机械预填**(探针 1 的 TODO/FIXME 命中清单、探针 3 的测试覆盖、探针 5 的 API 契约对账表、探针 6 的删除对账三态判定),文件落 \`{SPEC_ROOT}/changes/<change-name>/verify-result.md\`(CLI 替换出的**主仓绝对路径**;若当前 cwd 在 worktree 内也绝不落 worktree 副本——CLI 校验读主仓,副本随 worktree 清理蒸发)。你只需把各 \`<!--TODO-->\` 占位替换为语义结论;半语义探针(2 关键词覆盖 / 4 决策追踪)与断言抽查、集成盲区标注由你补在对应 TODO 处
218
- 4. 给出结论:PASS / PASS WITH NOTES / FAIL(受风险门控约束)——**结论必须写明 PASS/FAIL 字样,留「待填」会被 gate 判不过**
219
- 5. **核对 module-impact.md**(若 \`{SPEC_ROOT}/changes/<change>/module-impact.md\` 存在):对照本次实际代码变更(git diff)与 module-impact.md 的模块影响矩阵,发现不一致(漏标受影响模块 / 影响类型错误 / 实际未触碰的模块被误标)则在 verify-result.md 标注。module-impact 由 plan 首版生成、execute 各 Wave 更新,verify 是最后一次核对机会(archive 仅终审不再生成)。这是 advisory 核对(不阻断 verify 完成),但 module-impact 与实际严重背离应记为风险。
215
+ 3. **生成 verify-result.md 骨架(勿从零手写)**:先跑 \`sillyspec verify-probes --change <change-name> --init\`——一条命令生成十章节骨架(已存在不覆盖),其中**探针结果章节已机械预填**(探针 1 的 TODO/FIXME 命中清单、探针 3 的测试覆盖、探针 5 的 API 契约对账表、探针 6 的删除对账三态判定),文件落 \`{SPEC_ROOT}/changes/<change-name>/verify-result.md\`(CLI 替换出的**主仓绝对路径**;若当前 cwd 在 worktree 内也绝不落 worktree 副本——CLI 校验读主仓,副本随 worktree 清理蒸发)。你只需把各 \`<!--TODO-->\` 占位替换为语义结论;半语义探针(2 关键词覆盖 / 4 决策追踪)与断言抽查、集成盲区标注由你补在对应 TODO 处
216
+ 4. **预填探针段不可篡改或删除**:verify \`--done\` gate 会重跑探针对比 verify-result.md 正文,不符即拦(ERROR);对探针结果有异议只能在预填段旁追加说明,不得改写原文
217
+ 5. **verify-facts.json 为机器底稿,勿手改**:防篡改对比的基准是正文,改底稿无意义
218
+ 6. 给出结论:PASS / PASS WITH NOTES / FAIL(受风险门控约束)——**结论必须写明 PASS/FAIL 字样,留「待填」会被 gate 判不过**
219
+ 7. **核对 module-impact.md**(若 \`{SPEC_ROOT}/changes/<change>/module-impact.md\` 存在):对照本次实际代码变更(git diff)与 module-impact.md 的模块影响矩阵,发现不一致(漏标受影响模块 / 影响类型错误 / 实际未触碰的模块被误标)则在 verify-result.md 标注。module-impact 由 plan 首版生成、execute 各 Wave 更新,verify 是最后一次核对机会(archive 仅终审不再生成)。这是 advisory 核对(不阻断 verify 完成),但 module-impact 与实际严重背离应记为风险。
220
220
 
221
221
  ### verify-result.md 章节结构(骨架已含,占位替换即可)
222
222
 
@@ -1468,6 +1468,206 @@ export async function adoptTaskReviewMechanics({ changeName, cwd, platformOpts =
1468
1468
  return { adopted, unchanged, skipped, missing, downgraded, reasons, executeRunId }
1469
1469
  }
1470
1470
 
1471
+ /**
1472
+ * writeTaskReview —— 单 task review.json 的命令式写入(`sillyspec review write`,2026-09-07)。
1473
+ *
1474
+ * 背景(noAI 主流程批次 T2-4):task 级 review.json 此前由 agent/子代理手拼 JSON + 手算
1475
+ * base/head + 手抄 changedFiles——schemaVersion/目录创建/executeRunId 解析/git 锡点全部是
1476
+ * 纯机械活(generateTaskReviewDrafts/adoptTaskReviewMechanics 已证明 CLI 可代算),agent 只
1477
+ * 应贡献判断字段(verdict/notes/evidence)。命令式写入消灭「手拼 JSON→gate 拒→重写」循环。
1478
+ *
1479
+ * 职责分工:
1480
+ * - CLI 代算(mechanics):executeRunId(marker 优先)、base/head(worktree meta 优先,
1481
+ * --base/--head 显式覆盖)、changedFiles(diff × allowed_paths 归属切片,--changed-files
1482
+ * 显式覆盖)、diffPaths(allowed_paths 非空时)、repo 锡点(task 卡)。
1483
+ * - agent 供给(判断):--spec/--quality verdict、--notes、--evidence。
1484
+ *
1485
+ * 防伪/防误写:
1486
+ * - review.json 已存在 → 拒绝覆盖(--force 显式越过,控制台留痕)——保护独立 reviewer 产物。
1487
+ * - verdict=cannot_verify 且未给 --evidence → 报错(schema 硬要求非空 requiredEvidence),
1488
+ * 不静默填占位(与草稿语义不同:草稿是兜底待复核,这里是 agent 明确结论)。
1489
+ * - 归属切片为空(allowed_paths 未命中 diff)→ 报错并列出 diff 候选与 task allowed_paths
1490
+ * (帮 agent 当场发现「改了声明外文件 / 路径写错」),--changed-files 显式越过。
1491
+ *
1492
+ * @param {object} opts
1493
+ * @param {string} opts.changeName
1494
+ * @param {string} opts.cwd
1495
+ * @param {string} opts.taskId - 形如 task-NN
1496
+ * @param {string} opts.specVerdict - pass | fail | cannot_verify
1497
+ * @param {string} opts.qualityVerdict - 同上
1498
+ * @param {string} [opts.reviewerNotes]
1499
+ * @param {string[]} [opts.requiredEvidence]
1500
+ * @param {string|null} [opts.baseOverride] - 显式 base 锡点(无 worktree meta 时必填)
1501
+ * @param {string|null} [opts.headOverride]
1502
+ * @param {string[]|null} [opts.changedFilesOverride]
1503
+ * @param {boolean} [opts.force] - 覆盖已存在 review.json
1504
+ * @param {object} [opts.platformOpts]
1505
+ * @returns {Promise<{ok: boolean, reviewPath?: string, executeRunId?: string, errors: string[], warnings: string[]}>}
1506
+ */
1507
+ export async function writeTaskReview({
1508
+ changeName, cwd, taskId, specVerdict, qualityVerdict,
1509
+ reviewerNotes = '', requiredEvidence = [],
1510
+ baseOverride = null, headOverride = null, changedFilesOverride = null,
1511
+ force = false, platformOpts = {},
1512
+ }) {
1513
+ const errors = []
1514
+ const warnings = []
1515
+ const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
1516
+ const runtimeRoot = resolveRuntimeRoot(platformOpts, specBase)
1517
+ const changeDir = join(specBase, 'changes', changeName)
1518
+
1519
+ // ── 入参校验 ──
1520
+ if (!changeName || typeof changeName !== 'string') errors.push('缺少 --change <变更名>')
1521
+ if (!taskId || !/^task-\d+$/.test(taskId)) errors.push(`--task 格式应为 task-NN(实际 ${taskId})`)
1522
+ if (!VALID_VERDICTS.includes(specVerdict)) errors.push(`--spec verdict 无效:${specVerdict}(应为 ${VALID_VERDICTS.join('/')})`)
1523
+ if (!VALID_VERDICTS.includes(qualityVerdict)) errors.push(`--quality verdict 无效:${qualityVerdict}(应为 ${VALID_VERDICTS.join('/')})`)
1524
+ if ((specVerdict === 'cannot_verify' || qualityVerdict === 'cannot_verify')
1525
+ && (!Array.isArray(requiredEvidence) || requiredEvidence.filter(Boolean).length === 0)) {
1526
+ errors.push('verdict=cannot_verify 必须给 --evidence "<非空说明>"(schema 硬要求 requiredEvidence 非空)')
1527
+ }
1528
+ if (errors.length > 0) return { ok: false, errors, warnings }
1529
+
1530
+ // ── task 卡与 executeRunId 解析(与草稿生成同源)──
1531
+ const taskCardPath = join(changeDir, 'tasks', `${taskId}.md`)
1532
+ if (!existsSync(taskCardPath)) {
1533
+ return { ok: false, errors: [`task 卡不存在:${taskCardPath}(先由 plan 阶段生成或手建)`], warnings }
1534
+ }
1535
+ const markerFile = join(runtimeRoot, 'current-execute-run-id-' + changeName)
1536
+ let executeRunId = ''
1537
+ try {
1538
+ if (existsSync(markerFile)) {
1539
+ const c = readFileSync(markerFile, 'utf8').trim()
1540
+ if (isValidExecuteRunId(c)) executeRunId = c
1541
+ }
1542
+ } catch {}
1543
+ if (!executeRunId) {
1544
+ executeRunId = resolveLatestExecuteRunId({ runtimeRoot, changeName }) || ''
1545
+ }
1546
+ if (!executeRunId) {
1547
+ executeRunId = generateExecuteRunId()
1548
+ warnings.push(`无 execute run marker——已生成新 runId ${executeRunId} 并建目录(正常应先跑过 execute Wave 步)`)
1549
+ try {
1550
+ mkdirSync(join(runtimeRoot, 'execute-runs', executeRunId, 'tasks'), { recursive: true })
1551
+ writeFileSync(markerFile, executeRunId + '\n')
1552
+ stampExecuteRunChange(runtimeRoot, executeRunId, changeName)
1553
+ } catch (e) {
1554
+ return { ok: false, errors: [`execute run 目录/marker 写入失败:${e.message}`], warnings }
1555
+ }
1556
+ }
1557
+
1558
+ const reviewDir = join(runtimeRoot, 'execute-runs', executeRunId, 'tasks', taskId)
1559
+ const reviewPath = join(reviewDir, 'review.json')
1560
+
1561
+ // ── 已存在保护(--force 越过)──
1562
+ if (existsSync(reviewPath) && !force) {
1563
+ return { ok: false, errors: [`review.json 已存在:${reviewPath}——拒绝覆盖(保护既有 reviewer 结论);确认重写加 --force`], warnings }
1564
+ }
1565
+ if (existsSync(reviewPath)) warnings.push(`--force 覆盖已存在 review:${reviewPath}`)
1566
+
1567
+ // ── mechanics 代算:base/head(worktree meta 优先,显式覆盖次之)──
1568
+ let base = baseOverride
1569
+ let head = headOverride
1570
+ let reviewGitDir = cwd
1571
+ if (!base || !head) {
1572
+ try {
1573
+ const wm = new WorktreeManager({ cwd })
1574
+ const meta = wm.getMeta(changeName)
1575
+ if (!base) base = (meta && (meta.baselineCommit || meta.baseHash)) || null
1576
+ if (meta && meta.worktreePath && meta.mode !== 'in-place-fallback' && existsSync(meta.worktreePath)) {
1577
+ reviewGitDir = meta.worktreePath
1578
+ }
1579
+ } catch { /* meta 读取失败走显式覆盖路径 */ }
1580
+ }
1581
+ if (!head) {
1582
+ try { head = runGit(reviewGitDir, ['rev-parse', 'HEAD']) } catch { head = null }
1583
+ }
1584
+ if (!base || !head) {
1585
+ return {
1586
+ ok: false,
1587
+ errors: [`无法确定 base/head(无 worktree meta 且未显式覆盖)——用 --base <commit> --head <commit> 显式给出(git rev-parse 取值)`],
1588
+ warnings,
1589
+ }
1590
+ }
1591
+
1592
+ // ── mechanics 代算:changedFiles(diff × allowed_paths 归属切片;显式覆盖优先)──
1593
+ const content = readFileSync(taskCardPath, 'utf8')
1594
+ // 跨仓 task 守卫:base/head/changedFiles 的跨仓口径(task 卡锡点 + 跨仓仓 diff)本函数未实现
1595
+ //(草稿生成有专门分支)——显式拒绝并引导既有路径,防主仓锡点被误配到跨仓改动上
1596
+ const guardRepo = normalizeRepoKey(parseRepo(content))
1597
+ if (guardRepo && guardRepo !== 'main') {
1598
+ return {
1599
+ ok: false,
1600
+ errors: [`task 卡声明 repo=${guardRepo}(跨仓 task)——review write 暂不支持跨仓口径,请手写 review.json 后跑 sillyspec backfill-reviews --change ${changeName} --adopt 代填 mechanics(base/head 从 task 卡锡点取)`],
1601
+ warnings,
1602
+ }
1603
+ }
1604
+ const allowedPaths = parseAllowedPaths(content)
1605
+ let changedFiles = null
1606
+ if (Array.isArray(changedFilesOverride)) {
1607
+ // 显式覆盖(含空数组=纯验证任务的合法零 diff 声明)
1608
+ changedFiles = changedFilesOverride.map(p => String(p).replace(/\\/g, '/'))
1609
+ warnings.push(`changedFiles 采用显式覆盖(${changedFiles.length} 个)——evidence 校验仍会与 git diff 相交比对`)
1610
+ } else {
1611
+ let diffFiles = resolveVerifyChangedFiles(cwd, changeName, null, { specBase })
1612
+ // 并入 worktree 未提交改动(与草稿生成同源口径:子代理默认不 commit)
1613
+ try {
1614
+ const wm = new WorktreeManager({ cwd })
1615
+ const meta = wm.getMeta(changeName)
1616
+ if (meta) {
1617
+ const wtGitDir = (meta.worktreePath && meta.mode !== 'in-place-fallback' && existsSync(meta.worktreePath))
1618
+ ? meta.worktreePath
1619
+ : cwd
1620
+ const wtStatus = runGit(wtGitDir, ['status', '--porcelain', '--untracked-files=all'], { trim: false })
1621
+ const wtFiles = parsePorcelainFiles(wtStatus)
1622
+ .map(p => String(p).replace(/\\/g, '/'))
1623
+ .filter(p => p !== '.sillyspec' && !p.startsWith('.sillyspec/'))
1624
+ if (wtFiles.length > 0) diffFiles = [...new Set([...(Array.isArray(diffFiles) ? diffFiles : []), ...wtFiles])]
1625
+ }
1626
+ } catch { /* 退回 commit diff 口径 */ }
1627
+ const mainDiff = Array.isArray(diffFiles) ? diffFiles : []
1628
+ changedFiles = allowedPaths.length > 0
1629
+ ? mainDiff.filter(f => allowedPaths.some(p => pathMatches(f, p)))
1630
+ : []
1631
+ if (changedFiles.length === 0) {
1632
+ return {
1633
+ ok: false,
1634
+ errors: [
1635
+ `归属切片为空:task 卡 allowed_paths ${JSON.stringify(allowedPaths)} 未命中本次 diff 任何文件。` +
1636
+ `本变更 diff 共 ${mainDiff.length} 个文件:${mainDiff.slice(0, 10).join('、')}${mainDiff.length > 10 ? '…' : ''}。` +
1637
+ `确认改动确属本 task → 用 --changed-files <a,b> 显式给出;纯验证任务(无代码 diff 是本质属性)用 --changed-files ""(空覆盖)+ --evidence 披露验证区间`,
1638
+ ],
1639
+ warnings,
1640
+ }
1641
+ }
1642
+ }
1643
+
1644
+ // ── 组装 + schema 自检 + 落盘 ──
1645
+ const review = {
1646
+ schemaVersion: REVIEW_SCHEMA_VERSION,
1647
+ task: taskId,
1648
+ base,
1649
+ head,
1650
+ changedFiles,
1651
+ ...(allowedPaths.length > 0 ? { diffPaths: allowedPaths } : {}),
1652
+ specVerdict,
1653
+ qualityVerdict,
1654
+ ...(requiredEvidence.filter(Boolean).length > 0 ? { requiredEvidence: requiredEvidence.filter(Boolean) } : {}),
1655
+ reviewerNotes: reviewerNotes || `written by sillyspec review write (${base.slice(0, 8)}..${head.slice(0, 8)})`,
1656
+ }
1657
+ const schemaResult = validateReviewSchema(review)
1658
+ if (!schemaResult.ok) {
1659
+ return { ok: false, errors: schemaResult.errors, warnings }
1660
+ }
1661
+ try {
1662
+ mkdirSync(reviewDir, { recursive: true })
1663
+ writeFileSync(reviewPath, JSON.stringify(review, null, 2) + '\n')
1664
+ } catch (e) {
1665
+ return { ok: false, errors: [`review.json 落盘失败:${e.message}`], warnings }
1666
+ }
1667
+ return { ok: true, reviewPath, executeRunId, errors: [], warnings }
1668
+ }
1669
+ // 注:跨仓 task(repo≠main)在上方 mechanics 段前置守卫拒绝——review.repo 恒为 main,不再单独赋值
1670
+
1471
1671
  /**
1472
1672
  * 获取当前(或最新)execute run id
1473
1673
  * 从 runtime 目录下查找 execute-runs/ 子目录
package/src/taskcard.js CHANGED
@@ -55,7 +55,9 @@ function yamlScalar(v) {
55
55
  *
56
56
  * 硬校验 9 字段(plan-postcheck feasibility)+ 规范字段全部就位,占位符由子代理 Edit 填充;
57
57
  * 可选字段(provides/expects_from/related_tests)默认不生成——缩小 YAML 出错面,何时加的
58
- * 判据写在文件尾注释(规则详情见 templates/prompts/taskcard-rules.md)。
58
+ * 判据写在文件尾注释(规则详情见 templates/prompts/taskcard-rules.md)。例外:target_files
59
+ * 预置空占位注释行([] + 行内 # 注释)——占位符硬拦基于 taskcard-placeholders.js 的封闭
60
+ * 字面清单,本行值与注释刻意不含任何占位标记字面量,不会被 plan --done 拦截。
59
61
  * 字符串只含 \n:writeFileSync 直写不经任何行尾转换,Windows 下天然 LF 安全。
60
62
  * title/title_zh/author 经 yamlScalar 转义(自由文本防 YAML 指示符炸解析)。
61
63
  *
@@ -99,6 +101,7 @@ requirement_ids: ${fm.requirement_ids}
99
101
  decision_ids: ${fm.decision_ids}
100
102
  allowed_paths:
101
103
  - src/example/file.ts
104
+ target_files: [] # 可选:本 task 计划改动的文件(对账用精确路径清单,格式见下方注释;不填保留 [])
102
105
  goal: >
103
106
  一句话说明这个 task 要做什么、为什么。
104
107
  implementation:
@@ -119,6 +122,10 @@ constraints:
119
122
  勿用 Write 整文件重写——会引入 CRLF 行尾/漏闭合 ---/漏字段回归。
120
123
  ⚠️ plan --done 硬校验会拦截未替换的占位符(FR-XX / D-XXX / src/example/file.ts /
121
124
  一句话说明这个 task / 具体步骤 1 / 可验证的验收条件 1 / 边界约束 1)——占位符视同缺字段。
125
+ target_files 格式(可选,对账用精确文件级意图声明,与 allowed_paths 语义不同):
126
+ 精确文件路径(仓根相对、正斜杠),当前不存在、将由本 task 新建的文件加
127
+ NEW: 前缀(如 NEW:src/foo.js);禁 glob(src/**)、禁目录前缀(src/dir/)、
128
+ 禁绝对路径;无明确文件级意图时保留 [] 占位行不动。
122
129
  可选字段按需插进上方 frontmatter(规则见 taskcard-rules):
123
130
  repo: 仅跨仓 task 填(local.yaml repos: 注册的仓 key;缺省=main。allowed_paths 相对该仓根写,
124
131
  禁止带仓库名前缀/绝对路径——review 对账按仓根相对路径匹配,带前缀永不命中)