sillyspec 3.28.4 → 3.28.6
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/AGENTS.md +1 -0
- package/package.json +1 -1
- package/results-review-tier.md +18 -0
- package/sa-rename-probe/src/new.js +4 -0
- package/src/agent-session-log.js +4 -3
- package/src/archive-delta.js +2 -2
- package/src/change-list.js +11 -1
- package/src/commit-suggest.js +52 -1
- package/src/config-cat.js +6 -1
- package/src/config-schema.js +13 -0
- package/src/decision-distill.js +53 -7
- package/src/decisions-io.js +96 -0
- package/src/design-facts.js +3 -0
- package/src/dispatch/probe.js +31 -8
- package/src/docs-check.js +77 -8
- package/src/friction-tally.js +221 -0
- package/src/index.js +430 -18
- package/src/mcp-server.js +144 -0
- package/src/module-impact.js +8 -1
- package/src/modules.js +46 -34
- package/src/progress/change-registry.js +20 -0
- package/src/progress.js +16 -1
- package/src/quicklog.js +60 -6
- package/src/review-dispatch.js +377 -0
- package/src/run/archive-distill.js +48 -0
- package/src/run/command.js +35 -5
- package/src/run/complete-handlers.js +136 -3
- package/src/run/complete.js +170 -2
- package/src/run/gate-snapshot.js +159 -0
- package/src/run/gates.js +166 -69
- package/src/run/prompt.js +54 -9
- package/src/run/quick-audit.js +78 -16
- package/src/run/shared.js +138 -7
- package/src/run/stage.js +31 -2
- package/src/run/verify-quality-scan.js +383 -0
- package/src/scope-audit.js +941 -0
- package/src/sillyhub-mcp/client.js +58 -3
- package/src/stage-review-checklist.js +44 -0
- package/src/stage-review.js +122 -4
- package/src/stages/archive.js +57 -164
- package/src/stages/brainstorm.js +8 -12
- package/src/stages/execute.js +13 -10
- package/src/stages/plan.js +3 -10
- package/src/stages/verify.js +14 -33
- package/src/sync.js +51 -10
- package/src/task-review.js +108 -16
- package/src/taskcard.js +46 -3
- package/src/validate-artifacts.js +163 -0
- package/src/verify-postcheck.js +90 -10
- package/src/verify-probes.js +134 -12
- package/src/worktree-apply.js +119 -14
- package/templates/agents-instruction.md +1 -0
package/src/worktree-apply.js
CHANGED
|
@@ -90,6 +90,61 @@ export function filterDeliverableFiles(files) {
|
|
|
90
90
|
* excludedCount: number // EXCLUDE-DIRTY + EXCLUDE-MISMATCH 数
|
|
91
91
|
* }}
|
|
92
92
|
*/
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* EXCLUDE-DIRTY 重叠文件的自动三方合并(坑 apply-dirty-block-no-merge,2026-09-10 驾驭
|
|
96
|
+
* 小结第六批③,用户实证 daemon.ts 需手工 patch 合并——三仓并发活跃时 cp 清单不可用)。
|
|
97
|
+
*
|
|
98
|
+
* 语义:对每个 dirty∩changed 重叠文件做 git merge-file 三方合并——base = apply 锚点
|
|
99
|
+
* (baseHash)版本、ours = 主仓当前脏版本(并行会话在途)、theirs = worktree 版本(本变更
|
|
100
|
+
* 交付)。**clean 合并才写回主仓**(两侧改动都保留),该文件视为已应用、由调用方从 patch
|
|
101
|
+
* 集剔除;冲突文件不写回、维持原拦截 + rescue 出口(绝不留半合并现场)。审计:
|
|
102
|
+
* mergedDirtyFiles 逐文件留痕 + 每文件一行日志。
|
|
103
|
+
*
|
|
104
|
+
* fail-soft:base 版本取不到(文件在锚点不存在 = 全新文件)/ merge-file 不可用 → 计入
|
|
105
|
+
* conflicts 列表走原拦截路径,不阻断 apply 主流程。
|
|
106
|
+
*
|
|
107
|
+
* @param {{ projectRoot: string, worktreePath: string, baseHash: string }} ctx
|
|
108
|
+
* @param {string[]} overlapFiles 重叠文件(仓库根相对路径)
|
|
109
|
+
* @returns {{ merged: string[], conflicts: string[] }}
|
|
110
|
+
*/
|
|
111
|
+
export function mergeDirtyOverlapThreeWay({ projectRoot, worktreePath, baseHash }, overlapFiles) {
|
|
112
|
+
const merged = []
|
|
113
|
+
const conflicts = []
|
|
114
|
+
const tmpDir = mkdtempSync(join(tmpdir(), 'sillyspec-3way-'))
|
|
115
|
+
try {
|
|
116
|
+
for (const f of overlapFiles) {
|
|
117
|
+
try {
|
|
118
|
+
const baseRaw = git(projectRoot, ['show', `${baseHash}:${f}`], { encoding: 'buffer', timeout: 15000 })
|
|
119
|
+
const wtRaw = readFileSync(join(worktreePath, f))
|
|
120
|
+
const mainRaw = readFileSync(join(projectRoot, f))
|
|
121
|
+
const p = (tag) => join(tmpDir, `${tag}-${Math.random().toString(36).slice(2, 8)}`)
|
|
122
|
+
const baseP = p('base'); const oursP = p('ours'); const theirsP = p('theirs')
|
|
123
|
+
writeFileSync(baseP, baseRaw); writeFileSync(oursP, mainRaw); writeFileSync(theirsP, wtRaw)
|
|
124
|
+
// merge-file <current=ours> <base> <other=theirs> -p:stdout=合并结果;exit 0=clean,>0=冲突数
|
|
125
|
+
let outBuf, code = 0
|
|
126
|
+
try {
|
|
127
|
+
outBuf = git(projectRoot, ['merge-file', '-p', oursP, baseP, theirsP], { encoding: 'buffer', timeout: 15000 })
|
|
128
|
+
} catch (e) {
|
|
129
|
+
code = (typeof e.status === 'number') ? e.status : -1
|
|
130
|
+
outBuf = (e.stdout instanceof Buffer) ? e.stdout : Buffer.alloc(0)
|
|
131
|
+
if (code < 0) { conflicts.push(f); continue } // merge-file 本身不可用 → 原拦截
|
|
132
|
+
}
|
|
133
|
+
if (code === 0) {
|
|
134
|
+
writeFileSync(join(projectRoot, f), outBuf)
|
|
135
|
+
merged.push(f)
|
|
136
|
+
console.log(`🔀 EXCLUDE-DIRTY 三方合并成功(clean): ${f}——主仓在途改动与 worktree 交付均已保留`)
|
|
137
|
+
} else {
|
|
138
|
+
conflicts.push(f)
|
|
139
|
+
}
|
|
140
|
+
} catch { conflicts.push(f) }
|
|
141
|
+
}
|
|
142
|
+
} finally {
|
|
143
|
+
try { rmSync(tmpDir, { recursive: true, force: true }) } catch {}
|
|
144
|
+
}
|
|
145
|
+
return { merged, conflicts }
|
|
146
|
+
}
|
|
147
|
+
|
|
93
148
|
export function generateRescueCommands({ changedFiles, dirtyFiles, hashMismatchFiles, deletedFiles = [], worktreePath, projectRoot }) {
|
|
94
149
|
// 集合归一:dirtyFiles 接受 Set 或数组(调用方 step4.5/5a 可能传任一形态),其余统一数组→Set
|
|
95
150
|
const dirtySet = dirtyFiles instanceof Set ? dirtyFiles : new Set(dirtyFiles || []);
|
|
@@ -235,13 +290,19 @@ export function resolveApplyAllowSet(projectRoot, changeName, opts = {}) {
|
|
|
235
290
|
return repoMap.get(key);
|
|
236
291
|
};
|
|
237
292
|
const specBase = opts.specBase || _silentPointerSpecRoot(projectRoot) || join(projectRoot, '.sillyspec');
|
|
293
|
+
// 变更目录读侧归档回退(坑 apply-archived-evidence-recycled,2026-09-10 驾驭小结第二批①):
|
|
294
|
+
// archive 收尾对「未 apply 的 worktree」有意保留并期待归档后补 apply,但归档同时把变更目录
|
|
295
|
+
// rename 到 changes/archive/<name>/ ——旧读侧只认活跃路径,归档后补 apply 时 design/tasks
|
|
296
|
+
// 全部失联,allow 集恒空 → Gate1 整批误拦「不在清单」。活跃目录缺 design.md 而(且)归档
|
|
297
|
+
// 目录在 → 回退读归档版(取证自愈,不改归档回收策略)。
|
|
298
|
+
const changeDir = resolveActiveOrArchiveChangeDir(specBase, changeName);
|
|
238
299
|
// design §6 清单归属 main(清单路径相对主仓根;跨仓 task 的 allowed_paths 由 task 卡 repo 切片)
|
|
239
300
|
const mainSet = getOrCreate('main');
|
|
240
301
|
parseFileChangeList(
|
|
241
|
-
join(
|
|
302
|
+
join(changeDir, 'design.md'),
|
|
242
303
|
{ keepSillyspecDocs: true }
|
|
243
304
|
).forEach(p => mainSet.add(p));
|
|
244
|
-
const tasksDir = join(
|
|
305
|
+
const tasksDir = join(changeDir, 'tasks');
|
|
245
306
|
if (existsSync(tasksDir)) {
|
|
246
307
|
for (const tf of readdirSync(tasksDir).filter(f => /^task-\d+\.md$/.test(f))) {
|
|
247
308
|
const content = readFileSync(join(tasksDir, tf), 'utf8');
|
|
@@ -254,6 +315,24 @@ export function resolveApplyAllowSet(projectRoot, changeName, opts = {}) {
|
|
|
254
315
|
return repoMap;
|
|
255
316
|
}
|
|
256
317
|
|
|
318
|
+
/**
|
|
319
|
+
* 变更文档目录读侧解析:活跃 changes/<name> 优先;活跃目录缺 design.md 而归档
|
|
320
|
+
* changes/archive/<name>/design.md 存在 → 返回归档目录(apply 校验在「归档后补 apply」
|
|
321
|
+
* 场景仍能取到 design/tasks 取证,坑 apply-archived-evidence-recycled)。两处皆缺返回
|
|
322
|
+
* 活跃路径原样(调用方 existsSync 自行降级,行为与旧版一致)。行为经 resolveApplyAllowSet
|
|
323
|
+
* 直测(apply-archive-docs-fallback.test.mjs)覆盖。
|
|
324
|
+
* @param {string} specBase
|
|
325
|
+
* @param {string} changeName
|
|
326
|
+
* @returns {string} 变更目录路径(活跃或归档)
|
|
327
|
+
*/
|
|
328
|
+
function resolveActiveOrArchiveChangeDir(specBase, changeName) {
|
|
329
|
+
const active = join(specBase, 'changes', changeName);
|
|
330
|
+
if (existsSync(join(active, 'design.md'))) return active;
|
|
331
|
+
const archived = join(specBase, 'changes', 'archive', changeName);
|
|
332
|
+
if (existsSync(join(archived, 'design.md'))) return archived;
|
|
333
|
+
return active;
|
|
334
|
+
}
|
|
335
|
+
|
|
257
336
|
// 平台指针静默读(worktree-apply 内部用):只读 <projectRoot>/.sillyspec-platform.json 的
|
|
258
337
|
// specRoot 字段,不做存在性/自指/temp 守卫、不打 warn——allowlist 只需一个「读得到就用」的
|
|
259
338
|
// 基准目录,坏指针/缺字段落回本地(与 sync-noise.js bindSyncNoiseFromCwd 同款零副作用风格)。
|
|
@@ -940,27 +1019,50 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
940
1019
|
)].filter(f => !f.startsWith('.sillyspec/') && f !== 'meta.json');
|
|
941
1020
|
const overlapDirty = dirtyFiles.filter(f => changedFiles.includes(f));
|
|
942
1021
|
if (overlapDirty.length > 0) {
|
|
943
|
-
//
|
|
944
|
-
//
|
|
945
|
-
//
|
|
946
|
-
//
|
|
947
|
-
|
|
948
|
-
if (
|
|
949
|
-
|
|
950
|
-
|
|
1022
|
+
// 自动三方合并(坑 apply-dirty-block-no-merge,2026-09-10 驾驭小结第六批③):先对
|
|
1023
|
+
// 重叠文件尝试 merge-file(base=锚点版 ours=主仓脏版 theirs=worktree 版),clean 合并
|
|
1024
|
+
// 写回主仓、该文件视为已应用(从 patch 集剔除);仅剩冲突文件才走原拦截+rescue 出口。
|
|
1025
|
+
// checkOnly(assess 只读)不试合并——探查路径零写盘。
|
|
1026
|
+
let conflictDirty = overlapDirty;
|
|
1027
|
+
if (!checkOnly && deliverableBase && worktreePath && worktreePath !== projectRoot) {
|
|
1028
|
+
try {
|
|
1029
|
+
const m3 = mergeDirtyOverlapThreeWay({ projectRoot, worktreePath, baseHash: deliverableBase }, overlapDirty);
|
|
1030
|
+
if (m3.merged.length > 0) {
|
|
1031
|
+
result.mergedDirtyFiles = (result.mergedDirtyFiles || []).concat(m3.merged);
|
|
1032
|
+
const mergedSet = new Set(m3.merged);
|
|
1033
|
+
changedFiles = changedFiles.filter(f => !mergedSet.has(f));
|
|
1034
|
+
result.changedFiles = changedFiles;
|
|
1035
|
+
result.deletedFiles = deletedFiles.filter(f => !mergedSet.has(f));
|
|
1036
|
+
result.hashMismatchFiles = (result.hashMismatchFiles || []).filter(f => !mergedSet.has(f));
|
|
1037
|
+
conflictDirty = m3.conflicts;
|
|
1038
|
+
} else {
|
|
1039
|
+
conflictDirty = m3.conflicts.length > 0 ? m3.conflicts : overlapDirty;
|
|
1040
|
+
}
|
|
1041
|
+
} catch { conflictDirty = overlapDirty; /* 合并链路异常 → 原拦截 */ }
|
|
1042
|
+
}
|
|
1043
|
+
if (conflictDirty.length === 0) {
|
|
1044
|
+
result.warnings = (result.warnings || []).concat([
|
|
1045
|
+
`EXCLUDE-DIRTY 全部经三方合并消解(${overlapDirty.length} 个文件 clean 合并已写回,主仓在途改动与 worktree 交付均保留)——继续正常 apply 剩余文件`
|
|
1046
|
+
]);
|
|
1047
|
+
} else if (skipOverlap && !checkOnly) {
|
|
1048
|
+
// --skip-overlap(坑 apply-overlap-all-or-nothing,2026-08-23 实证):剔除仍冲突的
|
|
1049
|
+
// 重叠文件(三方合并已消解的不在此列)、应用干净子集。重叠文件留在 worktree——
|
|
1050
|
+
// step8 非 force cleanup 被 hasUnappliedChanges 护栏拦住,待主仓干净后重新 apply 或人工裁决。
|
|
1051
|
+
const skipSet = new Set(conflictDirty);
|
|
1052
|
+
result.skippedOverlapFiles = conflictDirty.slice();
|
|
951
1053
|
changedFiles = changedFiles.filter(f => !skipSet.has(f));
|
|
952
1054
|
result.changedFiles = changedFiles;
|
|
953
1055
|
result.deletedFiles = deletedFiles.filter(f => !skipSet.has(f));
|
|
954
1056
|
result.hashMismatchFiles = (result.hashMismatchFiles || []).filter(f => !skipSet.has(f));
|
|
955
1057
|
if (changedFiles.length === 0) {
|
|
956
1058
|
result.errors.push(
|
|
957
|
-
`--skip-overlap:本次 ${
|
|
1059
|
+
`--skip-overlap:本次 ${conflictDirty.length} 个变更文件与主仓未提交改动重叠且无法自动三方合并,无可应用子集:\n ${conflictDirty.join('\n ')}\n` +
|
|
958
1060
|
`请先提交/stash 主仓改动后重试(不带 --skip-overlap),或 sillyspec worktree apply ${changeName} --stash-dirty(工具内置 stash→apply→恢复,保暂存区状态,SHA 兜底可审计)。`
|
|
959
1061
|
);
|
|
960
1062
|
return result;
|
|
961
1063
|
}
|
|
962
1064
|
result.warnings = (result.warnings || []).concat([
|
|
963
|
-
`--skip-overlap:跳过 ${
|
|
1065
|
+
`--skip-overlap:跳过 ${conflictDirty.length} 个与主仓未提交改动重叠的文件(留在 worktree,未应用):${conflictDirty.join(', ')}——待主仓提交/stash 后重新 apply 只应用剩余文件,或确认放弃后 cleanup --force`
|
|
964
1066
|
]);
|
|
965
1067
|
} else {
|
|
966
1068
|
// 重叠拦截:只有与本次变更同文件的未提交改动才无法安全 apply(列重叠文件,非全部脏文件)
|
|
@@ -974,7 +1076,7 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
974
1076
|
projectRoot,
|
|
975
1077
|
});
|
|
976
1078
|
result.errors.push(
|
|
977
|
-
`主工作区以下未提交文件与本次 apply
|
|
1079
|
+
`主工作区以下未提交文件与本次 apply 的变更重叠,自动三方合并后仍冲突,无法安全应用:\n ${conflictDirty.join('\n ')}\n` +
|
|
978
1080
|
`请先提交或暂存这些改动,再重新 apply:\n git add -A && git commit -m "..." 或 git stash\n` +
|
|
979
1081
|
`或 sillyspec worktree apply ${changeName} --stash-dirty(工具内置 stash→apply→恢复,保暂存区状态,SHA 兜底可审计)\n` +
|
|
980
1082
|
`或应用非重叠部分(重叠文件留在 worktree,主仓干净后重新 apply 只补剩余):\n sillyspec worktree apply ${changeName} --skip-overlap\n` +
|
|
@@ -1730,8 +1832,11 @@ export function assessApplyRisk(changeName, { cwd } = {}) {
|
|
|
1730
1832
|
// design §6 标记为「顺带修复」的文件(坑 worktree-execute-apply-friction 坑1):合规修预存债,
|
|
1731
1833
|
// 不属任何 task 边界,assess 豁免 allowed_paths 严格校验(降级 warning),避免被迫 cherry-pick 绕过。
|
|
1732
1834
|
// specBase 走指针/本地解析(2026-09-08 平台模式修复,与 resolveApplyAllowSet 同族)。
|
|
1835
|
+
// 归档回退(坑 apply-archived-evidence-recycled):归档后补 apply 时变更目录已 rename,
|
|
1836
|
+
// design/tasks 从 changes/archive/<name>/ 读(与 resolveApplyAllowSet 同口径)。
|
|
1733
1837
|
const assessSpecBase = _silentPointerSpecRoot(projectRoot) || join(projectRoot, '.sillyspec');
|
|
1734
|
-
const
|
|
1838
|
+
const assessChangeDir = resolveActiveOrArchiveChangeDir(assessSpecBase, changeName);
|
|
1839
|
+
const designPath = join(assessChangeDir, 'design.md');
|
|
1735
1840
|
const incidentalSet = new Set(
|
|
1736
1841
|
parseFileChangeListDetailed(designPath).filter(e => e.incidental).map(e => e.path)
|
|
1737
1842
|
);
|
|
@@ -21,3 +21,4 @@
|
|
|
21
21
|
15. **quicklog 结构化落盘**:末步 `--done` 用四参数 `--req/--cause/--solution/--result`(CLI 合成结构化 output 并自动提取标题/四段分行);文件括注用 `--file-notes "path::注 || path2"`。骨架由 CLI 接管,`--done` 后按需核对即可,勿手拼模板。
|
|
22
22
|
16. **代码可能随时在修改**(多 agent 并行),Edit 前重跑 + 查最新态;破坏性 git op 前先备份。
|
|
23
23
|
17. **不奉承用户**,禁止"你说得对"类话术,直接给结论、依据、方案。
|
|
24
|
+
18. **禁止目录级 git add / git add -A**(多会话共享仓):.sillyspec/ 等目录里有并行会话的进行中文件与已提交文档,目录级暂存会夹带他者改动甚至误删已提交文件(2026-09-10 实证)。提交一律用显式 pathspec(git add -- 文件1 文件2),提交前 git status 核对暂存面只含本会话文件。
|