sillyspec 3.14.0 → 3.15.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.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  author: qinyi
3
3
  created_at: 2026-05-31 11:00:00
4
- updated_at: 2026-05-31 21:35:00
4
+ updated_at: 2026-06-03 16:00:00
5
5
  ---
6
6
 
7
7
  # SillySpec 文件生命周期描述
@@ -20,9 +20,11 @@ updated_at: 2026-05-31 21:35:00
20
20
  │ ├── history/ ← 已完成阶段的历史快照
21
21
  │ ├── logs/ ← 日志
22
22
  │ ├── templates/ ← 模板
23
+ │ ├── workflow-runs/ ← workflow check 结果归档(JSON)
24
+ │ │ └── <timestamp>-<workflow>-<project>-<status>.json
23
25
  │ └── worktrees/ ← git worktree 隔离环境
24
26
  │ └── <change-name>/
25
- │ └── meta.json ← worktree 元数据
27
+ │ └── meta.json ← worktree 元数据(含 baselineCommit/baselineHash)
26
28
  ├── changes/ ← 变更工作区(git tracked)
27
29
  │ ├── <change-name>/ ← 活跃变更目录
28
30
  │ │ ├── proposal.md ← 动机与变更范围
@@ -59,6 +61,9 @@ updated_at: 2026-05-31 21:35:00
59
61
  ├── knowledge/ ← 跨项目共享知识库
60
62
  │ ├── INDEX.md ← 知识索引(关键词匹配)
61
63
  │ └── uncategorized.md ← 待分类知识
64
+ ├── workflows/ ← workflow 定义(init 自动生成,可自定义)
65
+ │ ├── scan-docs.yaml ← scan 阶段产物检查 workflow
66
+ │ └── archive-impact.yaml ← archive 阶段影响分析 workflow
62
67
  ├── shared/ ← 共享目录
63
68
  ├── workspace/ ← 工作区
64
69
  ├── ROADMAP.md ← 路线图(可选)
@@ -529,6 +534,8 @@ PASS / PASS WITH NOTES / FAIL
529
534
 
530
535
  **断点续扫:** "检查已有扫描文档和子项目列表"步骤检查已有文档,**必须停下来问用户**选择「全部重新扫描」或「只补缺失文档」,不能自行决定跳过。
531
536
 
537
+ **Workflow 检查(post_check):** scan 阶段完成后(verify 步骤),`run.js` 自动加载 `workflows/scan-docs.yaml`,对每个项目的 7 份文档执行结构化产物校验。检查结果为结构化 JSON 对象,包含 `roles[].outputs[].checks[]` 和 `failures[]`。校验规则定义在 `scan-docs.yaml` 的各 role output checks 中(file_exists / min_lines / contains_sections / no_placeholder 等)。检查失败时自动生成 `retry_prompts`,精确到角色级别。
538
+
532
539
  **生命周期:** scan 阶段生成 → 被后续所有阶段(brainstorm/propose/plan/execute/verify)作为上下文读取
533
540
 
534
541
  ---
@@ -755,6 +762,51 @@ test_strategy: module
755
762
 
756
763
  ---
757
764
 
765
+ ### `workflow-runs/` — Workflow Check 结果归档
766
+
767
+ **创建时机:**
768
+ - `run.js` 在 scan/verify 步骤和 archive extract-module-impact 步骤执行 `runPostCheck()` 后自动归档
769
+ - CLI `sillyspec workflow check ... --save` 手动触发
770
+
771
+ **存储位置:** `.sillyspec/.runtime/workflow-runs/`
772
+
773
+ **命名格式:** `<YYYYMMDDHHmmss>-<workflow>-<project>-<status>.json`
774
+
775
+ **示例:** `20260603035731-scan-docs-dashboard-pass.json`
776
+
777
+ **内容结构(结构化结果对象):**
778
+ ```json
779
+ {
780
+ "run_id": "20260603035731-scan-docs-dashboard",
781
+ "created_at": "2026-06-03T03:57:31.000Z",
782
+ "source": "run.js",
783
+ "stage": "verify",
784
+ "workflow": "scan-docs",
785
+ "project": "dashboard",
786
+ "status": "pass",
787
+ "spec_version": 1,
788
+ "roles": [
789
+ { "id": "arch", "name": "技术架构", "status": "pass", "outputs": [...] }
790
+ ],
791
+ "workflow_checks": [...],
792
+ "failures": [],
793
+ "retry_prompts": []
794
+ }
795
+ ```
796
+
797
+ **写入方:**
798
+ - `run.js` — scan/verify 和 archive post_check 后自动调用 `saveWorkflowRun()`
799
+ - `index.js` — CLI `--save` 时调用 `saveWorkflowRun()`
800
+
801
+ **读取方:** 人工查阅 / 后续查询命令(待实现)
802
+
803
+ **注意:**
804
+ - 保存失败只输出 warning,不影响 workflow check 的 exit code
805
+ - CLI `--json --save` 时 stdout 仍为纯净 JSON,保存提示不混入
806
+ - 文件路径在 `.gitignore` 中(`.sillyspec/.runtime/`)
807
+
808
+ ---
809
+
758
810
  ## 8. 文件销毁与归档
759
811
 
760
812
  ### 变更归档流程
@@ -978,17 +1030,19 @@ graph LR
978
1030
 
979
1031
  **大体结构:**
980
1032
  ```markdown
981
- ## 2026-05-28 14:30:00 — 修复用户登录超时问题
1033
+ ## ql-20260603-001-a3f2 | 2026-06-03 14:30:00 — 修复用户登录超时问题
982
1034
  状态:进行中
983
1035
  文件:src/auth/login.js, src/config/timeout.yaml
984
1036
 
985
- ## 2026-05-28 15:00:00 — 修复用户登录超时问题
1037
+ ## ql-20260603-002-7b1c | 2026-06-03 15:00:00 — 修复用户登录超时问题
986
1038
  状态:已完成
987
1039
  文件:src/auth/login.js
988
1040
  结果:调整超时配置为 30s,新增重试逻辑
989
1041
  ```
990
1042
 
991
- **生命周期:** quick 步骤 1 创建("进行中")→ 步骤 5 更新为"已完成"并补充实际改动。超过 500 行轮转重命名为 `QUICKLOG-<USER>-YYYY-MM-DD.md`。
1043
+ **ID 规则:** 每条记录以 `ql-YYYYMMDD-NNN-XXXX` 开头作为唯一 ID。`XXXX` 为 4 位随机十六进制,防止多文件或并发冲突。追加前扫描文件中已有 `ql-<当天日期>-` 前缀的最大序号 +1,每天从 001 重新开始。此 ID 可被 design.md / plan.md / archive / 模块变更索引引用。
1044
+
1045
+ **生命周期:** quick 步骤 1 创建("进行中")→ 步骤 5 按 ql-ID 找到条目更新为"已完成"并补充实际改动。超过 500 行轮转重命名为 `QUICKLOG-<USER>-YYYY-MM-DD.md`(日期取最后一条记录),新文件需扫描同目录所有 QUICKLOG 文件中当天最大序号 +1 以继承 ql-ID。
992
1046
 
993
1047
  ---
994
1048
 
@@ -1039,6 +1093,7 @@ graph LR
1039
1093
  | `modules/_module-map.yaml` | scan 可选步骤 | scan | archive/plan/execute |
1040
1094
  | `modules/<module>.md` | scan 可选步骤(全量生成)+ archive sync-module-docs | scan/archive | propose/plan/execute/verify/quick |
1041
1095
  | `verify-result.md` | verify 阶段输出 | verify | 验证报告存档 |
1096
+ | `workflows/*.yaml` | init 自动生成(scan-docs.yaml, archive-impact.yaml) | init | run.js post_check + CLI workflow check |
1042
1097
 
1043
1098
 
1044
1099
  ```
@@ -1048,6 +1103,7 @@ sillyspec init
1048
1103
  ├─→ .sillyspec/.runtime/user-inputs.md
1049
1104
  ├─→ .sillyspec/projects/<name>.yaml
1050
1105
  ├─→ .sillyspec/docs/<name>/scan/ (骨架)
1106
+ ├─→ .sillyspec/workflows/ (scan-docs.yaml, archive-impact.yaml 模板)
1051
1107
  ├─→ .sillyspec/knowledge/
1052
1108
  └─→ .gitignore (追加 .sillyspec/.runtime/)
1053
1109
 
@@ -1064,6 +1120,9 @@ scan 阶段(12 步,完成后重置)
1064
1120
  ├─→ docs/<name>/modules/<module>.md (可选,需用户确认)
1065
1121
  └─→ .sillyspec/local.yaml
1066
1122
 
1123
+ [scan 完成后 run.js 自动执行 workflow check]
1124
+ └─→ .sillyspec/.runtime/workflow-runs/<ts>-scan-docs-<project>-<status>.json
1125
+
1067
1126
  brainstorm 阶段
1068
1127
 
1069
1128
  ├─→ sillyspec.db: changes/stages 记录自动创建
@@ -1112,4 +1171,7 @@ archive 阶段
1112
1171
  ├─→ changes/<name>/module-impact.md
1113
1172
  ├─→ docs/<name>/modules/<module>.md (通过 module-impact.md 同步)
1114
1173
  └─→ changes/<name>/ → changes/archive/YYYY-MM-DD-<name>/
1174
+
1175
+ [archive extract-module-impact 后 run.js 自动执行 workflow check]
1176
+ └─→ .sillyspec/.runtime/workflow-runs/<ts>-archive-impact-<project>-<status>.json
1115
1177
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.14.0",
3
+ "version": "3.15.0",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
package/src/init.js CHANGED
@@ -125,6 +125,22 @@ async function doInstall(projectDir, tools, subprojects = []) {
125
125
  const gitkeepPath = join(scanDir, '.gitkeep');
126
126
  if (!existsSync(gitkeepPath)) writeFileSync(gitkeepPath, '');
127
127
 
128
+ // 复制 workflow 模板到 .sillyspec/workflows/
129
+ const workflowsDir = join(projectDir, '.sillyspec', 'workflows');
130
+ const templatesDir = join(__dirname, '..', 'templates', 'workflows');
131
+ if (existsSync(templatesDir)) {
132
+ mkdirSync(workflowsDir, { recursive: true });
133
+ for (const file of readdirSync(templatesDir)) {
134
+ if (file.endsWith('.yaml')) {
135
+ const srcPath = join(templatesDir, file);
136
+ const dstPath = join(workflowsDir, file);
137
+ if (!existsSync(dstPath)) {
138
+ writeFileSync(dstPath, readFileSync(srcPath));
139
+ }
140
+ }
141
+ }
142
+ }
143
+
128
144
  // 创建 shared/workspace 目录
129
145
  mkdirSync(join(projectDir, '.sillyspec', 'shared'), { recursive: true });
130
146
  mkdirSync(join(projectDir, '.sillyspec', 'workspace'), { recursive: true });
@@ -300,6 +300,9 @@ function buildWavePrompt(wave, waveIndex, changeDir, worktreePath) {
300
300
  \`${worktreePath}\`
301
301
 
302
302
  不要在主工作区修改源码文件。所有代码变更只在 worktree 中进行。
303
+
304
+ ### 注意
305
+ 蓝图文件(tasks.md / design.md / proposal.md / requirements.md)在主工作区 .sillyspec/changes/<change>/ 下,它们可能不在 worktree 中。读取蓝图时使用主工作区路径,不要拼接到 worktree 路径下。
303
306
  子代理的 cwd 参数设为 \`${worktreePath}\`。
304
307
  `
305
308
  : ''
@@ -27,10 +27,15 @@ export const definition = {
27
27
  1. 使用预注入的 git 用户名:\`<git-user>\`
28
28
  2. 无 \`--change\`:创建 .sillyspec/quicklog/QUICKLOG-\`<git-user>\`.md\`(已存在则追加),写入:
29
29
  \`\`\`
30
- ## <now-datetime> <一句话任务描述>
30
+ ## ql-<YYYYMMDD>-<NNN>-<short4> | <now-datetime> | <一句话任务描述>
31
31
  状态:进行中
32
32
  文件:<预估要改的文件>
33
33
  \`\`\`
34
+ - ID 格式:\`ql-YYYYMMDD-NNN-XXXX\`(如 ql-20260603-001-a3f2)
35
+ - \`XXXX\` 是 4 位随机十六进制(防多文件/并发冲突)
36
+ - 追加前扫描文件中已有的 \`ql-<当天日期>-\` 前缀的最大序号,+1 作为新序号
37
+ - 每天从 001 开始,跨日重新计数
38
+ - 此 ID 可被 design.md / plan.md / archive / module 变更索引引用
34
39
  3. 有 \`--change\`:在 \`.sillyspec/changes/<change-name>/tasks.md\` 追加未勾选的 task
35
40
 
36
41
  这样 Gate 检测到 .sillyspec/\` 下有变更,就不会拦截后续的代码修改。
@@ -67,9 +72,9 @@ export const definition = {
67
72
  ### 操作
68
73
  1. \`git add -A\` — 暂存改动文件(不要 commit,由用户通过统一提交工具处理)
69
74
  2. 更新 Step 1 创建的记录:
70
- - 无 \`--change\`:更新 QUICKLOG 条目,将「状态:进行中」改为「状态:已完成」,补充实际改动文件和结果摘要
75
+ - 无 \`--change\`:找到对应 ql-ID 的条目,将「状态:进行中」改为「状态:已完成」,补充实际改动文件和结果摘要
71
76
  - 有 \`--change\`:勾选 tasks.md 中对应的 task checkbox
72
- 3. QUICKLOG 轮转:超过 500 行则重命名为 \`QUICKLOG-<USER>-YYYY-MM-DD.md\`
77
+ 3. QUICKLOG 轮转:超过 500 行则重命名为 \`QUICKLOG-<USER>-YYYY-MM-DD.md\`(日期取最后一条记录的日期)。新文件从空开始,ql-ID 需扫描同目录所有 QUICKLOG 文件中当天最大序号 +1
73
78
  4. 如果发现项目特有的坑,追加到 \`.sillyspec/knowledge/uncategorized.md\`
74
79
  5. 任务比预期复杂 → 建议用完整流程
75
80
 
@@ -78,7 +83,8 @@ export const definition = {
78
83
  7. 对比本次修改的文件(\`git diff --name-only\`)与模块映射
79
84
  8. 如果命中模块 → 直接同步模块文档:
80
85
  - 读取对应的 \`.sillyspec/docs/<project>/modules/<module>.md\`(如不存在则新建)
81
- - 根据本次改动内容更新模块文档(正文描述当前状态,底部追加变更索引)
86
+ - 根据本次改动内容更新模块文档(正文描述当前状态,底部变更索引追加本次 ql-ID)
87
+ - 变更索引格式:\`- ql-YYYYMMDD-NNN-XXXX | <一句话描述>\`
82
88
  - 写入模块文档
83
89
  - 将更新的模块文件加入 \`git add\`
84
90
  9. 未命中任何模块 → 跳过,不做额外操作
@@ -16,6 +16,7 @@ import { execSync } from 'child_process';
16
16
  import { existsSync, unlinkSync, writeFileSync, mkdtempSync, rmSync } from 'fs';
17
17
  import { join, resolve } from 'path';
18
18
  import { tmpdir } from 'os';
19
+ import { createHash } from 'crypto';
19
20
  import { WorktreeManager } from './worktree.js';
20
21
  import { parseFileChangeList } from './change-list.js';
21
22
 
@@ -77,7 +78,9 @@ export function applyWorktree(changeName, { cwd, checkOnly = false } = {}) {
77
78
  return result;
78
79
  }
79
80
 
80
- const { worktreePath, baseHash } = meta;
81
+ const { worktreePath, baseHash, baselineCommit } = meta;
82
+ // diff 起始点:有 baseline checkpoint 用它(只合子代理改动),否则 fallback 到 baseHash
83
+ const diffBase = baselineCommit || baseHash;
81
84
 
82
85
  if (!existsSync(worktreePath)) {
83
86
  result.errors.push(`worktree 目录不存在: ${worktreePath}`);
@@ -90,10 +93,10 @@ export function applyWorktree(changeName, { cwd, checkOnly = false } = {}) {
90
93
  let changedFiles;
91
94
  try {
92
95
  // tracked 文件的变更(modified/deleted)
93
- const trackedRaw = git(worktreePath, `diff --name-only ${baseHash}`);
96
+ const trackedRaw = git(worktreePath, `diff --name-only ${diffBase}`);
94
97
  const trackedFiles = trackedRaw ? trackedRaw.split('\n').filter(Boolean) : [];
95
98
 
96
- // untracked 新文件(baseHash 中不存在的文件)
99
+ // untracked 新文件(diffBase 中不存在的文件)
97
100
  const untrackedRaw = gitQuiet(worktreePath, `ls-files --others --exclude-standard`);
98
101
  const untrackedFiles = untrackedRaw
99
102
  ? untrackedRaw.split('\n').filter(Boolean).filter(f => !f.startsWith('.sillyspec/') && f !== 'meta.json')
@@ -136,6 +139,24 @@ export function applyWorktree(changeName, { cwd, checkOnly = false } = {}) {
136
139
  }
137
140
  }
138
141
 
142
+ // --- 4.5 校验:主工作区 baseline 是否变化(防 execute 期间主工作区被修改)---
143
+ if (meta.baselineHash) {
144
+ const staged = gitQuiet(projectRoot, 'diff --cached') || '';
145
+ const unstaged = gitQuiet(projectRoot, 'diff') || '';
146
+ const untracked = gitQuiet(projectRoot, 'ls-files --others --exclude-standard') || '';
147
+ const raw = `staged:${staged}\nunstaged:${unstaged}\nuntracked:${untracked}`;
148
+ const currentHash = createHash('sha256').update(raw).digest('hex').slice(0, 16);
149
+ if (currentHash !== meta.baselineHash) {
150
+ result.errors.push(
151
+ `主工作区 baseline 已变化(execute 前后不一致),不能直接 apply task.patch。\n` +
152
+ `建议:重新创建 worktree 或手动检查冲突。\n` +
153
+ `execute 前 baseline: ${meta.baselineHash}\n` +
154
+ `当前 baseline: ${currentHash}`
155
+ );
156
+ return result;
157
+ }
158
+ }
159
+
139
160
  // --- 5. 校验:主工作区文件 base hash 一致 ---
140
161
  // 5a. 检查主工作区是否有未 commit 的脏文件(会影响 apply)
141
162
  const mainDirtyRaw = gitQuiet(projectRoot, 'diff --name-only HEAD');
@@ -196,7 +217,7 @@ export function applyWorktree(changeName, { cwd, checkOnly = false } = {}) {
196
217
  // 分 tracked 变更和 untracked 新文件生成 patch
197
218
  const trackedFiles = patchFiles.filter(f => {
198
219
  // untracked 文件在 baseHash 的 tree 中不存在
199
- return gitQuiet(worktreePath, `cat-file -e ${baseHash}:${f}`) !== null;
220
+ return gitQuiet(worktreePath, `cat-file -e ${diffBase}:${f}`) !== null;
200
221
  });
201
222
  const untrackedPatchFiles = patchFiles.filter(f => !trackedFiles.includes(f));
202
223
 
@@ -204,7 +225,7 @@ export function applyWorktree(changeName, { cwd, checkOnly = false } = {}) {
204
225
  if (trackedFiles.length > 0) {
205
226
  const trackedArgs = trackedFiles.map(f => `-- ${f}`).join(' ');
206
227
  patchContent += execSync(
207
- `git diff --binary ${baseHash} ${trackedArgs}`,
228
+ `git diff --binary ${diffBase} ${trackedArgs}`,
208
229
  { cwd: worktreePath, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
209
230
  );
210
231
  }
package/src/worktree.js CHANGED
@@ -10,7 +10,8 @@
10
10
 
11
11
  import { execSync } from 'child_process';
12
12
  import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, readdirSync } from 'fs';
13
- import { join, resolve } from 'path';
13
+ import { join, resolve, dirname } from 'path';
14
+ import { createHash } from 'crypto';
14
15
 
15
16
  const WORKTREES_REL = '.sillyspec/.runtime/worktrees';
16
17
  const BRANCH_PREFIX = 'sillyspec/';
@@ -32,6 +33,16 @@ function parseJSON(raw) {
32
33
  try { return JSON.parse(raw); } catch { return null; }
33
34
  }
34
35
 
36
+ function computeBaselineHash(cwd) {
37
+ const staged = gitQuiet(cwd, 'diff --cached') || '';
38
+ const unstaged = gitQuiet(cwd, 'diff') || '';
39
+ const untracked = gitQuiet(cwd, 'ls-files --others --exclude-standard') || '';
40
+ const raw = `staged:${staged}
41
+ unstaged:${unstaged}
42
+ untracked:${untracked}`;
43
+ return createHash('sha256').update(raw).digest('hex').slice(0, 16);
44
+ }
45
+
35
46
  function validateChangeName(changeName) {
36
47
  if (!changeName || typeof changeName !== 'string' || changeName.trim() === '') {
37
48
  throw new Error('changeName 不能为空');
@@ -175,16 +186,29 @@ export class WorktreeManager {
175
186
  // fetch/merge 失败不影响 worktree 创建,只记录警告
176
187
  }
177
188
 
189
+ // 5.6 Dirty baseline overlay:将主工作区未提交变更同步到 worktree
190
+ const baselineResult = this._overlayBaseline(this.cwd, worktreePath);
191
+ const baselineFiles = baselineResult.files;
192
+ const baselineHash = baselineResult.baselineHash;
193
+
194
+ // 5.7 创建 baseline checkpoint(有 dirty baseline 时才创建)
195
+ let baselineCommit = null;
196
+ if (baselineFiles.length > 0) {
197
+ baselineCommit = this._createBaselineCheckpoint(worktreePath, name);
198
+ }
199
+
178
200
  // 6. 写入 meta.json
179
201
  const meta = {
180
202
  changeName: name,
181
203
  branch,
182
204
  baseBranch,
183
205
  baseHash,
184
- // actualBaseHash 记录 fetch+merge 后的实际 HEAD(可能与 baseHash 不同)
185
206
  actualBaseHash: gitQuiet(worktreePath, 'rev-parse HEAD') || baseHash,
186
207
  createdAt: new Date().toISOString(),
187
208
  worktreePath,
209
+ baselineFiles,
210
+ baselineCommit,
211
+ baselineHash, // 有 dirty baseline 时指向 checkpoint commit
188
212
  };
189
213
 
190
214
  const metaPath = join(worktreePath, META_FILE);
@@ -260,4 +284,118 @@ export class WorktreeManager {
260
284
  rmSync(worktreePath, { recursive: true, force: true });
261
285
  }
262
286
  }
287
+
288
+ /**
289
+ * 将主工作区未提交变更同步到 worktree(dirty baseline overlay)
290
+ * 覆盖 staged + unstaged 的文件变更,以及 untracked 文件。
291
+ * 使用 git diff + git apply 确保正确处理删除/rename/binary。
292
+ * @param {string} mainCwd - 主工作区路径
293
+ * @param {string} worktreePath - worktree 路径
294
+ * @returns {Array<string>} overlay 的文件列表
295
+ */
296
+ _overlayBaseline(mainCwd, worktreePath) {
297
+ const files = [];
298
+ const errors = [];
299
+
300
+ try {
301
+ // staged 变更
302
+ const staged = gitQuiet(mainCwd, 'diff --cached --name-only') || '';
303
+ if (staged) {
304
+ try {
305
+ const patchContent = execSync(`git diff --cached --binary`, { cwd: mainCwd, encoding: 'utf8', stdio: ['pipe','pipe','pipe'] });
306
+ if (patchContent) {
307
+ const patchFile = join(worktreePath, '.sillyspec-baseline-staged.patch');
308
+ writeFileSync(patchFile, patchContent);
309
+ git(worktreePath, `apply --binary ${patchFile}`);
310
+ rmSync(patchFile, { force: true });
311
+ }
312
+ } catch (e) {
313
+ errors.push(`staged: ${e.message}`);
314
+ }
315
+ files.push(...staged.split('\n').filter(Boolean));
316
+ }
317
+
318
+ // unstaged 变更
319
+ const unstaged = gitQuiet(mainCwd, 'diff --name-only') || '';
320
+ if (unstaged) {
321
+ try {
322
+ const patchContent = execSync(`git diff --binary`, { cwd: mainCwd, encoding: 'utf8', stdio: ['pipe','pipe','pipe'] });
323
+ if (patchContent) {
324
+ const patchFile = join(worktreePath, '.sillyspec-baseline-unstaged.patch');
325
+ writeFileSync(patchFile, patchContent);
326
+ git(worktreePath, `apply --binary ${patchFile}`);
327
+ rmSync(patchFile, { force: true });
328
+ }
329
+ } catch (e) {
330
+ errors.push(`unstaged: ${e.message}`);
331
+ }
332
+ files.push(...unstaged.split('\n').filter(Boolean));
333
+ }
334
+
335
+ // untracked 文件(排除 .sillyspec/.runtime 等)
336
+ const untracked = gitQuiet(mainCwd, 'ls-files --others --exclude-standard') || '';
337
+ if (untracked) {
338
+ for (const f of untracked.split('\n').filter(Boolean)) {
339
+ const src = join(mainCwd, f);
340
+ const dst = join(worktreePath, f);
341
+ if (existsSync(src)) {
342
+ mkdirSync(dirname(dst), { recursive: true });
343
+ try { writeFileSync(dst, readFileSync(src)); files.push(f); } catch {}
344
+ }
345
+ }
346
+ }
347
+
348
+ if (files.length > 0) {
349
+ console.log(`📁 baseline overlay: ${files.length} 个未提交文件已同步到 worktree`);
350
+ }
351
+ } catch (e) {
352
+ errors.push(`unexpected: ${e.message}`);
353
+ }
354
+
355
+ // 有 pending 文件但 overlay 部分失败 → fail-fast
356
+ if (errors.length > 0) {
357
+ throw new Error(`baseline overlay 失败 (${errors.length} 个错误): ${errors.join('; ')}`);
358
+ }
359
+
360
+ const uniqueFiles = [...new Set(files)];
361
+
362
+ // 计算 baseline hash(用于 merge 前校验主工作区是否变化)
363
+ const baselineHash = uniqueFiles.length > 0 ? computeBaselineHash(mainCwd) : null;
364
+
365
+ return { files: uniqueFiles, baselineHash };
366
+ }
367
+
368
+ /**
369
+ * 在 worktree 内创建 baseline checkpoint commit
370
+ * 用于区分 "前置 dirty baseline" 和 "子代理新增改动"
371
+ * @param {string} worktreePath
372
+ * @param {string} changeName
373
+ * @returns {string} commit hash
374
+ */
375
+ _createBaselineCheckpoint(worktreePath, changeName) {
376
+ // 使用临时 git identity,避免用户未配置 user.name/user.email 导致失败
377
+ const env = {
378
+ GIT_AUTHOR_NAME: 'sillyspec',
379
+ GIT_AUTHOR_EMAIL: 'sillyspec@baseline',
380
+ GIT_COMMITTER_NAME: 'sillyspec',
381
+ GIT_COMMITTER_EMAIL: 'sillyspec@baseline',
382
+ };
383
+ try {
384
+ execSync('git add -A', { cwd: worktreePath, encoding: 'utf8', stdio: ['pipe','pipe','pipe'], env });
385
+ // 检查是否有实际变更(可能 overlay 后和 HEAD 完全一致)
386
+ const status = gitQuiet(worktreePath, 'status --porcelain');
387
+ if (!status) {
388
+ return gitQuiet(worktreePath, 'rev-parse HEAD');
389
+ }
390
+ execSync(
391
+ `git commit -m "sillyspec: baseline checkpoint for ${changeName}"`,
392
+ { cwd: worktreePath, encoding: 'utf8', stdio: ['pipe','pipe','pipe'], env }
393
+ );
394
+ const hash = git(worktreePath, 'rev-parse HEAD');
395
+ console.log(`📌 baseline checkpoint: ${hash}`);
396
+ return hash;
397
+ } catch (e) {
398
+ throw new Error(`baseline checkpoint 创建失败: ${e.message}`);
399
+ }
400
+ }
263
401
  }
@@ -0,0 +1,79 @@
1
+ name: archive-impact
2
+ description: 分析变更影响的模块并同步模块文档
3
+ spec_version: 1
4
+
5
+ roles:
6
+ - id: impact-analyzer
7
+ name: "影响分析"
8
+ task: "分析 git diff,提取变更影响模块"
9
+ inputs:
10
+ paths: [".sillyspec/changes/", ".sillyspec/docs/*/modules/"]
11
+ hints:
12
+ grep_patterns: ["_module-map", "module-impact", "git diff"]
13
+ outputs:
14
+ - path: ".sillyspec/changes/<change-name>/module-impact.md"
15
+ required: true
16
+ checks:
17
+ - type: file_exists
18
+ - type: min_lines
19
+ min: 20
20
+ - type: contains_sections
21
+ sections: ["模块影响矩阵", "未匹配文件"]
22
+ constraints:
23
+ - "以 git diff 为准(真实 > 声明)"
24
+ - "影响类型:逻辑变更/数据结构变更/接口变更/调用关系变更/配置变更/新增"
25
+ - "需要 review 标记不确定的影响"
26
+
27
+ - id: doc-syncer
28
+ name: "文档同步"
29
+ task: "根据 module-impact.md 同步更新模块索引和卡片"
30
+ depends_on:
31
+ - impact-analyzer
32
+ inputs:
33
+ from_role: impact-analyzer
34
+ output: module-impact
35
+ output_description: "变更影响分析结果,包含模块影响矩阵"
36
+ paths: [".sillyspec/docs/*/modules/"]
37
+ hints:
38
+ grep_patterns: ["_module-map", "module-impact"]
39
+ outputs:
40
+ - path: ".sillyspec/docs/sillyspec/modules/_module-map.yaml"
41
+ required: false
42
+ checks:
43
+ - type: file_exists
44
+ - path: ".sillyspec/docs/sillyspec/modules/<module-id>.md"
45
+ required: false
46
+ # checks 跳过:路径含动态 <module-id> 占位符,不适合静态检查
47
+ constraints:
48
+ - "结构化事实改 _module-map.yaml,语义解释改模块卡片"
49
+ - "人工备注区域永远保护,不覆盖"
50
+ - "更新前展示 diff 摘要,请用户确认"
51
+
52
+ orchestration:
53
+ mode: sequential
54
+ timeout_per_role: 180
55
+
56
+ checks:
57
+ role_level:
58
+ - type: file_exists
59
+ - type: min_lines
60
+ min: 10
61
+ workflow_level:
62
+ - type: file_exists
63
+ path: ".sillyspec/changes/<change-name>/module-impact.md"
64
+
65
+ retry:
66
+ max_attempts: 1
67
+ include_failure_context: true
68
+ retry_scope: failed_role_only
69
+
70
+ on_check_failure: prompt_retry
71
+
72
+ permissions:
73
+ write_mode: patch_only
74
+ write_scope:
75
+ - ".sillyspec/docs/"
76
+ - ".sillyspec/changes/<change-name>/"
77
+ allow_shell: true
78
+ allow_network: false
79
+ allow_git: true
@@ -0,0 +1,132 @@
1
+ name: scan-docs
2
+ description: 并行生成项目的扫描文档(7份)
3
+ spec_version: 1
4
+
5
+ roles:
6
+ - id: arch
7
+ name: "技术架构"
8
+ task: "生成 ARCHITECTURE.md"
9
+ inputs:
10
+ paths:
11
+ - "src/*.js"
12
+ - "package.json"
13
+ hints:
14
+ grep_patterns: ["class ", "export ", "import ", "schema", "CREATE TABLE"]
15
+ outputs:
16
+ - path: ".sillyspec/docs/<project>/scan/ARCHITECTURE.md"
17
+ required: true
18
+ checks:
19
+ - type: file_exists
20
+ - type: min_lines
21
+ min: 20
22
+ - type: contains_sections
23
+ sections: ["技术栈", "架构概览"]
24
+ constraints:
25
+ - "禁止读源码全文,只用 grep/rg 搜索"
26
+ - "Schema 只记表名+说明+字段数"
27
+
28
+ - id: conventions
29
+ name: "代码约定"
30
+ task: "生成 CONVENTIONS.md"
31
+ inputs:
32
+ paths: ["src/"]
33
+ hints:
34
+ grep_patterns: ["function ", "const ", "async ", "try ", "catch "]
35
+ outputs:
36
+ - path: ".sillyspec/docs/<project>/scan/CONVENTIONS.md"
37
+ required: true
38
+ checks:
39
+ - type: file_exists
40
+ - type: min_lines
41
+ min: 15
42
+ - type: contains_sections
43
+ sections: ["框架隐形规则", "代码风格"]
44
+ constraints:
45
+ - "禁止读源码全文"
46
+ - "提取 3-5 个典型模式"
47
+
48
+ - id: structure
49
+ name: "目录结构+外部集成"
50
+ task: "生成 STRUCTURE.md 和 INTEGRATIONS.md"
51
+ inputs:
52
+ paths: ["./"]
53
+ hints:
54
+ grep_patterns: ["fetch", "http", "WebSocket", "ws", "chokidar"]
55
+ outputs:
56
+ - path: ".sillyspec/docs/<project>/scan/STRUCTURE.md"
57
+ required: true
58
+ checks:
59
+ - type: file_exists
60
+ - type: min_lines
61
+ min: 15
62
+ - path: ".sillyspec/docs/<project>/scan/INTEGRATIONS.md"
63
+ required: true
64
+ checks:
65
+ - type: file_exists
66
+ - type: min_lines
67
+ min: 15
68
+ constraints:
69
+ - "STRUCTURE: 目录树+模块说明"
70
+ - "INTEGRATIONS: 按类型分组外部依赖"
71
+
72
+ - id: quality
73
+ name: "测试+债务+概览"
74
+ task: "生成 TESTING.md、CONCERNS.md、PROJECT.md"
75
+ inputs:
76
+ paths: ["src/", "packages/"]
77
+ hints:
78
+ grep_patterns: ["TODO", "FIXME", "deprecated", "test", "describe"]
79
+ outputs:
80
+ - path: ".sillyspec/docs/<project>/scan/TESTING.md"
81
+ required: true
82
+ checks:
83
+ - type: file_exists
84
+ - type: min_lines
85
+ min: 10
86
+ - type: no_placeholder
87
+ - path: ".sillyspec/docs/<project>/scan/CONCERNS.md"
88
+ required: true
89
+ checks:
90
+ - type: file_exists
91
+ - type: min_lines
92
+ min: 10
93
+ - type: contains_sections
94
+ sections: ["代码质量", "依赖风险"]
95
+ - path: ".sillyspec/docs/<project>/scan/PROJECT.md"
96
+ required: true
97
+ checks:
98
+ - type: file_exists
99
+ - type: min_lines
100
+ min: 15
101
+ - type: contains_sections
102
+ sections: ["项目简介", "技术栈"]
103
+ constraints:
104
+ - "CONCERNS 按严重程度分组(🔴/🟡/🟢)"
105
+ - "不编造不存在的测试"
106
+
107
+ orchestration:
108
+ mode: parallel
109
+ max_concurrent: 4
110
+ timeout_per_role: 120
111
+
112
+ checks:
113
+ workflow_level:
114
+ - type: file_count
115
+ path: "scan/"
116
+ min: 7
117
+ - type: no_empty_files
118
+
119
+ retry:
120
+ max_attempts: 1
121
+ include_failure_context: true
122
+ retry_scope: failed_role_only
123
+
124
+ on_check_failure: prompt_retry
125
+
126
+ permissions:
127
+ write_mode: direct
128
+ write_scope:
129
+ - ".sillyspec/docs/<project>/scan/"
130
+ allow_shell: true
131
+ allow_network: false
132
+ allow_git: false