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
|
@@ -497,6 +497,22 @@ function parseSimpleYaml(content) {
|
|
|
497
497
|
if (typeof result[topKey] !== 'object' || Array.isArray(result[topKey])) result[topKey] = {}
|
|
498
498
|
if (!Array.isArray(result[topKey][childKey])) result[topKey][childKey] = []
|
|
499
499
|
result[topKey][childKey].push(parseValue(trimmed.slice(2)))
|
|
500
|
+
continue
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// indent>=4 的 `<子键>: <值>`(三层嵌套标量,坑 repo-inline-worktree-placement:repos 条目
|
|
504
|
+
// 对象形态 `repos: → <key>: → path:/worktree:`)——空数组占位(indent2 无值条目)转对象后
|
|
505
|
+
// 挂键。此前该形态被静默丢弃,analyzeCrossRepoCd 等消费方读不到三层配置。
|
|
506
|
+
if (indent >= 4 && childKey) {
|
|
507
|
+
const subMatch = trimmed.match(/^([^:]+):\s*(.*)$/)
|
|
508
|
+
if (!subMatch) continue
|
|
509
|
+
if (UNSAFE_KEYS.has(subMatch[1].trim())) continue
|
|
510
|
+
if (typeof result[topKey] !== 'object' || Array.isArray(result[topKey])) result[topKey] = {}
|
|
511
|
+
const cur = result[topKey][childKey]
|
|
512
|
+
if (Array.isArray(cur) && cur.length === 0) result[topKey][childKey] = {}
|
|
513
|
+
if (result[topKey][childKey] && typeof result[topKey][childKey] === 'object' && !Array.isArray(result[topKey][childKey])) {
|
|
514
|
+
result[topKey][childKey][subMatch[1].trim()] = parseValue(subMatch[2])
|
|
515
|
+
}
|
|
500
516
|
}
|
|
501
517
|
}
|
|
502
518
|
|
|
@@ -886,8 +902,25 @@ function analyzeCrossRepoCd(command, callerCwd, projectRoot, inWorktree) {
|
|
|
886
902
|
const shellResolved = path.isAbsolute(t) ? t : path.resolve(callerCwd, t)
|
|
887
903
|
const intended = path.isAbsolute(t) ? t : path.resolve(inWorktree ? projectRoot : callerCwd, t)
|
|
888
904
|
for (const [key, raw] of Object.entries(repos)) {
|
|
889
|
-
|
|
890
|
-
|
|
905
|
+
// 条目双形态(坑 repo-inline-worktree-placement):'path' 字符串(parseSimpleYaml 原样);
|
|
906
|
+
// 块式对象 {path, worktree}(parseSimpleYaml 嵌套);inline '{path: ..}' 字符串
|
|
907
|
+
// (parseSimpleYaml 不解析 inline flow,原样字符串——手动提取 path 子键)
|
|
908
|
+
let repoPath = null
|
|
909
|
+
if (typeof raw === 'string' && raw) {
|
|
910
|
+
const t = raw.trim()
|
|
911
|
+
if (t.startsWith('{') && t.endsWith('}')) {
|
|
912
|
+
for (const part of t.slice(1, -1).split(',')) {
|
|
913
|
+
const idx = part.indexOf(':')
|
|
914
|
+
if (idx !== -1 && part.slice(0, idx).trim() === 'path') { repoPath = part.slice(idx + 1).trim().replace(/^['"]|['"]$/g, ''); break }
|
|
915
|
+
}
|
|
916
|
+
} else {
|
|
917
|
+
repoPath = raw
|
|
918
|
+
}
|
|
919
|
+
} else if (raw && typeof raw === 'object' && typeof raw.path === 'string' && raw.path) {
|
|
920
|
+
repoPath = raw.path
|
|
921
|
+
}
|
|
922
|
+
if (!repoPath) continue
|
|
923
|
+
const repoRoot = path.isAbsolute(repoPath) ? repoPath : path.resolve(projectRoot, repoPath)
|
|
891
924
|
if (intended === repoRoot || isPathInside(intended, repoRoot)) {
|
|
892
925
|
hits.push({ key, repoRoot, resolved: intended, shellResolved, target: t })
|
|
893
926
|
break
|
package/src/index.js
CHANGED
|
@@ -246,7 +246,7 @@ async function main() {
|
|
|
246
246
|
|
|
247
247
|
// E22:重路径统一加载(轻路径 --version/help 已早退,未付此税)。
|
|
248
248
|
const { ProgressManager, resolvePlatformSpecDir, resolvePlatformOpts } = await import('./progress.js');
|
|
249
|
-
const { didYouMean, assertSafeChangeName, assertDatedChangeName, resolveSpecDir, detectWorktreeSpecDrift, detectCwdInsideWorktree, ancestorSpecDirs } = await import('./run/shared.js');
|
|
249
|
+
const { didYouMean, assertSafeChangeName, assertDatedChangeName, resolveSpecDir, detectWorktreeSpecDrift, detectCwdInsideWorktree, ancestorSpecDirs, resolveUninitCwdGate } = await import('./run/shared.js');
|
|
250
250
|
|
|
251
251
|
// 解析全局选项
|
|
252
252
|
let json = false;
|
|
@@ -354,7 +354,9 @@ async function main() {
|
|
|
354
354
|
return baseDir
|
|
355
355
|
}
|
|
356
356
|
|
|
357
|
-
|
|
357
|
+
// let:下方未初始化目录硬拦命中祖先链时重锚定到 spec 根(治裸 join(dir,'.sillyspec')
|
|
358
|
+
// 调用点如 review status rsSpecBase 的子目录漂移;resolveEffectiveDir 系本就上溯,变为无操作)
|
|
359
|
+
let dir = targetDir;
|
|
358
360
|
|
|
359
361
|
if (command === 'init' && !existsSync(dir)) {
|
|
360
362
|
const { mkdirSync } = await import('fs');
|
|
@@ -388,6 +390,33 @@ async function main() {
|
|
|
388
390
|
}
|
|
389
391
|
}
|
|
390
392
|
|
|
393
|
+
// ── 未初始化目录硬拦(坑:agent 在未初始化/错误目录跑非 init 命令,fail-soft 散点报错不说
|
|
394
|
+
// 正确 cwd——实测 status 在未初始化目录提示可开变更、flow start 到 local.yaml 才报错、
|
|
395
|
+
// knowledge 只报参数错,agent 多轮试错)──
|
|
396
|
+
// 判定与豁免清单收敛在 run/shared.js resolveUninitCwdGate(单一真相源,测试直测);
|
|
397
|
+
// 平台 pointer/接管声明/显式 --spec-dir/平台 flag 由其判 skip(平台项目不受本门误拦)。
|
|
398
|
+
// 未初始化目录无 ground truth——文案只给锚点(git root)与三条出路,不断言唯一正确 cwd。
|
|
399
|
+
const uninitGate = resolveUninitCwdGate(command, {
|
|
400
|
+
dir,
|
|
401
|
+
specDir,
|
|
402
|
+
// --spec-root 同为平台显式意图(平台模式首扫 run scan --spec-root 在无 pointer 的新目录
|
|
403
|
+
// 合法运行,spec 恒在仓外)——与 --workspace-id/--runtime-root 一并越过本门
|
|
404
|
+
platformFlags: platformWorkspaceId || platformRuntimeRoot || filteredArgs.includes('--spec-root'),
|
|
405
|
+
});
|
|
406
|
+
if (uninitGate.verdict === 'block') {
|
|
407
|
+
console.error(`\n❌ 当前目录未初始化(${dir}):${uninitGate.gitRoot
|
|
408
|
+
? `已上溯祖先链至 git root(${uninitGate.gitRoot})`
|
|
409
|
+
: '已上溯祖先链(当前不在 git 仓内)'}未找到 .sillyspec,平台指针亦无。`);
|
|
410
|
+
console.error(' 非 init/scan 类命令需要已初始化的项目根。');
|
|
411
|
+
console.error(' 修复:① cd 回项目根(含 .sillyspec 的目录)再跑;② 新项目先 sillyspec init(棕地项目 sillyspec scan);③ 或 --spec-dir <路径> 显式指定 .sillyspec 位置。');
|
|
412
|
+
console.error(' 查询/诊断类命令(status/doctor/progress/next/knowledge 等)不受此拦。');
|
|
413
|
+
process.exit(2);
|
|
414
|
+
}
|
|
415
|
+
if (uninitGate.verdict === 'pass' && uninitGate.anchor && resolve(dir) !== uninitGate.anchor) {
|
|
416
|
+
console.log(`ℹ️ cwd 在子目录(${dir}),已锚定到项目根:${uninitGate.anchor}(spec 解析与产物落点以项目根为准)`);
|
|
417
|
+
dir = uninitGate.anchor;
|
|
418
|
+
}
|
|
419
|
+
|
|
391
420
|
switch (command) {
|
|
392
421
|
case 'init':
|
|
393
422
|
await (await import('./init.js')).cmdInit(dir, { tool, tools: toolValues.length > 0 ? toolValues : null, interactive, specDir, noSkills, forceCards, platformOpts: (platformWorkspaceId || platformRuntimeRoot) ? { workspaceId: platformWorkspaceId, runtimeRoot: platformRuntimeRoot } : null });
|
|
@@ -4110,7 +4139,8 @@ SillySpec platform — SillyHub 平台同步
|
|
|
4110
4139
|
用法:
|
|
4111
4140
|
sillyspec platform connect <url> [--token <token>]
|
|
4112
4141
|
sillyspec platform disconnect
|
|
4113
|
-
sillyspec platform sync [--change <name>]
|
|
4142
|
+
sillyspec platform sync [--change <name>] 带 --change 同步该变更进度六表+spec 树;
|
|
4143
|
+
不带 --change 全树 spec 文件同步
|
|
4114
4144
|
sillyspec platform sync-docs [--change <name>]
|
|
4115
4145
|
sillyspec platform pull [--change <name>]
|
|
4116
4146
|
sillyspec platform resolve <change-name> <--keep-local|--take-platform|--abort>
|
|
@@ -4225,8 +4255,26 @@ SillySpec platform — SillyHub 平台同步
|
|
|
4225
4255
|
case 'sync': {
|
|
4226
4256
|
const syncChangeIdx = args.indexOf('--change');
|
|
4227
4257
|
const syncChangeName = syncChangeIdx >= 0 && args[syncChangeIdx + 1] && !String(args[syncChangeIdx + 1]).startsWith("--") ? args[syncChangeIdx + 1] : null;
|
|
4228
|
-
|
|
4229
|
-
|
|
4258
|
+
if (syncChangeName) {
|
|
4259
|
+
// manual:用户显式同步,change_deleted 回执噪音闸旁路(显式动作的回执必须可见)
|
|
4260
|
+
await syncModule.sync(syncChangeName, dir, { manual: true });
|
|
4261
|
+
} else {
|
|
4262
|
+
// 全树 spec 文件同步(2026-10-11-sync-conflict-tri-fix FR-03):不带 --change
|
|
4263
|
+
// 此前报「需要指定变更名称」——归档/历史变更无活跃会话标签时无从下手。现走
|
|
4264
|
+
// syncSpecTreeOnly(null) 全树 diff(以服务器清单为锚);进度六表仍按变更
|
|
4265
|
+
// 粒度走 --change。未连接明确报错不静默(对齐 platform pull 口径)。
|
|
4266
|
+
const smForCheck = new syncModule.SyncManager(dir);
|
|
4267
|
+
if (!smForCheck._getPlatform()) {
|
|
4268
|
+
console.error('❌ 未连接平台,请先 sillyspec platform connect');
|
|
4269
|
+
process.exit(1);
|
|
4270
|
+
}
|
|
4271
|
+
const r = await syncModule.syncSpecTreeOnly(null, dir, {});
|
|
4272
|
+
if (r && r.conflict) {
|
|
4273
|
+
console.error(`⚠️ 全树同步检测到冲突(${(r.serverVersions && Object.keys(r.serverVersions).length) || 0} 个文件待裁决),处理:sillyspec platform status 查看 / platform resolve <变更名> 处置`);
|
|
4274
|
+
process.exit(1);
|
|
4275
|
+
}
|
|
4276
|
+
console.log(`✅ 全树 spec 同步完成(${r ? r.synced : 0} 个文件差异)`);
|
|
4277
|
+
}
|
|
4230
4278
|
break;
|
|
4231
4279
|
}
|
|
4232
4280
|
case 'sync-docs': {
|
package/src/init.js
CHANGED
|
@@ -89,9 +89,9 @@ const TOOL_LABELS = {
|
|
|
89
89
|
opencode: 'OpenCode (通过 INSTRUCTIONS.md)',
|
|
90
90
|
};
|
|
91
91
|
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
92
|
+
// 指令文件工具(gemini/opencode)。codex/claude/zcode 共用 AGENTS.md(跨工具通用标准内容源);
|
|
93
|
+
// gemini/opencode 写各自指令文件,内容与 AGENTS.md 同源同全文(改 @AGENTS.md 指针需先验证
|
|
94
|
+
// 两家对 @ 导入语法的支持,留待后续变更)。
|
|
95
95
|
const INSTRUCTION_TOOLS = ['gemini', 'opencode'];
|
|
96
96
|
|
|
97
97
|
const INSTRUCTION_FILE_MAP = {
|
|
@@ -99,7 +99,13 @@ const INSTRUCTION_FILE_MAP = {
|
|
|
99
99
|
opencode: 'INSTRUCTIONS.md',
|
|
100
100
|
};
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
// ── 注入指令文件(全量方案,2026-10-10-init-full-injection:完整模板是唯一注入源)──
|
|
103
|
+
|
|
104
|
+
const AGENTS_TEMPLATE_PATH = join(__dirname, '..', 'templates', 'agents-instruction.md');
|
|
105
|
+
|
|
106
|
+
// 旧版小段(v≤3.32.3:AGENTS.md 追加态受管块与 injectInstructions 的注入源)。仅作老安装
|
|
107
|
+
// 迁移的精确匹配文本,不再是任何注入源——所有状态一律写完整模板。
|
|
108
|
+
const LEGACY_SMALL_BLOCK = `## SillySpec — 规范驱动开发
|
|
103
109
|
|
|
104
110
|
在执行开发任务时,遵循以下规范:
|
|
105
111
|
|
|
@@ -113,41 +119,23 @@ const INJECTION_CONTENT = `## SillySpec — 规范驱动开发
|
|
|
113
119
|
- 各阶段产出文件位于 \`.sillyspec/changes/<变更名>/\` 下
|
|
114
120
|
`;
|
|
115
121
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
// 文件不存在则创建
|
|
124
|
-
if (!existsSync(filePath)) {
|
|
125
|
-
writeFileSync(filePath, INJECTION_CONTENT);
|
|
126
|
-
return;
|
|
122
|
+
function readInstructionTemplate() {
|
|
123
|
+
try {
|
|
124
|
+
return readFileSync(AGENTS_TEMPLATE_PATH, 'utf8');
|
|
125
|
+
} catch {
|
|
126
|
+
console.error(`❌ [sillyspec] 未找到 Agent 指引模板:${AGENTS_TEMPLATE_PATH}`);
|
|
127
|
+
return null;
|
|
127
128
|
}
|
|
128
|
-
|
|
129
|
-
// 已存在 SillySpec 标记则跳过
|
|
130
|
-
const content = readFileSync(filePath, 'utf8');
|
|
131
|
-
if (content.includes('## SillySpec')) return;
|
|
132
|
-
|
|
133
|
-
// 追加到末尾
|
|
134
|
-
writeFileSync(filePath, content.trimEnd() + '\n\n' + INJECTION_CONTENT);
|
|
135
129
|
}
|
|
136
130
|
|
|
137
|
-
// ── 注入 AGENTS.md / CLAUDE.md(claude + codex,版本感知幂等)──
|
|
138
|
-
// AGENTS.md 是唯一承载完整指引的内容源(跨工具通用标准);CLAUDE.md 仅为 @AGENTS.md
|
|
139
|
-
// 导入指针(Claude Code 记忆导入语法)。取代 design: 2026-08-02-init-claude-md 的
|
|
140
|
-
// CLAUDE.md 单文件方案——其 D-004 预留的「marker 方案迁移」即本次变更。
|
|
141
|
-
const AGENTS_TEMPLATE_PATH = join(__dirname, '..', 'templates', 'agents-instruction.md');
|
|
142
|
-
|
|
143
131
|
/**
|
|
144
|
-
*
|
|
132
|
+
* 移除老安装的旧版小段(v≤3.32.3 方案:`## SillySpec` 段,追加在 EOF 或整文件即小段)。
|
|
145
133
|
* 优先精确匹配旧文本;用户编辑过 / CRLF 漂移导致匹配失败时回退按标题截到 EOF。
|
|
146
|
-
* @param {string} content -
|
|
134
|
+
* @param {string} content - 指引文件原文
|
|
147
135
|
* @returns {string} 截除旧段后的内容(已 trimEnd)
|
|
148
136
|
*/
|
|
149
|
-
function
|
|
150
|
-
const exact =
|
|
137
|
+
function stripLegacySmallBlock(content) {
|
|
138
|
+
const exact = LEGACY_SMALL_BLOCK.trimEnd();
|
|
151
139
|
const idx = content.indexOf(exact);
|
|
152
140
|
if (idx >= 0) return content.slice(0, idx).trimEnd();
|
|
153
141
|
const headingIdx = content.indexOf('## SillySpec');
|
|
@@ -156,50 +144,49 @@ function stripLegacyAgentsBlock(content) {
|
|
|
156
144
|
}
|
|
157
145
|
|
|
158
146
|
/**
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
* -
|
|
162
|
-
*
|
|
147
|
+
* 向指引文件注入完整模板(版本感知幂等,三态四分支 + 旧标记迁移)。
|
|
148
|
+
* AGENTS.md / GEMINI.md / INSTRUCTIONS.md 共用;完整模板(templates/agents-instruction.md)
|
|
149
|
+
* 是唯一注入源。取代 design: 2026-08-02-init-claude-md 的 CLAUDE.md 单文件方案与
|
|
150
|
+
* v≤3.32.3 的追加态小段方案(其 D-004 预留的「marker 方案迁移」即彼方案)。
|
|
151
|
+
* - 不存在:写完整模板 + 顶部版本注释
|
|
152
|
+
* - 存在无标记:追加受管段(完整模板全文)+ 版本块标记;
|
|
153
|
+
* 含旧 `## SillySpec` 小段(v≤3.32.3 安装)先截除再追加(marker 迁移,防双段)
|
|
163
154
|
* - 存在同版本标记:跳过(不写文件)
|
|
164
155
|
* - 存在异版本标记:追加态刷新块 / 完整态仅 stderr 提示(保留用户改动)
|
|
165
|
-
* @param {string}
|
|
156
|
+
* @param {string} filePath - 指引文件绝对路径
|
|
166
157
|
*/
|
|
167
|
-
|
|
168
|
-
const filePath = join(projectDir, 'AGENTS.md');
|
|
158
|
+
function injectFullInstructions(filePath) {
|
|
169
159
|
const version = getVersion();
|
|
160
|
+
const template = readInstructionTemplate();
|
|
161
|
+
if (template === null) return;
|
|
162
|
+
const fileName = basename(filePath);
|
|
170
163
|
// 完整态顶部注释(新文件首行,轻量、不限制后续编辑)
|
|
171
164
|
const fullHeader = `<!-- SillySpec v${version} — 由 sillyspec init 生成,可自由编辑;重跑 init 同版本不更新 -->`;
|
|
172
|
-
// 追加态受管段(包版本块标记,明确"勿手动编辑此段"
|
|
173
|
-
const
|
|
165
|
+
// 追加态受管段(包版本块标记,明确"勿手动编辑此段"),内容=完整模板全文
|
|
166
|
+
const managedBlock =
|
|
174
167
|
`<!-- SillySpec v${version} START — 由 sillyspec init 注入,勿手动编辑此段 -->\n` +
|
|
175
|
-
|
|
168
|
+
template.trimEnd() +
|
|
176
169
|
`\n<!-- SillySpec END -->`;
|
|
177
170
|
|
|
178
171
|
// 状态 1:文件不存在 → 写完整模板 + 顶部版本注释
|
|
179
172
|
if (!existsSync(filePath)) {
|
|
180
|
-
let template = '';
|
|
181
|
-
try {
|
|
182
|
-
template = readFileSync(AGENTS_TEMPLATE_PATH, 'utf8');
|
|
183
|
-
} catch {
|
|
184
|
-
console.error(`❌ [sillyspec] 未找到 Agent 指引模板:${AGENTS_TEMPLATE_PATH}`);
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
173
|
writeFileSync(filePath, fullHeader + '\n' + template);
|
|
188
|
-
console.log(chalk.green(` ✓
|
|
174
|
+
console.log(chalk.green(` ✓ ${fileName} 已生成(SillySpec v${version},完整指引)`));
|
|
189
175
|
return;
|
|
190
176
|
}
|
|
191
177
|
|
|
192
178
|
let content = readFileSync(filePath, 'utf8');
|
|
193
179
|
|
|
194
|
-
// 无任何 `<!-- SillySpec v` 标记 → 状态 2
|
|
180
|
+
// 无任何 `<!-- SillySpec v` 标记 → 状态 2:追加完整受管段(原文字节保留)
|
|
195
181
|
const markMatch = content.match(/<!-- SillySpec v(\S+)/);
|
|
196
182
|
if (!markMatch) {
|
|
197
183
|
if (content.includes('## SillySpec')) {
|
|
198
|
-
content =
|
|
199
|
-
console.log(chalk.green(' ✓
|
|
184
|
+
content = stripLegacySmallBlock(content);
|
|
185
|
+
console.log(chalk.green(' ✓ 旧版 SillySpec 小段已迁移为完整受管段'));
|
|
200
186
|
}
|
|
201
|
-
|
|
202
|
-
|
|
187
|
+
const body = content.trimEnd() === '' ? managedBlock : content.trimEnd() + '\n\n' + managedBlock;
|
|
188
|
+
writeFileSync(filePath, body + '\n');
|
|
189
|
+
console.log(chalk.green(` ✓ ${fileName} 已追加 SillySpec 完整指引受管段(v${version})`));
|
|
203
190
|
return;
|
|
204
191
|
}
|
|
205
192
|
|
|
@@ -210,15 +197,25 @@ export function injectAgentsInstructions(projectDir) {
|
|
|
210
197
|
// 状态 4:异版本(升级)。区分追加态(有 START...END 块)/ 完整态(仅顶部注释)。
|
|
211
198
|
const startBlockRe = /<!-- SillySpec v\S+\s+START[\s\S]*?<!-- SillySpec END -->/;
|
|
212
199
|
if (startBlockRe.test(content)) {
|
|
213
|
-
// 4a:追加态 →
|
|
214
|
-
writeFileSync(filePath, content.replace(startBlockRe,
|
|
215
|
-
console.log(chalk.green(` ✓
|
|
200
|
+
// 4a:追加态 → 用当前版本完整受管段替换该块(块外用户内容字节保留)
|
|
201
|
+
writeFileSync(filePath, content.replace(startBlockRe, managedBlock));
|
|
202
|
+
console.log(chalk.green(` ✓ ${fileName} 完整指引受管段已升级(v${existingVersion} → v${version})`));
|
|
216
203
|
} else {
|
|
217
204
|
// 4b:完整态 → 不覆盖(保留用户改动),仅 stderr 打印升级提示
|
|
218
|
-
console.error(`⚠️ [sillyspec] SillySpec 升级 v${existingVersion}→v${version}
|
|
205
|
+
console.error(`⚠️ [sillyspec] SillySpec 升级 v${existingVersion}→v${version},${fileName} 未自动更新(保留你的改动)。如需采用新模板:备份后删除 ${fileName} 再跑 sillyspec init。`);
|
|
219
206
|
}
|
|
220
207
|
}
|
|
221
208
|
|
|
209
|
+
export function injectAgentsInstructions(projectDir) {
|
|
210
|
+
injectFullInstructions(join(projectDir, 'AGENTS.md'));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function injectInstructions(tool, projectDir) {
|
|
214
|
+
const fileName = INSTRUCTION_FILE_MAP[tool];
|
|
215
|
+
if (!fileName) return;
|
|
216
|
+
injectFullInstructions(join(projectDir, fileName));
|
|
217
|
+
}
|
|
218
|
+
|
|
222
219
|
/**
|
|
223
220
|
* 为 Claude Code 写 CLAUDE.md 指针(@AGENTS.md 导入,薄文件)。
|
|
224
221
|
* - 不存在:写指针文件(版本头注释 + @AGENTS.md)
|
|
@@ -441,7 +438,7 @@ async function doInstall(projectDir, tools, subprojects = [], specDir = null, op
|
|
|
441
438
|
}
|
|
442
439
|
}
|
|
443
440
|
|
|
444
|
-
// 注入指令文件(gemini/opencode
|
|
441
|
+
// 注入指令文件(gemini/opencode:与 AGENTS.md 同源同全文,共用全量注入器)
|
|
445
442
|
for (let i = 0; i < tools.length; i++) {
|
|
446
443
|
const toolName = tools[i];
|
|
447
444
|
if (INSTRUCTION_TOOLS.includes(toolName)) {
|
|
@@ -449,7 +446,7 @@ async function doInstall(projectDir, tools, subprojects = [], specDir = null, op
|
|
|
449
446
|
}
|
|
450
447
|
}
|
|
451
448
|
|
|
452
|
-
// 注入 AGENTS.md 完整指引(claude/codex/zcode 共用:版本感知幂等三态四分支 + 旧 ## SillySpec
|
|
449
|
+
// 注入 AGENTS.md 完整指引(claude/codex/zcode 共用:版本感知幂等三态四分支 + 旧 ## SillySpec 小段迁移;
|
|
453
450
|
// zcode 2026-09-21-flow-command-cards FR-05 入面——AGENTS.md 是跨工具通用标准内容源)
|
|
454
451
|
if (tools.includes('claude') || tools.includes('codex') || tools.includes('zcode')) {
|
|
455
452
|
injectAgentsInstructions(projectDir);
|
package/src/run/command.js
CHANGED
|
@@ -1274,6 +1274,25 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
1274
1274
|
// 如果指定了变更名或有变更目录,自动初始化变更的 progress
|
|
1275
1275
|
const autoChange = changeName || resolveChangeNameAuto(cwd, specRoot)
|
|
1276
1276
|
if (autoChange) {
|
|
1277
|
+
// 归档同名幽灵复活门(坑 archived-change-resurrect-by-merge,2026-10-10 实证:远端
|
|
1278
|
+
// merge 把已归档变更的旧四件套带回活跃路径,run verify 见活跃目录即自动注册新进度行、
|
|
1279
|
+
// 幽灵态推上平台)。活跃目录与 changes/archive/<同名>/ 并存且库内无该变更活跃行 →
|
|
1280
|
+
// 拒绝注册,指向两出口(认归档:清幽灵目录 / 真要重开:换新名或先恢复归档)。
|
|
1281
|
+
try {
|
|
1282
|
+
const _archiveDup = join(specBase, 'changes', 'archive', autoChange)
|
|
1283
|
+
if (existsSync(_archiveDup) && !progress) {
|
|
1284
|
+
let _activeRows = []
|
|
1285
|
+
try { _activeRows = pm.listChanges(cwd) } catch { /* 列举失败按无活跃行处置 */ }
|
|
1286
|
+
if (!_activeRows.includes(autoChange)) {
|
|
1287
|
+
console.error(`❌ 拒绝注册:${autoChange} 已有归档目录(changes/archive/${autoChange}/),当前活跃目录疑似 merge 带回的幽灵复活(库内无活跃行)。`)
|
|
1288
|
+
console.error(' 两个出口:')
|
|
1289
|
+
console.error(` ① 认归档(多数场景):git rm -r .sillyspec/changes/${autoChange} 清幽灵目录后提交;归档侧工件在 changes/archive/${autoChange}/`)
|
|
1290
|
+
console.error(' ② 确要重开同名变更:先换新变更名(推荐),或恢复归档(mv 回活跃目录并清理库内 archived 行)后再跑')
|
|
1291
|
+
console.error(' 核对:递归 diff 活跃 vs 归档目录确认归档侧为超集零信息损失(坑内处置实录)')
|
|
1292
|
+
process.exit(2)
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
} catch { /* 守卫异常不拦正常注册(fail-open)*/ }
|
|
1277
1296
|
// 创建时即写 title(与 name/KEY 同时落 db):--input 首行推导 deriveChangeTitle(≤50 字,
|
|
1278
1297
|
// 用户需求 2026-09-29)优先,长输入兜底 sanitizeDesc 压行,无则用 name 兜底。
|
|
1279
1298
|
// proposal 还没写、无权威标题来源,这是临时值;brainstorm/plan 完成 proposal/design 落盘后由
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
import { existsSync, readFileSync } from 'fs';
|
|
26
26
|
import { join } from 'path';
|
|
27
27
|
import { git, gitQuiet } from '../git-helper.js';
|
|
28
|
+
import { resolveCrossWorktreePathCandidates } from '../cross-placement.js';
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* @typedef {Object} RepoEntry
|
|
@@ -152,14 +153,19 @@ export class MultiRepoContext {
|
|
|
152
153
|
`请补全路径(约束② fail-closed)。`
|
|
153
154
|
);
|
|
154
155
|
}
|
|
155
|
-
// worktree 模式检测:meta 在 = worktree-cross.js
|
|
156
|
-
//
|
|
156
|
+
// worktree 模式检测:meta 在 = worktree-cross.js 已建隔离(寻址经 cross-placement 叶子模块
|
|
157
|
+
// resolveCrossWorktreePath——注册表优先、公式兜底;execute 启动时创建,晚于本构造的调用会
|
|
158
|
+
// 命中。不 import worktree-cross 避免 shared↔multi-repo-context 循环,落位逻辑下沉的
|
|
159
|
+
// 零依赖叶子模块两侧共用无环——坑 cross-wt-toolchain-split 评审吸收①)。
|
|
157
160
|
const specBase = (this.platformOpts && this.platformOpts.specRoot) || join(this.cwd, '.sillyspec');
|
|
158
161
|
let crossMeta = null;
|
|
159
162
|
try {
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
163
|
+
// 同步读(构造函数是同步的);寻址候选逐级探测(注册表 → 仓内新公式 → 主仓 specBase 旧
|
|
164
|
+
// 公式 legacy 兜底)——无注册表条目的存量 worktree 在本读取面不失联
|
|
165
|
+
for (const cand of resolveCrossWorktreePathCandidates(specBase, this.changeName, repoKey, crossRepoPath)) {
|
|
166
|
+
const metaPath = join(cand, 'meta.json');
|
|
167
|
+
if (existsSync(metaPath)) { crossMeta = JSON.parse(readFileSync(metaPath, 'utf8')); break }
|
|
168
|
+
}
|
|
163
169
|
} catch { crossMeta = null }
|
|
164
170
|
const inWorktree = !!(crossMeta && crossMeta.isCross && crossMeta.worktreePath && existsSync(crossMeta.worktreePath));
|
|
165
171
|
const workRoot = inWorktree ? crossMeta.worktreePath : crossRepoPath;
|
package/src/run/shared.js
CHANGED
|
@@ -60,7 +60,12 @@ export function didYouMean(input, candidates) {
|
|
|
60
60
|
// ── 变更名路径穿越消毒(F6)──
|
|
61
61
|
// change 名会被 join 进 .sillyspec/changes/<name>/ 当目录用。若含 ../ 或路径分隔符,
|
|
62
62
|
// 会逃出 changes/ 写到任意位置(如 --change ../../etc/x 把 proposal.md 写到 changes 同级之外)。
|
|
63
|
-
// 在 --change 解析边界统一消毒,比在每个 join 点守卫更集中。合法名:字母/数字/._
|
|
63
|
+
// 在 --change 解析边界统一消毒,比在每个 join 点守卫更集中。合法名:字母/数字/._-/CJK,
|
|
64
|
+
// 禁分隔符与 ..
|
|
65
|
+
// 2026-10-11-sync-conflict-tri-fix FR-03:字符集扩 CJK(一-鿿,与下方 assertDatedChangeName
|
|
66
|
+
// slug 同集)——本守卫的本职是路径穿越消毒,ASCII 限制是历史误伤:磁盘既有中文名变更
|
|
67
|
+
//(实测 2026-10-10-取消LTC宽表校验规则)被拒致 platform sync/resolve 无法操作。
|
|
68
|
+
// 新建变更名仍受 assertDatedChangeName 日期前缀门约束,不因本放宽回归。
|
|
64
69
|
export function assertSafeChangeName(name, label = '变更名') {
|
|
65
70
|
if (name == null) return
|
|
66
71
|
const s = String(name)
|
|
@@ -69,8 +74,8 @@ export function assertSafeChangeName(name, label = '变更名') {
|
|
|
69
74
|
if (/[\\/]/.test(s) || /(^|[/\\])\.\.(?=$|[/\\])/.test(s) || s === '..' || s.includes('..')) {
|
|
70
75
|
throw new Error(`${label}「${s}」含路径分隔符或 ..,禁止路径穿越(变更名只能是 .sillyspec/changes/ 下的一个目录名,不能逃出)`)
|
|
71
76
|
}
|
|
72
|
-
if (!/^[\w
|
|
73
|
-
throw new Error(`${label}「${s}」含非法字符(仅允许字母、数字、._
|
|
77
|
+
if (!/^[\w.\-一-鿿]+$/.test(s)) {
|
|
78
|
+
throw new Error(`${label}「${s}」含非法字符(仅允许字母、数字、._-、中文)`)
|
|
74
79
|
}
|
|
75
80
|
}
|
|
76
81
|
|
|
@@ -83,7 +88,8 @@ export function assertSafeChangeName(name, label = '变更名') {
|
|
|
83
88
|
// 日期只校验形状(月 01-12 日 01-31),不校验「当天」——跨天续跑/次日重命名不应被拦。
|
|
84
89
|
// slug 首字符放宽含 CJK(一-鿿,与 flow 族 validateChangeName 同字符集):flow start 2026-09-28
|
|
85
90
|
// 起复用本校验器,中文名(平台键 2026-09-25-中文名-a1b2c3 形态)是 flow 族既有测试特性;
|
|
86
|
-
//
|
|
91
|
+
// 上游 assertSafeChangeName 自 2026-10-11-sync-conflict-tri-fix 起同放行 CJK(此前 ASCII
|
|
92
|
+
// 限宽拦中文名,属历史误伤,见该守卫注释)。
|
|
87
93
|
const DATED_CHANGE_NAME_RE = /^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])-[a-zA-Z0-9一-鿿][\w.\-一-鿿]*$/
|
|
88
94
|
export function assertDatedChangeName(name, label = '变更名') {
|
|
89
95
|
if (name == null) return
|
|
@@ -367,6 +373,81 @@ export function detectCwdInsideWorktree(dir) {
|
|
|
367
373
|
return null
|
|
368
374
|
}
|
|
369
375
|
|
|
376
|
+
// ── 未初始化目录硬拦(坑:agent 在未初始化/错误目录跑非 init 命令,fail-soft 散点报错不说
|
|
377
|
+
// 正确 cwd,agent 多轮试错)──
|
|
378
|
+
// 豁免面 = 按设计可在未初始化目录运行的命令。每项豁免依据(勿凭感觉增删):
|
|
379
|
+
// init/scan 初始化器与棕地入口,未初始化目录是本职(scan 亦是 monorepo 子项目独立建实例的合法入口)
|
|
380
|
+
// doctor/status/progress/next/workspace 未初始化是它们的设计输出态(run/next.js
|
|
381
|
+
// '未初始化(无 .sillyspec 产物)'、workspace.js '未初始化(无 .sillyspec)')
|
|
382
|
+
// setup/config 自带候选链 fail-closed 报错('请先运行 sillyspec init')
|
|
383
|
+
// knowledge 知识目录缺失走友好空态(stages/knowledge.js)
|
|
384
|
+
// local 纯 fs 嗅探生成 local.yaml,设计上 init 前可用
|
|
385
|
+
// mcp/dashboard/platform 服务/平台设施,自有路径解析
|
|
386
|
+
// wt-commit 子代理 workdir=worktree 内合法(worktree cwd 硬拦同款豁免)
|
|
387
|
+
// agent-log --detect 现场探测不落盘,自有解析链
|
|
388
|
+
export const UNINIT_CWD_GATE_EXEMPT = new Set([
|
|
389
|
+
'init', 'scan', 'doctor', 'status', 'progress', 'next', 'workspace',
|
|
390
|
+
'setup', 'knowledge', 'local', 'config', 'mcp', 'dashboard', 'platform',
|
|
391
|
+
'wt-commit', 'agent-log',
|
|
392
|
+
])
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* CLI 入口「未初始化目录」判定(与 detectCwdInsideWorktree 分工:那边判「在隔离 worktree
|
|
396
|
+
* 内」,文案更具体故先拦;这边判「任何形式的 spec 都解析不到」——覆盖未初始化目录与非
|
|
397
|
+
* sillspec 项目的误跑两类)。
|
|
398
|
+
*
|
|
399
|
+
* 判定复用 resolveSpecDir 全套守卫(home 拒绝 / tmp 边界 / .runtime 回环防护)——不重写
|
|
400
|
+
* 遍历,否则已修复的坑(2026-09-27-spec-sync-413 .runtime 回环等)会回归。
|
|
401
|
+
*
|
|
402
|
+
* 返回(风格仿 detectCwdInsideWorktree):
|
|
403
|
+
* { verdict: 'exempt' } 豁免命令 / 无命令(usage 已早退,保险不拦),不拦不锚
|
|
404
|
+
* { verdict: 'skip' } 显式意图(--spec-dir、平台 flag、平台 pointer/接管声明在
|
|
405
|
+
* dir)——spec 解析与错误面归命令自身的 fail-closed 文案
|
|
406
|
+
* (PointerUnreachableError 等),本门不重复判、更不能拿
|
|
407
|
+
* 「dir 里没有 .sillyspec」误拦平台项目
|
|
408
|
+
* { verdict: 'pass', anchor } 命中已初始化上下文。anchor=spec 根父目录(供调用方重锚定,
|
|
409
|
+
* 修裸 join(dir,'.sillyspec') 调用点如 review status rsSpecBase
|
|
410
|
+
* 的子目录漂移);linked worktree rescued 场景 anchor=null——
|
|
411
|
+
* 主仓 spec 不在 cwd 祖先链上,锚定交命令层自有逻辑
|
|
412
|
+
* (如 endpoints baseline 的主仓锚定),本门不越俎
|
|
413
|
+
* { verdict: 'block', gitRoot } 全链未命中 → 调用方硬报错 exit 2;gitRoot 供文案指路
|
|
414
|
+
* (未初始化目录无 ground truth,只能给锚点不能断言唯一 cwd)
|
|
415
|
+
*
|
|
416
|
+
* @param {string} command 顶层命令名
|
|
417
|
+
* @param {{ dir?: string, specDir?: string, platformFlags?: unknown }} opts
|
|
418
|
+
* dir=目标目录(cwd 或 --dir);specDir=显式 --spec-dir;platformFlags=--workspace-id/
|
|
419
|
+
* --runtime-root/--spec-root 任一显式给出即为真值(平台模式显式意图)
|
|
420
|
+
*/
|
|
421
|
+
export function resolveUninitCwdGate(command, { dir, specDir, platformFlags } = {}) {
|
|
422
|
+
if (!command || UNINIT_CWD_GATE_EXEMPT.has(command)) return { verdict: 'exempt' }
|
|
423
|
+
if (specDir || platformFlags) return { verdict: 'skip' }
|
|
424
|
+
const resolvedDir = resolve(dir || process.cwd())
|
|
425
|
+
if (existsSync(join(resolvedDir, '.sillyspec-platform.json')) || existsSync(join(resolvedDir, PLATFORM_MANAGED_FILENAME))) {
|
|
426
|
+
return { verdict: 'skip' }
|
|
427
|
+
}
|
|
428
|
+
const spec = resolveSpecDir(resolvedDir)
|
|
429
|
+
if (existsSync(spec)) return { verdict: 'pass', anchor: dirname(spec) }
|
|
430
|
+
// linked worktree 兜底(判据同 resolveEffectiveDir P1-1 / worktree.js _resolveMainRepoRoot:
|
|
431
|
+
// --git-dir ≠ --git-common-dir 且非 submodule):主仓 .sillyspec 不在 cwd 祖先链上(如
|
|
432
|
+
// `git worktree add` 到兄弟路径的副本),resolveSpecDir 必 miss 但项目实为已初始化——
|
|
433
|
+
// 误拦会打断 endpoints baseline 等命令层自有的主仓锚定逻辑。放行不重锚:锚定归命令层。
|
|
434
|
+
const gd = safeGit(resolvedDir, ['rev-parse', '--git-dir'])
|
|
435
|
+
const gcd = safeGit(resolvedDir, ['rev-parse', '--git-common-dir'])
|
|
436
|
+
const superProj = safeGit(resolvedDir, ['rev-parse', '--show-superproject-working-tree'])
|
|
437
|
+
if (gd.value && gcd.value && gd.value !== gcd.value && !superProj.value) {
|
|
438
|
+
// common-dir 可能是相对路径,须相对 resolvedDir 绝对化(与 worktree.js 同坑同解)
|
|
439
|
+
const absCommonDir = resolve(resolvedDir, gcd.value)
|
|
440
|
+
if (existsSync(absCommonDir)) {
|
|
441
|
+
const mainRoot = dirname(absCommonDir)
|
|
442
|
+
if (mainRoot !== resolvedDir && existsSync(join(mainRoot, '.sillyspec'))) {
|
|
443
|
+
return { verdict: 'pass', anchor: null }
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
const gitRoot = safeGit(resolvedDir, ['rev-parse', '--show-toplevel']).value || null
|
|
448
|
+
return { verdict: 'block', gitRoot }
|
|
449
|
+
}
|
|
450
|
+
|
|
370
451
|
/**
|
|
371
452
|
* 统一解析 .runtime 根目录(坑 execute-runs-isolation)。
|
|
372
453
|
*
|