sillyspec 3.22.9 → 3.23.1
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/package.json +1 -1
- package/src/hooks/worktree-guard.js +86 -31
- package/src/index.js +147 -7
- package/src/local-detect.js +110 -0
- package/src/machine-interface.js +471 -0
- package/src/progress.js +110 -5
- package/src/run.js +176 -48
- package/src/stage-contract.js +107 -1
- package/src/stages/execute.js +2 -2
- package/src/stages/quick.js +11 -0
- package/src/stages/scan.js +7 -16
- package/src/stages/verify.js +3 -1
- package/src/sync.js +73 -5
- package/src/task-review.js +110 -4
- package/src/verify-postcheck.js +433 -0
- package/src/worktree-apply.js +73 -6
- package/src/worktree.js +12 -0
package/src/worktree-apply.js
CHANGED
|
@@ -34,6 +34,20 @@ function gitQuiet(cwd, args) {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* 过滤掉 worktree 基础设施文件(非交付物),让 apply 只关心真正的变更产出:
|
|
39
|
+
* - meta.json:worktree 元数据,baseline commit 中被跟踪、working-tree 被 CLI 改写
|
|
40
|
+
* (provisioning→linked 等)。它必须保持 modified(其 baselineCommit 字段是 apply diff
|
|
41
|
+
* 的锚点,保证 baseline overlay 文件不被误判为变更)。若不排除,modified-tracked 的
|
|
42
|
+
* meta.json 会落入 changedFiles,触发「不在 design.md 清单」误判,导致每个 execute 的
|
|
43
|
+
* assess 恒 BLOCKED。
|
|
44
|
+
* - .sillyspec/:变更文档 / 运行时产物,不属于源码交付。
|
|
45
|
+
* 对 modified-tracked(git diff)与 untracked(ls-files --others)一视同仁。
|
|
46
|
+
*/
|
|
47
|
+
export function filterDeliverableFiles(files) {
|
|
48
|
+
return files.filter(f => !f.startsWith('.sillyspec/') && f !== 'meta.json');
|
|
49
|
+
}
|
|
50
|
+
|
|
37
51
|
/**
|
|
38
52
|
* 获取文件在 git 中的 blob hash(基于某个 commit/tree)
|
|
39
53
|
* @param {string} cwd - git 工作区路径
|
|
@@ -59,7 +73,7 @@ function getFileBlobHash(cwd, treeish, filePath) {
|
|
|
59
73
|
* errors: string[]
|
|
60
74
|
* }}
|
|
61
75
|
*/
|
|
62
|
-
export function applyWorktree(changeName, { cwd, checkOnly = false } = {}) {
|
|
76
|
+
export function applyWorktree(changeName, { cwd, checkOnly = false, merge = false } = {}) {
|
|
63
77
|
const projectRoot = cwd || process.cwd();
|
|
64
78
|
const wm = new WorktreeManager({ cwd: projectRoot });
|
|
65
79
|
const meta = wm.getMeta(changeName);
|
|
@@ -70,6 +84,7 @@ export function applyWorktree(changeName, { cwd, checkOnly = false } = {}) {
|
|
|
70
84
|
hashMismatchFiles: [],
|
|
71
85
|
patchPath: null,
|
|
72
86
|
errors: [],
|
|
87
|
+
merged: false,
|
|
73
88
|
};
|
|
74
89
|
|
|
75
90
|
// --- 1. 校验 worktree 存在 + meta.json 有效 ---
|
|
@@ -106,11 +121,12 @@ export function applyWorktree(changeName, { cwd, checkOnly = false } = {}) {
|
|
|
106
121
|
|
|
107
122
|
// untracked 新文件(diffBase 中不存在的文件)
|
|
108
123
|
const untrackedRaw = gitQuiet(worktreePath, `ls-files --others --exclude-standard`);
|
|
109
|
-
const untrackedFiles = untrackedRaw
|
|
110
|
-
? untrackedRaw.split('\n').filter(Boolean).filter(f => !f.startsWith('.sillyspec/') && f !== 'meta.json')
|
|
111
|
-
: [];
|
|
124
|
+
const untrackedFiles = untrackedRaw ? untrackedRaw.split('\n').filter(Boolean) : [];
|
|
112
125
|
|
|
113
|
-
|
|
126
|
+
// 排除 worktree 基础设施文件(meta.json / .sillyspec/,见 filterDeliverableFiles)。
|
|
127
|
+
// 对 modified-tracked(statusFiles)与 untracked 一视同仁——否则 modified 的 meta.json
|
|
128
|
+
// 会被误算入 changedFiles,触发 design.md 清单校验失败(assess 恒 BLOCKED)。
|
|
129
|
+
changedFiles = filterDeliverableFiles([...new Set([...statusFiles, ...untrackedFiles])]);
|
|
114
130
|
} catch (e) {
|
|
115
131
|
result.errors.push(`获取变更文件列表失败: ${e.message}`);
|
|
116
132
|
return result;
|
|
@@ -157,9 +173,13 @@ export function applyWorktree(changeName, { cwd, checkOnly = false } = {}) {
|
|
|
157
173
|
const raw = `staged:${staged}\nunstaged:${unstaged}\nuntracked:${untracked}`;
|
|
158
174
|
const currentHash = createHash('sha256').update(raw).digest('hex').slice(0, 16);
|
|
159
175
|
if (currentHash !== meta.baselineHash) {
|
|
176
|
+
if (merge && !checkOnly) {
|
|
177
|
+
// baseline 漂移 + --merge 降级(D-001):跳过步骤 5-7(patch 路径),走 git merge
|
|
178
|
+
return applyByMerge(result, changeName, projectRoot, wm);
|
|
179
|
+
}
|
|
160
180
|
result.errors.push(
|
|
161
181
|
`主工作区 baseline 已变化(execute 前后不一致),不能直接 apply task.patch。\n` +
|
|
162
|
-
`建议:重新创建 worktree
|
|
182
|
+
`建议:重新创建 worktree,或用 --merge 降级(git merge sillyspec/${changeName} 替代 patch,会引入合并提交)。\n` +
|
|
163
183
|
`execute 前 baseline: ${meta.baselineHash}\n` +
|
|
164
184
|
`当前 baseline: ${currentHash}`
|
|
165
185
|
);
|
|
@@ -305,6 +325,53 @@ export function applyWorktree(changeName, { cwd, checkOnly = false } = {}) {
|
|
|
305
325
|
return result;
|
|
306
326
|
}
|
|
307
327
|
|
|
328
|
+
/**
|
|
329
|
+
* baseline 漂移时的 merge 降级路径(D-001)。
|
|
330
|
+
*
|
|
331
|
+
* 当主工作区 baseline 在 execute 期间漂移、patch 无法干净应用时,用
|
|
332
|
+
* `git merge sillyspec/<change>` 替代 patch apply(BRANCH_PREFIX='sillyspec/',worktree.js:18)。
|
|
333
|
+
* git merge 比 patch 鲁棒,能处理 baseline 漂移 + 潜在冲突。会引入合并提交
|
|
334
|
+
* (D-002:与 worktree.md:84「patch 而非 merge 保持线性历史」架构决策的张力——
|
|
335
|
+
* 仅作 --merge 显式 opt-in,不改变默认 patch 行为)。
|
|
336
|
+
*
|
|
337
|
+
* merge 冲突时不自动解决:git merge --abort 回滚到合并前状态 + 报冲突文件列表。
|
|
338
|
+
*
|
|
339
|
+
* @param {object} result - applyWorktree 的 result 对象(mutate 后返回)
|
|
340
|
+
* @param {string} changeName
|
|
341
|
+
* @param {string} projectRoot - 主仓库根
|
|
342
|
+
* @param {object} wm - WorktreeManager 实例
|
|
343
|
+
* @returns {object} result(merged=true 表示走了 merge 降级)
|
|
344
|
+
*/
|
|
345
|
+
function applyByMerge(result, changeName, projectRoot, wm) {
|
|
346
|
+
try {
|
|
347
|
+
git(projectRoot, `merge --no-ff sillyspec/${changeName}`);
|
|
348
|
+
result.merged = true;
|
|
349
|
+
result.ok = true;
|
|
350
|
+
try { result.mergeSummary = git(projectRoot, `log --oneline -1`); } catch {}
|
|
351
|
+
// 成功后自动 cleanup(与 patch 路径步骤 8 一致;分支已合并,删除安全)
|
|
352
|
+
try {
|
|
353
|
+
wm.cleanup(changeName);
|
|
354
|
+
} catch (cleanupErr) {
|
|
355
|
+
result.warnings = result.warnings || [];
|
|
356
|
+
result.warnings.push(`cleanup 失败(不影响 merge 结果): ${cleanupErr.message}`);
|
|
357
|
+
}
|
|
358
|
+
} catch (e) {
|
|
359
|
+
// merge 冲突:先取冲突文件列表,再 abort 回滚(避免半成品合并)
|
|
360
|
+
let conflictFiles = [];
|
|
361
|
+
try {
|
|
362
|
+
const cf = gitQuiet(projectRoot, `diff --name-only --diff-filter=U`);
|
|
363
|
+
conflictFiles = cf ? cf.split('\n').filter(Boolean) : [];
|
|
364
|
+
} catch {}
|
|
365
|
+
try { gitQuiet(projectRoot, `merge --abort`); } catch {}
|
|
366
|
+
result.errors.push(
|
|
367
|
+
`git merge sillyspec/${changeName} 冲突,请手动解决。冲突文件:\n` +
|
|
368
|
+
(conflictFiles.length ? ` ${conflictFiles.join('\n ')}\n` : ` (未能获取冲突文件列表)\n`) +
|
|
369
|
+
`已执行 git merge --abort 回滚到合并前状态。`
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
return result;
|
|
373
|
+
}
|
|
374
|
+
|
|
308
375
|
/**
|
|
309
376
|
* 风险审计:评估 worktree 变更是否可以安全自动 apply
|
|
310
377
|
*
|
package/src/worktree.js
CHANGED
|
@@ -453,6 +453,16 @@ export class WorktreeManager {
|
|
|
453
453
|
return { branch: existingMeta.branch, worktreePath: existingMeta.worktreePath, baseHash: existingMeta.baseHash, mode: existingMeta.mode }
|
|
454
454
|
}
|
|
455
455
|
|
|
456
|
+
// 供给 deps(与 native worktree 路径一致)。in-place-fallback 模式漏写 depsStatus 会致
|
|
457
|
+
// enforceDepsGate(run.js)把 undefined 当 unknown 阻断 execute --done,死锁无法推进。
|
|
458
|
+
// 见 docs/sillyspec/execute-inplace-deps-gate.md(2026-07-08 发现)。
|
|
459
|
+
let deps = {}
|
|
460
|
+
try {
|
|
461
|
+
deps = provisionDeps(worktreePath, this.cwd, { specBase: join(this.cwd, '.sillyspec') }) || {}
|
|
462
|
+
} catch (e) {
|
|
463
|
+
deps = { depsStatus: 'failed', depsError: `provisionDeps crashed: ${e.message}` }
|
|
464
|
+
}
|
|
465
|
+
|
|
456
466
|
// 硬规则:禁止 self-overlay(source 和 target 相同时 overlay 必然冲突)
|
|
457
467
|
const resolvedSource = resolve(this.cwd)
|
|
458
468
|
const resolvedTarget = resolve(worktreePath)
|
|
@@ -474,6 +484,7 @@ export class WorktreeManager {
|
|
|
474
484
|
baselineFiles: [],
|
|
475
485
|
baselineCommit: null,
|
|
476
486
|
baselineHash: null,
|
|
487
|
+
...deps,
|
|
477
488
|
}
|
|
478
489
|
if (!existsSync(this.worktreeBase)) mkdirSync(this.worktreeBase, { recursive: true })
|
|
479
490
|
const metaDir = join(this.worktreeBase, name)
|
|
@@ -510,6 +521,7 @@ export class WorktreeManager {
|
|
|
510
521
|
baselineFiles,
|
|
511
522
|
baselineCommit,
|
|
512
523
|
baselineHash,
|
|
524
|
+
...deps,
|
|
513
525
|
};
|
|
514
526
|
|
|
515
527
|
// in-place 模式下 meta 写入 worktreeBase(避免污染主工作区)
|