sillyspec 3.25.3 → 3.25.5
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-brainstorm/SKILL.md +26 -0
- package/.claude/skills/sillyspec-execute/SKILL.md +56 -0
- package/.claude/skills/sillyspec-plan/SKILL.md +26 -0
- package/.claude/skills/sillyspec-propose/SKILL.md +26 -0
- package/.claude/skills/sillyspec-quick/SKILL.md +2 -1
- package/package.json +1 -1
- package/src/change-list.js +14 -0
- package/src/change-risk-profile.js +36 -1
- package/src/index.js +3 -0
- package/src/machine-interface.js +5 -1
- package/src/progress/stage-machine.js +17 -14
- package/src/run/command.js +1 -1
- package/src/run/complete-handlers.js +4 -2
- package/src/run/complete.js +110 -32
- package/src/run/prompt.js +20 -0
- package/src/stage-contract.js +33 -7
- package/src/stage-review.js +16 -6
- package/src/stages/archive.js +13 -9
- package/src/stages/brainstorm-auto.js +3 -2
- package/src/stages/brainstorm.js +4 -2
- package/src/stages/execute.js +4 -3
- package/src/stages/plan-postcheck.js +3 -2
- package/src/stages/plan.js +7 -0
- package/src/stages/propose.js +3 -2
- package/src/stages/quick.js +18 -4
- package/src/stages/status.js +2 -2
- package/src/stages/verify.js +13 -5
- package/src/sync.js +10 -3
- package/src/task-review.js +149 -3
- package/src/verify-postcheck.js +208 -38
- package/src/worktree-apply.js +101 -39
- package/src/worktree.js +106 -32
package/src/worktree.js
CHANGED
|
@@ -138,10 +138,11 @@ function printSyncReport(diag, baseHash, changeName) {
|
|
|
138
138
|
const ch = changeName || '<change>';
|
|
139
139
|
if (diag.status === 'behind') {
|
|
140
140
|
console.log(`⚠️ base 同步检测:落后 origin/${b} ${diag.behind} 个 commit(base ${short})`);
|
|
141
|
-
console.log(`
|
|
141
|
+
console.log(` apply 时 git --3way 会自动三路合并主干已提交推进;只有同文件同区域重叠才冲突。`);
|
|
142
|
+
console.log(` 建议先对齐以减小冲突面:`);
|
|
142
143
|
console.log(` git merge --ff-only origin/${b}`);
|
|
143
144
|
console.log(` sillyspec worktree cleanup ${ch} && sillyspec run execute --change ${ch}`);
|
|
144
|
-
console.log(`
|
|
145
|
+
console.log(` 或继续,重叠冲突时用 --merge 兜底(会引入合并提交)。`);
|
|
145
146
|
} else if (diag.status === 'diverged') {
|
|
146
147
|
console.log(`⚠️ base 同步检测:与 origin/${b} 分叉(领先 ${diag.ahead} / 落后 ${diag.behind},base ${short})`);
|
|
147
148
|
console.log(` apply 回 main 大概率冲突。建议先在主仓库对齐(rebase 或 merge origin/${b}),`);
|
|
@@ -170,11 +171,12 @@ function sleepMs(ms) {
|
|
|
170
171
|
export function computeBaselineHash(cwd) {
|
|
171
172
|
// 排除非交付物的元数据/文档 churn,避免多操作者仓库里别人改这些 → 整树 hash 变 → apply 误阻断
|
|
172
173
|
// (execute 自身也会改 .sillyspec/docs.sillyspec,否则每个 execute 收尾都因自身改动 BLOCKED)。
|
|
173
|
-
// 安全:coarse hash
|
|
174
|
-
//
|
|
174
|
+
// 安全:coarse hash 只挡「未提交」dirty(git apply --3way 危险区,实测必拦);真冲突由
|
|
175
|
+
// applyWorktree step5a(未提交∩changedFiles 精确点名)+step7(--3way 冲突回滚)兜底,
|
|
176
|
+
// 「已提交」推进不再拦(step5b 已放宽交 --3way 自动三路合并),不读这里的 exclude。
|
|
175
177
|
// - .sillyspec/:brainstorm/plan 蓝图 + runtime 产物
|
|
176
178
|
// - .claude/:agent 配置/skills/CLAUDE.md(多操作者 agent 指引 churn)
|
|
177
|
-
// - docs
|
|
179
|
+
// - docs/:文档(非代码交付物)
|
|
178
180
|
// - CLAUDE.md:根 agent 指引(多操作者常改)
|
|
179
181
|
// 必须和 applyWorktree step 4.5 (worktree-apply.js) 使用相同的排除规则。
|
|
180
182
|
const exclude = '-- . ":(exclude).sillyspec/" ":(exclude).claude/" ":(exclude)docs/" ":(exclude)CLAUDE.md"';
|
|
@@ -247,15 +249,16 @@ export class WorktreeManager {
|
|
|
247
249
|
|
|
248
250
|
let root = this.cwd;
|
|
249
251
|
try {
|
|
250
|
-
// git-common-dir 在主仓库内 = <main>/.git
|
|
251
|
-
//
|
|
252
|
+
// git-common-dir 在主仓库内 = <main>/.git,在 linked worktree 内 = <main>/.git(git 共享 .git 目录)。
|
|
253
|
+
// 但 git 可能返回**相对路径**(如 `.git`)。必须相对 this.cwd 绝对化,否则下面的
|
|
254
|
+
// existsSync/statSync 会相对 process.cwd() 解析——当 process.cwd 是另一个 git 仓库
|
|
255
|
+
// (未 chdir 的脚本、或 CLI 在别处跑)时,worktreeBase 会错解析到 process.cwd 仓库,
|
|
256
|
+
// getMeta 读错位置返回 null。resolve() 对绝对参数原样返回、对相对参数相对 this.cwd 解析,两者皆稳。
|
|
252
257
|
const commonDir = gitQuiet(this.cwd, 'rev-parse --git-common-dir');
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if (st.isDirectory()) {
|
|
258
|
-
root = dirname(commonDir);
|
|
258
|
+
if (commonDir) {
|
|
259
|
+
const absCommonDir = resolve(this.cwd, commonDir);
|
|
260
|
+
if (existsSync(absCommonDir) && statSync(absCommonDir).isDirectory()) {
|
|
261
|
+
root = dirname(absCommonDir);
|
|
259
262
|
}
|
|
260
263
|
}
|
|
261
264
|
} catch (e) {
|
|
@@ -989,7 +992,12 @@ export class WorktreeManager {
|
|
|
989
992
|
}
|
|
990
993
|
|
|
991
994
|
/**
|
|
992
|
-
* 检查 worktree
|
|
995
|
+
* 检查 worktree 是否有尚未落到主工作区的交付变更。
|
|
996
|
+
*
|
|
997
|
+
* 语义:worktree 相对 baseline 的交付变更(tracked + untracked,排除 .sillyspec//meta.json)里,
|
|
998
|
+
* 哪些还没 byte-identical 出现在主工作区 HEAD。全部已在 main HEAD(含 cherry-pick/rebase/merge/apply
|
|
999
|
+
* 落地)→ hasChanges:false(调用方可安全 cleanup);否则 hasChanges:true(保留 worktree)。
|
|
1000
|
+
* 检测失败/拿不准 → 保守 hasChanges:true(防误删未落代码)。
|
|
993
1001
|
* @param {string} changeName
|
|
994
1002
|
* @returns {{ hasChanges: boolean, changedFiles: string[], reason?: string }}
|
|
995
1003
|
*/
|
|
@@ -1007,36 +1015,102 @@ export class WorktreeManager {
|
|
|
1007
1015
|
if (meta.mode === 'in-place-fallback') {
|
|
1008
1016
|
return { hasChanges: false, changedFiles: [], reason: 'in-place mode' };
|
|
1009
1017
|
}
|
|
1018
|
+
// native-worktree 是用户外部隔离环境,不纳入"未应用"判定(与 execute 完成路径一致)
|
|
1019
|
+
if (meta.mode === 'native-worktree') {
|
|
1020
|
+
return { hasChanges: false, changedFiles: [], reason: 'native-worktree (external)' };
|
|
1021
|
+
}
|
|
1010
1022
|
|
|
1011
1023
|
const diffBase = meta.baselineCommit || meta.baseHash;
|
|
1012
1024
|
if (!diffBase) {
|
|
1013
1025
|
return { hasChanges: false, changedFiles: [], reason: 'no diff base' };
|
|
1014
1026
|
}
|
|
1015
1027
|
|
|
1028
|
+
const isDeliverable = f => f && !f.startsWith('.sillyspec/') && f !== 'meta.json';
|
|
1016
1029
|
try {
|
|
1017
|
-
//
|
|
1018
|
-
const
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1030
|
+
// 1) 候选交付变更(worktree 工作区相对 diffBase)。--no-renames:rename 退化成 D+A,两侧文件都进集
|
|
1031
|
+
const tracked = (gitQuiet(worktreePath, `diff --no-renames --name-only ${diffBase}`) || '')
|
|
1032
|
+
.split('\n').filter(Boolean).filter(isDeliverable);
|
|
1033
|
+
const untracked = (gitQuiet(worktreePath, `ls-files --others --exclude-standard`) || '')
|
|
1034
|
+
.split('\n').filter(Boolean).filter(isDeliverable);
|
|
1035
|
+
|
|
1036
|
+
if (tracked.length === 0 && untracked.length === 0) {
|
|
1037
|
+
return { hasChanges: false, changedFiles: [], reason: 'no changes in worktree' };
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// 2) 候选集中尚未落到主工作区 HEAD 的子集
|
|
1041
|
+
const pending = this._changesAlreadyOnMain(worktreePath, tracked, untracked);
|
|
1042
|
+
if (pending.length === 0) {
|
|
1043
|
+
return { hasChanges: false, changedFiles: [], reason: 'all changes already on main HEAD' };
|
|
1044
|
+
}
|
|
1045
|
+
return { hasChanges: true, changedFiles: pending };
|
|
1046
|
+
} catch (e) {
|
|
1047
|
+
// 检测失败时保守处理:视为有变更,保留 worktree
|
|
1048
|
+
return { hasChanges: true, changedFiles: [], reason: `check failed: ${e.message}` };
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* 判定候选变更里哪些还没到主工作区 HEAD。
|
|
1054
|
+
* - tracked:`git -C worktree diff --no-renames --name-only <mainHead> -- <files>`,
|
|
1055
|
+
* 比较 worktree 工作区(含未提交)vs main HEAD,限定到候选集;空 = 已在 main HEAD。
|
|
1056
|
+
* - untracked:worktree `hash-object`(默认带 filter,与 tree blob 同口径)vs main `ls-tree HEAD` blob;
|
|
1057
|
+
* 不等 = 该新文件未在 main HEAD。HEAD-only,不查 main 工作区未提交副本(防误删)。
|
|
1058
|
+
* 三种 mode 共享主 repo 对象库,mainHead 可在 worktree cwd 解析。
|
|
1059
|
+
* @private
|
|
1060
|
+
* @param {string} worktreePath
|
|
1061
|
+
* @param {string[]} trackedFiles
|
|
1062
|
+
* @param {string[]} untrackedFiles
|
|
1063
|
+
* @returns {string[]} 尚未落到 main HEAD 的文件
|
|
1064
|
+
*/
|
|
1065
|
+
_changesAlreadyOnMain(worktreePath, trackedFiles, untrackedFiles) {
|
|
1066
|
+
const mainHead = git(this.cwd, 'rev-parse HEAD'); // 失败即抛 → 外层 catch fail-safe
|
|
1067
|
+
const pending = [];
|
|
1068
|
+
|
|
1069
|
+
if (trackedFiles.length > 0) {
|
|
1070
|
+
const diverged = (gitQuiet(worktreePath,
|
|
1071
|
+
`diff --no-renames --name-only ${mainHead} -- ${trackedFiles.join(' ')}`) || '')
|
|
1072
|
+
.split('\n').filter(Boolean);
|
|
1073
|
+
pending.push(...diverged);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
if (untrackedFiles.length > 0) {
|
|
1077
|
+
// hash-object 按 argv 顺序逐行输出 blob hash;某文件不存在则整命令失败 → gitQuiet 返回 null
|
|
1078
|
+
const wtHashes = (gitQuiet(worktreePath, `hash-object -- ${untrackedFiles.join(' ')}`) || '')
|
|
1079
|
+
.split('\n');
|
|
1080
|
+
const mainBlobs = this._lsTreeBlobs(this.cwd, 'HEAD', untrackedFiles);
|
|
1081
|
+
for (let i = 0; i < untrackedFiles.length; i++) {
|
|
1082
|
+
// wtHashes[i] 缺失(命令失败/行数不齐)→ 视为未应用,保守保留
|
|
1083
|
+
if (wtHashes[i] !== (mainBlobs.get(untrackedFiles[i]) ?? null)) {
|
|
1084
|
+
pending.push(untrackedFiles[i]);
|
|
1025
1085
|
}
|
|
1026
1086
|
}
|
|
1087
|
+
}
|
|
1027
1088
|
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
const untrackedFiles = untrackedRaw
|
|
1031
|
-
? untrackedRaw.split('\n').filter(Boolean).filter(f => !f.startsWith('.sillyspec/') && f !== 'meta.json')
|
|
1032
|
-
: [];
|
|
1089
|
+
return [...new Set(pending)];
|
|
1090
|
+
}
|
|
1033
1091
|
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1092
|
+
/**
|
|
1093
|
+
* 一次 `git ls-tree <treeish> -- <files>` → Map<path, blobHash>(仅 tree 中存在者)。
|
|
1094
|
+
* 与 worktree-apply.js 的 getBlobHashMap 同逻辑;此处内联私有,避免反向 import 循环依赖。
|
|
1095
|
+
* @private
|
|
1096
|
+
* @param {string} cwd
|
|
1097
|
+
* @param {string} treeish
|
|
1098
|
+
* @param {string[]} files
|
|
1099
|
+
* @returns {Map<string, string>}
|
|
1100
|
+
*/
|
|
1101
|
+
_lsTreeBlobs(cwd, treeish, files) {
|
|
1102
|
+
const map = new Map();
|
|
1103
|
+
if (!files || files.length === 0) return map; // 空 pathspec 会列整棵树,必须拦
|
|
1104
|
+
const raw = gitQuiet(cwd, `ls-tree ${treeish} -- ${files.join(' ')}`);
|
|
1105
|
+
if (!raw) return map;
|
|
1106
|
+
for (const line of raw.split('\n')) {
|
|
1107
|
+
if (!line) continue;
|
|
1108
|
+
const tab = line.indexOf('\t');
|
|
1109
|
+
if (tab === -1) continue;
|
|
1110
|
+
const hash = line.slice(0, tab).split(' ')[2]; // "<mode> <type> <hash>"
|
|
1111
|
+
if (hash) map.set(line.slice(tab + 1), hash);
|
|
1039
1112
|
}
|
|
1113
|
+
return map;
|
|
1040
1114
|
}
|
|
1041
1115
|
|
|
1042
1116
|
/**
|