sillyspec 3.26.11 → 3.26.14
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 +1 -1
- package/.claude/skills/sillyspec-execute/SKILL.md +1 -1
- package/.claude/skills/sillyspec-knowledge/SKILL.md +1 -1
- package/.claude/skills/sillyspec-plan/SKILL.md +5 -5
- package/.claude/skills/sillyspec-quick/SKILL.md +5 -1
- package/README.md +7 -5
- package/SKILL.md +5 -4
- package/package.json +1 -1
- package/packages/dashboard/server/executor.js +10 -1
- package/packages/dashboard/server/index.js +108 -46
- package/src/config-schema.js +3 -0
- package/src/contract-matrix.js +22 -13
- package/src/db.js +24 -4
- package/src/dispatch/probe.js +99 -63
- package/src/docs-check.js +58 -13
- package/src/docs-debt.js +6 -2
- package/src/docs-gate.js +1 -0
- package/src/doctor-diagnostics.js +208 -21
- package/src/fs-atomic.js +3 -1
- package/src/git-helper.js +7 -4
- package/src/hooks/worktree-guard.js +8 -2
- package/src/index.js +200 -27
- package/src/init.js +3 -2
- package/src/local-detect.js +9 -1
- package/src/modules.js +7 -5
- package/src/progress/stage-machine.js +589 -578
- package/src/progress/step-store.js +1 -1
- package/src/progress.js +187 -30
- package/src/quicklog.js +47 -16
- package/src/run/command.js +88 -46
- package/src/run/complete-handlers.js +27 -15
- package/src/run/complete.js +1186 -1121
- package/src/run/concurrent-detect.js +3 -1
- package/src/run/gates.js +172 -25
- package/src/run/prompt.js +17 -9
- package/src/run/scan-profile.js +35 -23
- package/src/run/shared.js +164 -63
- package/src/run/stage.js +41 -1
- package/src/setup.js +8 -10
- package/src/sillyhub-mcp/client.js +74 -6
- package/src/spec-dir-typo.js +3 -24
- package/src/spec-sync.js +43 -8
- package/src/stage-contract.js +12 -38
- package/src/stage-review.js +40 -1
- package/src/stages/archive.js +4 -3
- package/src/stages/brainstorm-auto.js +1 -1
- package/src/stages/brainstorm.js +27 -13
- package/src/stages/doctor.js +29 -16
- package/src/stages/execute.js +170 -154
- package/src/stages/knowledge.js +18 -3
- package/src/stages/plan-postcheck.js +72 -37
- package/src/stages/plan.js +70 -48
- package/src/stages/quick.js +3 -1
- package/src/stages/verify.js +1 -1
- package/src/sync.js +339 -225
- package/src/task-review.js +281 -64
- package/src/taskcard.js +178 -0
- package/src/verify-postcheck.js +48 -35
- package/src/worktree-apply.js +58 -34
- package/src/worktree-deps.js +120 -20
- package/src/worktree.js +55 -9
- package/.npmrc-publish +0 -2
package/src/worktree-deps.js
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { existsSync, readFileSync, realpathSync } from 'fs';
|
|
12
|
-
import { join } from 'path';
|
|
13
|
-
import {
|
|
12
|
+
import { join, isAbsolute, resolve as resolvePath, sep as pathSep } from 'path';
|
|
13
|
+
import { execFileSync } from 'child_process';
|
|
14
14
|
import { createHash } from 'crypto';
|
|
15
15
|
|
|
16
16
|
const LOCKFILES = ['pnpm-lock.yaml', 'package-lock.json', 'yarn.lock'];
|
|
@@ -44,16 +44,28 @@ function detectLockfile(dir) {
|
|
|
44
44
|
return null;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
/**
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
47
|
+
/**
|
|
48
|
+
* 读取 local.yaml 文本(specBase 优先,回退 worktreePath/.sillyspec;不读 process.cwd 避免环境配置泄漏)。
|
|
49
|
+
* 返回 { text, source }——source 标记信任级(体检 SEC-01):
|
|
50
|
+
* - 'specBase':主仓配置,可信(其 commands.install 允许执行,仍过白名单+元字符门)
|
|
51
|
+
* - 'worktree':worktree 内副本,agent/子代理可写区——只作 project.type / modules 等
|
|
52
|
+
* 只读探测,其 commands.install 永不执行(否则绕过 worktree-guard 的危险命令拦截)
|
|
53
|
+
*/
|
|
54
|
+
function readLocalYamlSourced(specBase, worktreePath) {
|
|
55
|
+
if (specBase) {
|
|
56
|
+
const p = join(specBase, 'local.yaml');
|
|
57
|
+
if (existsSync(p)) return { text: readFileSync(p, 'utf8'), source: 'specBase' };
|
|
55
58
|
}
|
|
56
|
-
|
|
59
|
+
if (worktreePath) {
|
|
60
|
+
const p = join(worktreePath, '.sillyspec', 'local.yaml');
|
|
61
|
+
if (existsSync(p)) return { text: readFileSync(p, 'utf8'), source: 'worktree' };
|
|
62
|
+
}
|
|
63
|
+
return { text: null, source: null };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 兼容旧签名:只要文本 */
|
|
67
|
+
function readLocalYaml(specBase, worktreePath) {
|
|
68
|
+
return readLocalYamlSourced(specBase, worktreePath).text;
|
|
57
69
|
}
|
|
58
70
|
|
|
59
71
|
/** 从 local.yaml 文本提取 commands.install(轻量正则,不引 yaml 依赖,与 scan-postcheck 一致)*/
|
|
@@ -93,6 +105,10 @@ export function inferInstallCommand(projectType, worktreePath, userInstall) {
|
|
|
93
105
|
case 'maven':
|
|
94
106
|
return 'mvn -o test';
|
|
95
107
|
case 'gradle':
|
|
108
|
+
// win32 cmd.exe 跑不了 ./gradlew(体检 BUG-08 同型):优先 gradlew.bat,否则全局 gradle
|
|
109
|
+
if (process.platform === 'win32') {
|
|
110
|
+
return existsSync(join(worktreePath, 'gradlew.bat')) ? 'gradlew.bat test' : 'gradle test';
|
|
111
|
+
}
|
|
96
112
|
return './gradlew test';
|
|
97
113
|
case 'python':
|
|
98
114
|
// uv 优先(现代 Python 工具链,pyproject.toml/uv.lock 项目走 uv sync 建 .venv + 装依赖,
|
|
@@ -130,15 +146,80 @@ function tryLink(mainNodeModules, linkPath) {
|
|
|
130
146
|
}
|
|
131
147
|
}
|
|
132
148
|
|
|
133
|
-
/**
|
|
149
|
+
/**
|
|
150
|
+
* 执行 install 命令(带超时)。
|
|
151
|
+
*
|
|
152
|
+
* 安全收口(体检 SEC-01):commands.install 来自 local.yaml 配置,若经 execSync(shell)
|
|
153
|
+
* 原样执行,yaml 里的 `$(...)`/反引号/`;` 必然被解释——且 worktree 副本属 agent 可写区。
|
|
154
|
+
* 三道门:
|
|
155
|
+
* 1. 调用方保证来源可信(provisionDeps 只对 source==='specBase' 的 userInstall 走到这里)
|
|
156
|
+
* 2. 可执行前缀白名单(包管理器家族)
|
|
157
|
+
* 3. shell 元字符黑名单(win32 额外拦 %,cmd.exe 会展开 %VAR%),通过后拆 argv 数组执行不经 shell
|
|
158
|
+
*
|
|
159
|
+
* 链式命令支持(坑 worktree-install-whitelist-monorepo-chain,2026-08-20 实证):monorepo 的
|
|
160
|
+
* commands.install 常为链式写法(`cd web && pnpm install` / `npm install && npm run build:pkg`),
|
|
161
|
+
* 旧实现整条过白名单 + 元字符门,`&&` 必拒 → depsStatus=failed → execute deps 门控卡死且无自愈
|
|
162
|
+
* 路径(改 local.yaml 成单命令对需要 post-install build 的 monorepo 不可行)。现按 `&&` 拆段
|
|
163
|
+
* 逐段校验执行:每段独立过白名单 + 元字符门(拆分后残余的单 `&`/`|`/`;` 仍被元字符门拦),
|
|
164
|
+
* 段内允许 `cd <相对子路径>`(resolve 后必须仍在 worktree 根内,防越界建产物/读外部),
|
|
165
|
+
* 全程 argv 数组执行不经 shell、任一段失败即停(&& 语义)。安全面不变:每个执行段仍是
|
|
166
|
+
* 白名单包管理器命令,`||`/管道/`;`/后台均不支持。
|
|
167
|
+
*/
|
|
168
|
+
const INSTALL_BINARY_WHITELIST = /^(?:\.\/)?(?:pnpm|npm|yarn|bun|mvn|gradle|gradlew|uv|pip|pip3|python|python3|poetry|make)(?:\.cmd|\.bat|\.exe)?(?:\s|$)/;
|
|
169
|
+
const SHELL_METACHARS = /[;&|<>$`\n]/;
|
|
170
|
+
const WIN_SHELL_METACHARS = /[;&|<>$`\n%]/;
|
|
171
|
+
const CD_SEGMENT_RE = /^cd\s+([^\s]+)$/;
|
|
172
|
+
|
|
134
173
|
function tryInstall(cmd, cwd, timeout) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
return { ok:
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
174
|
+
const trimmed = String(cmd || '').trim();
|
|
175
|
+
if (!trimmed) {
|
|
176
|
+
return { ok: false, error: 'install 命令为空' };
|
|
177
|
+
}
|
|
178
|
+
const metaRe = process.platform === 'win32' ? WIN_SHELL_METACHARS : SHELL_METACHARS;
|
|
179
|
+
// && 拆段(单段命令与原路径完全一致,零回归);空段(`a &&`/`&& b`)视为畸形命令拒绝
|
|
180
|
+
const segments = trimmed.split('&&').map(s => s.trim());
|
|
181
|
+
if (segments.some(s => s === '')) {
|
|
182
|
+
return { ok: false, error: `install 链式命令含空段,拒绝执行: ${trimmed}` };
|
|
141
183
|
}
|
|
184
|
+
let curCwd = cwd;
|
|
185
|
+
const root = resolvePath(cwd);
|
|
186
|
+
const planned = [];
|
|
187
|
+
for (const seg of segments) {
|
|
188
|
+
// 白名单先于元字符(保持原判定顺序):`curl … | sh` 报非白名单而非元字符,
|
|
189
|
+
// `npm install; rm …` 过白名单后被元字符门拦——两类拒绝语义与原实现一致
|
|
190
|
+
const cd = seg.match(CD_SEGMENT_RE);
|
|
191
|
+
if (!cd && !INSTALL_BINARY_WHITELIST.test(seg)) {
|
|
192
|
+
return { ok: false, error: `install 命令不在包管理器白名单内,拒绝执行: ${seg}(支持 && 链式与 cd <子目录> 段,各执行段仍须为白名单包管理器命令)` };
|
|
193
|
+
}
|
|
194
|
+
if (metaRe.test(seg)) {
|
|
195
|
+
return { ok: false, error: `install 命令段含 shell 元字符,拒绝执行: ${seg}(链式仅支持 &&;|| / 管道 / ; / 后台不支持)` };
|
|
196
|
+
}
|
|
197
|
+
if (cd) {
|
|
198
|
+
const target = resolvePath(curCwd, cd[1]);
|
|
199
|
+
if (target !== root && !target.startsWith(root + pathSep)) {
|
|
200
|
+
return { ok: false, error: `install 命令的 cd 段越出 worktree 根,拒绝执行: ${seg}(cwd ${cwd})` };
|
|
201
|
+
}
|
|
202
|
+
curCwd = target;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
planned.push(seg);
|
|
206
|
+
}
|
|
207
|
+
for (const seg of planned) {
|
|
208
|
+
const argv = seg.split(/\s+/);
|
|
209
|
+
try {
|
|
210
|
+
if (process.platform === 'win32') {
|
|
211
|
+
// Windows 包管理器是 .cmd 垫片,无 shell 的 spawn 无法解析——经 cmd.exe /c 传参;
|
|
212
|
+
// 元字符(含 %)已拦,残余风险与 tryLink 的 mklink 同款
|
|
213
|
+
execFileSync('cmd.exe', ['/c', ...argv], { cwd: curCwd, timeout, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
214
|
+
} else {
|
|
215
|
+
execFileSync(argv[0], argv.slice(1), { cwd: curCwd, timeout, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
216
|
+
}
|
|
217
|
+
} catch (e) {
|
|
218
|
+
const msg = e.killed ? `timeout after ${timeout}ms` : ((e.stderr && e.stderr.toString()) || e.message);
|
|
219
|
+
return { ok: false, error: `${seg} failed (cwd ${curCwd}): ${msg}` };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return { ok: true };
|
|
142
223
|
}
|
|
143
224
|
|
|
144
225
|
/** 目录是否含 nodejs 标记(package.json 或任一 lockfile)——识别 monorepo 里的 nodejs 子模块 */
|
|
@@ -174,6 +255,16 @@ function extractModulePaths(yamlText) {
|
|
|
174
255
|
return [...new Set(paths)];
|
|
175
256
|
}
|
|
176
257
|
|
|
258
|
+
/**
|
|
259
|
+
* modules 块 path 安全校验(体检 SEC-07):只允许 worktree 内的相对子路径——
|
|
260
|
+
* 拒绝绝对路径与 `..` 段,防止 local.yaml 配置把 junction/symlink 建到 worktree 之外。
|
|
261
|
+
*/
|
|
262
|
+
function isSafeModulePath(p) {
|
|
263
|
+
if (!p || isAbsolute(p)) return false;
|
|
264
|
+
const segs = p.split(/[\\/]+/);
|
|
265
|
+
return !segs.includes('..');
|
|
266
|
+
}
|
|
267
|
+
|
|
177
268
|
/**
|
|
178
269
|
* 对单个子模块目录 tryLink main 的 node_modules → wt 的 node_modules(modules 子模块专用)。
|
|
179
270
|
* 仅走 link 快路径(不 install——子模块 install 慢且易失败;lockfile 不一致时交给用户 pnpm install)。
|
|
@@ -211,12 +302,17 @@ function linkOneDir(wtDir, mainDir) {
|
|
|
211
302
|
export function provisionDeps(worktreePath, mainCwd, opts = {}) {
|
|
212
303
|
const { specBase = null, timeout = DEFAULT_TIMEOUT_MS, force = false } = opts;
|
|
213
304
|
const depsCheckedAt = new Date().toISOString();
|
|
214
|
-
const yamlText =
|
|
305
|
+
const { text: yamlText, source: yamlSource } = readLocalYamlSourced(specBase, worktreePath);
|
|
215
306
|
const wtHash = lockfileHash(worktreePath);
|
|
216
307
|
|
|
217
308
|
// ── 1. 根目录供给 ──
|
|
218
309
|
const projectType = detectProjectType(worktreePath, specBase);
|
|
219
|
-
|
|
310
|
+
// SEC-01:commands.install 只信主仓 specBase 的 local.yaml——worktree 副本是 agent 可写区,
|
|
311
|
+
// 其 install 命令若执行等于绕过 worktree-guard 的危险命令拦截
|
|
312
|
+
const userInstall = yamlSource === 'specBase' ? extractUserInstall(yamlText) : null;
|
|
313
|
+
if (yamlSource === 'worktree' && extractUserInstall(yamlText)) {
|
|
314
|
+
console.warn('⚠️ worktree 内 local.yaml 的 commands.install 被忽略(agent 可写区,只信主仓 .sillyspec/local.yaml)');
|
|
315
|
+
}
|
|
220
316
|
const installCmd = inferInstallCommand(projectType, worktreePath, userInstall);
|
|
221
317
|
|
|
222
318
|
let result;
|
|
@@ -257,6 +353,10 @@ export function provisionDeps(worktreePath, mainCwd, opts = {}) {
|
|
|
257
353
|
const modulePaths = extractModulePaths(yamlText);
|
|
258
354
|
const moduleResults = [];
|
|
259
355
|
for (const mp of modulePaths) {
|
|
356
|
+
if (!isSafeModulePath(mp)) {
|
|
357
|
+
moduleResults.push({ path: mp, status: 'skipped', reason: 'path 越界(绝对路径或 .. 段),拒绝 link' });
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
260
360
|
const wtDir = join(worktreePath, mp);
|
|
261
361
|
const mainDir = mainCwd ? join(mainCwd, mp) : null;
|
|
262
362
|
if (!existsSync(wtDir) || !mainDir || !hasNodeMarker(wtDir)) continue;
|
package/src/worktree.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* 分支命名:sillyspec/<change-name>
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { execFileSync } from 'child_process';
|
|
12
12
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, readdirSync, statSync, lstatSync, readlinkSync, unlinkSync } from 'fs';
|
|
13
13
|
import { join, resolve, dirname, relative, isAbsolute } from 'path';
|
|
14
14
|
import { createHash } from 'crypto';
|
|
@@ -463,6 +463,12 @@ export class WorktreeManager {
|
|
|
463
463
|
if (!check.supported) {
|
|
464
464
|
throw new Error(`git worktree add 失败: ${e.stderr || e.message}\n\n${check.reason ? `原因: ${check.reason}` : ''}\n建议: 升级 git 到 >= 2.15;或运行 \`sillyspec worktree doctor --fix\` 检查 worktree 状态。`);
|
|
465
465
|
}
|
|
466
|
+
// 体检 BUG-16 并发竞态防御:多 agent 同时首建同名 change 时,败者的 add 失败可能只是
|
|
467
|
+
// 「赢家刚建成目录/分支」。此时降级 in-place 会造成一个 worktree + 一个 in-place 的
|
|
468
|
+
// 分裂状态(两 agent 各写各的基线)——先重查,并发赢家已建成则按 already exists 抛错。
|
|
469
|
+
if (existsSync(worktreePath) || gitQuiet(this.cwd, ['rev-parse', '--verify', `refs/heads/${branch}`])) {
|
|
470
|
+
throw new Error(`worktree already exists: ${name}(并发创建,另一进程已抢先建成,本进程不降级 in-place). Run cleanup first.`);
|
|
471
|
+
}
|
|
466
472
|
// sandbox/permission fallback: 降级为 in-place + baseline protection
|
|
467
473
|
console.log(`⚠️ git worktree add 失败(可能是沙箱权限限制),降级为 in-place 模式 + baseline protection`);
|
|
468
474
|
console.log(` 原因: ${e.stderr || e.message}`);
|
|
@@ -928,7 +934,11 @@ export class WorktreeManager {
|
|
|
928
934
|
const residual = [];
|
|
929
935
|
if (!isInPlace && existsSync(worktreePath)) residual.push(`worktree dir: ${worktreePath}`);
|
|
930
936
|
if (existsSync(metaDir)) residual.push(`meta dir: ${metaDir}`);
|
|
931
|
-
|
|
937
|
+
// 斜杠方向归一后比较(体检 BUG-07):Windows 下 git worktree list 输出正斜杠、
|
|
938
|
+
// worktreePath 是反斜杠,直接 includes 恒 false → 残留场景漏报、cleanup 误报 cleaned
|
|
939
|
+
const toPosixNorm = (p) => String(p || '').replace(/\\/g, '/').toLowerCase()
|
|
940
|
+
const wtListOut = gitQuiet(this.cwd, ['worktree', 'list'], { timeout: 30000 })
|
|
941
|
+
if (!isInPlace && wtListOut && toPosixNorm(wtListOut).includes(toPosixNorm(worktreePath))) {
|
|
932
942
|
residual.push('git worktree list still references this worktree');
|
|
933
943
|
}
|
|
934
944
|
if (residual.length > 0) {
|
|
@@ -1033,11 +1043,32 @@ export class WorktreeManager {
|
|
|
1033
1043
|
* 不传则全量扫(兼容现有行为)。
|
|
1034
1044
|
* @returns {{ issues: Array<{ type: string, name: string, detail: string, fixable: boolean }>, fixed: string[], unfixable: string[] }}
|
|
1035
1045
|
*/
|
|
1036
|
-
doctor({ fix = false, staleHours = 24, changeName = null } = {}) {
|
|
1046
|
+
async doctor({ fix = false, staleHours = 24, changeName = null } = {}) {
|
|
1037
1047
|
const issues = [];
|
|
1038
1048
|
const fixed = [];
|
|
1039
1049
|
const unfixable = [];
|
|
1040
1050
|
|
|
1051
|
+
// 活跃变更注册表(坑 doctor-fix-orphan-branch-parallel-active,2026-08-20 实证):孤儿分支
|
|
1052
|
+
// 判定原只看本地 meta 目录注册表(metaNames),与变更活跃态的权威注册表(进度库 changes 表)
|
|
1053
|
+
// 数据源不一致——并行会话的活跃变更(meta 已清/in-place/平台模式 meta 在别处)分支会被全局
|
|
1054
|
+
// doctor --fix 误删。删分支前交叉核对:分支名 ∈ 活跃变更 → 保留(fixable:false 提示人工确认)。
|
|
1055
|
+
// 先探 DB 文件存在再实例化 ProgressManager(坑7 读路径建库:_ensureDB 不存在会建库污染)。
|
|
1056
|
+
// null = 进度库存在但读失败(无法判定活跃态,保守不自动删);空 Set = 无库/无活跃变更
|
|
1057
|
+
// (git-only 工作流零回归,孤儿分支照删)。
|
|
1058
|
+
let activeChanges = null;
|
|
1059
|
+
try {
|
|
1060
|
+
const specBaseOfWt = dirname(dirname(this.worktreeBase)); // <specBase>(.runtime/worktrees 上两级)
|
|
1061
|
+
if (existsSync(join(specBaseOfWt, '.runtime', 'sillyspec.db'))) {
|
|
1062
|
+
const { ProgressManager } = await import('./progress.js');
|
|
1063
|
+
activeChanges = new Set(new ProgressManager({ specDir: specBaseOfWt }).listChanges(this.cwd));
|
|
1064
|
+
} else {
|
|
1065
|
+
activeChanges = new Set();
|
|
1066
|
+
}
|
|
1067
|
+
} catch (e) {
|
|
1068
|
+
console.warn(`⚠️ doctor 进度库读取失败,孤儿分支的活跃态交叉核对不可用(保守不自动删): ${e.message}`);
|
|
1069
|
+
activeChanges = null;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1041
1072
|
// 1. 列出 git worktree list 中的条目
|
|
1042
1073
|
let gitWorktreeList = [];
|
|
1043
1074
|
try {
|
|
@@ -1167,16 +1198,30 @@ export class WorktreeManager {
|
|
|
1167
1198
|
}
|
|
1168
1199
|
}
|
|
1169
1200
|
|
|
1170
|
-
// 5. 检查 SillySpec
|
|
1201
|
+
// 5. 检查 SillySpec 分支残留(活跃变更交叉核对,见方法头 activeChanges 注释)
|
|
1171
1202
|
try {
|
|
1172
|
-
|
|
1203
|
+
// QUAL-01 收口:裸 execFileSync → git-helper git(原调用点未传 timeout,统一入口默认 5s 足够 branch --list)
|
|
1204
|
+
const branches = git(this.cwd, ['branch', '--list', `${BRANCH_PREFIX}*`]);
|
|
1173
1205
|
if (branches) {
|
|
1174
1206
|
for (const line of branches.split('\n').filter(Boolean)) {
|
|
1175
1207
|
const branch = line.replace(/^\*?\s+/, '').trim();
|
|
1176
1208
|
const name = branch.replace(BRANCH_PREFIX, '');
|
|
1177
1209
|
if (changeName && name !== changeName) continue; // --change 过滤:仅扫指定 change
|
|
1178
1210
|
if (!metaNames.has(name)) {
|
|
1179
|
-
|
|
1211
|
+
if (activeChanges && activeChanges.has(name)) {
|
|
1212
|
+
// 变更仍注册活跃:分支大概率是并行会话在用(或 apply 后被 review 引用审计保护保留)
|
|
1213
|
+
// ——数据源不一致的正解是保留 + 人工确认,不是删
|
|
1214
|
+
issues.push({ type: 'active-branch', name, fixable: false,
|
|
1215
|
+
detail: `分支 ${branch} 无本地 meta,但变更 "${name}" 在进度库仍注册为活跃(并行会话在跑 / meta 已清分支在用)——保留。确认废弃后:先归档/结束该变更,或手动 git branch -D ${branch}` });
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
if (activeChanges === null) {
|
|
1219
|
+
// 进度库读失败:无法判定活跃态,保守不自动删(宁可漏删不可误删并行会话分支)
|
|
1220
|
+
issues.push({ type: 'orphan-branch', name, fixable: false,
|
|
1221
|
+
detail: `分支疑似残留(无对应 meta,且进度库不可读无法交叉核对活跃态): ${branch}——保守不自动删,人工确认后 git branch -D ${branch}` });
|
|
1222
|
+
continue;
|
|
1223
|
+
}
|
|
1224
|
+
issues.push({ type: 'orphan-branch', name, detail: `分支残留(无对应 meta,且非活跃变更): ${branch}`, fixable: true });
|
|
1180
1225
|
if (fix) {
|
|
1181
1226
|
try { gitQuiet(this.cwd, ['branch', '-D', branch]); fixed.push(`deleted orphan branch: ${branch}`); } catch { unfixable.push(`branch delete failed: ${branch}`); }
|
|
1182
1227
|
}
|
|
@@ -1357,7 +1402,8 @@ export class WorktreeManager {
|
|
|
1357
1402
|
if (staged) {
|
|
1358
1403
|
try {
|
|
1359
1404
|
// 用 Buffer 模式读取,避免二进制 patch 被 UTF-8 解码损坏
|
|
1360
|
-
|
|
1405
|
+
// QUAL-01 收口:git-helper 新增 encoding:'buffer' 支持(二进制输出专用,跳过 trim)
|
|
1406
|
+
const patchBuf = git(mainCwd, ['diff', '--cached', '--binary'], { encoding: 'buffer', timeout: 30000 });
|
|
1361
1407
|
if (patchBuf && patchBuf.length > 0) {
|
|
1362
1408
|
const patchFile = join(worktreePath, '.sillyspec-baseline-staged.patch');
|
|
1363
1409
|
try {
|
|
@@ -1379,8 +1425,8 @@ export class WorktreeManager {
|
|
|
1379
1425
|
const unstaged = gitQuiet(mainCwd, ['diff', '--name-only'], { timeout: 30000 }) || '';
|
|
1380
1426
|
if (unstaged) {
|
|
1381
1427
|
try {
|
|
1382
|
-
// 用 Buffer 模式读取,避免二进制 patch 被 UTF-8
|
|
1383
|
-
const patchBuf =
|
|
1428
|
+
// 用 Buffer 模式读取,避免二进制 patch 被 UTF-8 解码损坏(QUAL-01 收口,同上)
|
|
1429
|
+
const patchBuf = git(mainCwd, ['diff', '--binary'], { encoding: 'buffer', timeout: 30000 });
|
|
1384
1430
|
if (patchBuf && patchBuf.length > 0) {
|
|
1385
1431
|
const patchFile = join(worktreePath, '.sillyspec-baseline-unstaged.patch');
|
|
1386
1432
|
try {
|
package/.npmrc-publish
DELETED