sillyspec 3.32.3 → 3.32.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/README.md +4 -4
- package/SKILL.md +2 -4
- package/package.json +1 -1
- package/src/command-cards.js +1 -1
- package/src/config-schema.js +8 -0
- package/src/cross-placement.js +246 -0
- package/src/cross-repo-reconcile.js +8 -5
- package/src/doctor-diagnostics.js +21 -1
- package/src/flow-parity.js +41 -10
- package/src/flow-review.js +12 -2
- package/src/flow.js +45 -2
- package/src/hooks/worktree-guard.js +35 -2
- package/src/index.js +53 -5
- package/src/init.js +58 -61
- package/src/run/command.js +19 -0
- package/src/run/multi-repo-context.js +11 -5
- package/src/run/shared.js +85 -4
- package/src/scope-audit.js +130 -85
- package/src/spec-sync.js +45 -16
- package/src/stages/plan-postcheck.js +67 -9
- package/src/sync.js +3 -0
- package/src/verify-postcheck.js +56 -8
- package/src/verify-probes.js +14 -7
- package/src/worktree-apply.js +62 -8
- package/src/worktree-cross.js +188 -27
- package/src/worktree-deps.js +50 -9
- package/src/worktree.js +43 -9
- package/assets/command-cards/run-quick.md +0 -20
package/src/worktree-apply.js
CHANGED
|
@@ -1046,6 +1046,43 @@ export async function withMainRepoLock(projectRoot, changeName, purpose, fn, opt
|
|
|
1046
1046
|
}
|
|
1047
1047
|
}
|
|
1048
1048
|
|
|
1049
|
+
/**
|
|
1050
|
+
* 跨仓主副本直写检测(FR-06,D-003@v1——坑 cross-wt-toolchain-split 2026-10-10 用户实证:
|
|
1051
|
+
* agent 绕开 worktree 回主副本直写 master,5 个 fire commit 落主干、空 worktree 被静默清理,
|
|
1052
|
+
* 全程零提示)。
|
|
1053
|
+
*
|
|
1054
|
+
* 判定:主副本 base 锚(meta.baselineCommit||baseHash,与 worktree 交付 diff 同锚)之后有
|
|
1055
|
+
* commit 推进,且推进触及的文件与本变更该仓声明文件面(resolveApplyAllowSet 切片)以
|
|
1056
|
+
* pathMatches 容差相交 → 返回 { commits, files }(交集文件,声明面命中口径与清单校验一致)。
|
|
1057
|
+
* 纯检测不写盘:advisory,调用方(applyCrossRepoWorktrees)负责出 warning。
|
|
1058
|
+
*
|
|
1059
|
+
* fail-open(FR-07):git 不可达 / baseHash 缺失 / allowSet 空 / 任何异常 → null(调用方静默
|
|
1060
|
+
* 跳过——并行会话无关推进是合法态,误报成本高于漏报面已由 D-003 故障面接受)。
|
|
1061
|
+
*
|
|
1062
|
+
* @param {string} crossRoot 跨仓主副本仓根
|
|
1063
|
+
* @param {string} baseHash base 锚(worktree meta 的 baselineCommit||baseHash)
|
|
1064
|
+
* @param {Set<string>} allowSet 该仓声明文件面(相对跨仓仓根;空 = 声明面缺失不检测)
|
|
1065
|
+
* @returns {{ commits: number, files: string[] }|null} null = 无推进/无交集/检测不可得
|
|
1066
|
+
*/
|
|
1067
|
+
export function detectCrossMainCopyBypass(crossRoot, baseHash, allowSet) {
|
|
1068
|
+
try {
|
|
1069
|
+
if (!crossRoot || !baseHash || !(allowSet instanceof Set) || allowSet.size === 0) return null;
|
|
1070
|
+
const countRaw = gitQuiet(crossRoot, ['rev-list', '--count', `${baseHash}..HEAD`]);
|
|
1071
|
+
if (countRaw === null) return null; // git 失败(非仓/锚悬空)→ fail-open
|
|
1072
|
+
const commits = Number(String(countRaw).trim());
|
|
1073
|
+
if (!Number.isFinite(commits) || commits <= 0) return null; // 无推进 → 合法态
|
|
1074
|
+
const raw = gitQuiet(crossRoot, ['diff', '--name-only', baseHash, 'HEAD']);
|
|
1075
|
+
if (raw === null) return null;
|
|
1076
|
+
const touched = raw.split('\n').map(s => s.trim()).filter(Boolean);
|
|
1077
|
+
if (touched.length === 0) return null;
|
|
1078
|
+
const files = touched.filter(f => [...allowSet].some(ap => pathMatches(f, ap)));
|
|
1079
|
+
if (files.length === 0) return null; // 推进但与声明面无交集 → 并行会话,不告警
|
|
1080
|
+
return { commits, files };
|
|
1081
|
+
} catch {
|
|
1082
|
+
return null;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1049
1086
|
/**
|
|
1050
1087
|
* 跨仓 worktree 真实 apply(坑 cross-repo-no-worktree-isolation,2026-08-27)。
|
|
1051
1088
|
*
|
|
@@ -1104,6 +1141,31 @@ function applyCrossRepoWorktrees(changeName, projectRoot, ctx, { checkOnly = fal
|
|
|
1104
1141
|
continue;
|
|
1105
1142
|
}
|
|
1106
1143
|
|
|
1144
|
+
// 声明面解析(提前到空清理分支前——直写检测需要 allowSet 覆盖「空 worktree 被静默清理」
|
|
1145
|
+
// 的 D-007 签名场景;清单校验段复用同一份,零行为差)。
|
|
1146
|
+
// specBase 显式传(平台模式 changes/ 在 specRoot;本地模式与旧硬编码同值零回归)。
|
|
1147
|
+
// review 声明收紧(D-003@v1,2026-09-14-change-ownership-guards task-03):声明文件须与
|
|
1148
|
+
// allow 面(design 清单 ∪ 各 task target_files/allowed_paths ∪ linked-change 声明 =
|
|
1149
|
+
// resolveApplyAllowSet 现有并集)相交(pathMatches 容差)才计入;不相交的外来声明剔除出
|
|
1150
|
+
// 放行面、进 violations 报告行(「review 声明了越权文件」嫌疑标注),admission 增量归零。
|
|
1151
|
+
const allowMap = resolveApplyAllowSet(projectRoot, changeName, { specBase });
|
|
1152
|
+
const allowSet = allowMap.get(repoKey) || new Set();
|
|
1153
|
+
const reviewDeclared = collectReviewDeclaredFiles(projectRoot, changeName, { runtimeRoot: join(specBase, '.runtime') }).get(repoKey);
|
|
1154
|
+
|
|
1155
|
+
// 跨仓主副本直写检测(FR-06,D-003@v1 advisory——坑 cross-wt-toolchain-split 2026-10-10
|
|
1156
|
+
// 实证:agent 绕开 worktree 回主副本直写 master,空 worktree 在此被静默清理零提示):
|
|
1157
|
+
// 主副本 base 锚之后有提交推进且推进文件与声明面相交 = 本变更签名,warning 列证。
|
|
1158
|
+
// 交集空/无推进/声明面缺失不告警(并行会话无关推进合法);检测异常 fail-open(FR-07)。
|
|
1159
|
+
try {
|
|
1160
|
+
const bypass = detectCrossMainCopyBypass(crossRoot, deliverableBase, allowSet);
|
|
1161
|
+
if (bypass) {
|
|
1162
|
+
out.warnings.push(
|
|
1163
|
+
`跨仓 ${repoKey}:疑似绕过 worktree 直写主干——base ${String(deliverableBase).slice(0, 8)} 之后主副本有 ${bypass.commits} 个 commit,以下文件与本变更声明面相交:\n ${bypass.files.join('\n ')}\n` +
|
|
1164
|
+
`(worktree 隔离被绕开:请核对该仓执行期决策留痕;若属误写应把提交摘到分支重走 apply。advisory 警告不阻断 apply。)`
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
} catch { /* 直写检测 fail-open,不阻断 apply */ }
|
|
1168
|
+
|
|
1107
1169
|
if (changedFiles.length === 0) {
|
|
1108
1170
|
if (!checkOnly) {
|
|
1109
1171
|
const cr = cleanupCrossWorktrees({ cwd: crossRoot, changeName, specBase, force: true });
|
|
@@ -1117,14 +1179,6 @@ function applyCrossRepoWorktrees(changeName, projectRoot, ctx, { checkOnly = fal
|
|
|
1117
1179
|
}
|
|
1118
1180
|
|
|
1119
1181
|
// 清单校验(per-repo 切片 + review 声明相交过滤,与主仓 Gate1/3b 同语义)。
|
|
1120
|
-
// specBase 显式传(平台模式 changes/ 在 specRoot;本地模式与旧硬编码同值零回归)。
|
|
1121
|
-
// review 声明收紧(D-003@v1,2026-09-14-change-ownership-guards task-03):声明文件须与
|
|
1122
|
-
// allow 面(design 清单 ∪ 各 task target_files/allowed_paths ∪ linked-change 声明 =
|
|
1123
|
-
// resolveApplyAllowSet 现有并集)相交(pathMatches 容差)才计入;不相交的外来声明剔除出
|
|
1124
|
-
// 放行面、进 violations 报告行(「review 声明了越权文件」嫌疑标注),admission 增量归零。
|
|
1125
|
-
const allowMap = resolveApplyAllowSet(projectRoot, changeName, { specBase });
|
|
1126
|
-
const allowSet = allowMap.get(repoKey) || new Set();
|
|
1127
|
-
const reviewDeclared = collectReviewDeclaredFiles(projectRoot, changeName, { runtimeRoot: join(specBase, '.runtime') }).get(repoKey);
|
|
1128
1182
|
const reviewForeign = [];
|
|
1129
1183
|
if (reviewDeclared && allowSet.size > 0) {
|
|
1130
1184
|
const face = [...allowSet];
|
package/src/worktree-cross.js
CHANGED
|
@@ -27,56 +27,117 @@
|
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
29
|
import { existsSync, readFileSync, readdirSync, mkdirSync } from 'fs';
|
|
30
|
-
import { join } from 'path';
|
|
30
|
+
import { join, dirname, resolve as resolvePath, isAbsolute, relative as relativePath } from 'path';
|
|
31
31
|
import { git, gitQuiet } from './git-helper.js';
|
|
32
32
|
import { writeAtomicSync } from './fs-atomic.js';
|
|
33
33
|
import { WorktreeManager, unlinkNodeModulesLinks, safeRemoveWorktreeDir } from './worktree.js';
|
|
34
34
|
import { provisionDeps } from './worktree-deps.js';
|
|
35
35
|
import { aggregateDeclaredRepos } from './run/shared.js';
|
|
36
|
-
import { parseRepoRegistry } from './stages/plan-postcheck.js';
|
|
36
|
+
import { parseRepoRegistry, parseRepoWorktreePlacements } from './stages/plan-postcheck.js';
|
|
37
|
+
import {
|
|
38
|
+
crossWorktreePath as _placementCrossPath,
|
|
39
|
+
crossWorktreeDirName,
|
|
40
|
+
resolveCrossWorktreePath,
|
|
41
|
+
resolveCrossWorktreePathCandidates,
|
|
42
|
+
readCrossPlacementConfig,
|
|
43
|
+
resolvePlacementRoot,
|
|
44
|
+
readPlacementRegistry,
|
|
45
|
+
updatePlacementRegistry,
|
|
46
|
+
removePlacementRegistryEntry,
|
|
47
|
+
sweepPlacementRegistry,
|
|
48
|
+
isWslSplit,
|
|
49
|
+
} from './cross-placement.js';
|
|
37
50
|
|
|
38
51
|
const META_FILE = 'meta.json';
|
|
39
52
|
const BRANCH_PREFIX = 'sillyspec/';
|
|
40
53
|
|
|
41
54
|
/**
|
|
42
|
-
* 跨仓 worktree
|
|
55
|
+
* 跨仓 worktree 路径(唯一路径公式;实现已下沉 cross-placement 叶子模块——multi-repo-context.js
|
|
56
|
+
* 经该叶子模块共用,不再与 worktree-cross 经 run/shared 成环)。
|
|
57
|
+
* placementRoot 可选(坑 cross-wt-toolchain-split 落位配置):给定时时 <placementRoot>/<change>--<repoKey>。
|
|
43
58
|
* @param {string} specBase 主仓 spec 根(<主仓>/.sillyspec)
|
|
44
59
|
* @param {string} changeName 变更名
|
|
45
60
|
* @param {string} repoKey 跨仓 repo 键(local.yaml repos: 段的 key)
|
|
61
|
+
* @param {string|null} [placementRoot] 自定义落位根(null/缺省走默认公式——三参调用零回归)
|
|
46
62
|
*/
|
|
47
|
-
export function crossWorktreePath(specBase, changeName, repoKey) {
|
|
48
|
-
return
|
|
63
|
+
export function crossWorktreePath(specBase, changeName, repoKey, placementRoot = null) {
|
|
64
|
+
return _placementCrossPath(specBase, changeName, repoKey, placementRoot);
|
|
49
65
|
}
|
|
50
66
|
|
|
51
67
|
/**
|
|
52
68
|
* 读跨仓 worktree meta(不存在/损坏返 null = legacy 直写模式)。
|
|
69
|
+
* 寻址经 resolveCrossWorktreePath(注册表优先、公式兜底)——worktree 挪到 crossPlacement
|
|
70
|
+
* 自定义落位后 meta 仍可达(坑 cross-wt-toolchain-split)。
|
|
53
71
|
* @returns {object|null}
|
|
54
72
|
*/
|
|
55
73
|
export function getCrossWorktreeMeta(specBase, changeName, repoKey) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
74
|
+
// repoRoot 从 repos 注册表解析(无 repoKey 注册/解析失败 → null,候选列表少一级)
|
|
75
|
+
const mainRoot = dirname(specBase);
|
|
76
|
+
const repoRoot = parseRepoRegistry(readLocalYamlText(mainRoot)).get(repoKey) || null;
|
|
77
|
+
const resolvedRoot = repoRoot && !isAbsolute(repoRoot) ? resolvePath(mainRoot, repoRoot) : repoRoot;
|
|
78
|
+
// 寻址候选逐级探测:注册表 → 仓内新公式 → 主仓 specBase 旧公式(legacy 存量兜底)
|
|
79
|
+
for (const dir of resolveCrossWorktreePathCandidates(specBase, changeName, repoKey, resolvedRoot)) {
|
|
80
|
+
const metaPath = join(dir, META_FILE);
|
|
81
|
+
if (!existsSync(metaPath)) continue; // 未命中 → 下一候选
|
|
82
|
+
try {
|
|
83
|
+
const m = JSON.parse(readFileSync(metaPath, 'utf8'));
|
|
84
|
+
return m && m.isCross ? m : null; // 非跨仓/损坏 = 命中但无效
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
62
88
|
}
|
|
89
|
+
return null;
|
|
63
90
|
}
|
|
64
91
|
|
|
65
92
|
/**
|
|
66
|
-
* 列出某变更已建的全部跨仓 worktree meta
|
|
93
|
+
* 列出某变更已建的全部跨仓 worktree meta(双源:worktrees 目录 `<change>--*` 扫描 ∪ 落位
|
|
94
|
+
* 注册表条目——坑 cross-wt-toolchain-split:worktree 挪到 crossPlacement 自定义目录后不在
|
|
95
|
+
* 默认存储目录下,纯目录扫描盲区)。注册表只提供位置:条目指向处须读到可解析且 isCross 的
|
|
96
|
+
* meta.json 才入列(与目录扫描同判据——不合成无 meta 条目,消费方 gates.js 解引用
|
|
97
|
+
* cm.depsStatus 零 TypeError 面)。按 repoKey 去重(注册表与目录扫描可能同指一处)。
|
|
67
98
|
* @returns {Array<{repoKey: string, meta: object}>}
|
|
68
99
|
*/
|
|
69
100
|
export function listCrossWorktreeMetas(specBase, changeName) {
|
|
70
101
|
const base = join(specBase, '.runtime', 'worktrees');
|
|
71
|
-
|
|
102
|
+
const candidates = []; // {dir}
|
|
103
|
+
const scanDir = (dir) => {
|
|
104
|
+
if (!existsSync(dir)) return;
|
|
105
|
+
try {
|
|
106
|
+
for (const name of readdirSync(dir)) {
|
|
107
|
+
if (!name.startsWith(`${changeName}--`)) continue;
|
|
108
|
+
candidates.push(join(dir, name));
|
|
109
|
+
}
|
|
110
|
+
} catch { /* 目录不可读跳过 */ }
|
|
111
|
+
};
|
|
112
|
+
// 源① 主仓 specBase 旧默认(legacy-main-specbase 存量)
|
|
113
|
+
scanDir(base);
|
|
114
|
+
// 源② 仓内新默认(<repoRoot>/.sillyspec/.runtime/worktrees——按 local.yaml repos 注册表枚举各仓)
|
|
115
|
+
const mainRoot = dirname(specBase);
|
|
116
|
+
const registryYaml = readLocalYamlText(mainRoot);
|
|
117
|
+
for (const repoRoot of parseRepoRegistry(registryYaml).values()) {
|
|
118
|
+
const root = isAbsolute(repoRoot) ? repoRoot : resolvePath(mainRoot, repoRoot);
|
|
119
|
+
scanDir(join(root, '.sillyspec', '.runtime', 'worktrees'));
|
|
120
|
+
}
|
|
121
|
+
// 源③ 注册表(注册表读取 fail-open 返回 {}——损坏降级纯目录扫描)
|
|
122
|
+
const registry = readPlacementRegistry(specBase);
|
|
123
|
+
const prefix = `${changeName}--`;
|
|
124
|
+
for (const [key, entry] of Object.entries(registry)) {
|
|
125
|
+
if (!key.startsWith(prefix)) continue;
|
|
126
|
+
if (entry && typeof entry.worktreePath === 'string' && entry.worktreePath) {
|
|
127
|
+
candidates.push(entry.worktreePath);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
72
130
|
const out = [];
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const metaPath = join(
|
|
76
|
-
if (!existsSync(metaPath)) continue;
|
|
131
|
+
const seen = new Set();
|
|
132
|
+
for (const dir of candidates) {
|
|
133
|
+
const metaPath = join(dir, META_FILE);
|
|
134
|
+
if (!existsSync(metaPath)) continue; // 无 meta 不入列(含注册表悬挂键——sweep 前的过渡态)
|
|
77
135
|
try {
|
|
78
136
|
const meta = JSON.parse(readFileSync(metaPath, 'utf8'));
|
|
79
|
-
if (meta && meta.isCross && meta.repoKey
|
|
137
|
+
if (meta && meta.isCross && meta.repoKey && !seen.has(meta.repoKey)) {
|
|
138
|
+
seen.add(meta.repoKey);
|
|
139
|
+
out.push({ repoKey: meta.repoKey, meta });
|
|
140
|
+
}
|
|
80
141
|
} catch { /* 损坏 meta 跳过(cleanup 阶段仍按目录处理) */ }
|
|
81
142
|
}
|
|
82
143
|
return out;
|
|
@@ -105,17 +166,55 @@ function aggregateCrossRepoKeys(specBase, changeName) {
|
|
|
105
166
|
return aggregateDeclaredRepos(planContent).filter(k => k !== 'main');
|
|
106
167
|
}
|
|
107
168
|
|
|
169
|
+
/**
|
|
170
|
+
* 读主仓 local.yaml 文本(路径口径与 run/shared.js readLocalYamlRaw 一致:cwd/.sillyspec/local.yaml)。
|
|
171
|
+
* @returns {string}
|
|
172
|
+
*/
|
|
173
|
+
function readLocalYamlText(cwd) {
|
|
174
|
+
const p = join(cwd, '.sillyspec', 'local.yaml');
|
|
175
|
+
if (!existsSync(p)) return '';
|
|
176
|
+
try { return readFileSync(p, 'utf8') } catch { return '' }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** worktreePath 是否落在主仓 specBase 旧默认(legacy-main-specbase 形态——该形态不写注册表,公式可寻址) */
|
|
180
|
+
function isLegacyMainSpecbasePlacement(worktreePath, specBase) {
|
|
181
|
+
try {
|
|
182
|
+
return resolvePath(dirname(worktreePath)) === resolvePath(join(specBase, '.runtime', 'worktrees'));
|
|
183
|
+
} catch { return false }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* 仓内落位的 untracked 保障(坑 cross-wt-repo-local-placement):.git/info/exclude 幂等追加
|
|
188
|
+
* 条目——本仓生效、不进版本库(不碰用户 .gitignore 工作区文件、无 pull 冲突面)。创建 worktree
|
|
189
|
+
* 前调用,worktree 目录一出现即被 git 忽略。追加失败(只读仓等)仅 warn 不阻断——最坏回落
|
|
190
|
+
* 「worktree 成 untracked 噪音」,apply 侧清单校验本就过滤。
|
|
191
|
+
* @param {string} repoRoot 跨仓仓根
|
|
192
|
+
* @param {string} entry exclude 条目(如 '.sillyspec/')
|
|
193
|
+
*/
|
|
194
|
+
function ensureRepoLocalExclude(repoRoot, entry) {
|
|
195
|
+
try {
|
|
196
|
+
const excludePath = join(repoRoot, '.git', 'info', 'exclude');
|
|
197
|
+
let content = '';
|
|
198
|
+
if (existsSync(excludePath)) {
|
|
199
|
+
try { content = readFileSync(excludePath, 'utf8') } catch { content = '' }
|
|
200
|
+
}
|
|
201
|
+
const entryRe = new RegExp(`^${entry.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'm');
|
|
202
|
+
if (entryRe.test(content)) return; // 幂等:已有条目不动
|
|
203
|
+
const sep = content && !content.endsWith('\n') ? '\n' : '';
|
|
204
|
+
const stamp = `${sep}# sillyspec cross-repo worktree placement (auto)\n${entry}\n`;
|
|
205
|
+
mkdirSync(join(repoRoot, '.git', 'info'), { recursive: true });
|
|
206
|
+
writeAtomicSync(excludePath, content + stamp);
|
|
207
|
+
} catch (e) {
|
|
208
|
+
console.warn(`⚠️ 跨仓仓 .git/info/exclude 追加失败(${e.message})——worktree 目录可能成 untracked 噪音,apply 清单校验仍会过滤,建议手工补 ignore。`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
108
212
|
/**
|
|
109
213
|
* 读主仓 local.yaml 的 repos: 段(路径口径与 run/shared.js readLocalYamlRaw 一致:cwd/.sillyspec/local.yaml)。
|
|
110
214
|
* @returns {Map<string,string>}
|
|
111
215
|
*/
|
|
112
216
|
function readRepoRegistry(cwd) {
|
|
113
|
-
|
|
114
|
-
let text = '';
|
|
115
|
-
if (existsSync(p)) {
|
|
116
|
-
try { text = readFileSync(p, 'utf8') } catch { text = '' }
|
|
117
|
-
}
|
|
118
|
-
return parseRepoRegistry(text);
|
|
217
|
+
return parseRepoRegistry(readLocalYamlText(cwd));
|
|
119
218
|
}
|
|
120
219
|
|
|
121
220
|
/**
|
|
@@ -132,6 +231,11 @@ export function ensureCrossWorktrees({ cwd, changeName, specBase }) {
|
|
|
132
231
|
const base = specBase || join(cwd, '.sillyspec');
|
|
133
232
|
const keys = aggregateCrossRepoKeys(base, changeName);
|
|
134
233
|
const registry = readRepoRegistry(cwd);
|
|
234
|
+
// 落位配置双源(坑 cross-wt-toolchain-split 配置简化,2026-10-10 用户反馈):推荐 repos 条目
|
|
235
|
+
// 内联(repos.<key>: {path, worktree}——注册与落位一处声明);worktree.crossPlacement 段兼容
|
|
236
|
+
// 保留作 legacy 读面。优先级:repos.<key>.worktree > worktree.crossPlacement.<key> > 默认公式。
|
|
237
|
+
const inlinePlacements = parseRepoWorktreePlacements(readLocalYamlText(cwd));
|
|
238
|
+
const placementCfg = readCrossPlacementConfig(cwd);
|
|
135
239
|
const created = [];
|
|
136
240
|
const reused = [];
|
|
137
241
|
const skippedLegacy = [];
|
|
@@ -156,9 +260,36 @@ export function ensureCrossWorktrees({ cwd, changeName, specBase }) {
|
|
|
156
260
|
try { gitQuiet(repoRoot, ['worktree', 'prune'], { timeout: 30000 }) } catch {}
|
|
157
261
|
}
|
|
158
262
|
|
|
159
|
-
|
|
263
|
+
// 落位解析(FR-01/FR-02):显式(repos.<key>.worktree 内联 > crossPlacement.<key> legacy)>
|
|
264
|
+
// 新默认=仓内 <repoRoot>/.sillyspec/.runtime/worktrees(坑 cross-wt-repo-local-placement:
|
|
265
|
+
// 天然同盘同文件系统,WSL 主仓+Windows 盘跨仓的工具链分裂自动消解——用户 2026-10-10 提议,
|
|
266
|
+
// 对齐主仓 worktree 的同构形态)。旧默认(主仓 specBase 下)降 legacy 兜底(resolve/扫描
|
|
267
|
+
// 仍认,存量 worktree 全链可达)。
|
|
268
|
+
// 显式配置指向仓根内维持拒绝(手配仓内=误配——untracked 噪音面);新默认仓内合法,untracked
|
|
269
|
+
// 面由 ensureRepoLocalExclude 的 .git/info/exclude 保障(不动用户 .gitignore、不进版本库)。
|
|
270
|
+
const rawPlacement = inlinePlacements.get(key) || placementCfg.get(key) || null;
|
|
271
|
+
let placementRoot = null;
|
|
272
|
+
let placementMode = 'repo-local';
|
|
273
|
+
if (rawPlacement) {
|
|
274
|
+
placementRoot = resolvePlacementRoot(rawPlacement, cwd);
|
|
275
|
+
placementMode = inlinePlacements.has(key) ? 'explicit-inline' : 'explicit-legacy';
|
|
276
|
+
const rel = relativePath(resolvePath(repoRoot), placementRoot);
|
|
277
|
+
if (rel === '' || (!rel.startsWith('..') && !isAbsolute(rel))) {
|
|
278
|
+
throw new Error(
|
|
279
|
+
`跨仓 repo "${key}" 的落位配置(${placementRoot})落在该仓仓根(${repoRoot})之内——` +
|
|
280
|
+
`显式落位应选该仓之外的目录(仓内落位请删掉该配置走默认)。重跑 execute 前修正配置。`
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const worktreePath = crossWorktreePath(base, changeName, key, placementRoot || join(repoRoot, '.sillyspec', '.runtime', 'worktrees'));
|
|
160
285
|
const branch = BRANCH_PREFIX + changeName;
|
|
161
286
|
|
|
287
|
+
// 仓内落位的 untracked 保障:.git/info/exclude 幂等追加 .sillyspec/(本仓生效、不进版本库、
|
|
288
|
+
// 不碰用户 .gitignore)。创建 worktree 前落盘——worktree 目录一出现即被忽略。
|
|
289
|
+
if (!rawPlacement) {
|
|
290
|
+
ensureRepoLocalExclude(repoRoot, '.sillyspec/');
|
|
291
|
+
}
|
|
292
|
+
|
|
162
293
|
// base 快照:跨仓仓当前 HEAD(与主仓 create 的默认 base 语义一致)
|
|
163
294
|
let baseHash;
|
|
164
295
|
try {
|
|
@@ -175,13 +306,27 @@ export function ensureCrossWorktrees({ cwd, changeName, specBase }) {
|
|
|
175
306
|
` ③ 换变更名重跑。`
|
|
176
307
|
);
|
|
177
308
|
}
|
|
178
|
-
|
|
309
|
+
if (rawPlacement) {
|
|
310
|
+
mkdirSync(placementRoot, { recursive: true });
|
|
311
|
+
} else {
|
|
312
|
+
mkdirSync(join(repoRoot, '.sillyspec', '.runtime', 'worktrees'), { recursive: true });
|
|
313
|
+
}
|
|
179
314
|
try {
|
|
180
315
|
git(repoRoot, ['worktree', 'add', worktreePath, '-b', branch, baseHash], { timeout: 120000 });
|
|
181
316
|
} catch (e) {
|
|
182
317
|
throw new Error(`跨仓仓 ${key} worktree 创建失败(${worktreePath}):${e.stderr || e.message}`);
|
|
183
318
|
}
|
|
184
319
|
|
|
320
|
+
// WSL 跨文件系统分裂警告(FR-03,advisory 不阻断——坑 cross-wt-toolchain-split:主仓在
|
|
321
|
+
// WSL /root、跨仓在 /mnt/<盘>/ Windows 盘时,worktree 默认落 WSL 原生 fs,该仓 Windows
|
|
322
|
+
// 侧工具链(mvn/JDK)够不着,隔离形同虚设——fire-equipment 2026-10-10 实证被迫绕开
|
|
323
|
+
// worktree 直写主干。配置同侧落位后本警告自然消音)。
|
|
324
|
+
if (isWslSplit(repoRoot, worktreePath)) {
|
|
325
|
+
console.warn(`⚠️ 跨仓 ${key}: 工具链分裂风险——仓根在 WSL automount Windows 盘(${repoRoot})而 worktree 落在 WSL 原生文件系统(${worktreePath})。`);
|
|
326
|
+
console.warn(` 该仓若依赖 Windows 侧构建工具链(mvn/JDK/IDE),将无法访问 worktree——2026-10-10 实证此形态下 agent 被迫绕开 worktree 直写主副本。`);
|
|
327
|
+
console.warn(` 建议:local.yaml 配 worktree.crossPlacement.${key} 到该仓可达的目录(如同盘符路径),重跑 execute 前先 sillyspec worktree cleanup ${changeName} --force。`);
|
|
328
|
+
}
|
|
329
|
+
|
|
185
330
|
const meta = {
|
|
186
331
|
name_zh: 'worktree 元数据',
|
|
187
332
|
changeName,
|
|
@@ -195,6 +340,7 @@ export function ensureCrossWorktrees({ cwd, changeName, specBase }) {
|
|
|
195
340
|
createdAt: new Date().toISOString(),
|
|
196
341
|
worktreePath,
|
|
197
342
|
mode: 'worktree',
|
|
343
|
+
placementMode,
|
|
198
344
|
};
|
|
199
345
|
writeAtomicSync(join(worktreePath, META_FILE), JSON.stringify(meta, null, 2) + '\n');
|
|
200
346
|
|
|
@@ -209,6 +355,12 @@ export function ensureCrossWorktrees({ cwd, changeName, specBase }) {
|
|
|
209
355
|
console.log(`ℹ️ 跨仓 ${key}: 跳过 dirty baseline checkpoint(跨仓主副本在途改动不进本变更分支——含并行会话外来文件时防污染;锚点 = 跨仓仓 HEAD ${String(baseHash).slice(0, 8)})`);
|
|
210
356
|
writeAtomicSync(join(worktreePath, META_FILE), JSON.stringify(meta, null, 2) + '\n');
|
|
211
357
|
|
|
358
|
+
// 落位注册表(FR-02):仓内新默认与显式落位都记录位置(新默认位置依赖 repoRoot,读取方
|
|
359
|
+
// 无 repoRoot 上下文时公式兜底不可用——注册表是主寻址);落主仓 specBase 的旧默认不写。
|
|
360
|
+
if (placementRoot || !isLegacyMainSpecbasePlacement(worktreePath, base)) {
|
|
361
|
+
updatePlacementRegistry(base, changeName, key, { worktreePath, placementRoot: placementRoot || join(repoRoot, '.sillyspec', '.runtime', 'worktrees') });
|
|
362
|
+
}
|
|
363
|
+
|
|
212
364
|
// deps 供给:specBase 传 null —— sniff worktree 自身(主仓 local.yaml 的 project.type/install
|
|
213
365
|
// 描述的是主仓,跨仓仓类型常不同,如实测 maven 主仓 + nodejs 前端仓,沿用会把 mvn 命令
|
|
214
366
|
// 打到前端仓上)。失败不阻断(与主仓 create 同语义,doctor --fix 可修)
|
|
@@ -229,7 +381,7 @@ export function ensureCrossWorktrees({ cwd, changeName, specBase }) {
|
|
|
229
381
|
writeAtomicSync(join(worktreePath, META_FILE), JSON.stringify(meta, null, 2) + '\n');
|
|
230
382
|
}
|
|
231
383
|
|
|
232
|
-
created.push({ repoKey: key, worktreePath });
|
|
384
|
+
created.push({ repoKey: key, worktreePath, meta });
|
|
233
385
|
}
|
|
234
386
|
return { created, reused, skippedLegacy };
|
|
235
387
|
}
|
|
@@ -255,6 +407,7 @@ export function cleanupCrossWorktrees({ cwd, changeName, specBase, force = false
|
|
|
255
407
|
const wtPath = meta.worktreePath || crossWorktreePath(base, changeName, repoKey);
|
|
256
408
|
if (!existsSync(wtPath)) {
|
|
257
409
|
try { if (repoRoot) gitQuiet(repoRoot, ['worktree', 'prune'], { timeout: 30000 }) } catch {}
|
|
410
|
+
removePlacementRegistryEntry(base, changeName, repoKey); // 挪位清理后注册表键回收(FR-02)
|
|
258
411
|
results.push({ repoKey, result: 'skipped', details: ['目录不存在(已清理)'], residual });
|
|
259
412
|
continue;
|
|
260
413
|
}
|
|
@@ -282,7 +435,15 @@ export function cleanupCrossWorktrees({ cwd, changeName, specBase, force = false
|
|
|
282
435
|
try { gitQuiet(repoRoot, ['worktree', 'prune'], { timeout: 30000 }) } catch { residual.push(`git worktree prune 失败(${repoRoot})`) }
|
|
283
436
|
}
|
|
284
437
|
details.push(`分支 ${meta.branch} 保留作 review 锚点(确认无需回溯后可 git -C ${repoRoot} branch -D ${meta.branch})`);
|
|
438
|
+
if (residual.length === 0) {
|
|
439
|
+
removePlacementRegistryEntry(base, changeName, repoKey); // cleaned 后注册表键回收(FR-02)
|
|
440
|
+
}
|
|
285
441
|
results.push({ repoKey, result: residual.length > 0 ? 'partial' : 'cleaned', details, residual });
|
|
286
442
|
}
|
|
443
|
+
// 差集 sweep(评审 P2 吸收):cleanup 迭代源=listCrossWorktreeMetas 只列有 meta 条目,
|
|
444
|
+
// 「worktree remove 连目录带 meta 同亡、仅注册表键存」的悬挂键够不着——末尾对本变更名下
|
|
445
|
+
// 全部键按「worktreePath 处无 meta.json」差集清(活键不删)。
|
|
446
|
+
const swept = sweepPlacementRegistry(base, changeName);
|
|
447
|
+
if (swept.length > 0) results.push({ repoKey: '(registry-sweep)', result: 'cleaned', details: [`悬挂注册表键已清: ${swept.join('、')}`], residual: [] });
|
|
287
448
|
return { results };
|
|
288
449
|
}
|
package/src/worktree-deps.js
CHANGED
|
@@ -38,21 +38,24 @@ const DEFAULT_TIMEOUT_MS = 300 * 1000;
|
|
|
38
38
|
*/
|
|
39
39
|
const ECOSYSTEMS = [
|
|
40
40
|
{
|
|
41
|
+
// JVM 系(maven/gradle)install=null(坑 maven-provision-runs-tests,2026-10-10 用户实证):
|
|
42
|
+
// 依赖在用户级仓库(~/.m2、~/.gradle),worktree 内无本地产物可供给——根供给诚实落 n/a
|
|
43
|
+
// (deps 门控放行集含 n/a)。旧默认 'mvn -o test' 把跑测试当供给:冷缓存 + 离线 + 工具链
|
|
44
|
+
// 不在 PATH 必败 → depsStatus=failed 卡死 execute --done(agent 被迫手改 meta)。构建/测试
|
|
45
|
+
// 归 task 级验证(mvn compile / gradle build 在任务验收面显式执行);显式 commands.install
|
|
46
|
+
// 配置时仍按白名单执行(inferInstallCommand 优先级不变)。
|
|
41
47
|
type: 'maven',
|
|
42
48
|
manifests: ['pom.xml'],
|
|
43
49
|
marker: null,
|
|
44
50
|
detect: (d) => existsSync(join(d, 'pom.xml')),
|
|
45
|
-
install:
|
|
51
|
+
install: null,
|
|
46
52
|
},
|
|
47
53
|
{
|
|
48
54
|
type: 'gradle',
|
|
49
55
|
manifests: ['build.gradle.kts', 'build.gradle', 'settings.gradle', 'gradle.lockfile'],
|
|
50
56
|
marker: null,
|
|
51
57
|
detect: (d) => existsSync(join(d, 'build.gradle')) || existsSync(join(d, 'build.gradle.kts')),
|
|
52
|
-
|
|
53
|
-
install: (d) => (process.platform === 'win32'
|
|
54
|
-
? (existsSync(join(d, 'gradlew.bat')) ? 'gradlew.bat test' : 'gradle test')
|
|
55
|
-
: './gradlew test'),
|
|
58
|
+
install: null,
|
|
56
59
|
},
|
|
57
60
|
{
|
|
58
61
|
type: 'nodejs',
|
|
@@ -210,7 +213,10 @@ export function detectProjectType(worktreePath, specBase) {
|
|
|
210
213
|
export function inferInstallCommand(projectType, worktreePath, userInstall) {
|
|
211
214
|
if (userInstall) return userInstall;
|
|
212
215
|
const eco = ECOSYSTEMS.find(e => e.type === projectType);
|
|
213
|
-
|
|
216
|
+
// install:null 表项(maven/gradle——依赖在用户级仓库无 worktree 内供给面)与表外类型同归
|
|
217
|
+
// null → 根供给 n/a(坑 maven-provision-runs-tests)
|
|
218
|
+
if (!eco || typeof eco.install !== 'function') return null;
|
|
219
|
+
return eco.install(worktreePath);
|
|
214
220
|
}
|
|
215
221
|
|
|
216
222
|
/** 在 worktreePath 创建 node_modules 链接到 mainNodeModules;失败回退 */
|
|
@@ -640,10 +646,45 @@ export function provisionDeps(worktreePath, mainCwd, opts = {}) {
|
|
|
640
646
|
const lines = String(yamlText || '').replace(/\r\n?/g, '\n').split('\n');
|
|
641
647
|
const start = lines.findIndex(l => /^repos:\s*(?:#.*)?$/.test(l));
|
|
642
648
|
if (start === -1) return roots;
|
|
649
|
+
const cleanVal = (v) => String(v || '').replace(/\s+#.*$/, '').trim().replace(/^['"]|['"]$/g, '');
|
|
643
650
|
for (let i = start + 1; i < lines.length; i++) {
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
651
|
+
const line = lines[i];
|
|
652
|
+
if (line.length > 0 && !/\s/.test(line[0]) && line.trim() !== '' && !line.startsWith('#')) break;
|
|
653
|
+
const m = line.match(/^\s+([A-Za-z0-9_.\-]+):\s*(.*)$/);
|
|
654
|
+
if (!m) continue;
|
|
655
|
+
const value = (m[2] || '').trim();
|
|
656
|
+
if (value !== '' && !value.startsWith('#')) {
|
|
657
|
+
if (value.startsWith('{') && value.endsWith('}')) {
|
|
658
|
+
// inline 对象条目(repos.<key>: {path: .., worktree: ..})——只收 path 子键
|
|
659
|
+
for (const part of value.slice(1, -1).split(',')) {
|
|
660
|
+
const idx = part.indexOf(':');
|
|
661
|
+
if (idx !== -1 && part.slice(0, idx).trim() === 'path') {
|
|
662
|
+
const p = cleanVal(part.slice(idx + 1));
|
|
663
|
+
if (p) roots.push(p);
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
} else {
|
|
668
|
+
roots.push(cleanVal(value));
|
|
669
|
+
}
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
// 块式对象条目(value 空):消费子键块,只收 path 子键值(worktree 落位根不是仓根,
|
|
673
|
+
// 混入会放宽越界豁免面——坑 repo-inline-worktree-placement 收口)
|
|
674
|
+
const indent = line.length - line.trimStart().length;
|
|
675
|
+
let j = i + 1;
|
|
676
|
+
for (; j < lines.length; j++) {
|
|
677
|
+
const l = lines[j];
|
|
678
|
+
if (l.trim() === '' || l.trim().startsWith('#')) continue;
|
|
679
|
+
const subIndent = l.length - l.trimStart().length;
|
|
680
|
+
if (subIndent <= indent) break;
|
|
681
|
+
const sub = l.match(/^\s+([A-Za-z0-9_.\-]+):\s*(.*)$/);
|
|
682
|
+
if (sub && sub[1] === 'path') {
|
|
683
|
+
const p = cleanVal(sub[2]);
|
|
684
|
+
if (p && !p.startsWith('#')) roots.push(p);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
i = j - 1;
|
|
647
688
|
}
|
|
648
689
|
return roots;
|
|
649
690
|
})();
|
package/src/worktree.js
CHANGED
|
@@ -1326,9 +1326,17 @@ export class WorktreeManager {
|
|
|
1326
1326
|
* 写进 worktree 内的 .sillyspec——而 apply 的 filterDeliverableFiles 把 .sillyspec/changes/ 排除在
|
|
1327
1327
|
* 交付外(spec 文档不进代码 patch),cleanup 删除 worktree 这些产物即蒸发、主仓流程看不到。
|
|
1328
1328
|
* 删除前扫 worktree 内两棵子树做最后打捞:
|
|
1329
|
-
* changes/<name>/** →
|
|
1330
|
-
*
|
|
1331
|
-
*
|
|
1329
|
+
* changes/<name>/** → 三态判定(坑 archive-resurrect,2026-10-10 下游实证
|
|
1330
|
+
* 2026-10-09-attachment-inline-reference 残留 13 文件:归档把 changes/<name>/
|
|
1331
|
+
* 整体 rename 进 changes/archive/ 后,原路径缺失被误判 worktree 独有,旧快照
|
|
1332
|
+
* 整批复制回原路径复活已归档目录):
|
|
1333
|
+
* · 原路径同名存在 → 内容不同仅列清单 warn 不覆盖(主仓版本可能是更新的)
|
|
1334
|
+
* · 原路径缺、changes/archive/<name>/ 有副本 → 判归档搬运,跳过不复制
|
|
1335
|
+
* (不复活原路径、不覆盖 archive 权威副本)
|
|
1336
|
+
* · 原路径与 archive 均缺 → 真 worktree 独有照捞:归档态捞进
|
|
1337
|
+
* changes/archive/<name>/(原路径已注销禁止重建),未归档态捞回原路径
|
|
1338
|
+
* docs/** → 主仓缺失则 copy 回(模块文档卡片;同名差异不提示——tracked checkout 副本常态;
|
|
1339
|
+
* docs 不参与归档搬运,无三态)
|
|
1332
1340
|
* 只扫本变更子树(其它 change 的副本文件不越权搬运)。异常只 warn 不阻断清理。
|
|
1333
1341
|
* @private
|
|
1334
1342
|
*/
|
|
@@ -1340,7 +1348,11 @@ export class WorktreeManager {
|
|
|
1340
1348
|
if (resolve(wtSpec) === resolve(mainSpec)) return; // in-place:同一目录,无需打捞
|
|
1341
1349
|
const salvaged = [];
|
|
1342
1350
|
const conflicts = [];
|
|
1343
|
-
const
|
|
1351
|
+
const archivedSkipped = [];
|
|
1352
|
+
const salvagedIntoArchive = [];
|
|
1353
|
+
const archivedBase = join('changes', 'archive', name);
|
|
1354
|
+
const archivedBaseExists = existsSync(join(mainSpec, archivedBase));
|
|
1355
|
+
const salvageTree = (subDir, reportConflict, trackArchive) => {
|
|
1344
1356
|
const srcRoot = join(wtSpec, subDir);
|
|
1345
1357
|
if (!existsSync(srcRoot)) return;
|
|
1346
1358
|
const walk = (dir) => {
|
|
@@ -1349,28 +1361,50 @@ export class WorktreeManager {
|
|
|
1349
1361
|
if (entry.isDirectory()) { walk(src); continue; }
|
|
1350
1362
|
if (!entry.isFile()) continue;
|
|
1351
1363
|
const rel = relative(wtSpec, src);
|
|
1364
|
+
const rest = relative(srcRoot, src);
|
|
1352
1365
|
const dst = join(mainSpec, rel);
|
|
1366
|
+
const archivedDst = trackArchive ? join(mainSpec, archivedBase, rest) : null;
|
|
1353
1367
|
try {
|
|
1354
|
-
if (
|
|
1368
|
+
if (existsSync(dst)) {
|
|
1369
|
+
if (reportConflict && readFileSync(src, 'utf8') !== readFileSync(dst, 'utf8')) {
|
|
1370
|
+
conflicts.push(rel);
|
|
1371
|
+
}
|
|
1372
|
+
} else if (archivedDst && existsSync(archivedDst)) {
|
|
1373
|
+
archivedSkipped.push(rest);
|
|
1374
|
+
} else if (archivedDst && archivedBaseExists) {
|
|
1375
|
+
mkdirSync(dirname(archivedDst), { recursive: true });
|
|
1376
|
+
copyFileSync(src, archivedDst);
|
|
1377
|
+
salvagedIntoArchive.push(rest);
|
|
1378
|
+
} else {
|
|
1355
1379
|
mkdirSync(dirname(dst), { recursive: true });
|
|
1356
1380
|
copyFileSync(src, dst);
|
|
1357
1381
|
salvaged.push(rel);
|
|
1358
|
-
} else if (reportConflict && readFileSync(src, 'utf8') !== readFileSync(dst, 'utf8')) {
|
|
1359
|
-
conflicts.push(rel);
|
|
1360
1382
|
}
|
|
1361
1383
|
} catch { /* 单文件失败不拖垮整体打捞 */ }
|
|
1362
1384
|
}
|
|
1363
1385
|
};
|
|
1364
1386
|
walk(srcRoot);
|
|
1365
1387
|
};
|
|
1366
|
-
salvageTree(join('changes', name), true);
|
|
1367
|
-
salvageTree('docs', false);
|
|
1388
|
+
salvageTree(join('changes', name), true, true);
|
|
1389
|
+
salvageTree('docs', false, false);
|
|
1368
1390
|
if (salvaged.length > 0) {
|
|
1369
1391
|
console.warn(`⚠️ worktree 清理前打捞:${salvaged.length} 个 spec 流程产物只存在于 worktree 副本,已复制回主仓(apply 不搬运 .sillyspec/changes,不捞即随清理蒸发):`);
|
|
1370
1392
|
for (const f of salvaged.slice(0, 20)) console.warn(` ${join(mainSpec, f)}`);
|
|
1371
1393
|
if (salvaged.length > 20) console.warn(` …等共 ${salvaged.length} 个`);
|
|
1372
1394
|
details.push(`salvaged ${salvaged.length} spec artifacts to main repo`);
|
|
1373
1395
|
}
|
|
1396
|
+
if (salvagedIntoArchive.length > 0) {
|
|
1397
|
+
console.warn(`⚠️ worktree 清理前打捞:${salvagedIntoArchive.length} 个真独有产物(原路径与归档副本均缺)已捞进归档副本(变更已归档,原路径不重建):`);
|
|
1398
|
+
for (const f of salvagedIntoArchive.slice(0, 20)) console.warn(` ${join(mainSpec, archivedBase, f)}`);
|
|
1399
|
+
if (salvagedIntoArchive.length > 20) console.warn(` …等共 ${salvagedIntoArchive.length} 个`);
|
|
1400
|
+
details.push(`salvaged ${salvagedIntoArchive.length} spec artifacts into archive copy`);
|
|
1401
|
+
}
|
|
1402
|
+
if (archivedSkipped.length > 0) {
|
|
1403
|
+
console.warn(`⏭️ worktree 清理前打捞:${archivedSkipped.length} 个流程产物判定为归档搬运(主仓 changes/${name}/ 原路径不存在、changes/archive/${name}/ 已有副本),跳过不复制——防复活已归档目录:`);
|
|
1404
|
+
for (const f of archivedSkipped.slice(0, 20)) console.warn(` ${join(mainSpec, archivedBase, f)}`);
|
|
1405
|
+
if (archivedSkipped.length > 20) console.warn(` …等共 ${archivedSkipped.length} 个`);
|
|
1406
|
+
details.push(`skipped ${archivedSkipped.length} archived-moved spec artifacts (resurrect guard: changes/${name}/ not rebuilt)`);
|
|
1407
|
+
}
|
|
1374
1408
|
if (conflicts.length > 0) {
|
|
1375
1409
|
console.warn(`⚠️ worktree 与主仓存在同名但内容不同的流程产物(不覆盖,请人工确认是否需要合并):`);
|
|
1376
1410
|
for (const f of conflicts.slice(0, 20)) console.warn(` ${join(mainSpec, f)}`);
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: sillyspec:run-quick
|
|
3
|
-
description: 已退役重定向 — quick 通道已拒绝新会话(v3.31.0 起),小修补请用 /sillyspec:flow
|
|
4
|
-
---
|
|
5
|
-
# 通道已退役
|
|
6
|
-
|
|
7
|
-
quick 通道已于 v3.31.0 直接退役:`sillyspec run quick` 对新会话一律拒绝(exit 1 并指路 flow start)。本卡只保留触发词重定向,勿执行任何 quick 新会话命令。
|
|
8
|
-
|
|
9
|
-
范围明确的小修补请走**轻量变更**(/sillyspec:flow):
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
sillyspec flow start --change <变更名> --input "<动机与背景>
|
|
13
|
-
|
|
14
|
-
成功标准:
|
|
15
|
-
- <可验证标准>"
|
|
16
|
-
# → 直接干活(改代码写测试,治理工件 CLI 机器起草)
|
|
17
|
-
sillyspec flow done --change <变更名>
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
升级前遗留的在途 quick 会话仍可收尾:`sillyspec run quick --change <会话ID>` 续跑 / `--done` 收口 / `--cancel` 取消(以 CLI 拒绝文案里的指引为准)。
|