sillyspec 3.32.2 → 3.32.4
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-auto/SKILL.md +1 -1
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +3 -2
- package/.claude/skills/sillyspec-explore/SKILL.md +0 -2
- package/.claude/skills/sillyspec-flow/SKILL.md +1 -0
- package/README.md +4 -4
- package/SKILL.md +2 -4
- package/package.json +1 -1
- package/src/command-cards.js +1 -1
- package/src/cross-repo-reconcile.js +33 -0
- package/src/flow-parity.js +46 -35
- package/src/flow-review.js +12 -2
- package/src/flow.js +23 -8
- package/src/friction-tally.js +2 -1
- package/src/git-helper.js +23 -1
- package/src/index.js +54 -13
- package/src/init.js +58 -61
- package/src/module-impact.js +3 -1
- package/src/progress/stage-machine.js +8 -1
- package/src/run/code-face-key.js +84 -0
- package/src/run/complete.js +16 -7
- package/src/run/gate-snapshot.js +19 -0
- package/src/run/gates.js +109 -67
- package/src/run/green-cache.js +11 -19
- package/src/run/shared.js +75 -0
- package/src/run/verify-quality-scan.js +88 -45
- package/src/scope-audit.js +50 -24
- package/src/spec-sync.js +98 -20
- package/src/stages/brainstorm.js +2 -1
- package/src/stages/cmd-existence.js +5 -1
- package/src/test-bindings.js +2 -0
- package/src/verify-postcheck.js +141 -18
- package/src/verify-probes.js +141 -0
- package/src/worktree.js +43 -9
- package/src/wt-commit.js +82 -3
- package/.claude/skills/sillyspec-quick/SKILL.md +0 -116
- package/assets/command-cards/run-quick.md +0 -20
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 });
|
|
@@ -1334,7 +1363,7 @@ task done 四合一(r5l 方案1):review write(落 review.json+自动勾
|
|
|
1334
1363
|
process.exit(2);
|
|
1335
1364
|
}
|
|
1336
1365
|
assertSafeChangeName(vpChange, '--change 变更名');
|
|
1337
|
-
const { runVerifyProbes, renderVerifyProbesReport, generateVerifyResultSkeleton, resolveVerifyProbesSpecBase, writeVerifyFacts, formatPlatformPathNote, backupVerifyResult, refreshProbeSections } = await import('./verify-probes.js');
|
|
1366
|
+
const { runVerifyProbes, renderVerifyProbesReport, generateVerifyResultSkeleton, resolveVerifyProbesSpecBase, writeVerifyFacts, formatPlatformPathNote, backupVerifyResult, refreshProbeSections, applySkeletonPreservingHuman } = await import('./verify-probes.js');
|
|
1338
1367
|
// spec 根统一走漂移锚定(坑 worktree-spec-artifact-misplace):在 worktree 内跑时锚回主仓,
|
|
1339
1368
|
// 探针读取与 --init 骨架都落主仓——与 plan/execute/verify/archive 的 command.js 守卫同口径。
|
|
1340
1369
|
// resolvePlatformSpecDir 仍先调(保留平台接管 fail-closed 检查副作用),但仅 pointer 存在时
|
|
@@ -1429,21 +1458,29 @@ task done 四合一(r5l 方案1):review write(落 review.json+自动勾
|
|
|
1429
1458
|
await mirrorInitArtifact(vpReportPath, 'verify-result.md', vpInjected);
|
|
1430
1459
|
console.log(`\n📄 存量旧格式 verify-result.md 已补注入探针预填段: ${vpReportPath}(正文其余未动;补注后按新预填段如实核对结论)${vpPlatformNote}`);
|
|
1431
1460
|
} else if (vpForce) {
|
|
1432
|
-
// quick-B:--force 覆盖重生成全骨架(含接口矩阵等 CLI
|
|
1461
|
+
// quick-B:--force 覆盖重生成全骨架(含接口矩阵等 CLI 预填段)。
|
|
1433
1462
|
// 2026-10-06-verify-friction-fix:覆盖前自动备份(postmortem 实证 38 格手填复核成果
|
|
1434
1463
|
// 被清后只能靠对话记录重建——「从 git 或备份找回」的话术备份此前并不存在);备份
|
|
1435
1464
|
// 失败时醒目提示手动复制再重跑(fail-soft 但不静默)。
|
|
1465
|
+
// 2026-10-09-verify-papercuts ②:语义升级——机器段全量重刷 + 人工面按段携载
|
|
1466
|
+
// (applySkeletonPreservingHuman:结论槽/移交项表/矩阵已填行不再被 wipe),备份
|
|
1467
|
+
// 保留为兜底通道。
|
|
1436
1468
|
const { resolveRuntimeRoot: vpForceRTR } = await import('./run/shared.js');
|
|
1437
1469
|
const vpBackupPath = backupVerifyResult({ mdPath: vpReportPath, runtimeRoot: vpForceRTR({}, vpSpecBase), changeName: vpChange });
|
|
1438
1470
|
const vpSkeleton = generateVerifyResultSkeleton(vpResult);
|
|
1439
|
-
|
|
1440
|
-
|
|
1471
|
+
const vpOldText = readFileSync(vpReportPath, 'utf8');
|
|
1472
|
+
const vpApplied = applySkeletonPreservingHuman(vpOldText, vpSkeleton);
|
|
1473
|
+
writeFileSync(vpReportPath, vpApplied.text);
|
|
1474
|
+
await mirrorInitArtifact(vpReportPath, 'verify-result.md', vpApplied.text);
|
|
1441
1475
|
if (vpBackupPath) {
|
|
1442
|
-
console.log(`\n🗄️ --force 覆盖前已自动备份: ${vpBackupPath}
|
|
1476
|
+
console.log(`\n🗄️ --force 覆盖前已自动备份: ${vpBackupPath}(携载语义之外的极端形态从这里找回)`);
|
|
1443
1477
|
} else {
|
|
1444
1478
|
console.error(`\n⚠️ 自动备份失败——若 verify-result.md 含手填内容,请立即手动复制保存后重跑本命令。`);
|
|
1445
1479
|
}
|
|
1446
|
-
|
|
1480
|
+
const vpCarriedNote = vpApplied.carriedSections.length > 0
|
|
1481
|
+
? `已填人工面按段携载保留:${vpApplied.carriedSections.join('、')}(携载表格行 ${vpApplied.carriedRows})——与机器段新渲染不符的携载内容由 gate 一致性抽查把关`
|
|
1482
|
+
: `未发现已填人工面(全文占位形态——行为与旧版全量重置一致)`;
|
|
1483
|
+
console.log(`\n📄 --force 已重生成 verify-result.md 骨架: ${vpReportPath}(探针与预填段已刷新;${vpCarriedNote})${vpPlatformNote}`);
|
|
1447
1484
|
} else {
|
|
1448
1485
|
console.log(`\nℹ️ verify-result.md 已存在,不覆盖: ${vpReportPath}${vpPlatformNote}(预填段过期需刷新时用 --init --force)`);
|
|
1449
1486
|
}
|
|
@@ -2172,9 +2209,10 @@ task done 四合一(r5l 方案1):review write(落 review.json+自动勾
|
|
|
2172
2209
|
const fpSpecBase = resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec');
|
|
2173
2210
|
const fpDir = join(fpSpecBase, 'changes', fpChange);
|
|
2174
2211
|
mkdirSync(fpDir, { recursive: true });
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
const
|
|
2212
|
+
// 本地墙钟(datetime.js 坑 taskcard-created-at-utc 同族:frontmatter 人读时间禁
|
|
2213
|
+
// toISOString 写 UTC 裸形状——无时区标记被读取端按本地解析,2026-10-09 平台时间线 born 锚偏 8h 实证)
|
|
2214
|
+
const { nowWallClock } = await import('./datetime.js');
|
|
2215
|
+
const fpStamp = nowWallClock();
|
|
2178
2216
|
const generated = [];
|
|
2179
2217
|
const mk = (fname, body) => {
|
|
2180
2218
|
const p = join(fpDir, fname);
|
|
@@ -3373,8 +3411,11 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
|
|
|
3373
3411
|
else if (wtCommitFlags[i] === '--pathspec-from-file' && wtCommitFlags[i + 1]) wtCommitPathspecFile = wtCommitFlags[++i];
|
|
3374
3412
|
}
|
|
3375
3413
|
if (!wtCommitChange) {
|
|
3376
|
-
|
|
3377
|
-
|
|
3414
|
+
// FR-08(2026-10-09-verify-reuse-friction / D-006@v1):跨仓 <change>--<repoKey>
|
|
3415
|
+
// 注册表校验剥后缀——逻辑单点在 wt-commit.js(导出 inferChangeFromWorktreeCwd
|
|
3416
|
+
// 供三态回归直测,index.js dispatch 不留内联逻辑)
|
|
3417
|
+
const { inferChangeFromWorktreeCwd } = await import('./wt-commit.js')
|
|
3418
|
+
wtCommitChange = await inferChangeFromWorktreeCwd(dir)
|
|
3378
3419
|
}
|
|
3379
3420
|
try {
|
|
3380
3421
|
const { runWtCommit } = await import('./wt-commit.js');
|
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/module-impact.js
CHANGED
|
@@ -155,7 +155,9 @@ export function syncModuleDocSidecars({ cwd, changeName, note, specDir = null })
|
|
|
155
155
|
const cardPath = join(modulesDir, `${mod}.md`)
|
|
156
156
|
try {
|
|
157
157
|
const card = readFileSync(cardPath, 'utf8')
|
|
158
|
-
|
|
158
|
+
// 全量 ISO(机器可比时间戳,scan-postcheck 同字段同口径)——旧式 UTC 数字拼硬编码
|
|
159
|
+
// '+08:00' 的瞬间恒早真实时刻 8h(数字取 UTC、偏移写死,机器无关地错)
|
|
160
|
+
const stamped = card.replace(/^updated_at:.*$/m, `updated_at: ${new Date().toISOString()}`)
|
|
159
161
|
if (stamped !== card) writeFileSync(cardPath, stamped)
|
|
160
162
|
} catch { /* 卡不存在——sidecar 照写,卡内容留给 agent */ }
|
|
161
163
|
synced.push(proj ? `${proj}/${mod}` : mod)
|
|
@@ -369,12 +369,19 @@ export class StageMachine {
|
|
|
369
369
|
if (!(isProgress || f.startsWith('spec-sync-conflict-')) || !f.endsWith('.json')) continue;
|
|
370
370
|
let change = f.replace(/^spec-sync-conflict-|^sync-conflict-/, '').replace(/\.json$/, '');
|
|
371
371
|
let createdAt = null;
|
|
372
|
+
let kind = null;
|
|
372
373
|
try {
|
|
373
374
|
const cf = JSON.parse(readFileSync(join(runtimeDir, f), 'utf8'));
|
|
374
375
|
change = cf.change || change;
|
|
375
376
|
createdAt = cf.created_at || null;
|
|
377
|
+
kind = typeof cf.kind === 'string' ? cf.kind : null;
|
|
376
378
|
} catch { /* 损坏文件按文件名兜底,不崩 */ }
|
|
377
|
-
|
|
379
|
+
// 2026-10-09-tombstone-conflict-root-fix FR-01:kind='tombstone' 优先透传
|
|
380
|
+
// (归因记账新形态——daemon 摘要/前端据此区分墓碑冲突与版本冲突);文件名
|
|
381
|
+
// 前缀判定保留兜底(旧格式/损坏文件)。
|
|
382
|
+
const type = kind === 'tombstone' ? 'tombstone'
|
|
383
|
+
: isProgress ? 'progress' : 'spec-tree';
|
|
384
|
+
conflicts.push({ change, created_at: createdAt, type });
|
|
378
385
|
}
|
|
379
386
|
return conflicts;
|
|
380
387
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* code-face-key.js — 代码面口径单点(2026-10-09-verify-reuse-friction FR-03 / D-002@v1)。
|
|
3
|
+
*
|
|
4
|
+
* 问题:两类复用指纹(green-cache 的 computeGateFingerprint / 质量扫描的
|
|
5
|
+
* computeQualityScanFingerprint)原先以 git HEAD 为「代码态」分量——收口窗口内任何提交
|
|
6
|
+
* (哪怕纯文档的声明修正)都换 HEAD → 缓存必 miss(2026-10-09 multi-agent-platform
|
|
7
|
+
* tombstone 取证:13:35-14:10 五笔提交每笔击穿,2-3 轮 ×3.5min 重测纯浪费)。
|
|
8
|
+
*
|
|
9
|
+
* 修复:HEAD 分量替换为「代码树内容键」——`git ls-tree -r -z HEAD` 条目按既有非代码
|
|
10
|
+
* 路径口径(.sillyspec/、docs/、*.md——与 filterCodePorcelain 同源判据)过滤后哈希。
|
|
11
|
+
* ls-tree 条目自带 git 对象哈希(内容寻址):纯文档提交不进过滤集 → 键不变 → 缓存存活;
|
|
12
|
+
* 代码提交必进/必变 → 键变 → 必失配。整树 oid(HEAD^{tree})作快路径:oid 与上次相同
|
|
13
|
+
* → 直接沿用上次键(纯缓存优化,语义不变——oid 相同 ⇔ 整树逐字节相同)。
|
|
14
|
+
*
|
|
15
|
+
* 红线合规:非代码判据是**路径口径**(沿既有单点),不是语言/框架/扩展名枚举;
|
|
16
|
+
* ls-tree 是 git 对象枚举,与语言无关。-z(NUL 分隔)输出不加引号不转义——非 ASCII
|
|
17
|
+
* 路径的 endsWith 判据不被尾部引号击穿(独立审查指出的 quotepath 边界)。
|
|
18
|
+
*
|
|
19
|
+
* fail-open:git 不可用/任何异常 → null(调用方按 miss 处理,多跑一轮不误复用——
|
|
20
|
+
* 与两指纹既有的 git 不可用语义同向)。
|
|
21
|
+
*/
|
|
22
|
+
import { execFileSync } from 'node:child_process';
|
|
23
|
+
import { createHash } from 'node:crypto';
|
|
24
|
+
|
|
25
|
+
function safeGit(cwd, args) {
|
|
26
|
+
try {
|
|
27
|
+
return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 60000, windowsHide: true });
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 非代码路径判据(口径单点):.sillyspec/、docs/ 与 *.md——verify 窗口内的合法产出
|
|
35
|
+
* (规范文档/声明修正)不算代码面。green-cache.filterCodePorcelain 与质量扫描
|
|
36
|
+
* porcelainCodeLines 均消费本判据,不再各自实现。
|
|
37
|
+
*/
|
|
38
|
+
export function isNonCodePath(p) {
|
|
39
|
+
return p.startsWith('.sillyspec/') || p.startsWith('docs/') || /\.md$/i.test(p);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* porcelain 行过滤为「代码面」(原 green-cache.js 实现迁入,语义不变):
|
|
44
|
+
* rename 行「old -> new」两侧任一是文档面即整行剔除(防 rename 伪装穿透);排序保序。
|
|
45
|
+
*/
|
|
46
|
+
export function filterCodePorcelain(porcelain) {
|
|
47
|
+
return String(porcelain || '')
|
|
48
|
+
.split(/\r?\n/)
|
|
49
|
+
.filter((line) => {
|
|
50
|
+
if (!line.trim()) return false;
|
|
51
|
+
const raw = line.slice(3).trim();
|
|
52
|
+
if (!raw) return false;
|
|
53
|
+
const parts = raw.split(' -> ').map((p) => p.replace(/^"|"$/g, ''));
|
|
54
|
+
return !parts.every((p) => isNonCodePath(p));
|
|
55
|
+
})
|
|
56
|
+
.sort();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 整树 oid 快路径缓存:cwd → { treeOid, key }(模块级;进程短命令生命周期内有效) */
|
|
60
|
+
const treeKeyMemo = new Map();
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 代码树内容键:`git ls-tree -r -z HEAD` 条目(mode type oid\tpath)过滤非代码路径后
|
|
64
|
+
* 对逐条目原文(含 git 对象哈希——内容寻址)做 sha256。
|
|
65
|
+
* @returns {string|null} git 不可用/无 HEAD → null(调用方 fail-open miss)
|
|
66
|
+
*/
|
|
67
|
+
export function computeCodeTreeKey({ cwd }) {
|
|
68
|
+
const treeOid = safeGit(cwd, ['rev-parse', 'HEAD^{tree}']);
|
|
69
|
+
if (treeOid === null) return null;
|
|
70
|
+
const oid = treeOid.trim();
|
|
71
|
+
const memo = treeKeyMemo.get(cwd);
|
|
72
|
+
if (memo && memo.treeOid === oid) return memo.key;
|
|
73
|
+
const out = safeGit(cwd, ['ls-tree', '-r', '-z', 'HEAD']);
|
|
74
|
+
if (out === null) return null;
|
|
75
|
+
const entries = String(out).split('\0').filter((e) => {
|
|
76
|
+
if (!e) return false;
|
|
77
|
+
const tab = e.indexOf('\t');
|
|
78
|
+
if (tab === -1) return false;
|
|
79
|
+
return !isNonCodePath(e.slice(tab + 1));
|
|
80
|
+
});
|
|
81
|
+
const key = createHash('sha256').update(entries.join('\n')).digest('hex');
|
|
82
|
+
treeKeyMemo.set(cwd, { treeOid: oid, key });
|
|
83
|
+
return key;
|
|
84
|
+
}
|
package/src/run/complete.js
CHANGED
|
@@ -1126,7 +1126,8 @@ async function printExecuteScopeAudit({ cwd, changeName, specBase, platformOpts,
|
|
|
1126
1126
|
snap.closedBy = 'execute --done'
|
|
1127
1127
|
// 沉淀资产面投影(2026-10-07-unify-close-trace):files/totals = 主仓实改行(planned+
|
|
1128
1128
|
// unplanned;untouched 0/0 是声明不是改动、crossRepo 行 patch 不在主仓——均不入)。
|
|
1129
|
-
// 经共用 writeCloseTraceArtifacts 与 thin flow done
|
|
1129
|
+
// 经共用 writeCloseTraceArtifacts 与 thin flow done 同一处单套写两件(2026-10-09-
|
|
1130
|
+
// close-trace-single-set:对账面并入 change-patch.json 的 scopeAudit 子对象);
|
|
1130
1131
|
// 投影口径抽 flow-parity.projectTraceFaceRows 纯函数(排除路径有单测钉住)。
|
|
1131
1132
|
const face = projectTraceFaceRows(snap.rows)
|
|
1132
1133
|
const closeTrace = writeCloseTraceArtifacts({
|
|
@@ -1143,7 +1144,7 @@ async function printExecuteScopeAudit({ cwd, changeName, specBase, platformOpts,
|
|
|
1143
1144
|
note: 'execute --done 时点冻结(沉淀资产面:主仓实改行投影——thin flow done 对等;治理工件目录与其后 verify/archive 演进不在内)',
|
|
1144
1145
|
},
|
|
1145
1146
|
})
|
|
1146
|
-
console.log(` 📦
|
|
1147
|
+
console.log(` 📦 范围快照落盘(单套):change-patch.json${closeTrace.patchStatus === 'ok' ? ' + change.patch' : ''}(对账面并入 scopeAudit 子对象${closeTrace.patchStatus === 'ok' ? ',收尾时点冻结 sha256 已锚' : '——patch 采集失败已留痕'})`)
|
|
1147
1148
|
} catch (e) {
|
|
1148
1149
|
console.warn(` ⚠️ 范围对账快照写入失败(不阻断,verify 漂移对比将按无快照降级):${e && e.message ? String(e.message).split('\n')[0] : e}`)
|
|
1149
1150
|
}
|
|
@@ -1161,13 +1162,21 @@ async function printVerifyScopeDrift({ cwd, changeName, specBase, platformOpts,
|
|
|
1161
1162
|
return
|
|
1162
1163
|
}
|
|
1163
1164
|
let snapshot = null
|
|
1164
|
-
// 快照读取链(
|
|
1165
|
+
// 快照读取链(2026-10-09-close-trace-single-set):change-patch.json.scopeAudit(单套
|
|
1166
|
+
// 新形态)→ 旧 scope-audit.json(存量归档)→ .runtime/scope-audit-<change>.json(更早
|
|
1167
|
+
// 存量兼容)——scopeAuditPathSet 只吃 rows,子对象与旧顶级形态同构直用
|
|
1165
1168
|
try {
|
|
1166
|
-
|
|
1167
|
-
|
|
1169
|
+
const merged = JSON.parse(readFileSync(join(specBase, 'changes', changeName, 'change-patch.json'), 'utf8'))
|
|
1170
|
+
if (merged && typeof merged.scopeAudit === 'object' && merged.scopeAudit) snapshot = merged.scopeAudit
|
|
1171
|
+
} catch { /* 新形态缺失 → 旧名兜底 */ }
|
|
1172
|
+
if (!snapshot) {
|
|
1168
1173
|
try {
|
|
1169
|
-
snapshot = JSON.parse(readFileSync(join(
|
|
1170
|
-
} catch {
|
|
1174
|
+
snapshot = JSON.parse(readFileSync(join(specBase, 'changes', changeName, 'scope-audit.json'), 'utf8'))
|
|
1175
|
+
} catch {
|
|
1176
|
+
try {
|
|
1177
|
+
snapshot = JSON.parse(readFileSync(join(resolveRuntimeRoot(platformOpts, specBase), `scope-audit-${changeName}.json`), 'utf8'))
|
|
1178
|
+
} catch { /* 快照缺失/损坏 → 跳过对比只打当前 totals */ }
|
|
1179
|
+
}
|
|
1171
1180
|
}
|
|
1172
1181
|
if (!snapshot || snapshot.ok !== true) {
|
|
1173
1182
|
console.log(` ${scopeLine}(无 execute 时点快照可对比——完整表跑 sillyspec scope-audit --change ${changeName})`)
|
package/src/run/gate-snapshot.js
CHANGED
|
@@ -41,6 +41,25 @@ function git(cwd, args) {
|
|
|
41
41
|
return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 60000, windowsHide: true }).trim()
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* 快照回退主仓的可见性 + 摩擦记账(2026-10-09-verify-reuse-friction FR-02)。
|
|
46
|
+
* 2026-10-09 取证:快照创建慢性失败静默回退主仓口径——apply 前实测的是「无变更代码」的
|
|
47
|
+
* HEAD 且绿结论可被 --done 复用消费(正确性风险),全程零可见零记账,只能靠 .runtime 考古
|
|
48
|
+
* 发现。本 helper 统一两处调用点(gates.js verify 实测门 / verify-quality-scan.js noAI 动作)
|
|
49
|
+
* 的告警块与 gate_snapshot_fallback 摩擦事件;记账 best-effort 不影响门禁。
|
|
50
|
+
*/
|
|
51
|
+
export async function reportGateSnapshotFallback({ cwd, changeName, platformOpts = null, where, reason }) {
|
|
52
|
+
console.warn(`⚠️ [gate-snapshot-fallback] ${where}:隔离快照创建失败——实测回退主仓口径。`)
|
|
53
|
+
console.warn(` 影响并行 WIP 可能混入判定;apply 前主仓 HEAD 可能不含本变更代码——实测结论须按主仓口径解读,收口前确认变更代码已落主仓。`)
|
|
54
|
+
console.warn(` 原因:${reason}`)
|
|
55
|
+
console.warn(` 排查:git worktree 可用性(.git/worktrees 可写)与变更文件集非空;确要主仓口径显式设 SILLYSPEC_VERIFY_GATE_SNAPSHOT_OFF=1(口径切换会使复用指纹失配一次后收敛)。`)
|
|
56
|
+
try {
|
|
57
|
+
const { recordFrictionEvent } = await import('../friction-tally.js')
|
|
58
|
+
await recordFrictionEvent({ cwd, platformOpts, changeName, type: 'gate_snapshot_fallback', detail: `${where}: ${String(reason).slice(0, 160)}` })
|
|
59
|
+
} catch { /* 记账失败不影响门禁 */ }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
44
63
|
/** worktree 注册是否仍登记该 root(跨平台路径归一 + win32 大小写不敏感) */
|
|
45
64
|
function worktreeListContains(cwd, snapshotRoot) {
|
|
46
65
|
const out = git(cwd, ['worktree', 'list', '--porcelain'])
|