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.
- package/.claude/skills/sillyspec-archive/SKILL.md +2 -0
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +1 -1
- package/.claude/skills/sillyspec-commit/SKILL.md +18 -60
- package/.claude/skills/sillyspec-continue/SKILL.md +2 -0
- package/.claude/skills/sillyspec-execute/SKILL.md +10 -4
- package/.claude/skills/sillyspec-plan/SKILL.md +10 -2
- package/.claude/skills/sillyspec-quick/SKILL.md +2 -2
- package/.claude/skills/sillyspec-resume/SKILL.md +2 -0
- package/.claude/skills/sillyspec-verify/SKILL.md +2 -0
- package/.claude/skills/sillyspec-workspace/SKILL.md +7 -1
- package/package.json +1 -1
- package/src/commit-suggest.js +202 -0
- package/src/config-cat.js +88 -0
- package/src/docs-check.js +48 -12
- package/src/doctor-diagnostics.js +49 -0
- package/src/hooks/worktree-guard.js +20 -1
- package/src/index.js +547 -21
- package/src/init.js +1 -1
- package/src/local-register.js +98 -0
- package/src/module-impact.js +146 -0
- package/src/progress/change-registry.js +43 -4
- package/src/progress/stage-machine.js +10 -5
- package/src/progress.js +18 -3
- package/src/quicklog.js +106 -1
- package/src/run/command.js +151 -38
- package/src/run/complete-handlers.js +62 -4
- package/src/run/complete.js +10 -8
- package/src/run/concurrent-detect.js +3 -1
- package/src/run/gates.js +86 -4
- package/src/run/multi-repo-context.js +3 -1
- package/src/run/next.js +128 -0
- package/src/run/prompt.js +28 -6
- package/src/run/scan-profile.js +35 -23
- package/src/run/shared.js +28 -7
- package/src/scan-postcheck.js +142 -2
- package/src/stage-contract-spec.js +1 -1
- package/src/stage-review.js +55 -7
- package/src/stages/archive.js +1 -1
- package/src/stages/brainstorm.js +26 -12
- package/src/stages/doctor.js +2 -4
- package/src/stages/execute.js +110 -26
- package/src/stages/knowledge.js +21 -5
- package/src/stages/plan-postcheck.js +24 -10
- package/src/stages/plan.js +14 -9
- package/src/stages/quick.js +14 -6
- package/src/stages/scan.js +1 -0
- package/src/stages/verify.js +7 -49
- package/src/sync.js +19 -7
- package/src/task-review.js +256 -10
- package/src/verify-probes.js +264 -0
- package/src/workspace.js +125 -0
- package/src/worktree-apply.js +15 -1
- package/src/worktree.js +58 -8
- package/templates/claude-instruction.md +1 -1
- package/templates/prompts/taskcard-rules.md +1 -1
- 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
|
|
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
|
-
|
|
24
|
+
```bash
|
|
25
|
+
sillyspec commit
|
|
26
|
+
```
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
git add -A
|
|
28
|
+
CLI 已接管全部手工收集(上次 commit 时间过滤 QUICKLOG 条目、活跃变更已勾 task、按路径模式归类阶段产出、diff stat),并按下方 type 表生成建议 message + 可照抄的 `git add -A && git commit -m ...` 命令。**它只建议不提交。**
|
|
30
29
|
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
35
|
+
把 `sillyspec commit` 的建议 message + 文件统计原样展示给用户(AskUserQuestion):
|
|
39
36
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45
|
+
提交成功后展示 commit hash + 文件数 + 行数统计。
|
|
65
46
|
|
|
66
|
-
|
|
47
|
+
### 参考:message 生成口径(CLI 同源实现,src/commit-suggest.js)
|
|
67
48
|
|
|
68
49
|
| 来源 | type | 示例 |
|
|
69
50
|
|---|---|---|
|
|
70
|
-
| 只有 quick 条目 | fix
|
|
71
|
-
| 只有 execute
|
|
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
|
|
@@ -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
|
-
|
|
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
|
|
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:`
|
|
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
|
|
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
|
|
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
|
-
|
|
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 --
|
|
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
|
|
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
|
## 用户指令
|
|
@@ -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
|
|
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
|
@@ -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
|
+
}
|