sillyspec 3.32.6 → 3.32.8
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 +2 -0
- package/package.json +1 -1
- package/src/change-list.js +30 -7
- package/src/config-schema.js +20 -0
- package/src/doctor-diagnostics.js +61 -1
- package/src/flow-draft.js +68 -6
- package/src/flow-parity.js +215 -4
- package/src/flow-review.js +64 -8
- package/src/flow-state.js +21 -0
- package/src/flow.js +425 -71
- package/src/fr-index.js +10 -1
- package/src/index.js +141 -1
- package/src/local-register.js +20 -1
- package/src/patch-reanchor.js +131 -0
- package/src/progress.js +16 -0
- package/src/review-dispatch.js +18 -3
- package/src/review-probes.js +161 -0
- package/src/run/complete-handlers.js +48 -15
- package/src/run/gates.js +17 -2
- package/src/run/shared.js +14 -0
- package/src/sentinel-assertions.js +13 -7
- package/src/stage-contract.js +2 -2
- package/src/stage-review.js +70 -1
- package/src/stages/knowledge.js +22 -0
- package/src/stages/plan-postcheck.js +42 -7
- package/src/task-tick.js +43 -2
- package/src/verify-probes.js +1 -1
- package/src/worktree-apply.js +22 -0
- package/src/worktree-cross.js +158 -9
package/src/fr-index.js
CHANGED
|
@@ -594,8 +594,17 @@ export function indexRequirements({ changeDir, knowledgeRoot, headHash = '', del
|
|
|
594
594
|
// 落域机械改进(2026-09-27-knowledge-digest):伪域告警附交付路径推导的建议域——
|
|
595
595
|
// 归档时转达人工 1 秒确认(建议器与 digest 信号卡共用 suggestDomainFromFiles 单源)
|
|
596
596
|
const suggested = suggestDomainFromFiles(deliverableFiles || [], { knowledgeRoot })
|
|
597
|
+
// 坑 fr-redomain-advisory-and-selectivity-gap(2026-10-13):告警接专用命令 +
|
|
598
|
+
// --by-change 条目级过滤 + D-001 迁域不换号铁律明示(防手工迁移按序重编号断
|
|
599
|
+
// supersede 链)。锚定域取告警列出的伪域文件名(auto-x.md → auto-x)。
|
|
600
|
+
const pseudoDomains = [...new Set(pseudoHits.map((w) => String(w.file || '').replace(/\.md$/, '')))].filter(Boolean)
|
|
597
601
|
console.warn(`⚠️ [FR 域路由降级] 本变更 ${pseudoHits.length} 条 FR 落伪域/unmapped(${[...new Set(pseudoHits.map((w) => w.file))].join('、')})${suggested ? `——按交付路径建议域:${suggested}(确认后可迁移;` : '('}补模块卡(docs/<项目>/modules/_module-map.yaml 登记该目录)后,新变更将自动落回真域;绿地仓可跑 sillyspec run scan 校准)`)
|
|
598
|
-
if (suggested)
|
|
602
|
+
if (suggested) {
|
|
603
|
+
console.warn(` 域级整体迁移:sillyspec tests --redomain --from <伪域> --to ${suggested} --write(干跑去 --write);只迁本变更条目:加 --by-change <本变更名>`)
|
|
604
|
+
if (pseudoDomains.length > 0) console.warn(` 例:sillyspec tests --redomain --from ${pseudoDomains[0]} --to ${suggested} --by-change <本变更名> --write`)
|
|
605
|
+
console.warn(` ⚠️ 迁域不换号(D-001 铁律):条目 ID 保持原样(绑定/supersede 链按 ID 寻址)——手工搬移时勿按目标域接续重编号`)
|
|
606
|
+
console.warn(` 迁移/存量清单:sillyspec knowledge digest(伪域信号卡)`)
|
|
607
|
+
}
|
|
599
608
|
}
|
|
600
609
|
const unm = all.get('unmapped')
|
|
601
610
|
if (unm && unm.sections.length > 50) {
|
package/src/index.js
CHANGED
|
@@ -85,6 +85,9 @@ SillySpec CLI — 规范驱动开发工具包
|
|
|
85
85
|
cleanup <change> [--force] 清理 worktree
|
|
86
86
|
doctor [--fix] [--stale-hours N] 健康检查 + 修复
|
|
87
87
|
--cleanup-ghosts [--confirm] 归档幽灵 active 记录(db 有行无目录 / 空壳目录 0 文件超 7 天,SS-2/2b)
|
|
88
|
+
--prune-cross-worktrees [--confirm] 清跨仓仓幽灵 worktree 注册(目录已删注册残留——dry-run 预览,--confirm 执行 prune;locked 条目只提示)
|
|
89
|
+
|
|
90
|
+
sillyspec patch-reanchor --change <名> 归档收口留痕锚点回写主仓(worktree 收口后 apply 重放致 baseline/head 悬空的修复;幂等,doctor 检出时指路至此)
|
|
88
91
|
|
|
89
92
|
sillyspec local detect [--dir <path>] 生成本地配置 local.yaml(纯 fs 嗅探,零 token、不跑 scan)
|
|
90
93
|
sillyspec local register-repo <key> <path> 注册跨仓仓到 local.yaml repos: 段(外科手术式写入,其余内容保留)
|
|
@@ -410,6 +413,12 @@ async function main() {
|
|
|
410
413
|
console.error(' 非 init/scan 类命令需要已初始化的项目根。');
|
|
411
414
|
console.error(' 修复:① cd 回项目根(含 .sillyspec 的目录)再跑;② 新项目先 sillyspec init(棕地项目 sillyspec scan);③ 或 --spec-dir <路径> 显式指定 .sillyspec 位置。');
|
|
412
415
|
console.error(' 查询/诊断类命令(status/doctor/progress/next/knowledge 等)不受此拦。');
|
|
416
|
+
// 跨仓精确指路(2026-10-12-cli-friction-signal):register-repo 登记过的子仓跑 CLI——
|
|
417
|
+
// 通用三出路之上叠加登记事实与工作区路径(dogfood:三连跑错目录靠人工发现)。无标记零变化。
|
|
418
|
+
if (uninitGate.crossRepo) {
|
|
419
|
+
console.error(` 🎯 此仓已登记为 SillySpec 工作区的跨仓${uninitGate.crossRepo.key ? `「${uninitGate.crossRepo.key}」` : ''}——工作区根:${uninitGate.crossRepo.workspace}`);
|
|
420
|
+
console.error(` 请回工作区根运行本命令,或 --dir "${uninitGate.crossRepo.workspace}" 显式指定。(标记:${uninitGate.gitRoot}/.git/sillyspec-crossrepo.json)`);
|
|
421
|
+
}
|
|
413
422
|
process.exit(2);
|
|
414
423
|
}
|
|
415
424
|
if (uninitGate.verdict === 'pass' && uninitGate.anchor && resolve(dir) !== uninitGate.anchor) {
|
|
@@ -923,6 +932,9 @@ task done 四合一(r5l 方案1):review write(落 review.json+自动勾
|
|
|
923
932
|
const tTask = tTaskIdx >= 0 && filteredArgs[tTaskIdx + 1] && !String(filteredArgs[tTaskIdx + 1]).startsWith("--") ? filteredArgs[tTaskIdx + 1] : null;
|
|
924
933
|
const tNoteIdx = filteredArgs.indexOf('--note');
|
|
925
934
|
const tNote = tNoteIdx >= 0 && filteredArgs[tNoteIdx + 1] && !String(filteredArgs[tNoteIdx + 1]).startsWith("--") ? filteredArgs[tNoteIdx + 1] : null;
|
|
935
|
+
// --evidence(2026-10-12-tick-evidence-multi-anchor):tick 自愿证据补录(自由文本,@路径 读文件)
|
|
936
|
+
const tEvidenceIdx = filteredArgs.indexOf('--evidence');
|
|
937
|
+
const tEvidence = tEvidenceIdx >= 0 && filteredArgs[tEvidenceIdx + 1] && !String(filteredArgs[tEvidenceIdx + 1]).startsWith("--") ? filteredArgs[tEvidenceIdx + 1] : null;
|
|
926
938
|
if (!tChange) { console.error('❌ 缺 --change <变更名>'); process.exit(2); }
|
|
927
939
|
if (taskSub !== 'list') {
|
|
928
940
|
if (!tTask || !/^task-\d+$/.test(tTask)) { console.error('❌ --task 格式应为 task-NN'); process.exit(2); }
|
|
@@ -960,7 +972,7 @@ task done 四合一(r5l 方案1):review write(落 review.json+自动勾
|
|
|
960
972
|
// ── 轻量勾选动词(2026-10-03-voluntary-task-tick):翻格+进度回显+下一任务指针——
|
|
961
973
|
// 纯反馈闭环(watcher task-done 事件自动上平台),自愿使用;幂等,无 review/verdict 仪式。──
|
|
962
974
|
const { runTaskTick } = await import('./task-tick.js')
|
|
963
|
-
await runTaskTick({ changeName: tChange, cwd: dir, taskId: tTask, specBase: tSpecBase })
|
|
975
|
+
await runTaskTick({ changeName: tChange, cwd: dir, taskId: tTask, specBase: tSpecBase, evidence: tEvidence })
|
|
964
976
|
} else if (taskSub === 'done') {
|
|
965
977
|
// ── task done 四合一(r5l-forensic-verdict 方案 1 / 评审护栏#3):13 任务 × 4 连收尾
|
|
966
978
|
// 往返(review write+勾选 / finish / wt-commit)合并为单进程串行 + 合并输出——每次省
|
|
@@ -2528,6 +2540,27 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
|
|
|
2528
2540
|
console.log(` docHash 已与当前 ${result.mainDoc} 一致,跳过重写(幂等 no-op,不动文件)`);
|
|
2529
2541
|
} else {
|
|
2530
2542
|
console.log(` 下一步:独立审查子代理对照 ${result.mainDoc} 填 verdict/checklist 后重跑 --done`);
|
|
2543
|
+
// 派发前任务书出口(2026-10-12-thick-taskbook-cli):新生成骨架 = 待派发形态——打印
|
|
2544
|
+
// 可原样粘贴的完整评审任务书(检查单生产权在 CLI,治主代理手写 leading prompt)。
|
|
2545
|
+
// 条件性:--from adopt(评审已产)与 --refresh-hash(刷新既有)不打印;--json 面不混文案。
|
|
2546
|
+
if (!rsrFrom && !wantRefresh) {
|
|
2547
|
+
try {
|
|
2548
|
+
const { renderStageReviewerTaskbook } = await import('./stage-review.js');
|
|
2549
|
+
const { assembleProbeLinesForScope } = await import('./review-probes.js');
|
|
2550
|
+
const rsrSpecBase = rsrPlatformOpts.specRoot || join(dir, '.sillyspec');
|
|
2551
|
+
const _probeAsm = assembleProbeLinesForScope(rsrSpecBase, 'thick');
|
|
2552
|
+
for (const _w of _probeAsm.warnings) console.warn(`⚠️ review_probes:${_w}`);
|
|
2553
|
+
console.log(`\n📋 评审员任务书(原样复制给干净上下文子代理——检查单生产权在 CLI,勿手写改写):`);
|
|
2554
|
+
console.log(renderStageReviewerTaskbook({
|
|
2555
|
+
stage: st,
|
|
2556
|
+
changeDir: join(rsrSpecBase, 'changes', rsrChange),
|
|
2557
|
+
changeName: rsrChange,
|
|
2558
|
+
reviewRunId: result.reviewRunId,
|
|
2559
|
+
reviewPath: result.reviewPath,
|
|
2560
|
+
probeLines: _probeAsm.lines.length > 0 ? _probeAsm.lines : null,
|
|
2561
|
+
}));
|
|
2562
|
+
} catch { /* 任务书打印 best-effort:失败不影响注册主流程 */ }
|
|
2563
|
+
}
|
|
2531
2564
|
}
|
|
2532
2565
|
}
|
|
2533
2566
|
} catch (e) {
|
|
@@ -2911,6 +2944,7 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
|
|
|
2911
2944
|
// 执行流:--cleanup-remnant / --cleanup-ghosts / --gc-unstamped-runs / --dump-db(结构化诊断之外的修复/取证动作)
|
|
2912
2945
|
const cleanupRemnant = filteredArgs.includes('--cleanup-remnant');
|
|
2913
2946
|
const cleanupGhosts = filteredArgs.includes('--cleanup-ghosts');
|
|
2947
|
+
const pruneCrossWts = filteredArgs.includes('--prune-cross-worktrees');
|
|
2914
2948
|
const gcUnstampedRuns = filteredArgs.includes('--gc-unstamped-runs');
|
|
2915
2949
|
const dumpDbFlag = filteredArgs.includes('--dump-db');
|
|
2916
2950
|
const doctorConfirm = filteredArgs.includes('--confirm');
|
|
@@ -3017,6 +3051,31 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
|
|
|
3017
3051
|
process.exitCode = r.errors.length > 0 ? 1 : 0;
|
|
3018
3052
|
break;
|
|
3019
3053
|
}
|
|
3054
|
+
if (pruneCrossWts) {
|
|
3055
|
+
// 坑 cross-wt-ghost-metadata 缺口③(fire-equipment 2026-10-10 实证):跨仓仓 .git/worktrees/
|
|
3056
|
+
// 幽灵注册(目录已删、注册残留占分支)对 cleanup/归档链隐身(meta.json 随目录同亡)——
|
|
3057
|
+
// 本动作从注册面反扫清理。默认 dry-run,--confirm 才 prune;locked 条目只提示 unlock
|
|
3058
|
+
// 不自动解除(用户显式保护意图)。
|
|
3059
|
+
const { pruneCrossWorktreeGhosts } = await import('./worktree-cross.js');
|
|
3060
|
+
const r = pruneCrossWorktreeGhosts({ cwd: doctorEffectiveDir, confirm: doctorConfirm });
|
|
3061
|
+
if (json) {
|
|
3062
|
+
console.log(JSON.stringify(r, null, 2));
|
|
3063
|
+
} else {
|
|
3064
|
+
console.log(
|
|
3065
|
+
doctorConfirm
|
|
3066
|
+
? `👻 跨仓幽灵 worktree 注册清理(repos: ${r.repos_scanned ?? '—'}):${(r.pruned || []).length} 已 prune / ${(r.skipped_locked || []).length} locked 跳过 / ${(r.remaining || []).length} 残留`
|
|
3067
|
+
: `👻 跨仓幽灵 worktree 注册扫描(repos: ${r.repos_scanned ?? '—'}):待清理 ${r.count} 个(dry-run)`,
|
|
3068
|
+
);
|
|
3069
|
+
for (const g of (doctorConfirm ? r.pruned : r.ghosts) || []) console.log(` ${doctorConfirm ? '✅' : '-'} ${g.repoKey}: ${g.worktreePath}`);
|
|
3070
|
+
for (const g of (r.skipped_locked || [])) console.log(` 🔒 ${g.repoKey}: ${g.worktreePath}(locked——先 git -C ${g.repoRoot} worktree unlock ${g.worktreePath} 后重跑)`);
|
|
3071
|
+
const lockedPaths = new Set((r.skipped_locked || []).map((s) => s.worktreePath));
|
|
3072
|
+
for (const g of (r.remaining || []).filter((x) => !lockedPaths.has(x.worktreePath))) console.log(` ⚠️ ${g.repoKey}: ${g.worktreePath}(prune 后复扫仍在)`);
|
|
3073
|
+
for (const e of (r.errors || [])) console.log(` ❌ ${e.repoKey || ''}${e.repoRoot ? `(${e.repoRoot})` : ''}: ${e.error}`);
|
|
3074
|
+
if (!doctorConfirm && r.count > 0) console.log(`\n加 --confirm 执行 prune(git 只清指向不存在路径的注册,不动活 worktree、不碰分支)。`);
|
|
3075
|
+
}
|
|
3076
|
+
process.exitCode = (r.errors && r.errors.length > 0) ? 1 : 0;
|
|
3077
|
+
break;
|
|
3078
|
+
}
|
|
3020
3079
|
if (gcUnstampedRuns) {
|
|
3021
3080
|
// ql-20260908-006-5f04:无戳 execute-runs 一次性清扫。默认 dry-run,--confirm 才删。
|
|
3022
3081
|
// 不进 archive 热路径(pruneArchivedChangeRuntime 故意不猜无戳 run)。
|
|
@@ -3985,6 +4044,53 @@ SillySpec worktree — git worktree 隔离管理
|
|
|
3985
4044
|
}
|
|
3986
4045
|
break;
|
|
3987
4046
|
}
|
|
4047
|
+
case 'deps': {
|
|
4048
|
+
// deps 状态显式覆写(坑 deps-gate-offline-fragile,2026-10-12 实证:离线环境
|
|
4049
|
+
// npm ci 必败 + lockfile 本地改动致链接快路径失效,agent 被迫手改 meta.json 绕过
|
|
4050
|
+
// ——未定义行为且零留痕。本命令把绕过通道 CLI 化:--set 显式值 + --reason 强制
|
|
4051
|
+
// 留痕(写 depsOverrideNote / depsOverrideAt),审计面可对账。合法值收敛工具
|
|
4052
|
+
// 认知集(n/a/linked/installed/failed)。
|
|
4053
|
+
const setIdx = filteredArgs.indexOf('--set')
|
|
4054
|
+
const setVal = setIdx !== -1 && filteredArgs[setIdx + 1] && !String(filteredArgs[setIdx + 1]).startsWith('--') ? String(filteredArgs[setIdx + 1]).trim() : null
|
|
4055
|
+
const rIdx = filteredArgs.indexOf('--reason')
|
|
4056
|
+
const reason = rIdx !== -1 && filteredArgs[rIdx + 1] ? String(filteredArgs[rIdx + 1]).trim() : null
|
|
4057
|
+
if (!wtName || !setVal || !reason) {
|
|
4058
|
+
console.error('用法: sillyspec worktree deps --change <变更名> --set <n/a|linked|installed|failed> --reason "<事实依据——必填留痕>"')
|
|
4059
|
+
console.error(' 离线/内网环境供给链失败时的显式覆写通道(写 meta.json + 覆写留痕);先确认 node_modules 确实可用再 set linked')
|
|
4060
|
+
process.exit(2)
|
|
4061
|
+
}
|
|
4062
|
+
if (!['n/a', 'linked', 'installed', 'failed'].includes(setVal)) {
|
|
4063
|
+
console.error(`❌ --set 值非法 "${setVal}"(合法:n/a | linked | installed | failed)`)
|
|
4064
|
+
process.exit(2)
|
|
4065
|
+
}
|
|
4066
|
+
try {
|
|
4067
|
+
const specBaseWd = resolvePlatformSpecDir(dir, specDir)
|
|
4068
|
+
const wtBase = join(specBaseWd, '.runtime', 'worktrees')
|
|
4069
|
+
const wtDir = join(wtBase, wtName)
|
|
4070
|
+
const metaPath = join(wtDir, 'meta.json')
|
|
4071
|
+
// 跨仓 worktree:主 specBase 下无实体时扫各注册仓(简化:本命令聚焦主仓 worktree,
|
|
4072
|
+
// 跨仓路径用 --spec-dir 指到对应 specBase;两形态 meta.json 同款)
|
|
4073
|
+
if (!existsSync(metaPath)) {
|
|
4074
|
+
console.error(`❌ 找不到 worktree meta:${metaPath}`)
|
|
4075
|
+
console.error(' 跨仓 worktree 用 --spec-dir 指向其 specBase 后重跑;变更名核对 sillyspec worktree list')
|
|
4076
|
+
process.exit(1)
|
|
4077
|
+
}
|
|
4078
|
+
const meta = JSON.parse(String(readFileSync(metaPath, 'utf8')).replace(/^\uFEFF/, ''))
|
|
4079
|
+
const prev = meta.depsStatus
|
|
4080
|
+
meta.depsStatus = setVal
|
|
4081
|
+
meta.depsOverrideNote = `手工覆写: ${reason}(前值 ${prev ?? 'null'})`
|
|
4082
|
+
meta.depsOverrideAt = new Date().toISOString()
|
|
4083
|
+
const { writeAtomicSync } = await import('./fs-atomic.js')
|
|
4084
|
+
writeAtomicSync(metaPath, JSON.stringify(meta, null, 2) + '\n')
|
|
4085
|
+
console.log(`✅ deps 状态已覆写:${wtName} depsStatus ${prev ?? 'null'} → ${setVal}`)
|
|
4086
|
+
console.log(` 留痕:${meta.depsOverrideNote}`)
|
|
4087
|
+
console.log(' verify/archive 对账时以 depsOverrideNote 为审计锚——事实依据不实属未定义行为')
|
|
4088
|
+
} catch (e) {
|
|
4089
|
+
console.error(`❌ 覆写失败: ${e.message}`)
|
|
4090
|
+
process.exit(1)
|
|
4091
|
+
}
|
|
4092
|
+
break;
|
|
4093
|
+
}
|
|
3988
4094
|
default:
|
|
3989
4095
|
console.error(`❌ 未知子命令: worktree ${wtSubCmd}`);
|
|
3990
4096
|
console.log(' 运行 sillyspec worktree --help 查看帮助');
|
|
@@ -3999,6 +4105,40 @@ SillySpec worktree — git worktree 隔离管理
|
|
|
3999
4105
|
await cmdAgentLog(filteredArgs.slice(1), { json, cwd: dir, specDir });
|
|
4000
4106
|
break;
|
|
4001
4107
|
}
|
|
4108
|
+
case 'patch-reanchor': {
|
|
4109
|
+
// 收口留痕锚点回写(2026-10-12-close-trace-freeze-integrity FR-03):worktree 内收口的
|
|
4110
|
+
// change-patch.json 记 worktree 本地 SHA,apply 重放回主仓后悬空——按提交主题归属推导
|
|
4111
|
+
// 主仓锚点区间回写 + change.patch 重冻结(幂等;doctor 检出悬空时指路本命令)。
|
|
4112
|
+
const prChangeIdx = filteredArgs.indexOf('--change');
|
|
4113
|
+
const prChange = prChangeIdx >= 0 && filteredArgs[prChangeIdx + 1] && !String(filteredArgs[prChangeIdx + 1]).startsWith('--') ? filteredArgs[prChangeIdx + 1] : null;
|
|
4114
|
+
if (!prChange) {
|
|
4115
|
+
console.error('用法: sillyspec patch-reanchor --change <名> [--spec-dir <路径>] [--json]');
|
|
4116
|
+
process.exit(2);
|
|
4117
|
+
}
|
|
4118
|
+
const specRootPr = resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec');
|
|
4119
|
+
const { locateChangeDir, reAnchorChangePatch } = await import('./patch-reanchor.js');
|
|
4120
|
+
const changeDirPr = locateChangeDir(specRootPr, prChange);
|
|
4121
|
+
if (!changeDirPr) {
|
|
4122
|
+
console.error(`❌ 变更目录未找到(活跃区与归档区均无):${prChange}`);
|
|
4123
|
+
process.exit(1);
|
|
4124
|
+
}
|
|
4125
|
+
const r = reAnchorChangePatch({ projectRoot: dir, changeDir: changeDirPr, changeName: prChange });
|
|
4126
|
+
if (!r.ok) {
|
|
4127
|
+
console.error(`❌ 锚点回写未执行:${r.reason}`);
|
|
4128
|
+
process.exit(1);
|
|
4129
|
+
}
|
|
4130
|
+
if (json) {
|
|
4131
|
+
console.log(JSON.stringify({ schema_version: 1, change: prChange, noop: r.noop === true, baseline: r.baseline || null, head: r.head || null, rewritten: r.rewritten || [] }, null, 2));
|
|
4132
|
+
} else if (r.noop) {
|
|
4133
|
+
console.log(`✅ 锚点已可解析,无需回写(幂等 no-op):${prChange}`);
|
|
4134
|
+
} else {
|
|
4135
|
+
console.log(`📦 锚点已回写主仓:${prChange}`);
|
|
4136
|
+
console.log(` baseline → ${r.baseline.slice(0, 10)}(原 worktree 锚留档 worktreeBaseline)`);
|
|
4137
|
+
console.log(` head → ${r.head.slice(0, 10)}(原 worktree 锚留档 worktreeHead)`);
|
|
4138
|
+
console.log(` change.patch 已按主仓区间重冻结,patchSha256 重锚(${(r.rewritten || []).join('、')})`);
|
|
4139
|
+
}
|
|
4140
|
+
break;
|
|
4141
|
+
}
|
|
4002
4142
|
case 'handoff': {
|
|
4003
4143
|
// 阶段跑者瘦会话交接块(P1-5 v1,2026-09-20 对撞实验驱动):打印下一阶段新会话
|
|
4004
4144
|
// 启动块(保持 SILLYSPEC_SESSION_ID)+ 状态摘要。纯只读。CLI prompt 自足——
|
package/src/local-register.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* `sillyspec local detect` / `platform connect` 各自补齐)。落盘走 writeAtomicSync
|
|
16
16
|
* (tmp+rename,防多会话并发写半截,与 local detect 写入口径一致)。
|
|
17
17
|
*/
|
|
18
|
-
import { existsSync, readFileSync, mkdirSync } from 'fs'
|
|
18
|
+
import { existsSync, readFileSync, mkdirSync, statSync } from 'fs'
|
|
19
19
|
import { dirname, join } from 'path'
|
|
20
20
|
import { writeAtomicSync } from './fs-atomic.js'
|
|
21
21
|
import { withFileLock } from './quicklog.js'
|
|
@@ -58,9 +58,27 @@ export async function registerRepoInLocalYaml(yamlPath, key, repoPath) {
|
|
|
58
58
|
const posixPath = String(repoPath).replace(/\\/g, '/')
|
|
59
59
|
const entryLine = ` ${key}: ${posixPath}`
|
|
60
60
|
|
|
61
|
+
// 跨仓标记(2026-10-12-cli-friction-signal):注册成功后 best-effort 向目标仓 .git/ 内写
|
|
62
|
+
// 指路标记——uninit 门据此把「在子仓跑 CLI」精确指回工作区(dogfood:三连跑错目录靠人工
|
|
63
|
+
// 发现)。.git/ 内零工作区污染;linked worktree 的 .git 是文件则跳过;失败仅告警不拦注册。
|
|
64
|
+
const tryWriteMarker = () => {
|
|
65
|
+
try {
|
|
66
|
+
const gitDir = join(String(repoPath), '.git')
|
|
67
|
+
if (!existsSync(gitDir) || !statSync(gitDir).isDirectory()) return
|
|
68
|
+
writeAtomicSync(join(gitDir, 'sillyspec-crossrepo.json'), JSON.stringify({
|
|
69
|
+
workspace: dirname(dirname(yamlPath)).replace(/\\/g, '/'),
|
|
70
|
+
key,
|
|
71
|
+
registeredAt: new Date().toISOString(),
|
|
72
|
+
}, null, 2) + '\n')
|
|
73
|
+
} catch (e) {
|
|
74
|
+
console.warn(`⚠️ 跨仓标记写入失败(${e && e.message ? e.message : e})——注册仍成功,未初始化门指路退化为通用文案`)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
61
78
|
const write = (text) => {
|
|
62
79
|
mkdirSync(dirname(yamlPath), { recursive: true })
|
|
63
80
|
writeAtomicSync(yamlPath, text)
|
|
81
|
+
tryWriteMarker()
|
|
64
82
|
}
|
|
65
83
|
|
|
66
84
|
const lines = raw.split('\n')
|
|
@@ -93,6 +111,7 @@ export async function registerRepoInLocalYaml(yamlPath, key, repoPath) {
|
|
|
93
111
|
// 编辑器写入),内存归一后比对相等直接跳过 → 磁盘永不治愈 + CLI 报 ✅ 假成功 → 解析侧
|
|
94
112
|
// (修复前)空 Map → MultiRepoContext 报「未注册」→ 死循环。幂等路径也落盘一次治愈。
|
|
95
113
|
if (hadCr) write(lines.join('\n'))
|
|
114
|
+
tryWriteMarker() // 幂等重注册也补标记(标记被删后可经重注册恢复——uninit 门指路依赖)
|
|
96
115
|
return { fileCreated: false, sectionCreated: false, replaced: false } // 值未变,幂等跳过
|
|
97
116
|
}
|
|
98
117
|
lines[i] = entryLine
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* patch-reanchor.js — 收口留痕锚点回写(2026-10-12-close-trace-freeze-integrity FR-03)。
|
|
3
|
+
*
|
|
4
|
+
* 事故形态(multi-agent-platform 2026-10-12 ws-ref 五变更取证):thin 流程在会话专属
|
|
5
|
+
* worktree(独立克隆/事后清理)内收口,change-patch.json 冻结的 baseline/head 是 worktree
|
|
6
|
+
* 本地 SHA;worktree apply 重放回主仓时 SHA 全部改写且原对象被清理——归档锚点悬空,
|
|
7
|
+
* `git diff baseline..head` 不可解析,归档溯源死链。
|
|
8
|
+
*
|
|
9
|
+
* 回写约定:按「提交主题含变更名」归属(仓内提交规范 `(<变更名>)` 后缀族,与
|
|
10
|
+
* filterCommittedFace 的 commitAttribution 同一约定),`git log --all --grep` 取归属提交链:
|
|
11
|
+
* head=拓扑末笔、baseline=首笔^;区间一致性(首笔^ 必须是 head 祖先、区间 diff 与留档
|
|
12
|
+
* files[] 至少一文件重叠)不过即 fail-closed 不改写。原 worktree 锚降级为
|
|
13
|
+
* worktreeBaseline/worktreeHead 溯源字段,patchSha256 按重冻正文重锚。
|
|
14
|
+
*
|
|
15
|
+
* 依赖边界:本模块被 worktree-apply.js 静态引入(apply 尾声自动回写),而 scope-audit.js
|
|
16
|
+
* 已静态引入 worktree-apply(评审 G3「内核绝不 import scope-audit」)——故本模块禁止
|
|
17
|
+
* import scope-audit/worktree-apply(防环),diff 采集自实现(pathspec 分块)。
|
|
18
|
+
*/
|
|
19
|
+
import { createHash } from 'node:crypto'
|
|
20
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
|
21
|
+
import { join } from 'node:path'
|
|
22
|
+
import { execFileSync } from 'node:child_process'
|
|
23
|
+
import { writeAtomicSync } from './fs-atomic.js'
|
|
24
|
+
|
|
25
|
+
function git(cwd, args, opts = {}) {
|
|
26
|
+
try {
|
|
27
|
+
return execFileSync('git', args, { cwd, encoding: 'utf8', timeout: 60000, windowsHide: true, ...opts }).trim()
|
|
28
|
+
} catch { return null }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const resolves = (cwd, sha) => Boolean(sha) && /^[0-9a-f]{40}$/i.test(String(sha)) && git(cwd, ['cat-file', '-e', `${sha}^{commit}`]) !== null
|
|
32
|
+
|
|
33
|
+
/** pathspec 分块(命令行长度上限防御,与 worktree-apply.chunkPaths 同上限语义,本地实现防环) */
|
|
34
|
+
function chunkPaths(paths, maxChars = 8000) {
|
|
35
|
+
const chunks = []
|
|
36
|
+
let cur = []
|
|
37
|
+
let len = 0
|
|
38
|
+
for (const p of paths) {
|
|
39
|
+
if (cur.length > 0 && len + p.length + 1 > maxChars) { chunks.push(cur); cur = []; len = 0 }
|
|
40
|
+
cur.push(p); len += p.length + 1
|
|
41
|
+
}
|
|
42
|
+
if (cur.length > 0) chunks.push(cur)
|
|
43
|
+
return chunks
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 定位变更目录:活跃区 → 归档区精确名 → 归档区 `<日期>-<名>` 后缀匹配(归档重命名形态)。
|
|
48
|
+
* @returns {string|null}
|
|
49
|
+
*/
|
|
50
|
+
export function locateChangeDir(specBase, changeName) {
|
|
51
|
+
const active = join(specBase, 'changes', changeName)
|
|
52
|
+
if (existsSync(join(active, 'change-patch.json')) || existsSync(join(active, 'design.md'))) return active
|
|
53
|
+
const archiveExact = join(specBase, 'changes', 'archive', changeName)
|
|
54
|
+
if (existsSync(join(archiveExact, 'change-patch.json')) || existsSync(join(archiveExact, 'design.md'))) return archiveExact
|
|
55
|
+
try {
|
|
56
|
+
const archiveRoot = join(specBase, 'changes', 'archive')
|
|
57
|
+
if (existsSync(archiveRoot)) {
|
|
58
|
+
const suffix = `-${changeName}`
|
|
59
|
+
const hit = readdirSync(archiveRoot).filter((d) => d.endsWith(suffix)).sort()
|
|
60
|
+
if (hit.length === 1) return join(archiveRoot, hit[0])
|
|
61
|
+
}
|
|
62
|
+
} catch { /* 归档扫描失败走 null */ }
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 归档锚点回写核心(幂等)。
|
|
68
|
+
* @param projectRoot 主仓根(锚点可解析性判定与重冻区间的仓)
|
|
69
|
+
* @param changeDir 变更目录(活跃或归档侧)
|
|
70
|
+
* @param changeName 变更名(提交主题归属 grep 锚)
|
|
71
|
+
* @returns {{ ok: boolean, noop?: boolean, baseline?: string, head?: string, reason?: string, rewritten?: string[] }}
|
|
72
|
+
*/
|
|
73
|
+
export function reAnchorChangePatch({ projectRoot, changeDir, changeName }) {
|
|
74
|
+
const cpPath = join(changeDir, 'change-patch.json')
|
|
75
|
+
if (!existsSync(cpPath)) return { ok: false, reason: 'change-patch.json 不在案(本变更无收口冻结件)' }
|
|
76
|
+
let meta
|
|
77
|
+
try { meta = JSON.parse(readFileSync(cpPath, 'utf8')) } catch (e) { return { ok: false, reason: `change-patch.json 不可解析:${e && e.message ? e.message.split('\n')[0] : e}` } }
|
|
78
|
+
|
|
79
|
+
const headOk = resolves(projectRoot, meta.head)
|
|
80
|
+
const baseOk = resolves(projectRoot, meta.baseline)
|
|
81
|
+
if (headOk && (baseOk || !meta.baseline)) return { ok: true, noop: true }
|
|
82
|
+
|
|
83
|
+
// 归属提交链:主题含变更名(提交规范后缀族);--all 覆盖主干/分支,date-order 保拓扑可读
|
|
84
|
+
const grepRaw = git(projectRoot, ['log', '--all', '--date-order', '--format=%H%x01%s', `--grep=${changeName}`])
|
|
85
|
+
if (!grepRaw) return { ok: false, reason: `主仓无可归属提交(git log --all --grep=${changeName} 空/失败)——锚点推导无据,fail-closed` }
|
|
86
|
+
const rows = grepRaw.split('\n').map((l) => l.trim()).filter(Boolean).map((l) => { const i = l.indexOf('\u0001'); return [l.slice(0, i), l.slice(i + 1)] })
|
|
87
|
+
if (rows.length === 0) return { ok: false, reason: `主仓无可归属提交——锚点推导无据,fail-closed` }
|
|
88
|
+
const oldest = rows[rows.length - 1][0]
|
|
89
|
+
const newest = rows[0][0]
|
|
90
|
+
const isAncestor = (a, b) => git(projectRoot, ['merge-base', '--is-ancestor', a, b]) !== null
|
|
91
|
+
if (!isAncestor(oldest, newest)) return { ok: false, reason: `归属提交链拓扑不一致(首笔 ${oldest.slice(0, 10)} 非末笔 ${newest.slice(0, 10)} 祖先)——疑似同名异源提交混入,fail-closed` }
|
|
92
|
+
const baseRaw = git(projectRoot, ['rev-parse', `${oldest}^`])
|
|
93
|
+
if (!baseRaw || !resolves(projectRoot, baseRaw)) return { ok: false, reason: `首笔归属提交无第一父(根提交形态)——baseline 不可推导,fail-closed` }
|
|
94
|
+
if (!isAncestor(baseRaw, newest)) return { ok: false, reason: `推导区间不一致(首笔^ 非 head 祖先)——fail-closed` }
|
|
95
|
+
|
|
96
|
+
// 区间与留档 files[] 至少一文件重叠(防错归属整链改写)
|
|
97
|
+
const rangeFiles = String(git(projectRoot, ['diff', '--name-only', '--no-color', baseRaw, newest]) || '')
|
|
98
|
+
.split('\n').map((s) => s.trim().replace(/\\/g, '/')).filter(Boolean)
|
|
99
|
+
const recorded = (Array.isArray(meta.files) ? meta.files : []).map((f) => String(f).replace(/\\/g, '/'))
|
|
100
|
+
const overlap = rangeFiles.filter((f) => recorded.includes(f) || recorded.some((r) => r.startsWith(`${f}/`) || f.startsWith(`${r}/`)))
|
|
101
|
+
if (recorded.length > 0 && overlap.length === 0) {
|
|
102
|
+
return { ok: false, reason: `推导区间(${baseRaw.slice(0, 10)}..${newest.slice(0, 10)})与留档 files[] 零重叠——归属可疑,fail-closed` }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 重冻正文:区间 diff 按 files[] pathspec 限面(重放后的主仓区间即归档真相)
|
|
106
|
+
const face = recorded.length > 0 ? recorded : rangeFiles
|
|
107
|
+
const parts = []
|
|
108
|
+
for (const chunk of chunkPaths(face)) {
|
|
109
|
+
const d = git(projectRoot, ['diff', '--no-color', baseRaw, newest, '--', ...chunk])
|
|
110
|
+
if (d) parts.push(d)
|
|
111
|
+
}
|
|
112
|
+
const patchBody = parts.length > 0 ? (parts.join('\n').endsWith('\n') ? parts.join('\n') : parts.join('\n') + '\n') : null
|
|
113
|
+
if (!patchBody) return { ok: false, reason: `主仓区间 diff 为空(${baseRaw.slice(0, 10)}..${newest.slice(0, 10)} × ${face.length} 文件)——重冻无据,fail-closed` }
|
|
114
|
+
|
|
115
|
+
const sha256 = createHash('sha256').update(patchBody.replace(/\r\n/g, '\n'), 'utf8').digest('hex')
|
|
116
|
+
const rewritten = []
|
|
117
|
+
if (meta.baseline !== baseRaw) { meta.worktreeBaseline = meta.baseline ?? null; meta.baseline = baseRaw; rewritten.push('baseline') }
|
|
118
|
+
if (meta.head !== newest) { meta.worktreeHead = meta.head ?? null; meta.head = newest; rewritten.push('head') }
|
|
119
|
+
if (meta.scopeAudit && typeof meta.scopeAudit === 'object' && meta.scopeAudit.baseAnchor !== baseRaw) {
|
|
120
|
+
meta.scopeAudit = { ...meta.scopeAudit, baseAnchor: baseRaw }
|
|
121
|
+
rewritten.push('scopeAudit.baseAnchor')
|
|
122
|
+
}
|
|
123
|
+
meta.patchSha256 = sha256
|
|
124
|
+
meta.patchStatus = 'ok'
|
|
125
|
+
meta.reanchoredAt = new Date().toISOString()
|
|
126
|
+
meta.reanchorNote = `apply/回填后主仓重锚(原 worktree 锚悬空:${meta.worktreeBaseline ? String(meta.worktreeBaseline).slice(0, 10) : '—'}/${meta.worktreeHead ? String(meta.worktreeHead).slice(0, 10) : '—'};归属约定=提交主题含变更名)——2026-10-12-close-trace-freeze-integrity FR-03`
|
|
127
|
+
writeAtomicSync(cpPath, JSON.stringify(meta, null, 2) + '\n')
|
|
128
|
+
writeAtomicSync(join(changeDir, 'change.patch'), patchBody)
|
|
129
|
+
rewritten.push('change.patch', 'patchSha256')
|
|
130
|
+
return { ok: true, noop: false, baseline: baseRaw, head: newest, rewritten }
|
|
131
|
+
}
|
package/src/progress.js
CHANGED
|
@@ -23,6 +23,10 @@ import { StepStore } from './progress/step-store.js';
|
|
|
23
23
|
import { StageMachine } from './progress/stage-machine.js';
|
|
24
24
|
import { stageRegistry } from './stages/index.js';
|
|
25
25
|
import { STAGE_ORDER, MAIN_FLOW_ORDER, VALID_STAGES, STAGE_LABELS, SPEC_DIR_NAME, CURRENT_VERSION, emptyStage } from './progress/shared.js';
|
|
26
|
+
// 2026-10-13-thin-sync-stage:叶子模块(无内部依赖),同步投影读 thin 出身用——
|
|
27
|
+
// 叶子化避免本文件为读一个状态文件拉入 flow 协议整条依赖链(评审 P3 勘误:
|
|
28
|
+
// flow.js 静态 import 闭包实测不达本文件,非循环依赖问题)。
|
|
29
|
+
import { readFlowState } from './flow-state.js';
|
|
26
30
|
// resolveSpecDir 单一真相源在 src/run/shared.js(含 home 拒绝守卫,坑 cwd-correction-home-collision
|
|
27
31
|
// 根治:home 下 .sillyspec 恒不命中,防 smoke/临时目录污染自我延续)。此处 re-export 保持
|
|
28
32
|
// 既有 import 路径兼容;run/shared.js 对 progress.js 只有动态 import,无静态循环。
|
|
@@ -600,6 +604,18 @@ export class ProgressManager {
|
|
|
600
604
|
// → 上行恒无标题,平台只能落 change_key(「英文 key」病灶的同步链一侧)。
|
|
601
605
|
title: changeRow.title ?? null,
|
|
602
606
|
};
|
|
607
|
+
// thin 出身上行投影(2026-10-13-thin-sync-stage):tier=thin 且行 status=active 时
|
|
608
|
+
// current_stage 上报 'thin'——平台 StageEnum.THIN / 列表「轻量」徽章与筛选 / ingest
|
|
609
|
+
// 双守卫均按该值消费;本地行出生恒 'brainstorm'(thin 不走阶段机),照抄上行会让
|
|
610
|
+
// 平台读侧 is_thin 判定三分支全不命中(实证:进行中轻量变更无徽章)。仅覆写载荷,
|
|
611
|
+
// 本地 DB 行不动;归档(status=archived,行已翻 'archive')与升厚(tier=thick)不
|
|
612
|
+
// 覆写;flow-state 缺失/损坏 readFlowState→null 按行值上行(fail-open 现状行为)。
|
|
613
|
+
if (
|
|
614
|
+
changeRow.status === 'active' &&
|
|
615
|
+
readFlowState(this._changePath(cwd, changeName))?.tier === 'thin'
|
|
616
|
+
) {
|
|
617
|
+
changeProj.current_stage = 'thin';
|
|
618
|
+
}
|
|
603
619
|
|
|
604
620
|
// 2. project 全局单行(只投影稳定字段 name/schema_version;created_at/updated_at 是本地库元数据,
|
|
605
621
|
// 同步无意义且破坏 round-trip 跨库等值——与 changes 排除 created_at 同理,B2)
|
package/src/review-dispatch.js
CHANGED
|
@@ -22,6 +22,7 @@ import { join, dirname } from 'node:path'
|
|
|
22
22
|
import jsYaml from 'js-yaml'
|
|
23
23
|
import { writeAtomicSync } from './fs-atomic.js'
|
|
24
24
|
import { REVIEW_CHECKLISTS } from './stage-review-checklist.js'
|
|
25
|
+
import { assembleProbeLinesForScope } from './review-probes.js'
|
|
25
26
|
import { CEREMONY_TIERS } from './ceremony-tier.js'
|
|
26
27
|
import { withFileLock } from './quicklog.js'
|
|
27
28
|
import {
|
|
@@ -161,15 +162,20 @@ export function extractReviewFromArtifacts(artifacts) {
|
|
|
161
162
|
* docblock 规则)②review.json 契约(与 gate 同源)③期望产出方式(read_only worker:
|
|
162
163
|
* 结论经 artifacts 返回,**不写任何文件**——落盘归 CLI)④铁律段(禁改文件/禁 git commit)。
|
|
163
164
|
*/
|
|
164
|
-
export function buildReviewerTaskBook({ stage, changeDir, reviewRunId, channelPriority }) {
|
|
165
|
+
export function buildReviewerTaskBook({ stage, changeDir, reviewRunId, channelPriority, probeLines = null }) {
|
|
165
166
|
const mainDoc = STAGE_MAIN_DOC[stage] || 'design.md'
|
|
166
167
|
const items = (REVIEW_CHECKLISTS[stage] || []).map((t, i) => `${i + 1}. ${t}`).join('\n')
|
|
167
168
|
const contract = renderReviewJsonContract({ stage, changeDir, reviewRunId, tier: 'independent', channelPriority })
|
|
169
|
+
// 项目评审探针(2026-10-12-review-probe-injection):local.yaml 数据驱动注入,缺省零变化。
|
|
170
|
+
const probeSection = Array.isArray(probeLines) && probeLines.length > 0
|
|
171
|
+
? ['', '## 项目评审探针(local.yaml 注入——逐条 pass/gap/fail + 证据锚点,未答=未审)', ...probeLines.map((l) => `- ${l}`)]
|
|
172
|
+
: []
|
|
168
173
|
const workerPrompt = [
|
|
169
174
|
`你是独立审查者(platform 通道派发)。审查对象:${changeDir ? join(changeDir, mainDoc) : mainDoc}`,
|
|
170
175
|
'',
|
|
171
176
|
'## 审查清单(逐条给 pass/gap/fail + 证据锚点)',
|
|
172
177
|
items,
|
|
178
|
+
...probeSection,
|
|
173
179
|
'',
|
|
174
180
|
contract,
|
|
175
181
|
'',
|
|
@@ -285,7 +291,7 @@ export async function runReviewDispatch(opts = {}) {
|
|
|
285
291
|
} catch { /* marker 写失败不阻断派发(status 回收时再补) */ }
|
|
286
292
|
}
|
|
287
293
|
const changeDir = join(specBase, 'changes', changeName)
|
|
288
|
-
const book = buildReviewerTaskBook({ stage, changeDir, reviewRunId: runId, channelPriority: readReviewChannelPriority(cwd) })
|
|
294
|
+
const book = buildReviewerTaskBook({ stage, changeDir, reviewRunId: runId, channelPriority: readReviewChannelPriority(cwd), probeLines: probeLinesForThick(specBase) })
|
|
289
295
|
const mission = await client.createMission({ objective: book.objective, orchestrationMode: 'external', budgetUsd })
|
|
290
296
|
const missionId = mission && mission.missionId
|
|
291
297
|
if (!missionId) return { ok: false, reason: 'create_mission 未返回 missionId', guidance: renderFallbackGuidance(cwd) }
|
|
@@ -694,7 +700,7 @@ export async function dispatchShadowReview(opts = {}) {
|
|
|
694
700
|
// 影子 runId 独立生成(R-06 隔离通道②:绝不读写主线 marker / stageReviewMarkerPath)
|
|
695
701
|
const shadowRunId = generateShadowReviewRunId()
|
|
696
702
|
const changeDir = join(specBase, 'changes', changeName)
|
|
697
|
-
const book = buildReviewerTaskBook({ stage, changeDir, reviewRunId: shadowRunId, channelPriority: readReviewChannelPriority(cwd) })
|
|
703
|
+
const book = buildReviewerTaskBook({ stage, changeDir, reviewRunId: shadowRunId, channelPriority: readReviewChannelPriority(cwd), probeLines: probeLinesForThick(specBase) })
|
|
698
704
|
const workerPrompt = book.workerPrompt + '\n(影子审查:本结论将由 CLI 回收到 stage-reviews-shadow/ 独立命名空间做轻/重对照记账,不影响主线任何判定。)\n'
|
|
699
705
|
const mission = await client.createMission({ objective: book.objective, orchestrationMode: 'external', budgetUsd: budget })
|
|
700
706
|
const missionId = mission && mission.missionId
|
|
@@ -840,6 +846,15 @@ export async function dispatchShadowReview(opts = {}) {
|
|
|
840
846
|
|
|
841
847
|
// task-06 消费契约预留(配置读取在该 task 落地;此处常量为缺省值单一来源)
|
|
842
848
|
const DEFAULT_REVIEW_DISPATCH_CONFIG = { budget_usd: 1.0, stall_ms: 900_000, timeout_ms: 0 }
|
|
849
|
+
/** 厚档任务书探针装配(2026-10-12-review-probe-injection):失败/未配置返回 null(零注入零阻断)。 */
|
|
850
|
+
function probeLinesForThick(specBase) {
|
|
851
|
+
try {
|
|
852
|
+
const { lines, warnings } = assembleProbeLinesForScope(specBase, 'thick')
|
|
853
|
+
for (const w of warnings) console.warn(`⚠️ review_probes:${w}`)
|
|
854
|
+
return lines.length > 0 ? lines : null
|
|
855
|
+
} catch { return null }
|
|
856
|
+
}
|
|
857
|
+
|
|
843
858
|
export function readReviewDispatchConfig(cwd) {
|
|
844
859
|
try {
|
|
845
860
|
const p = join(cwd || process.cwd(), '.sillyspec', 'local.yaml')
|