sillyspec 3.14.1 → 3.15.2
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/docs/sillyspec/file-lifecycle.md +67 -5
- package/package.json +1 -1
- package/src/run.js +16 -1
- package/src/stages/execute.js +3 -0
- package/src/stages/quick.js +10 -4
- package/src/worktree-apply.js +129 -9
- package/src/worktree.js +142 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
author: qinyi
|
|
3
3
|
created_at: 2026-05-31 11:00:00
|
|
4
|
-
updated_at: 2026-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
package/src/run.js
CHANGED
|
@@ -10,6 +10,7 @@ import { ProgressManager } from './progress.js'
|
|
|
10
10
|
import { stageRegistry, auxiliaryStages } from './stages/index.js'
|
|
11
11
|
import { buildExecuteSteps } from './stages/execute.js'
|
|
12
12
|
import { buildPlanSteps } from './stages/plan.js'
|
|
13
|
+
import { formatExecuteSummary } from './worktree-apply.js'
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* 同步触发辅助函数:_write 后 best-effort 同步到平台
|
|
@@ -745,7 +746,21 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
745
746
|
console.log(`✅ ${stageName} 阶段已完成(${total}/${total} 步)`)
|
|
746
747
|
|
|
747
748
|
if (stageName === 'execute') {
|
|
748
|
-
|
|
749
|
+
// execute run summary:展示真实可得的结构化信息
|
|
750
|
+
try {
|
|
751
|
+
const lastOutput = steps[steps.length - 1]?.output || ''
|
|
752
|
+
const summary = formatExecuteSummary({
|
|
753
|
+
changeName,
|
|
754
|
+
stepsCompleted: total,
|
|
755
|
+
stepsTotal: total,
|
|
756
|
+
agentSummary: lastOutput,
|
|
757
|
+
cwd,
|
|
758
|
+
})
|
|
759
|
+
console.log(`\n${summary}`)
|
|
760
|
+
} catch (e) {
|
|
761
|
+
// summary 失败不影响主流程
|
|
762
|
+
console.log('\n👉 下一步:sillyspec run verify(验证通过后才能归档)')
|
|
763
|
+
}
|
|
749
764
|
} else if (stageName === 'verify') {
|
|
750
765
|
console.log('\n👉 下一步:sillyspec run archive(验证通过,可以归档了)')
|
|
751
766
|
} else if (stageName === 'archive') {
|
package/src/stages/execute.js
CHANGED
|
@@ -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
|
: ''
|
package/src/stages/quick.js
CHANGED
|
@@ -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
|
|
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. 未命中任何模块 → 跳过,不做额外操作
|
package/src/worktree-apply.js
CHANGED
|
@@ -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}`);
|
|
@@ -89,17 +92,25 @@ export function applyWorktree(changeName, { cwd, checkOnly = false } = {}) {
|
|
|
89
92
|
// 同时检测 untracked 新文件(git diff 不包含 untracked)
|
|
90
93
|
let changedFiles;
|
|
91
94
|
try {
|
|
92
|
-
//
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
+
// 用 --name-status 捕获 rename/delete(--name-only 会丢失 rename 源文件)
|
|
96
|
+
const statusRaw = git(worktreePath, `diff --name-status ${diffBase}`);
|
|
97
|
+
const statusFiles = new Set();
|
|
98
|
+
if (statusRaw) {
|
|
99
|
+
for (const line of statusRaw.split('\n').filter(Boolean)) {
|
|
100
|
+
const parts = line.split('\t');
|
|
101
|
+
// R100 old.txt new.txt → 提取两个文件
|
|
102
|
+
if (parts.length >= 2) statusFiles.add(parts[parts.length - 1]);
|
|
103
|
+
if (parts.length >= 3) statusFiles.add(parts[parts.length - 2]);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
95
106
|
|
|
96
|
-
// untracked 新文件(
|
|
107
|
+
// untracked 新文件(diffBase 中不存在的文件)
|
|
97
108
|
const untrackedRaw = gitQuiet(worktreePath, `ls-files --others --exclude-standard`);
|
|
98
109
|
const untrackedFiles = untrackedRaw
|
|
99
110
|
? untrackedRaw.split('\n').filter(Boolean).filter(f => !f.startsWith('.sillyspec/') && f !== 'meta.json')
|
|
100
111
|
: [];
|
|
101
112
|
|
|
102
|
-
changedFiles = [...new Set([...
|
|
113
|
+
changedFiles = [...new Set([...statusFiles, ...untrackedFiles])];
|
|
103
114
|
} catch (e) {
|
|
104
115
|
result.errors.push(`获取变更文件列表失败: ${e.message}`);
|
|
105
116
|
return result;
|
|
@@ -136,6 +147,24 @@ export function applyWorktree(changeName, { cwd, checkOnly = false } = {}) {
|
|
|
136
147
|
}
|
|
137
148
|
}
|
|
138
149
|
|
|
150
|
+
// --- 4.5 校验:主工作区 baseline 是否变化(防 execute 期间主工作区被修改)---
|
|
151
|
+
if (meta.baselineHash) {
|
|
152
|
+
const staged = gitQuiet(projectRoot, 'diff --cached') || '';
|
|
153
|
+
const unstaged = gitQuiet(projectRoot, 'diff') || '';
|
|
154
|
+
const untracked = gitQuiet(projectRoot, 'ls-files --others --exclude-standard') || '';
|
|
155
|
+
const raw = `staged:${staged}\nunstaged:${unstaged}\nuntracked:${untracked}`;
|
|
156
|
+
const currentHash = createHash('sha256').update(raw).digest('hex').slice(0, 16);
|
|
157
|
+
if (currentHash !== meta.baselineHash) {
|
|
158
|
+
result.errors.push(
|
|
159
|
+
`主工作区 baseline 已变化(execute 前后不一致),不能直接 apply task.patch。\n` +
|
|
160
|
+
`建议:重新创建 worktree 或手动检查冲突。\n` +
|
|
161
|
+
`execute 前 baseline: ${meta.baselineHash}\n` +
|
|
162
|
+
`当前 baseline: ${currentHash}`
|
|
163
|
+
);
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
139
168
|
// --- 5. 校验:主工作区文件 base hash 一致 ---
|
|
140
169
|
// 5a. 检查主工作区是否有未 commit 的脏文件(会影响 apply)
|
|
141
170
|
const mainDirtyRaw = gitQuiet(projectRoot, 'diff --name-only HEAD');
|
|
@@ -195,8 +224,11 @@ export function applyWorktree(changeName, { cwd, checkOnly = false } = {}) {
|
|
|
195
224
|
|
|
196
225
|
// 分 tracked 变更和 untracked 新文件生成 patch
|
|
197
226
|
const trackedFiles = patchFiles.filter(f => {
|
|
198
|
-
//
|
|
199
|
-
|
|
227
|
+
// 文件在 diffBase 的 tree 中存在 → tracked(包括 rename 目标可能的情况)
|
|
228
|
+
if (gitQuiet(worktreePath, `cat-file -e ${diffBase}:${f}`) !== null) return true;
|
|
229
|
+
// 文件在工作区 index 中已存在(比如被 git mv 处理过)→ 也视为 tracked
|
|
230
|
+
if (gitQuiet(worktreePath, `ls-files --error-unmatch ${f}`) !== null) return true;
|
|
231
|
+
return false;
|
|
200
232
|
});
|
|
201
233
|
const untrackedPatchFiles = patchFiles.filter(f => !trackedFiles.includes(f));
|
|
202
234
|
|
|
@@ -204,7 +236,7 @@ export function applyWorktree(changeName, { cwd, checkOnly = false } = {}) {
|
|
|
204
236
|
if (trackedFiles.length > 0) {
|
|
205
237
|
const trackedArgs = trackedFiles.map(f => `-- ${f}`).join(' ');
|
|
206
238
|
patchContent += execSync(
|
|
207
|
-
`git diff --binary ${
|
|
239
|
+
`git diff --binary ${diffBase} ${trackedArgs}`,
|
|
208
240
|
{ cwd: worktreePath, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
209
241
|
);
|
|
210
242
|
}
|
|
@@ -269,3 +301,91 @@ export function applyWorktree(changeName, { cwd, checkOnly = false } = {}) {
|
|
|
269
301
|
|
|
270
302
|
return result;
|
|
271
303
|
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* 格式化 execute run summary(人类可读)
|
|
307
|
+
*
|
|
308
|
+
* 只展示 CLI 真实掌握的信息,不声称知道 per-task 状态。
|
|
309
|
+
* @param {object} opts
|
|
310
|
+
* @param {string} opts.changeName - 变更名
|
|
311
|
+
* @param {number} opts.stepsCompleted - 已完成步骤数
|
|
312
|
+
* @param {number} opts.stepsTotal - 总步骤数
|
|
313
|
+
* @param {string} opts.agentSummary - Agent 最终输出摘要
|
|
314
|
+
* @param {string} [opts.cwd] - 项目根目录(默认 process.cwd())
|
|
315
|
+
* @returns {string} 格式化的 summary 文本
|
|
316
|
+
*/
|
|
317
|
+
export function formatExecuteSummary({ changeName, stepsCompleted, stepsTotal, agentSummary, cwd }) {
|
|
318
|
+
const wm = new WorktreeManager({ cwd });
|
|
319
|
+
const meta = wm.getMeta(changeName);
|
|
320
|
+
const lines = [];
|
|
321
|
+
|
|
322
|
+
const SEPARATOR = '─'.repeat(32);
|
|
323
|
+
|
|
324
|
+
// --- Header ---
|
|
325
|
+
lines.push(`Execute Summary`);
|
|
326
|
+
lines.push(SEPARATOR);
|
|
327
|
+
|
|
328
|
+
// --- Status ---
|
|
329
|
+
if (!meta) {
|
|
330
|
+
// worktree 不存在(可能已 cleanup 或没有用过 worktree)
|
|
331
|
+
lines.push(`Status: COMPLETED`);
|
|
332
|
+
lines.push(`Steps: ${stepsCompleted} / ${stepsTotal}`);
|
|
333
|
+
lines.push(`Apply: N/A`);
|
|
334
|
+
} else {
|
|
335
|
+
const hasBaseline = meta.baselineCommit != null;
|
|
336
|
+
const wtExists = existsSync(meta.worktreePath);
|
|
337
|
+
|
|
338
|
+
const applyStatus = wtExists ? 'pending' : 'applied';
|
|
339
|
+
const baselineCount = meta.baselineFiles?.length || 0;
|
|
340
|
+
const baselineStatus = hasBaseline
|
|
341
|
+
? `dirty (${baselineCount} baseline file${baselineCount === 1 ? '' : 's'} protected)`
|
|
342
|
+
: 'clean';
|
|
343
|
+
|
|
344
|
+
lines.push(`Status: COMPLETED`);
|
|
345
|
+
lines.push(`Steps: ${stepsCompleted} / ${stepsTotal}`);
|
|
346
|
+
lines.push(`Baseline: ${baselineStatus}`);
|
|
347
|
+
lines.push(`Apply: ${applyStatus}`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// --- Changed files ---
|
|
351
|
+
// 从主工作区 diff 获取(worktree 已 apply)或从 worktree diff 获取
|
|
352
|
+
if (meta && existsSync(meta.worktreePath)) {
|
|
353
|
+
// worktree 还在,用 baselineCommit 或 baseHash 做 diff
|
|
354
|
+
try {
|
|
355
|
+
const diffBase = meta.baselineCommit || meta.baseHash;
|
|
356
|
+
const { execSync: es } = require('child_process');
|
|
357
|
+
const filesRaw = es(`git -C ${meta.worktreePath} diff --name-only ${diffBase} 2>/dev/null`, { encoding: 'utf8' });
|
|
358
|
+
const files = filesRaw ? filesRaw.trim().split('\n').filter(Boolean) : [];
|
|
359
|
+
if (files.length > 0) {
|
|
360
|
+
lines.push(``);
|
|
361
|
+
const maxShow = 10;
|
|
362
|
+
const showFiles = files.slice(0, maxShow);
|
|
363
|
+
const remain = files.length - maxShow;
|
|
364
|
+
lines.push(`Changed Files (${files.length})`);
|
|
365
|
+
showFiles.forEach(f => lines.push(` ${f}`));
|
|
366
|
+
if (remain > 0) {
|
|
367
|
+
lines.push(` ... ${remain} more`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
} catch {}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// --- Agent Summary ---
|
|
374
|
+
if (agentSummary) {
|
|
375
|
+
lines.push(``);
|
|
376
|
+
lines.push(`Agent Summary`);
|
|
377
|
+
// 缩进每行,截断过长内容
|
|
378
|
+
const maxLen = 200;
|
|
379
|
+
const summary = agentSummary.length > maxLen
|
|
380
|
+
? agentSummary.slice(0, maxLen) + '...'
|
|
381
|
+
: agentSummary;
|
|
382
|
+
summary.split('\n').forEach(l => lines.push(` ${l}`));
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// --- Next ---
|
|
386
|
+
lines.push(``);
|
|
387
|
+
lines.push(`Next`);
|
|
388
|
+
lines.push(` → sillyspec run verify`);
|
|
389
|
+
|
|
390
|
+
return lines.join('\n');
|
|
391
|
+
}
|
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,120 @@ 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
|
+
// 用 Buffer 模式读取,避免二进制 patch 被 UTF-8 解码损坏
|
|
306
|
+
const patchBuf = execSync(`git diff --cached --binary`, { cwd: mainCwd, stdio: ['pipe','pipe','pipe'] });
|
|
307
|
+
if (patchBuf && patchBuf.length > 0) {
|
|
308
|
+
const patchFile = join(worktreePath, '.sillyspec-baseline-staged.patch');
|
|
309
|
+
writeFileSync(patchFile, patchBuf);
|
|
310
|
+
git(worktreePath, `apply --binary ${patchFile}`);
|
|
311
|
+
rmSync(patchFile, { force: true });
|
|
312
|
+
}
|
|
313
|
+
} catch (e) {
|
|
314
|
+
errors.push(`staged: ${e.message}`);
|
|
315
|
+
}
|
|
316
|
+
files.push(...staged.split('\n').filter(Boolean));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// unstaged 变更
|
|
320
|
+
const unstaged = gitQuiet(mainCwd, 'diff --name-only') || '';
|
|
321
|
+
if (unstaged) {
|
|
322
|
+
try {
|
|
323
|
+
// 用 Buffer 模式读取,避免二进制 patch 被 UTF-8 解码损坏
|
|
324
|
+
const patchBuf = execSync(`git diff --binary`, { cwd: mainCwd, stdio: ['pipe','pipe','pipe'] });
|
|
325
|
+
if (patchBuf && patchBuf.length > 0) {
|
|
326
|
+
const patchFile = join(worktreePath, '.sillyspec-baseline-unstaged.patch');
|
|
327
|
+
writeFileSync(patchFile, patchBuf);
|
|
328
|
+
git(worktreePath, `apply --binary ${patchFile}`);
|
|
329
|
+
rmSync(patchFile, { force: true });
|
|
330
|
+
}
|
|
331
|
+
} catch (e) {
|
|
332
|
+
errors.push(`unstaged: ${e.message}`);
|
|
333
|
+
}
|
|
334
|
+
files.push(...unstaged.split('\n').filter(Boolean));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// untracked 文件(排除 .sillyspec/.runtime 等)
|
|
338
|
+
const untracked = gitQuiet(mainCwd, 'ls-files --others --exclude-standard') || '';
|
|
339
|
+
if (untracked) {
|
|
340
|
+
for (const f of untracked.split('\n').filter(Boolean)) {
|
|
341
|
+
const src = join(mainCwd, f);
|
|
342
|
+
const dst = join(worktreePath, f);
|
|
343
|
+
if (existsSync(src)) {
|
|
344
|
+
mkdirSync(dirname(dst), { recursive: true });
|
|
345
|
+
try { writeFileSync(dst, readFileSync(src)); files.push(f); } catch {}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (files.length > 0) {
|
|
351
|
+
console.log(`📁 baseline overlay: ${files.length} 个未提交文件已同步到 worktree`);
|
|
352
|
+
}
|
|
353
|
+
} catch (e) {
|
|
354
|
+
errors.push(`unexpected: ${e.message}`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// 有 pending 文件但 overlay 部分失败 → fail-fast
|
|
358
|
+
if (errors.length > 0) {
|
|
359
|
+
throw new Error(`baseline overlay 失败 (${errors.length} 个错误): ${errors.join('; ')}`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const uniqueFiles = [...new Set(files)];
|
|
363
|
+
|
|
364
|
+
// 计算 baseline hash(用于 merge 前校验主工作区是否变化)
|
|
365
|
+
const baselineHash = uniqueFiles.length > 0 ? computeBaselineHash(mainCwd) : null;
|
|
366
|
+
|
|
367
|
+
return { files: uniqueFiles, baselineHash };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* 在 worktree 内创建 baseline checkpoint commit
|
|
372
|
+
* 用于区分 "前置 dirty baseline" 和 "子代理新增改动"
|
|
373
|
+
* @param {string} worktreePath
|
|
374
|
+
* @param {string} changeName
|
|
375
|
+
* @returns {string} commit hash
|
|
376
|
+
*/
|
|
377
|
+
_createBaselineCheckpoint(worktreePath, changeName) {
|
|
378
|
+
// 使用临时 git identity,避免用户未配置 user.name/user.email 导致失败
|
|
379
|
+
const env = {
|
|
380
|
+
GIT_AUTHOR_NAME: 'sillyspec',
|
|
381
|
+
GIT_AUTHOR_EMAIL: 'sillyspec@baseline',
|
|
382
|
+
GIT_COMMITTER_NAME: 'sillyspec',
|
|
383
|
+
GIT_COMMITTER_EMAIL: 'sillyspec@baseline',
|
|
384
|
+
};
|
|
385
|
+
try {
|
|
386
|
+
execSync('git add -A', { cwd: worktreePath, encoding: 'utf8', stdio: ['pipe','pipe','pipe'], env });
|
|
387
|
+
// 检查是否有实际变更(可能 overlay 后和 HEAD 完全一致)
|
|
388
|
+
const status = gitQuiet(worktreePath, 'status --porcelain');
|
|
389
|
+
if (!status) {
|
|
390
|
+
return gitQuiet(worktreePath, 'rev-parse HEAD');
|
|
391
|
+
}
|
|
392
|
+
execSync(
|
|
393
|
+
`git commit -m "sillyspec: baseline checkpoint for ${changeName}"`,
|
|
394
|
+
{ cwd: worktreePath, encoding: 'utf8', stdio: ['pipe','pipe','pipe'], env }
|
|
395
|
+
);
|
|
396
|
+
const hash = git(worktreePath, 'rev-parse HEAD');
|
|
397
|
+
console.log(`📌 baseline checkpoint: ${hash}`);
|
|
398
|
+
return hash;
|
|
399
|
+
} catch (e) {
|
|
400
|
+
throw new Error(`baseline checkpoint 创建失败: ${e.message}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
263
403
|
}
|