sillyspec 3.26.13 → 3.26.15

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 (56) hide show
  1. package/.claude/skills/sillyspec-archive/SKILL.md +2 -0
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +1 -1
  3. package/.claude/skills/sillyspec-commit/SKILL.md +18 -60
  4. package/.claude/skills/sillyspec-continue/SKILL.md +2 -0
  5. package/.claude/skills/sillyspec-execute/SKILL.md +10 -4
  6. package/.claude/skills/sillyspec-plan/SKILL.md +10 -2
  7. package/.claude/skills/sillyspec-quick/SKILL.md +2 -2
  8. package/.claude/skills/sillyspec-resume/SKILL.md +2 -0
  9. package/.claude/skills/sillyspec-verify/SKILL.md +2 -0
  10. package/.claude/skills/sillyspec-workspace/SKILL.md +7 -1
  11. package/package.json +1 -1
  12. package/src/commit-suggest.js +202 -0
  13. package/src/config-cat.js +88 -0
  14. package/src/docs-check.js +48 -12
  15. package/src/doctor-diagnostics.js +49 -0
  16. package/src/hooks/worktree-guard.js +20 -1
  17. package/src/index.js +547 -21
  18. package/src/init.js +1 -1
  19. package/src/local-register.js +98 -0
  20. package/src/module-impact.js +146 -0
  21. package/src/progress/change-registry.js +43 -4
  22. package/src/progress/stage-machine.js +10 -5
  23. package/src/progress.js +18 -3
  24. package/src/quicklog.js +106 -1
  25. package/src/run/command.js +151 -38
  26. package/src/run/complete-handlers.js +62 -4
  27. package/src/run/complete.js +10 -8
  28. package/src/run/concurrent-detect.js +3 -1
  29. package/src/run/gates.js +86 -4
  30. package/src/run/multi-repo-context.js +3 -1
  31. package/src/run/next.js +128 -0
  32. package/src/run/prompt.js +28 -6
  33. package/src/run/scan-profile.js +35 -23
  34. package/src/run/shared.js +28 -7
  35. package/src/scan-postcheck.js +142 -2
  36. package/src/stage-contract-spec.js +1 -1
  37. package/src/stage-review.js +55 -7
  38. package/src/stages/archive.js +1 -1
  39. package/src/stages/brainstorm.js +26 -12
  40. package/src/stages/doctor.js +2 -4
  41. package/src/stages/execute.js +110 -26
  42. package/src/stages/knowledge.js +21 -5
  43. package/src/stages/plan-postcheck.js +24 -10
  44. package/src/stages/plan.js +14 -9
  45. package/src/stages/quick.js +14 -6
  46. package/src/stages/scan.js +1 -0
  47. package/src/stages/verify.js +7 -49
  48. package/src/sync.js +19 -7
  49. package/src/task-review.js +256 -10
  50. package/src/verify-probes.js +264 -0
  51. package/src/workspace.js +125 -0
  52. package/src/worktree-apply.js +15 -1
  53. package/src/worktree.js +58 -8
  54. package/templates/claude-instruction.md +1 -1
  55. package/templates/prompts/taskcard-rules.md +1 -1
  56. package/templates/prompts/verify-probes.md +13 -47
@@ -9,6 +9,8 @@ description: 用于归档已验证完成的变更。适合用户说"归档、arc
9
9
  - verify 已通过,把变更包归档沉淀
10
10
  - 5 步:任务完成度检查 → 模块影响分析 → 同步模块文档 → 确认归档 → 更新路线图
11
11
 
12
+ > 🔧 模块影响分析步:先跑 `sillyspec module-impact --change <变更名>` 生成 module-impact.md 骨架——文件×模块归属按 `_module-map.yaml` paths 前缀匹配预填 + 未匹配文件清单,你只需逐行填「影响类型」与 review 标记(以 git diff 为准,真实 > 声明);已存在不覆盖。
13
+
12
14
  ## 多变更说明
13
15
 
14
16
  项目有多个活跃变更(`.sillyspec/changes/` 下有多个目录)时,所有 `sillyspec run` 命令需加 `--change <变更名>` 指定操作目标;只有一个变更时可省略(CLI 自动检测)。
@@ -100,7 +100,7 @@ brainstorm 在 `tier=independent` 规模下除 design.md 等四件套外,还
100
100
  }
101
101
  ```
102
102
 
103
- - `docHash` = `sha256(主审查文档内容)`(hex)—— brainstorm 主文档是 `design.md`(即 `reviewedFiles[0]`)。CLI 重算 sha256 比对,不符判伪造(fail-closed)。改 design.md 后须重算 docHash。`tier=self`(≤3 文件)降级为当前 agent 自审,不强制独立子代理。
103
+ - `docHash` = `sha256(主审查文档内容)`(hex)—— brainstorm 主文档是 `design.md`(即 `reviewedFiles[0]`)。CLI 重算 sha256 比对,不符判伪造(fail-closed)。docHash 可先占位(如 "TODO");改 design.md 后勿手算 sha256——跑 `sillyspec register-stage-review --change <变更名> --stage brainstorm --refresh-hash` 一键重算代填(verdict 保留;design 改版多阶段联动加 --all)。`tier=self`(≤3 文件)降级为当前 agent 自审,不强制独立子代理。
104
104
  - 运行时 CLI 会把精确 schema 表 + 完整 JSON 示例 + docHash 算法注入到该步 prompt,以你实际收到的注入版契约为权威逐字模板。
105
105
 
106
106
  ## 铁律
@@ -19,85 +19,43 @@ $ARGUMENTS
19
19
 
20
20
  ## 流程
21
21
 
22
- ### 1. 检查是否有未提交的修改
23
- git diff --quiet 2>/dev/null; UNSTAGED=$?
24
- git diff --cached --quiet 2>/dev/null; STAGED=$?
22
+ ### 1. 收集变更上下文 + 建议 message(一条命令)
25
23
 
26
- 两者都为 0(无修改)→ 提示"没有需要提交的修改",结束。
24
+ ```bash
25
+ sillyspec commit
26
+ ```
27
27
 
28
- ### 2. 暂存所有修改
29
- git add -A
28
+ CLI 已接管全部手工收集(上次 commit 时间过滤 QUICKLOG 条目、活跃变更已勾 task、按路径模式归类阶段产出、diff stat),并按下方 type 表生成建议 message + 可照抄的 `git add -A && git commit -m ...` 命令。**它只建议不提交。**
30
29
 
31
- ### 3. 收集变更语义信息
32
- # 上次 commit 时间
33
- LAST_COMMIT_TIME=$(git log -1 --format=%ci 2>/dev/null)
30
+ - 命令输出「没有需要提交的修改」→ 结束。
31
+ - 无语义来源匹配(输出会明说)→ 展示 diff stat,让用户自己写。
34
32
 
35
- # 修改的文件列表
36
- git diff --cached --stat
33
+ ### 2. 展示确认
37
34
 
38
- 从以下来源收集语义信息(筛选时间戳 > LAST_COMMIT_TIME 的条目):
35
+ 把 `sillyspec commit` 的建议 message + 文件统计原样展示给用户(AskUserQuestion):
39
36
 
40
- 来源 A — QUICKLOG(quick 产生的修改):
41
- USER=$(git config user.name 2>/dev/null || echo "default")
42
- cat .sillyspec/quicklog/QUICKLOG-${USER}.md 2>/dev/null
43
- # 同时扫描活跃变更目录下的归属 quicklog
44
- for dir in .sillyspec/changes/*/quicklog/*.md; do [ -f "$dir" ] && cat "$dir"; done 2>/dev/null
45
- # 注:QUICKLOG 现由 CLI 接管写入(src/quicklog.js),描述取 quick 启动的任务参数、
46
- # 结果取 step3 --output 一句话,粒度可能较粗;条目仍含 标题|时间|描述 + 状态 + 结果 结构,
47
- # 可直接读取。描述过泛时结合 diff stat 与 tasks.md 补充语义。
37
+ 选择:确认提交 / 编辑 message / 取消
38
+
39
+ 用户选择编辑 → 让用户输入新 message → 再次确认。
48
40
 
49
- 来源 B — tasks.md(execute 产生的修改):
50
- LATEST=$(ls -d .sillyspec/changes/*/ 2>/dev/null | grep -v archive | tail -1)
51
- cat "$LATEST/tasks.md" 2>/dev/null
52
- 筛选时间戳 > LAST_COMMIT_TIME 的已勾选 task。
41
+ ### 3. 执行提交
53
42
 
54
- 来源 C — 阶段产出(scan/brainstorm/plan/archive 等):
55
- 检查 .sillyspec/ 下新增或修改的文件,根据路径识别来源阶段:
56
- - .sillyspec/docs/<project>/scan/*.md → scan 产出
57
- - changes/<name>/design.md → brainstorm 产出
58
- - changes/<name>/proposal.md → brainstorm 产出
59
- - changes/<name>/tasks.md → plan 产出
60
- - changes/<name>/plan.md → plan 产出
61
- - changes/archive/ → archive 产出
62
- - knowledge/*.md → 知识库更新
43
+ 用户确认后执行 `git add -A` + 输出里给出的 `git commit` 命令(或用户编辑后的 message)。
63
44
 
64
- ### 4. 自动生成 commit message
45
+ 提交成功后展示 commit hash + 文件数 + 行数统计。
65
46
 
66
- 按 conventional commits 格式,根据来源生成 message:
47
+ ### 参考:message 生成口径(CLI 同源实现,src/commit-suggest.js)
67
48
 
68
49
  | 来源 | type | 示例 |
69
50
  |---|---|---|
70
- | 只有 quick 条目 | fix/refactor | fix: 手机号校验修复(含正则修正) |
71
- | 只有 execute 条目 | feat | feat(user): 用户模块 task 1~3 |
51
+ | 只有 quick 条目 | fix | fix: 手机号校验修复(含正则修正) |
52
+ | 只有 execute 已勾 task | feat | feat: <变更名> 完成 task ×N |
72
53
  | 只有阶段产出 | docs | docs: sillyspec scan 完成 |
73
54
  | 混合来源 | 取最主要的 type | body 里列出所有条目 |
74
55
  | 无匹配来源 | — | 展示 diff stat,让用户自己写 |
75
56
 
76
57
  **如果 $ARGUMENTS 非空**,直接使用用户指定的 message,跳过自动生成,但仍展示 diff stat。
77
58
 
78
- 多条 quick 合并规则:
79
- - 2 条以内 → 逐条列出
80
- - 3 条以上 → 摘要 + body 详情
81
-
82
- ### 5. 展示确认
83
-
84
- 用 AskUserQuestion 展示:
85
- 📝 建议的 commit message:
86
- fix: 手机号校验修复(含正则修正)
87
-
88
- 📁 修改文件(N 个):
89
- src/UserService.java (+15 -3)
90
- src/PhoneValidator.java (+28 -0)
91
-
92
- 选择:确认提交 / 编辑 message / 取消
93
-
94
- 用户选择编辑 → 让用户输入新 message → 再次确认。
95
-
96
- ### 6. 执行提交
97
- git commit -m "{确认后的 message}"
98
-
99
- 提交成功后展示 commit hash + 文件数 + 行数统计。
100
-
101
59
  ## 多项目
102
60
  检查修改的文件分别属于哪个子项目:
103
61
  - 修改集中在一个子项目 → 在该子项目目录中 commit
@@ -9,6 +9,8 @@ description: 自动判断并执行下一步
9
9
 
10
10
  ## 判断逻辑
11
11
 
12
+ > 🔧 **先跑 `sillyspec next`**——CLI 已内置下方探测表(含活跃变更逐个推断 + task 勾选进度),输出「状态 + 下一步命令 + 依据」。直接按输出的建议命令执行;CLI 异常/输出与实际不符时才按下方手工表兜底。
13
+
12
14
  按顺序检查,第一个未完成的就执行:
13
15
 
14
16
  ```
@@ -61,6 +61,7 @@ execute 前缀步「加载上下文」的 `--done` 会硬校验符号影响面
61
61
 
62
62
  - 路径:`{SPEC_ROOT}/changes/<变更名>/symbol-impact.md`(步骤 prompt 中会输出具体路径)。
63
63
  - 要求:plan.md 中**每个 task-XX 都要有一行结论**——涉及签名级变更(构造函数参数/接口/DTO/方法签名增删改)就列变更类型 + 受影响调用点 + 是否在任务范围内;无签名级变更也要显式写「无签名级变更」。
64
+ - 🔧 **骨架生成**:`sillyspec symbol-impact --change <变更名>` 从 tasks.md 生成逐 task `<!--TODO-->` 骨架(撞 gate 时 CLI 也会自动落一份),逐行替换为结论即可;**未替换的 TODO 占位会被 gate 拒绝**(骨架不能直接过门)。
64
65
  - 文件缺失或未覆盖全部 task → `--done` 被阻断(进度不推进),补全后重跑即可。
65
66
 
66
67
  ### Task Review Gate
@@ -88,7 +89,10 @@ execute 完成时,每个 task 必须有 `review.json` 且 verdict 通过,否
88
89
  ```
89
90
 
90
91
  - `base` / `head` 必须是**真实 git commit**(`git rev-parse --verify`),否则判伪造并阻断。`changedFiles` 与实际 `git diff base..head` 完全不相交也判伪造。base..head 空 commit diff 但 working-tree 有未提交改动 → 视为有效改动(warning 不阻断)。
91
- - 后端 router task 另需在 `.sillyspec/.runtime/contract-artifacts/<task-name>/endpoints.json` 写 API 端点清单(扫 `@router.get/post/...`)。
92
+
93
+ > 🔧 **mechanics 字段不要手算**:`schemaVersion`/`task`/`base`/`head`/`changedFiles`(/跨仓 `repo`) 全部可由 CLI 从 git + task 卡推导。子代理/你只需写**语义字段**(`specVerdict`/`qualityVerdict`/`reviewerNotes`/`requiredEvidence`),mechanics 字段可以瞎填占位(如 `"base": "TODO"`),写完跑 `sillyspec backfill-reviews --change <变更名> --adopt` 一键重算代填(verdict 原样保留)。gate 拦下 mechanics 错误时也跑同一条命令修复。
94
+
95
+ - 后端 router task 另需产出 API 端点清单:`sillyspec endpoints extract --change <变更名> --task <task-NN>`(CLI 静态扫描 FastAPI/Express/Spring 路由装饰器生成 endpoints.json,verify 探针 5 消费;扫描面默认取 task 卡 allowed_paths ∪ design 清单,也可 `--dir`/`--files` 显式指定)。勿手扫装饰器手写——易漏 endpoint。
92
96
 
93
97
  ### Stage Review Gate(execute 末尾的 acceptance review.json)
94
98
 
@@ -114,7 +118,7 @@ execute 还有**第二道**独立的 stage 级审查:除逐 task review.json
114
118
  }
115
119
  ```
116
120
 
117
- - `docHash` = `sha256(主审查文档内容)`(hex)—— execute 主审查文档是 `design.md`,即 `reviewedFiles[0]`。CLI 会重算 sha256 比对,不符判伪造(fail-closed);找不到主文档也 fail。**改 design.md 后须重算 docHash 再写**,可用 `sillyspec run execute --done` 触发的 prompt 注入版契约(运行时注入的 schema 表 + JSON 示例 + docHash 算法)逐字改值。
121
+ - `docHash` = `sha256(主审查文档内容)`(hex)—— execute 主审查文档是 `design.md`,即 `reviewedFiles[0]`。CLI 会重算 sha256 比对,不符判伪造(fail-closed);找不到主文档也 fail。**docHash 可先占位(如 "TODO")勿手算**——改 design.md 后跑 `sillyspec register-stage-review --change <变更名> --stage execute --refresh-hash` 一键重算代填(verdict 保留;design 改版多阶段联动加 --all);gate 拦 docHash 失配时也用同一条命令修复。
118
122
  - `tier=independent` 时必须由独立 QA 子代理产出该 review.json(独立上下文,不共享实现者分析);`tier=self`(变更 ≤3 文件)降级为当前 agent 自审。
119
123
  - **审查范围分级**(tier=independent 时省重复消耗):Task Review Gate 已产出 review.json 且 specVerdict/qualityVerdict 双 pass 的 task,QA 子代理只抽查(读 1-2 个核心 diff 文件抽验 reviewerNotes 与实际改动相符);fail/cannot_verify/缺失的 task 全量重审。三项始终必查:跨 task 交界、design.md 整体对照、组装行为(全量测试/构建/启动)——task review 只看单 task diff,覆盖不到这三项。
120
124
  - 该 acceptance review 同时覆盖"代码审查"视角,后续代码审查步骤仅需轻量复审。
@@ -127,6 +131,8 @@ execute 还有**第二道**独立的 stage 级审查:除逐 task review.json
127
131
 
128
132
  可选:`sillyspec dispatch probe` 查看 SillyHub 是否可用。
129
133
 
134
+ > 📍 `local.yaml` 恒在 `.sillyspec/local.yaml`——项目根目录**没有**这个文件,别去那里找或新建(hook 会拦)。worktree 内该文件不随 checkout 出现,读配置直接跑 `sillyspec config cat`(自动定位到主仓真实配置);可用键清单见 `sillyspec config schema`。
135
+
130
136
  ## 跨仓 task(一个 change 改多个仓库)
131
137
 
132
138
  单个 change 的 task 可以分散到主仓 + 多个跨仓仓实现(典型场景:dogfood 自指、monorepo 多包仓、共享库 + 调用方联合改造)。**单仓 change 不需要任何跨仓配置**(所有 task 不写 `repo:` 即走原流程,零回归)。
@@ -134,7 +140,7 @@ execute 还有**第二道**独立的 stage 级审查:除逐 task review.json
134
140
  ### 跨仓 task 配置(plan 阶段产出,execute 阶段消费)
135
141
 
136
142
  - **task 卡片 `repo:` 字段**:跨仓 task 在 `tasks/task-NN.md` frontmatter 写 `repo: <key>`(缺省='main'=主仓 task,不写即主仓)。
137
- - **local.yaml `repos:` 段**:在 `.sillyspec/local.yaml` 注册跨仓仓路径(`main` 不用注册,隐式=当前项目):
143
+ - **local.yaml `repos:` 段**:跨仓仓路径注册(`main` 不用注册,隐式=当前项目)。**用命令注册,勿手编 YAML**:`sillyspec local register-repo <key> <跨仓仓根路径>`(execute 启动报缺注册时报错文案里就带这条命令)。手写格式参考:
138
144
  ```yaml
139
145
  repos:
140
146
  shared-lib: ../shared-lib
@@ -152,7 +158,7 @@ execute 还有**第二道**独立的 stage 级审查:除逐 task review.json
152
158
  ### 跨仓 task 的双锡点(CLI 写入,子代理不改)
153
159
 
154
160
  - `base_commit`:CLI 派发跨仓 task 前实时 `git -C <跨仓仓根> rev-parse HEAD` 落盘到 task 卡 frontmatter(锁 base,防同 Wave 多 task 改同跨仓仓时 HEAD 推进致 diff 漂移)。
155
- - `head_commit`:跨仓 task 子代理 commit 完成后、写 review.json / 勾选 checkbox **之前**,由你(主 agent)运行 `git -C <跨仓仓根> rev-parse HEAD` 把结果写入该 task 卡 frontmatter `head_commit:` 字段。
161
+ - `head_commit`:**同样由 CLI 自动落盘**——execute `--done` 时 CLI 实时 `git -C <跨仓仓根> rev-parse HEAD` 幂等写入 task 卡(已存在不覆盖,你手写的精确锚点优先)。你无需手跑 rev-parse。review.json 的 base/head 无需手算,跑 `sillyspec backfill-reviews --change <变更名> --adopt` 从 task 卡锡点一键代填。
156
162
  - review.json 的 `base`/`head` 取这两个锡点(非瞬时 HEAD)。
157
163
 
158
164
  ### 跨仓 task 的 review.json
@@ -69,7 +69,7 @@ plan 的步骤是动态的:`generate_plan`(生成分级计划)→ `review_
69
69
  }
70
70
  ```
71
71
 
72
- - `docHash` = `sha256(主审查文档内容)`(hex)—— plan 主文档是 `plan.md`(`reviewedFiles[0]`)。CLI 重算 sha256 比对,不符判伪造(fail-closed)。改 plan.md 后须重算 docHash。`tier=self`(≤3 文件)降级为当前 agent 自审。
72
+ - `docHash` = `sha256(主审查文档内容)`(hex)—— plan 主文档是 `plan.md`(`reviewedFiles[0]`)。CLI 重算 sha256 比对,不符判伪造(fail-closed)。docHash 可先占位(如 "TODO");改 plan.md 后勿手算 sha256——跑 `sillyspec register-stage-review --change <变更名> --stage plan --refresh-hash` 一键重算代填(verdict 保留;多阶段联动加 --all)。`tier=self`(≤3 文件)降级为当前 agent 自审。
73
73
  - 运行时 CLI 会把精确 schema 表 + 完整 JSON 示例 + docHash 算法注入到该步 prompt,以你实际收到的注入版契约为权威逐字模板。
74
74
 
75
75
  ### 契约门控(阻断完成)
@@ -95,7 +95,15 @@ plan 完成校验会检查:design 提到入口文件(cli.ts/main.ts/server.t
95
95
 
96
96
  ### local.yaml `repos:` 段(跨仓注册表)
97
97
 
98
- 跨仓 change 必须在 `.sillyspec/local.yaml` 注册所有跨仓仓路径:
98
+ > 📍 `local.yaml` 恒在 `.sillyspec/local.yaml`——项目根目录**没有**这个文件,别去那里找或新建(hook 会拦)。worktree 内该文件不随 checkout 出现,读配置直接跑 `sillyspec config cat`(自动定位到主仓真实配置);可用键清单见 `sillyspec config schema`。
99
+
100
+ 跨仓 change 的每个 repo key 都要注册(execute 启动 fail-closed 校验)。**用命令注册,勿手编 YAML**:
101
+
102
+ ```
103
+ sillyspec local register-repo <key> <跨仓仓根路径>
104
+ ```
105
+
106
+ (外科手术式写入,只动 `repos:` 段,其余内容保留;execute 启动报缺注册时,报错文案里就带这条命令。)手写格式参考:
99
107
 
100
108
  ```yaml
101
109
  repos:
@@ -43,7 +43,7 @@ sillyspec run quick --done --change quick-<hash> --output "…" # 完成该会
43
43
  ```
44
44
 
45
45
  - 这里的 `--change quick-<hash>` 是「恢复到该 CLI 生成的 session」,与下方 `--linked-changes` 的关联业务变更语义不同:想关联真实业务变更才用 `--linked-changes`。
46
- - 误启动的空壳会话:`sillyspec run quick --reset --change quick-<hash>` 重置其进度;QUICKLOG 里残留的「(quick 任务)」骨架条目需手动删除,不留占位条目。
46
+ - 误启动的空壳会话:`sillyspec run quick --cancel --change quick-<hash>` 一键取消(QUICKLOG 条目翻「已取消」+ tasks.md 挂载行移除 + 会话目录清理 + db 行注销;已完成/已勾选的拒绝取消)。
47
47
 
48
48
  ## 通用参数(所有阶段适用)
49
49
 
@@ -108,7 +108,7 @@ sillyspec run quick --files src/phone.ts,src/phone.test.ts
108
108
  - QUICKLOG 记录的**骨架由 CLI 接管**:启动时 CLI 自动分配 ql-ID 并在 `.sillyspec/quicklog/QUICKLOG-<user>.md` 写「进行中」条目(含关联变更 tasks.md),`--done` 时 CLI 自动翻「已完成」+ 勾选 task + 回填文件路径。ql-ID 分配/状态/task 你无需手写,只需用注入的 `<quicklog-id>` 在模块文档变更索引引用
109
109
  - **QUICKLOG 落盘已结构化(`--done` 后按需核对,多数无需手改)**:CLI 已落盘结构化条目——标题从 `--output` 的「需求:」自动提取、正文四字段自动分行、文件行用 `--file-notes` 时为多行带括注。`--done` 后只需核对:标题弱才改(禁留 `(quick 任务)` 占位);没用 `--file-notes` 时文件行是单行、可事后补括注(参照同文件早期丰富条目);正文 `需求:`/`根因:`/`方案:`/`结果:` 四段按需充实(禁只留一段「结果:」)。一条 quick = 一条独立 ql,不追加到旧条目
110
110
  - **收尾顺序(模块文档在 `--done` 前,QUICKLOG 在 `--done` 后,别记混)**:① 命中模块→改模块文档→`git add`;② `sillyspec run quick --done --change <id> --output "四字段" [--file-notes "..."]`(CLI 自动翻完成 + 勾 task + 落盘 QUICKLOG 标题/文件/正文;若 `--linked-changes` 关联的真实变更其 tasks.md 已全部勾选**且该变更未进入完整流程**(进度库阶段停在 brainstorm 及之前),CLI 会自动将其归档到 `changes/archive/`;已走到 plan/execute/verify/archive 的变更不自动归档(tasks.md 全勾不等于流程收尾),须走原流程收尾);③ 核对 QUICKLOG 标题(CLI 已从「需求:」提取并截断,写成短标题则无需改)→若改了再 `git add`
111
- - **最后一步 `--done --output` 必须按结构化结果模板给全四字段**(逐项一句话):`需求:… 根因:… 方案:… 结果:…`。这是 QUICKLOG「结果:」归档的唯一来源;CLI 校验缺字段会拒绝 `--done`(exit 1),补全后重跑即可。**「需求:」写一句语义化短标题**(写「改了什么」,如「登录限流修复——INCR 计数误清」)——CLI 把它提取为 QUICKLOG 条目标题,截到首个标点、超 80 字截断;写完整需求长句会被截成语义不完整的状语前半段,需求背景放「根因:/方案:」。正文内避免嵌套全角冒号(如「方案:(说明 xxx):」这样标签后紧接嵌套冒号会被拆分判定缺字段),直接让标签接正文。前两个 step 的 `--output` 是中间摘要,不用此模板
111
+ - **最后一步 `--done` 必须给全四字段**(逐项一句话):🔧 **推荐四参数形式**(CLI 自动合成结构化模板,无格式事故面):`--done --change <id> --req "一句语义化短标题" --cause "为什么改" --solution "怎么改的" --result "验证结果"`。兼容旧形式 `--output "需求:… 根因:… 方案:… 结果:…"`(手拼须懂截断规则、禁嵌套全角冒号,缺字段被拒 exit 1)。这是 QUICKLOG「结果:」归档的唯一来源。**`--req` 写一句语义化短标题**(写「改了什么」,如「登录限流修复——INCR 计数误清」)——CLI 把它提取为 QUICKLOG 条目标题,截到首个标点、超 80 字截断;写完整需求长句会被截成语义不完整的状语前半段,需求背景放 `--cause`/`--solution`。前两个 step 的 `--output` 是中间摘要,不用此模板
112
112
  - **禁止**在没有运行 CLI 的情况下自行决定流程
113
113
 
114
114
  ## 用户指令
@@ -32,6 +32,8 @@ sillyspec progress show
32
32
 
33
33
  ### 3. 如果没有活跃变更
34
34
 
35
+ > 🔧 优先跑 `sillyspec next`——CLI 已内置下方探测表,直接输出「状态 + 下一步命令 + 依据」,按输出执行;异常时才按下方手工探测兜底。
36
+
35
37
  自动探测项目状态:
36
38
 
37
39
  ```bash
@@ -51,6 +51,8 @@ verify 是只读阶段(**禁止改代码/改 git 状态**,只检查 + 写报
51
51
 
52
52
  ## verify-result.md 格式
53
53
 
54
+ > 🔧 **机械探针 + 报告骨架一条命令**:`sillyspec verify-probes --change <变更名> --init`——探针 1(TODO 标记)/3(测试覆盖)/5(API 契约对账)/6(删除对账)CLI 跑完并预填进 verify-result.md 骨架(七章节、已存在不覆盖);探针 2/4(关键词/决策追踪)与断言抽查、集成盲区标注是语义判断,替换骨架里的 `<!--TODO-->` 完成。结论必须写明 PASS/FAIL——留「待填」会被 gate 判不过。
55
+
54
56
  ```markdown
55
57
  # 验证报告
56
58
  ## 结论
@@ -14,7 +14,13 @@ description: 工作区管理 — 初始化、管理多项目工作区,查看
14
14
 
15
15
  ## CLI 边界(重要)
16
16
 
17
- sillyspec CLI **没有 `workspace` 顶层命令**。本 skill 通过直接读写 `.sillyspec/projects/*.yaml` 管理工作区,配套用 `sillyspec scan` 扫描子项目、`sillyspec init` 安装命令模板。**不要编造 `sillyspec workspace ...` 子命令。**
17
+ `sillyspec workspace` 命令已存在(2026-08-21 落地):
18
+ ```bash
19
+ sillyspec workspace add <名> <相对路径> [--role <角色>] [--repo <仓库地址>] # 登记子项目(外科写入 yaml,已有字段保留)
20
+ sillyspec workspace remove <名> # 删除登记(项目本体不动)
21
+ sillyspec workspace status # 三态探测表(已扫描/已初始化/未初始化)
22
+ ```
23
+ 优先用命令;命令异常时才按下文手工兜底(yaml 字段口径:init 写 name/path/status 三字段,role/repo 由 add 追加)。
18
24
 
19
25
  ## 相关 CLI 命令
20
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.26.13",
3
+ "version": "3.26.15",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
@@ -0,0 +1,202 @@
1
+ /**
2
+ * commit-suggest.js — `sillyspec commit` 的语义收集 + commit message 建议生成
3
+ * (2026-08-21 agent-手工产出审计第二批 G1-G3)。
4
+ *
5
+ * commit skill 此前让 agent 手工收集语义:取 LAST_COMMIT_TIME、cat 多处 QUICKLOG、
6
+ * 扫变更目录、按时间戳过滤已勾 task、按路径模式识别来源阶段——进度库/quicklog/
7
+ * git 全在 CLI 手里,这是「CLI 有数据却让 agent 重新收集」的最典型案例。
8
+ *
9
+ * 本模块一条命令产出全部上下文 + conventional commits 建议 message + 可照抄的
10
+ * git commit 命令。**只建议不执行**(对齐 skill 绝对规则「不要自动提交」——确认权在人)。
11
+ *
12
+ * 语义来源(与 skill 手工流程同源):
13
+ * A. QUICKLOG:specBase/quicklog/QUICKLOG-<user>.md + changes/<change>/quicklog/ 下的归属条目,
14
+ * 条目时间 > 上次 commit 时间者(首次提交全收)
15
+ * B. execute 已勾 task:活跃变更 tasks.md 的 [x] 行(tasks.md mtime > 上次 commit 才计入,
16
+ * 防旧变更全勾 task 永久污染建议)
17
+ * C. 阶段产出:changed paths 按路径模式归 scan/brainstorm/plan/execute/archive/knowledge
18
+ */
19
+ import { existsSync, readdirSync, readFileSync, statSync } from 'fs'
20
+ import { join } from 'path'
21
+ import { gitQuiet } from './git-helper.js'
22
+ import { resolveSpecDir } from './run/shared.js'
23
+
24
+ function listActiveChanges(specBase) {
25
+ try {
26
+ return readdirSync(join(specBase, 'changes'), { withFileTypes: true })
27
+ .filter(e => e.isDirectory() && e.name !== 'archive')
28
+ .map(e => e.name)
29
+ .sort()
30
+ } catch {
31
+ return []
32
+ }
33
+ }
34
+
35
+ /**
36
+ * 解析 QUICKLOG 文件里的条目(## ql-xxx | time | title + 状态:行)
37
+ */
38
+ function parseQuicklogEntries(file) {
39
+ let content
40
+ try {
41
+ content = readFileSync(file, 'utf8')
42
+ } catch {
43
+ return []
44
+ }
45
+ const entries = []
46
+ const lines = content.split('\n')
47
+ let cur = null
48
+ for (const line of lines) {
49
+ const h = line.match(/^## (ql-\S+) \| ([0-9: -]+?) \| (.*)$/)
50
+ if (h) {
51
+ if (cur) entries.push(cur)
52
+ cur = { qlId: h[1], time: h[2].trim(), title: h[3].trim(), status: '未知' }
53
+ continue
54
+ }
55
+ if (cur) {
56
+ const s = line.match(/^状态[::]\s*(\S+)/)
57
+ if (s) cur.status = s[1]
58
+ }
59
+ }
60
+ if (cur) entries.push(cur)
61
+ return entries
62
+ }
63
+
64
+ function classifyStagePath(p) {
65
+ const posix = p.replace(/\\/g, '/')
66
+ if (/^\.sillyspec\/docs\/[^/]+\/scan\//.test(posix)) return 'scan'
67
+ if (/^\.sillyspec\/changes\/archive\//.test(posix)) return 'archive'
68
+ if (/^\.sillyspec\/changes\/[^/]+\/(proposal|design)\.md/.test(posix)) return 'brainstorm'
69
+ if (/^\.sillyspec\/changes\/[^/]+\/(tasks|plan)\.md/.test(posix)) return 'plan'
70
+ if (/^\.sillyspec\/knowledge\//.test(posix)) return 'knowledge'
71
+ return null
72
+ }
73
+
74
+ /**
75
+ * 收集提交上下文并生成建议 message。
76
+ * @param {{ cwd: string, specDir?: string|null }} opts
77
+ * @returns {{
78
+ * lastCommitTime: string|null, hasChanges: boolean, changedCount: number, stat: string,
79
+ * quickEntries: Array<{qlId,time,title,status,file}>, checkedTasks: Array<{change,item}>,
80
+ * stageArtifacts: string[], suggestion: {type, subject, body}|null
81
+ * }}
82
+ */
83
+ export function collectCommitContext({ cwd, specDir = null }) {
84
+ const specBase = resolveSpecDir(cwd, { specDir })
85
+ const lastCommitTime = gitQuiet(cwd, ['log', '-1', '--format=%ci'])
86
+ const lastCommitKey = lastCommitTime ? lastCommitTime.slice(0, 19) : null
87
+
88
+ // -uall:未跟踪目录不折叠(.sillyspec/ 整体未跟踪时默认 porcelain 只给一行 `?? .sillyspec/`,
89
+ // 路径模式分类会瞎;-uall 逐文件列出,scan/brainstorm/plan 分类才有输入)
90
+ const porcelain = gitQuiet(cwd, ['status', '--porcelain', '-uall']) || ''
91
+ const changedLines = porcelain.split('\n').filter(Boolean)
92
+ const changedPaths = changedLines.map(l => l.slice(3).trim()).filter(Boolean)
93
+ const untrackedCount = changedLines.filter(l => l.startsWith('??')).length
94
+ const stat = gitQuiet(cwd, ['diff', 'HEAD', '--stat']) || (changedPaths.length > 0 ? `(${changedPaths.length} 个文件待首提交)` : '')
95
+
96
+ // ── 来源 A:QUICKLOG(主 quicklog + 各活跃变更归属 quicklog)──
97
+ const quickFiles = []
98
+ try {
99
+ for (const f of readdirSync(join(specBase, 'quicklog'))) {
100
+ if (/^QUICKLOG.*\.md$/.test(f)) quickFiles.push(join(specBase, 'quicklog', f))
101
+ }
102
+ } catch {}
103
+ for (const c of listActiveChanges(specBase)) {
104
+ try {
105
+ for (const f of readdirSync(join(specBase, 'changes', c, 'quicklog'))) {
106
+ if (f.endsWith('.md')) quickFiles.push(join(specBase, 'changes', c, 'quicklog', f))
107
+ }
108
+ } catch {}
109
+ }
110
+ const quickEntries = quickFiles
111
+ .flatMap(file => parseQuicklogEntries(file).map(e => ({ ...e, file })))
112
+ .filter(e => !lastCommitKey || e.time > lastCommitKey)
113
+ .sort((a, b) => (a.time < b.time ? -1 : 1))
114
+ const doneQuick = quickEntries.filter(e => e.status === '已完成')
115
+
116
+ // ── 来源 B:活跃变更已勾 task(tasks.md mtime 晚于上次 commit 才计入)──
117
+ const lastCommitMs = lastCommitTime ? Date.parse(lastCommitTime) : null
118
+ const checkedTasks = []
119
+ for (const c of listActiveChanges(specBase)) {
120
+ const tasksPath = join(specBase, 'changes', c, 'tasks.md')
121
+ if (!existsSync(tasksPath)) continue
122
+ if (lastCommitMs !== null && !Number.isNaN(lastCommitMs)) {
123
+ try {
124
+ // epoch 对比(非字符串):git %ci 带本地时区偏移,Date.parse 归一;字符串对比会因
125
+ // UTC/本地时差把刚改过的 tasks.md 误判为早于上次 commit 而漏收
126
+ if (statSync(tasksPath).mtime.getTime() <= lastCommitMs) continue
127
+ } catch { continue }
128
+ }
129
+ for (const line of readFileSync(tasksPath, 'utf8').split('\n')) {
130
+ const m = line.match(/^[-*]\s*\[[xX]\]\s*(task-\d+)\s*[::]?\s*(.*)/)
131
+ if (m) checkedTasks.push({ change: c, item: `${m[1]}: ${m[2].trim()}` })
132
+ }
133
+ }
134
+
135
+ // ── 来源 C:阶段产出(changed paths 按模式归类去重)──
136
+ const stageSet = new Set()
137
+ for (const p of changedPaths) {
138
+ const stage = classifyStagePath(p)
139
+ if (stage) stageSet.add(stage)
140
+ }
141
+ const stageArtifacts = [...stageSet]
142
+
143
+ // ── 建议 message(对齐 commit skill 的 type 表)──
144
+ let suggestion = null
145
+ const bodyLines = []
146
+ const hasQuick = doneQuick.length > 0
147
+ const hasTasks = checkedTasks.length > 0
148
+ const hasStages = stageArtifacts.length > 0
149
+
150
+ if (hasQuick || hasTasks || hasStages) {
151
+ let type
152
+ let subject
153
+ if (hasTasks && !hasQuick) {
154
+ type = 'feat'
155
+ const byChange = new Map()
156
+ for (const t of checkedTasks) {
157
+ if (!byChange.has(t.change)) byChange.set(t.change, [])
158
+ byChange.get(t.change).push(t.item)
159
+ }
160
+ const [firstChange, items] = [...byChange][0]
161
+ subject = `feat: ${firstChange} 完成 task ×${items.length}`
162
+ } else if (hasQuick && !hasTasks) {
163
+ type = 'fix'
164
+ if (doneQuick.length === 1) {
165
+ subject = `fix: ${doneQuick[0].title}`
166
+ } else if (doneQuick.length === 2) {
167
+ subject = `fix: ${doneQuick[0].title};${doneQuick[1].title}`
168
+ } else {
169
+ subject = `fix: quick 修复 ${doneQuick.length} 项`
170
+ }
171
+ } else if (hasTasks) {
172
+ type = 'feat'
173
+ subject = `feat: 变更交付 + quick 修复 ${doneQuick.length} 项`
174
+ } else {
175
+ type = 'fix'
176
+ subject = `fix: quick 修复 + 阶段产出`
177
+ }
178
+ if (doneQuick.length >= 2 || hasTasks || hasStages) {
179
+ for (const q of doneQuick) bodyLines.push(`- quick ${q.qlId}: ${q.title}`)
180
+ }
181
+ for (const t of checkedTasks) bodyLines.push(`- ${t.change} ${t.item}`)
182
+ for (const s of stageArtifacts) bodyLines.push(`- 阶段产出: ${s}`)
183
+ if (!hasStages && !hasTasks && doneQuick.length === 1) bodyLines.length = 0
184
+ suggestion = {
185
+ type,
186
+ subject: subject.length > 100 ? subject.slice(0, 100) + '…' : subject,
187
+ body: bodyLines.length > 0 ? bodyLines.join('\n') : '',
188
+ }
189
+ }
190
+
191
+ return {
192
+ lastCommitTime,
193
+ hasChanges: changedPaths.length > 0,
194
+ changedCount: changedPaths.length,
195
+ untrackedCount,
196
+ stat,
197
+ quickEntries,
198
+ checkedTasks,
199
+ stageArtifacts,
200
+ suggestion,
201
+ }
202
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * config-cat.js — local.yaml 实际值读取的权威路径解析器(`sillyspec config cat` 数据源)。
3
+ *
4
+ * 背景(2026-08-21 root-local-yaml 治理):`config schema` 只列键不含值,agent 拿实际配置
5
+ * 只能翻文件——worktree 内 `.sillyspec/` 是 checkout 副本,gitignored 的 local.yaml 不随
6
+ * checkout 出现,agent 找不到就漂去项目根乱找甚至乱建。cat 按「文件存在」解析真实路径,
7
+ * agent 不需要知道文件在哪。
8
+ *
9
+ * 候选链(首个「文件存在」者胜):
10
+ * 1. specBase(--spec-dir / 平台 pointer / resolveSpecDir 祖先链首个 .sillyspec 目录)——
11
+ * resolveSpecDir 按「目录存在」命中,worktree 内会停在副本目录,文件并不在那里;
12
+ * 2. cwd 祖先链逐级 <dir>/.sillyspec/local.yaml——worktree 副本无此文件会继续向上命中主仓
13
+ * (sillyspec worktree 恒挂在 <主仓>/.sillyspec/.runtime/worktrees/ 下,祖先链必达);
14
+ * 3. git common-dir 反推主仓根——用户手动 `git worktree add` 到任意位置时的兜底。
15
+ *
16
+ * 候选链不含项目根 <dir>/local.yaml——那里从来没有这个文件(hook 同步拦写)。
17
+ * 纯只读;平台 pointer 异常由调用方吞掉走祖先链(真实 reader 各自 fail-closed,cat 是
18
+ * 只读便利口,不必加重)。
19
+ */
20
+ import { existsSync } from 'fs'
21
+ import { join, resolve, dirname } from 'path'
22
+ import { gitQuiet } from './git-helper.js'
23
+
24
+ /**
25
+ * 解析真实 local.yaml 路径。
26
+ * @param {string} dir 起点目录(CLI --dir / cwd)
27
+ * @param {{ specBase?: string|null }} [opts] specBase = resolvePlatformSpecDir 预解析结果(可 null)
28
+ * @returns {{ exists: boolean, path: string|null, source: string|null, searched: string[] }}
29
+ * searched 为候选链前几个路径(未命中时供人看「找过哪里」,最多 6 条)
30
+ */
31
+ export function resolveLocalYaml(dir, opts = {}) {
32
+ const start = resolve(dir || process.cwd())
33
+ const seen = new Set()
34
+ const candidates = []
35
+ const push = (p, source) => {
36
+ const key = resolve(p)
37
+ if (seen.has(key)) return
38
+ seen.add(key)
39
+ candidates.push({ path: key, source })
40
+ }
41
+
42
+ if (opts.specBase) push(join(opts.specBase, 'local.yaml'), 'spec 根(--spec-dir/平台 pointer/最近 .sillyspec)')
43
+ for (let d = start; ; d = dirname(d)) {
44
+ push(join(d, '.sillyspec', 'local.yaml'), 'cwd 祖先链(worktree 内向上命中主仓)')
45
+ const parent = dirname(d)
46
+ if (parent === d) break
47
+ }
48
+ // git 兜底:linked worktree 内 --git-common-dir 指向主仓共享 .git,dirname 即主仓根
49
+ const commonDir = gitQuiet(start, ['rev-parse', '--git-common-dir'])
50
+ if (commonDir) {
51
+ const mainRoot = dirname(resolve(start, String(commonDir).trim()))
52
+ push(join(mainRoot, '.sillyspec', 'local.yaml'), 'git common-dir 反推主仓根')
53
+ }
54
+
55
+ const hit = candidates.find(c => existsSync(c.path))
56
+ return {
57
+ exists: !!hit,
58
+ path: hit ? hit.path : null,
59
+ source: hit ? hit.source : null,
60
+ searched: candidates.slice(0, 6).map(c => c.path),
61
+ }
62
+ }
63
+
64
+ /**
65
+ * 写侧目标解析(`sillyspec local register-repo` 用):已存在 local.yaml → 就地更新该文件
66
+ * (含 worktree → 主仓解析);全不存在 → 取 specBase(或 cwd 链)候选,但候选落在 worktree
67
+ * 副本下(<主仓>/.sillyspec/.runtime/worktrees/ 内)时重定向 git common-dir 主仓根——
68
+ * local.yaml 是 gitignored 不随 worktree checkout,写进副本目录是死配置。
69
+ *
70
+ * @param {string} dir 起点目录
71
+ * @param {{ specBase?: string|null }} [opts]
72
+ * @returns {{ path: string, existed: boolean }}
73
+ */
74
+ export function resolveLocalYamlWriteTarget(dir, opts = {}) {
75
+ const r = resolveLocalYaml(dir, opts)
76
+ if (r.exists) return { path: r.path, existed: true }
77
+ const start = resolve(dir || process.cwd())
78
+ const preferred = opts.specBase ? join(opts.specBase, 'local.yaml') : join(start, '.sillyspec', 'local.yaml')
79
+ const posix = resolve(preferred).replace(/\\/g, '/')
80
+ if (posix.includes('/.sillyspec/.runtime/worktrees/')) {
81
+ const commonDir = gitQuiet(start, ['rev-parse', '--git-common-dir'])
82
+ if (commonDir) {
83
+ const mainRoot = dirname(resolve(start, String(commonDir).trim()))
84
+ return { path: join(mainRoot, '.sillyspec', 'local.yaml'), existed: false }
85
+ }
86
+ }
87
+ return { path: resolve(preferred), existed: false }
88
+ }