sillyspec 3.29.3 → 3.29.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.
Files changed (32) hide show
  1. package/asset-audit-2026-09-20/README.md +42 -0
  2. package/asset-audit-2026-09-20/asset-audit.cjs +253 -0
  3. package/asset-audit-2026-09-20/extract-candidates.cjs +37 -0
  4. package/asset-audit-2026-09-20/platform-archive.csv +294 -0
  5. package/asset-audit-2026-09-20/platform-fr.csv +12 -0
  6. package/asset-audit-2026-09-20/platform-gotchas.csv +6 -0
  7. package/asset-audit-2026-09-20/platform-knownissues.csv +23 -0
  8. package/asset-audit-2026-09-20/platform-modules.csv +222 -0
  9. package/asset-audit-2026-09-20/platform-quicklog.csv +1076 -0
  10. package/asset-audit-2026-09-20/platform-troubleshooting-candidates.csv +88 -0
  11. package/asset-audit-2026-09-20/platform-troubleshooting.csv +1 -0
  12. package/asset-audit-2026-09-20/sillyspec-archive.csv +108 -0
  13. package/asset-audit-2026-09-20/sillyspec-fr.csv +51 -0
  14. package/asset-audit-2026-09-20/sillyspec-gotchas.csv +4 -0
  15. package/asset-audit-2026-09-20/sillyspec-knownissues.csv +34 -0
  16. package/asset-audit-2026-09-20/sillyspec-modules.csv +30 -0
  17. package/asset-audit-2026-09-20/sillyspec-quicklog.csv +460 -0
  18. package/asset-audit-2026-09-20/sillyspec-troubleshooting.csv +69 -0
  19. package/asset-audit-2026-09-20/summary.json +241 -0
  20. package/package.json +1 -1
  21. package/src/cross-repo-reconcile.js +232 -46
  22. package/src/fr-index.js +58 -2
  23. package/src/index.js +2 -2
  24. package/src/run/bg-sync.js +231 -0
  25. package/src/run/complete-handlers.js +208 -5
  26. package/src/run/gates.js +10 -5
  27. package/src/run/prompt.js +1 -1
  28. package/src/run/shared.js +32 -0
  29. package/src/scope-audit.js +303 -20
  30. package/src/stages/quick.js +2 -0
  31. package/src/sync.js +12 -0
  32. package/src/verify-postcheck.js +9 -5
@@ -0,0 +1,231 @@
1
+ /**
2
+ * spec-sync 后台异步执行(detached 子进程)。
3
+ *
4
+ * 背景(2026-09-20 用户反馈:末步 --done 因 spec-sync 网络同步约 4 分钟不返回):
5
+ * triggerSync 的 17 个调用点全部 fire-and-forget,但 Node 进程退出要等事件循环排空——
6
+ * 在飞 fetch 拖住进程,CLI 输出打完后仍要等网络收尾才返回 shell(平台慢时分钟级体感
7
+ * hang;本机制落地当天的会话启动命令尾部 3 行 [spec-sync] 已同步 逐条等网络,现场即
8
+ * 实证)。8s 总熔断(raceWithAbort)只保证「最多等 8s」,治标;本模块治本:同步整体
9
+ * 挪进 detached 后台子进程,父进程 spawn + unref 后立即返回,命令零网络尾巴。
10
+ *
11
+ * 架构(父侧/子侧同一模块,职责分开):
12
+ * - 父侧 spawnBackgroundSync(shared.js triggerSync 唯一接入点调用):
13
+ * ① 未连接平台预判(sync.js peekPlatformConnected 同源判据)——未连接不 spawn,
14
+ * 本地独立用户零开销零行为变化;② 单飞锁判定:活锁 → rerunQueued 置位合并
15
+ * (跑完当前轮再补一轮,防进程堆积),无锁/死锁 → spawn detached 子进程。
16
+ * - 子侧 runBgSyncFromEnv(node -e bootstrap 动态 import 本模块):写锁 → 循环
17
+ * 〔清 rerunQueued → triggerSync inline 执行一轮 → 期间有新请求则再来一轮〕,
18
+ * 上限 5 轮 / 总预算 ~2.5min → 清锁退出。轮前清位 + 轮后查位:清位后本轮树 hash
19
+ * 晚于清位时刻,天然覆盖此后到达的状态;hash 之后到达的靠置位标记触发下一轮。
20
+ * - 子进程 stdout/stderr 接 <runtimeRoot>/spec-sync-bg.log(append,超 1MB 截尾保
21
+ * 后 512KB):[spec-sync]/[sync] 的 warn/冲突横幅全量留痕,不进主命令输出也不再丢。
22
+ *
23
+ * 与既有机制的交互:
24
+ * - raceWithAbort / SILLYSPEC_SYNC_TIMEOUT_MS 不动——inline 路径原样。后台轮预算 =
25
+ * max(resolveSyncTotalTimeoutMs(), 45s):后台无人等待,放宽到 45s 提高「慢平台真的
26
+ * 同步成功」概率(2026-09-07 平台 >8s 响应间歇熔断的 pain 点在后台模式下自然消解)。
27
+ * - sync-noise.js 噪音闸 / spec-sync-conflict-*.json / 基线快照在子进程内照常工作
28
+ * (同一 .runtime 目录,跨进程 marker 语义不变)。
29
+ * - 冲突可见性不降级:横幅进日志文件 + 冲突文件照落,下一条命令 progress show 照常
30
+ * 红标提示(resolve 三态处置入口不变)。
31
+ * - SILLYSPEC_SYNC_BG=0 逃生阀:triggerSync 回落 inline(测试断言进程内行为/用户自救)。
32
+ *
33
+ * 循环 import 说明:本模块与 shared.js 双向引用(shared 调本模块 spawnBackgroundSync、
34
+ * 本模块两侧调 shared 的 triggerSync/resolveRuntimeRoot/resolveSyncTotalTimeoutMs)。
35
+ * 对 shared 的引用一律走函数内动态 import,避免静态环(ESM 环可运行但初始化序脆弱,
36
+ * 刻意不赌);父进程内 shared.js 已加载,动态 import 走缓存无额外成本。
37
+ */
38
+ import { spawn } from 'child_process';
39
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'fs';
40
+ import { join } from 'path';
41
+
42
+ export const BG_LOCK_FILENAME = 'spec-sync-bg.lock';
43
+ export const BG_LOG_FILENAME = 'spec-sync-bg.log';
44
+
45
+ // 后台轮参数:单轮预算下限(与 env SILLYSPEC_SYNC_TIMEOUT_MS 取 max)、轮数与总预算帽
46
+ // (防慢平台下无限续轮);锁时效(pid 活性之外的第二道判死——pid 复用假活兜底,
47
+ // 子进程自身总预算 ~2.5min + 收尾余量远小于该值,超时必是异常残留)。
48
+ const BG_ROUND_FLOOR_MS = 45_000;
49
+ const BG_MAX_ROUNDS = 5;
50
+ const BG_TOTAL_BUDGET_MS = 150_000;
51
+ const LOCK_STALE_MS = 15 * 60 * 1000;
52
+ const LOG_TRUNCATE_THRESHOLD = 1_000_000;
53
+ const LOG_TRUNCATE_KEEP = 512 * 1024;
54
+
55
+ /** pid 活性探测(跨平台):signal 0 探测;EPERM = 进程存在但属他户,保守按活。 */
56
+ function isPidAlive(pid) {
57
+ if (!Number.isInteger(pid) || pid <= 0) return false;
58
+ try {
59
+ process.kill(pid, 0);
60
+ return true;
61
+ } catch (e) {
62
+ return e && e.code === 'EPERM';
63
+ }
64
+ }
65
+
66
+ /** 读后台单飞锁(缺失/损坏 → null)。 */
67
+ export function readBgSyncLock(runtimeRoot) {
68
+ try {
69
+ const raw = readFileSync(join(runtimeRoot, BG_LOCK_FILENAME), 'utf8');
70
+ const obj = JSON.parse(raw);
71
+ return obj && typeof obj === 'object' ? obj : null;
72
+ } catch {
73
+ return null;
74
+ }
75
+ }
76
+
77
+ function writeBgSyncLock(runtimeRoot, lock) {
78
+ mkdirSync(runtimeRoot, { recursive: true });
79
+ const tmp = join(runtimeRoot, `${BG_LOCK_FILENAME}.${process.pid}.tmp`);
80
+ writeFileSync(tmp, JSON.stringify(lock) + '\n', 'utf8');
81
+ renameSync(tmp, join(runtimeRoot, BG_LOCK_FILENAME));
82
+ }
83
+
84
+ function removeBgSyncLock(runtimeRoot) {
85
+ try {
86
+ const p = join(runtimeRoot, BG_LOCK_FILENAME);
87
+ if (existsSync(p)) unlinkSync(p);
88
+ } catch { /* 清锁失败:残留锁按时效/pid 判死后自动接管,不影响正确性 */ }
89
+ }
90
+
91
+ /**
92
+ * 锁活性判定(pid 活 + 未超时)。纯函数(pid 探测单列 isPidAlive 便于注入测试)。
93
+ * @param {object|null} lock readBgSyncLock 产物
94
+ * @param {number} [now] 注入时钟(测试用)
95
+ */
96
+ export function isBgSyncLockLive(lock, now = Date.now()) {
97
+ if (!lock || !Number.isInteger(lock.pid) || lock.pid <= 0) return false;
98
+ if (typeof lock.startedAt !== 'number') return false;
99
+ if (now - lock.startedAt > LOCK_STALE_MS) return false;
100
+ return isPidAlive(lock.pid);
101
+ }
102
+
103
+ /** 子进程起步截尾日志(>1MB 保后 512KB,从整行边界起)。失败只影响日志体积。 */
104
+ function truncateLogIfNeeded(runtimeRoot) {
105
+ try {
106
+ const p = join(runtimeRoot, BG_LOG_FILENAME);
107
+ if (statSync(p).size <= LOG_TRUNCATE_THRESHOLD) return;
108
+ const buf = readFileSync(p);
109
+ const keep = buf.subarray(Math.max(0, buf.length - LOG_TRUNCATE_KEEP));
110
+ const nl = keep.indexOf(10);
111
+ writeFileSync(p, nl >= 0 ? keep.subarray(nl + 1) : keep);
112
+ } catch { /* 截断失败:继续 append */ }
113
+ }
114
+
115
+ /** 子进程 bootstrap:node -e 动态 import 本模块(file URL 精确定位,不依赖 CLI 安装形态)。 */
116
+ function buildChildBootstrap() {
117
+ const selfUrl = new URL('./bg-sync.js', import.meta.url).href;
118
+ return `import(${JSON.stringify(selfUrl)}).then(m=>m.runBgSyncFromEnv()).catch(e=>{console.error('[spec-sync-bg] 后台同步进程异常退出:',(e&&e.message)||e)});`;
119
+ }
120
+
121
+ /**
122
+ * 父侧后台同步入口(triggerSync 调用,不直接被业务代码使用)。
123
+ *
124
+ * @returns {Promise<{status:'spawned'|'coalesced'|'not-connected', logPath?:string}>}
125
+ * spawned=已起后台子进程;coalesced=活锁在跑、本轮状态已并入其下一轮(rerunQueued);
126
+ * not-connected=未连接平台(调用方回落 inline 路径=与旧行为一致的静默 no-op)。
127
+ * spawn 失败抛异常(调用方 catch 降级 inline,绝不因后台化阻断主流程)。
128
+ */
129
+ export async function spawnBackgroundSync(cwd, changeName, platformOpts = {}, opts = {}) {
130
+ const syncMod = await import('../sync.js');
131
+ if (!syncMod.peekPlatformConnected(cwd)) return { status: 'not-connected' };
132
+
133
+ const { resolveRuntimeRoot } = await import('./shared.js');
134
+ const runtimeRoot = resolveRuntimeRoot(platformOpts, platformOpts?.specRoot || join(cwd, '.sillyspec'));
135
+
136
+ const lock = readBgSyncLock(runtimeRoot);
137
+ if (isBgSyncLockLive(lock)) {
138
+ // 单飞合并:活后台轮在跑,把「还有新状态要同步」并入其下一轮。置位失败(并发写锁)
139
+ // 只损失合并信号——后台轮收尾锁自清,下一条命令重新 spawn,收敛不丢。
140
+ try { writeBgSyncLock(runtimeRoot, { ...lock, rerunQueued: true }); } catch { /* 见上 */ }
141
+ return { status: 'coalesced', logPath: join(runtimeRoot, BG_LOG_FILENAME) };
142
+ }
143
+
144
+ // 日志 fd:子进程 stdout/stderr 直接 append 进日志文件(跨平台 fd 继承);开不了退 ignore
145
+ const logPath = join(runtimeRoot, BG_LOG_FILENAME);
146
+ let stdio = ['ignore', 'ignore', 'ignore'];
147
+ let logFd = null;
148
+ try {
149
+ mkdirSync(runtimeRoot, { recursive: true });
150
+ logFd = openSync(logPath, 'a');
151
+ stdio = ['ignore', logFd, logFd];
152
+ } catch {
153
+ logFd = null;
154
+ }
155
+
156
+ const env = {
157
+ ...process.env,
158
+ SILLYSPEC_BG_SYNC_CHANGE: String(changeName),
159
+ SILLYSPEC_BG_SYNC_CWD: String(cwd),
160
+ };
161
+ if (platformOpts?.specRoot) env.SILLYSPEC_BG_SYNC_SPEC_ROOT = String(platformOpts.specRoot);
162
+ if (platformOpts?.runtimeRoot) env.SILLYSPEC_BG_SYNC_RUNTIME_ROOT = String(platformOpts.runtimeRoot);
163
+
164
+ try {
165
+ const child = (opts.spawnImpl || spawn)(process.execPath, ['-e', buildChildBootstrap()], {
166
+ cwd,
167
+ detached: true,
168
+ stdio,
169
+ windowsHide: true,
170
+ env,
171
+ });
172
+ child.unref?.();
173
+ } finally {
174
+ if (logFd !== null) {
175
+ try { closeSync(logFd); } catch { /* 父侧 fd 副本关闭;子进程持有继承副本 */ }
176
+ }
177
+ }
178
+ return { status: 'spawned', logPath };
179
+ }
180
+
181
+ /**
182
+ * 子侧入口(bootstrap 经 env 传参调起;env 注入默认值供测试覆盖)。
183
+ *
184
+ * 单飞循环:写锁(本 pid)→ 每轮清 rerunQueued 后 inline 跑一轮 triggerSync →
185
+ * 轮末查位,被置位且预算未尽则续轮(迟到状态收敛),否则清锁退出。
186
+ * 锁 pid 非本进程(双 spawn 竞态窗口被后写者接管)→ 让位退出防双跑。
187
+ */
188
+ export async function runBgSyncFromEnv(env = process.env) {
189
+ const changeName = env.SILLYSPEC_BG_SYNC_CHANGE;
190
+ const cwd = env.SILLYSPEC_BG_SYNC_CWD;
191
+ if (!changeName || !cwd) return;
192
+
193
+ const platformOpts = {};
194
+ if (env.SILLYSPEC_BG_SYNC_SPEC_ROOT) platformOpts.specRoot = env.SILLYSPEC_BG_SYNC_SPEC_ROOT;
195
+ if (env.SILLYSPEC_BG_SYNC_RUNTIME_ROOT) platformOpts.runtimeRoot = env.SILLYSPEC_BG_SYNC_RUNTIME_ROOT;
196
+
197
+ const { triggerSync, resolveRuntimeRoot, resolveSyncTotalTimeoutMs } = await import('./shared.js');
198
+ const runtimeRoot = resolveRuntimeRoot(platformOpts, platformOpts.specRoot || join(cwd, '.sillyspec'));
199
+ truncateLogIfNeeded(runtimeRoot);
200
+ const startedAt = Date.now();
201
+ writeBgSyncLock(runtimeRoot, { pid: process.pid, startedAt, change: changeName, rerunQueued: false });
202
+
203
+ const roundTimeoutMs = Math.max(resolveSyncTotalTimeoutMs(), BG_ROUND_FLOOR_MS);
204
+ const deadline = startedAt + BG_TOTAL_BUDGET_MS;
205
+ for (let round = 1; round <= BG_MAX_ROUNDS; round++) {
206
+ // 轮前清位:本轮 triggerSync 的树 hash 晚于清位时刻,覆盖此后到达的全部状态;
207
+ // 读→写间隙内的置位存在丢失窗口(毫秒级),由下一条命令的同步兜底收敛。
208
+ const cur = readBgSyncLock(runtimeRoot);
209
+ writeBgSyncLock(runtimeRoot, { pid: process.pid, startedAt: cur?.startedAt || startedAt, change: changeName, rerunQueued: false });
210
+ console.log(`[spec-sync-bg] 第 ${round}/${BG_MAX_ROUNDS} 轮开始 ${new Date().toISOString()}: ${changeName}`);
211
+ try {
212
+ await triggerSync(cwd, changeName, platformOpts, { inline: true, timeoutMs: roundTimeoutMs });
213
+ } catch (e) {
214
+ console.warn(`[spec-sync-bg] 本轮同步异常(best-effort,下一条命令自动重试): ${(e && e.message) || e}`);
215
+ }
216
+ const lock = readBgSyncLock(runtimeRoot);
217
+ if (!lock || lock.pid !== process.pid) {
218
+ console.warn('[spec-sync-bg] 锁已不属于本进程(被并行 spawn 接管),让位退出');
219
+ return;
220
+ }
221
+ if (!lock.rerunQueued) break;
222
+ if (Date.now() + roundTimeoutMs > deadline) {
223
+ console.warn('[spec-sync-bg] 总预算用尽,残留待同步状态由下一条命令的同步接管');
224
+ break;
225
+ }
226
+ }
227
+ console.log(`[spec-sync-bg] 后台同步完成 ${new Date().toISOString()}: ${changeName}`);
228
+ removeBgSyncLock(runtimeRoot);
229
+ }
230
+
231
+ export default { spawnBackgroundSync, runBgSyncFromEnv, readBgSyncLock, isBgSyncLockLive, isPidAlive };
@@ -30,11 +30,11 @@
30
30
  * (非被外层吞);process.exit 不可被 try 捕获 → 搬迁行为完全等价。
31
31
  */
32
32
  import { basename, dirname, join, resolve, relative, isAbsolute } from 'node:path'
33
- import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, rmSync } from 'node:fs'
33
+ import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, rmSync, renameSync } from 'node:fs'
34
34
  import { createHash } from 'node:crypto'
35
35
  import { renameSyncRetry, writeAtomicSync } from '../fs-atomic.js'
36
36
  import { gitQuiet } from '../git-helper.js'
37
- import { resolveChangeDir, resolveQuickSessionsDir, safeGit, auditQuickCompletion, triggerSync, isQuickMetadata, isQuicklogFileLineNoise, resolveRuntimeRoot, collectOtherQuickSessionDeclarations, mergeQuickBoundaryFiles } from './shared.js'
37
+ import { resolveChangeDir, resolveQuickSessionsDir, safeGit, auditQuickCompletion, triggerSync, isQuickMetadata, isQuicklogFileLineNoise, resolveRuntimeRoot, collectOtherQuickSessionDeclarations, mergeQuickBoundaryFiles, QUICK_SID_RE } from './shared.js'
38
38
  import { detectConcurrentChanges, formatConcurrentWarning, resolveConcurrentAnchor } from './concurrent-detect.js'
39
39
  import { stageRegistry } from '../stages/index.js'
40
40
  import { SCAN_STATUS, POINTER_STATUS } from '../constants.js'
@@ -1399,6 +1399,70 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
1399
1399
  progress.lastQuickReview = review
1400
1400
  }
1401
1401
 
1402
+ // ── quick 资产尾(2026-09-20-quick-asset-tail,D-001/D-002/D-005):门禁过后三件机械
1403
+ // 事,agent 零新增命令/零写作义务,fail-open 全链(任何异常 warn 不拦 quick 完成)。
1404
+ // ① 薄通道蒸馏尾:linked 真变更(非 quick-<hex>)→ decision-distill + fr-index(幂等)
1405
+ // + lite 归档(治「薄通道零资产 + 僵尸 brainstorm 态」——autocompact 对撞实证)。
1406
+ // 挂点在门禁后(D-002 时序:防未实现设计进索引)。
1407
+ // ② 模块 changelog 机械追加:changedFiles×moduleIndex join → 边车存在才 append 一行
1408
+ // (纯 quick 的检索面——QUICKLOG 是时间流水账,模块边车是空间索引)。
1409
+ // ③ 根因 classify 提示:--cause 原文 × INDEX 关键词命中 → 打一行确切命令(非空提示)。
1410
+ try {
1411
+ // linkedChangesAuto 排除(S2 执行审查缺陷③):auto-link 命中的他者中途/僵尸变更不走蒸馏
1412
+ // 尾(quick-single-change-auto-link §3 契约——即便 requirements.md 在场也不蒸馏不归档,
1413
+ // 防未实现设计进索引 + 误归档他者 brainstorm 进行中变更)
1414
+ const linkedChanges = Array.isArray(mergedGuard?.linkedChanges) ? mergedGuard.linkedChanges : []
1415
+ const autoLinked = new Set(Array.isArray(mergedGuard?.linkedChangesAuto) ? mergedGuard.linkedChangesAuto : [])
1416
+ const manualLinked = linkedChanges.filter((c) => c && !QUICK_SID_RE.test(c) && !autoLinked.has(c))
1417
+ const { distillLinkedChangeAssets } = await import('./complete-handlers.js')
1418
+ const tail = await distillLinkedChangeAssets({ pm, cwd, specBase, changeName, linkedChanges: manualLinked, platformOpts })
1419
+ for (const w of tail.warnings) console.warn(`⚠️ [资产尾] ${w}`)
1420
+ } catch (e) {
1421
+ console.warn(`⚠️ [资产尾] 蒸馏尾异常(fail-open 不拦 quick 完成):${e && e.message ? e.message : e}`)
1422
+ }
1423
+ try {
1424
+ // ② changelog 边车追加——projectName 同源修复(S2 执行审查缺陷①a):progress.project
1425
+ // (与 outputStep 占位符渲染同源,平台模式真实项目名)三级回退,不再硬编码 null。
1426
+ const projName = progress.project
1427
+ || steps[currentIdx].project
1428
+ || (steps[currentIdx].name.match(/\[([^\]]+)\]\s*$/) || [])[1]
1429
+ || null
1430
+ const { loadQuickModuleIndex } = await import('./shared.js')
1431
+ const moduleIndex2 = await loadQuickModuleIndex(specBase, projName)
1432
+ const changed2 = (progress.lastQuickReview?.changedFiles ?? []).map(f => String(f).replace(/\\/g, '/'))
1433
+ if (changed2.length > 0 && moduleIndex2 && projName) {
1434
+ const touchedMods = new Map()
1435
+ for (const [modId, mod] of Object.entries(moduleIndex2)) {
1436
+ const prefixes = [...(mod && Array.isArray(mod.paths) ? mod.paths : []), ...(mod && Array.isArray(mod.core_files) ? mod.core_files : [])]
1437
+ for (const raw of prefixes) {
1438
+ const p = String(raw).replace(/\\/g, '/').replace(/\/+$/, '')
1439
+ if (p && changed2.some(f => f === p || f.startsWith(p + '/'))) { touchedMods.set(modId, mod); break }
1440
+ }
1441
+ }
1442
+ // 修复①b:边车路径补 <project> 段——map 的 doc 字段相对 docs/<project>/(如 modules/x.md)
1443
+ const modulesDir = join(specBase, 'docs', projName)
1444
+ for (const [modId, mod] of touchedMods) {
1445
+ try {
1446
+ const docRel = mod && mod.doc
1447
+ if (!docRel) continue
1448
+ const docNorm = String(docRel).replace(/\\/g, '/')
1449
+ const sidecarRel = docNorm.replace(/\.md$/, '.changelog.md')
1450
+ const target = join(modulesDir, sidecarRel)
1451
+ if (!existsSync(target)) continue // 边车不存在静默跳过(建卡是 scan 职责)
1452
+ const line = `- ${changeName} | quick 机械留痕(${changed2.length} 文件触达 ${modId};详账见 QUICKLOG)`
1453
+ appendFileSync(target, line + '\n')
1454
+ console.log(`🗂️ 模块留痕:${modId} changelog 边车 +1 行(${changeName})`)
1455
+ } catch { /* 单模块失败不连坐 */ }
1456
+ }
1457
+ }
1458
+ } catch (e) {
1459
+ console.warn(`⚠️ [资产尾] changelog 追加异常(fail-open):${e && e.message ? e.message : e}`)
1460
+ }
1461
+ // ③ 根因 classify 提示——S2 复审缺陷②终裁:**删除本块**。既有「待归类提议」块
1462
+ // (下方 extractQuickCauseField → matchKnowledge → 确切 classify 命令)已完全承担此职责
1463
+ // 且传参/qlId/文件路径全正确(1836-1844);本块首版传对象给字符串参数(TypeError 被吞)
1464
+ // 二版仍是重复建设——机械件③的 classify 职责由既有块唯一承担,本变更不再另造。
1465
+
1402
1466
  // task-02 并发预检(FR-05/FR-07,纯副作用 advisory):auditQuickCompletion 返回后、推进前
1403
1467
  // 扫工作树,识别他者未提交改动 / 他者脏变更目录,有则 console.warn。不改 status/gate、
1404
1468
  // 不 exit、不 return early(FR-07 铁律)。ownFiles 必含 baselineFiles(D-001:脏工作树 quick
@@ -1897,6 +1961,15 @@ async function closeSingleQuickLinkedChange({ pm, cwd, specBase, changeName, pla
1897
1961
  * 缺陷①):current_stage=brainstorm 但该阶段 status=completed(brainstorm 收尾到 plan
1898
1962
  * 开始之间的空窗)≠ 僵尸变更——此时关联 quick --done 会把即将进 plan 的进行中变更误
1899
1963
  * 轻量归档。stage_status=completed 一律走原流程收尾。
1964
+ *
1965
+ * 转轨放行(2026-09-20 僵尸状态实证缺口):缺陷①②两闸把 d192f89 的原始目标场景——
1966
+ * brainstorm 精判 scale:small、设计裁定实现路径即 `quick --linked-changes <本变更>` 的
1967
+ * 刻意转轨变更——也拦成了永真(brainstorm --done 必然置 completed、转轨间隔天然分钟级
1968
+ * 必中时近性闸),linked quick --done 后变更永留 active/brainstorm 成僵尸。识别信号用
1969
+ * brainstorm 末步已落盘的机器可读产物:current_stage=brainstorm + stage_status=completed
1970
+ * + design.md frontmatter scale:small(readDesignScale,gates.js)三条件齐 = 转轨变更,
1971
+ * 放行两闸且 tasks 判定按 quick 语义(无 tasks.md=无待办);scale=large/未写 scale 的
1972
+ * 在途变更两闸原样生效(缺陷①②防护面不变)。
1900
1973
  */
1901
1974
  const QUICK_CLOSE_ALLOWED_STAGES = new Set(['', 'scan', 'brainstorm'])
1902
1975
 
@@ -1913,6 +1986,10 @@ const QUICK_CLOSE_ACTIVITY_WINDOW_MS = 60 * 60 * 1000
1913
1986
  * scan/brainstorm 才继续 tasks 判定;plan/execute/verify/archive 一律 skip 走原流程收尾。
1914
1987
  * pm 缺 getChangeStage 接口或查询抛错同样 skip,不静默放行。
1915
1988
  *
1989
+ * 转轨放行:current_stage=brainstorm + stage_status=completed + design.md scale:small =
1990
+ * brainstorm 精判裁定走 quick 的刻意转轨变更(见 QUICK_CLOSE_ALLOWED_STAGES 上注释),
1991
+ * 豁免「阶段完成态」「时近性」两闸与「无 tasks.md」判定,linked quick --done 即收尾。
1992
+ *
1916
1993
  * @param {Object} opts
1917
1994
  * @param {ProgressManager} opts.pm
1918
1995
  * @param {string} opts.cwd
@@ -1952,13 +2029,25 @@ export async function closeQuickLinkedChanges({ pm, cwd, specBase, linkedChanges
1952
2029
  })
1953
2030
  continue
1954
2031
  }
2032
+ // 刻意转轨判定(见 QUICK_CLOSE_ALLOWED_STAGES 上注释「转轨放行」段):scale:small 是
2033
+ // brainstorm 末步写进 design.md frontmatter 的精判产物(stages/brainstorm.js「生成规范
2034
+ // 文件」步),其设计实现路径就是本 quick 会话——linked quick --done 即本变更流程收尾点。
2035
+ // readDesignScale 动态导入破环(gates.js 静态导入本模块,其 :1175 反向动态 import 先例);
2036
+ // 导入/执行异常 fail-safe 按「非转轨」走原闸,不废既有护栏。
2037
+ let quickHandoff = false
2038
+ if (stageInfo !== null && stageInfo.current_stage === 'brainstorm' && stageInfo.stage_status === 'completed') {
2039
+ try {
2040
+ const { readDesignScale } = await import('./gates.js')
2041
+ quickHandoff = readDesignScale(specBase, changeName) === 'small'
2042
+ } catch { /* fail-safe:识别不了按非转轨走原闸 */ }
2043
+ }
1955
2044
  // ql-20260819-010(quick-done-autoarchive-misfire 缺陷①):当前阶段已完成 ≠
1956
2045
  // 从未进入完整流程的僵尸。brainstorm 收尾到 plan 开始之间的空窗里 current_stage
1957
2046
  // 仍读 brainstorm,只看阶段名会把即将进 plan 的变更误轻量归档(propose 骨架
1958
2047
  // tasks.md 除 quick 追加的 ql 行外没有任务行,「无未勾选框=全勾」恒真)。
1959
2048
  // stage_status=completed 一律原流程收尾;null(无阶段行/brownfield)与旧调用方
1960
2049
  // mock 缺字段(undefined)均按未完成放行,僵尸逃生通道行为不变。
1961
- if (stageInfo !== null && stageInfo.stage_status === 'completed') {
2050
+ if (!quickHandoff && stageInfo !== null && stageInfo.stage_status === 'completed') {
1962
2051
  skipped.push({
1963
2052
  name: changeName,
1964
2053
  reason: `变更当前阶段「${stageInfo.current_stage || '(空)'}」已完成(推进/收尾中,非僵尸变更),不自动归档——请走原流程收尾(sillyspec progress show 查看进度)`,
@@ -1974,7 +2063,10 @@ export async function closeQuickLinkedChanges({ pm, cwd, specBase, linkedChanges
1974
2063
  //(既有「真·僵尸 in_progress→closed」行为保留)。pm 无此方法(旧 mock/旧进度库)
1975
2064
  // 按无近期活动放行——与 getChangeStage 缺失 skip 的 fail-safe 不同向:本闸误放行的
1976
2065
  // 最坏后果是回到缺陷②现状(有 D-002 防护兜底),误拦截则僵尸永不清——权衡取放行。
1977
- if (typeof pm.getLatestActivityAt === 'function') {
2066
+ // (转轨变更例外:brainstorm --done → quick 启动/--done 间隔天然分钟级,时近性对它
2067
+ // 是永真拦截信号——quickHandoff 已用 scale:small 坐实「等 quick 落地」,不在「在途」
2068
+ // 语义面内,放行。)
2069
+ if (!quickHandoff && typeof pm.getLatestActivityAt === 'function') {
1978
2070
  const latest = pm.getLatestActivityAt(cwd, changeName)
1979
2071
  if (latest && Date.now() - new Date(latest).getTime() < QUICK_CLOSE_ACTIVITY_WINDOW_MS) {
1980
2072
  skipped.push({
@@ -1984,7 +2076,13 @@ export async function closeQuickLinkedChanges({ pm, cwd, specBase, linkedChanges
1984
2076
  continue
1985
2077
  }
1986
2078
  }
1987
- if (!isChangeTasksComplete(specBase, changeName)) {
2079
+ // tasks 判定(转轨例外):scale=small 按 brainstorm 末步约定不生成 tasks.md
2080
+ //(「proposal/requirements/tasks 对 quick 无用」)——转轨变更无 tasks.md = 无待办
2081
+ // 放行;文件存在(propose 骨架等)仍按全勾判定,未勾任务行照拦。
2082
+ const tasksComplete = quickHandoff && !existsSync(join(specBase, 'changes', changeName, 'tasks.md'))
2083
+ ? true
2084
+ : isChangeTasksComplete(specBase, changeName)
2085
+ if (!tasksComplete) {
1988
2086
  skipped.push({ name: changeName, reason: 'tasks.md 未全勾选或不存在' })
1989
2087
  continue
1990
2088
  }
@@ -2733,3 +2831,108 @@ async function runArchiveCeremonyDualRunExit({ cwd, specBase, changeName, platfo
2733
2831
  }
2734
2832
  }
2735
2833
 
2834
+
2835
+ // ── quick 资产尾(2026-09-20-quick-asset-tail,D-001/D-002/D-004)─────────────────
2836
+
2837
+ /**
2838
+ * lite 归档(D-004):薄通道(brainstorm→linked quick)变更的轻量收口——所有权 assert →
2839
+ * 命名 → rename → unregisterChange。不复用 archiveChangeDirectory 的 9 处重门(未 apply
2840
+ * 交付面等 worktree 导向,linked quick 变更无 worktree 语义会被误拦);重件
2841
+ * (module-impact/ROADMAP/delta)明确豁免。自愈:源目录缺失但已在 archive/ 时补
2842
+ * unregister 不留永久 skipped(与重路径 586-604 同款语义)。
2843
+ * @returns {Promise<{archivedTo: string}|{skipped: string}>}
2844
+ */
2845
+ export async function liteArchiveChange({ pm, cwd, specBase, changeName, platformOpts = {} }) {
2846
+ const changesDir = join(specBase, 'changes')
2847
+ const archiveDir = join(changesDir, 'archive')
2848
+ const srcDir = join(changesDir, changeName)
2849
+ // 所有权(同重路径语义:他人活跃拒;self/stale/forced 放行并接管)
2850
+ const { session } = resolveSessionIdentity({ flagSession: platformOpts.sessionFlag || null, cwd })
2851
+ const check = pm.assertChangeOwnership(cwd, changeName, { selfSession: session, nowMs: Date.now() })
2852
+ if (!check.allowed) return { skipped: `所有权拒(owner=${check.owner},活跃窗内)` }
2853
+ if (check.action !== 'self') pm.setChangeOwner(cwd, changeName, session)
2854
+ // 自愈:目录已被手动/前次部分流程移到 archive/ → 只补 unregister(不留永久 desync)
2855
+ if (!existsSync(srcDir)) {
2856
+ // lite 变更目录常无 plan.md(findAlreadyArchivedDir 精确命中的硬条件)——本场景放宽:
2857
+ // archive/ 下有以变更名为末段的目录且含 requirements.md 或 decisions.md 即认自愈
2858
+ const already = (() => {
2859
+ try {
2860
+ const entries = readdirSync(archiveDir, { withFileTypes: true }).filter(e => e.isDirectory()).map(e => e.name)
2861
+ const hit = entries.find(e => (e === changeName || e.endsWith('-' + changeName)) &&
2862
+ (existsSync(join(archiveDir, e, 'requirements.md')) || existsSync(join(archiveDir, e, 'decisions.md'))))
2863
+ return hit ? join(archiveDir, hit) : null
2864
+ } catch { return null }
2865
+ })()
2866
+ if (already) {
2867
+ pm.unregisterChange(cwd, changeName, { archiveStepNames: typeof pm.archiveStepNamesForArchive === 'function' ? pm.archiveStepNamesForArchive() : null })
2868
+ console.log(`📦 lite 自愈归档:${changeName} 已在 archive/(${basename(already)}),补注销 DB 行`)
2869
+ return { archivedTo: already }
2870
+ }
2871
+ return { skipped: `源目录不存在且 archive/ 无此变更(requirements/decisions 均缺的空关联)` }
2872
+ }
2873
+ const destName = archiveDestDirName(new Date().toISOString().slice(0, 10), changeName)
2874
+ const destDir = join(archiveDir, destName)
2875
+ mkdirSync(archiveDir, { recursive: true })
2876
+ renameSync(srcDir, destDir) // 先 rename 后 unregister:失败时 change 留 active 可重试(D-004 顺序保证)
2877
+ pm.unregisterChange(cwd, changeName, { archiveStepNames: typeof pm.archiveStepNamesForArchive === 'function' ? pm.archiveStepNamesForArchive() : null })
2878
+ console.log(`📦 lite 归档:${changeName} → archive/${destName}/(薄通道轻收口:蒸馏已先行,module-impact/ROADMAP 豁免)`)
2879
+ return { archivedTo: destDir }
2880
+ }
2881
+
2882
+ /**
2883
+ * 薄通道蒸馏尾(D-001/D-002):quick --done 门禁过后对 linked 真变更(非 quick-<hex>)跑
2884
+ * decision-distill + fr-index(均幂等)→ lite 归档。挂点在质量闸后(D-002:防未实现设计进
2885
+ * 索引——quick 中途废弃永不 --done 永不蒸馏);两文件均缺失零打扰跳过;fail-open 全链
2886
+ * (任何异常 warn 留痕不拦 quick 完成)。
2887
+ * @returns {Promise<{distilled: boolean, frCount: number, decisionCount: number, archived: boolean, warnings: string[]}>}
2888
+ */
2889
+ export async function distillLinkedChangeAssets({ pm, cwd, specBase, changeName, linkedChanges = [], platformOpts = {} }) {
2890
+ const out = { distilled: false, frCount: 0, decisionCount: 0, archived: false, warnings: [] }
2891
+ const realChanges = (linkedChanges || []).filter((c) => c && !QUICK_SID_RE.test(c))
2892
+ if (realChanges.length === 0) return out // 纯 quick:无关联真变更,零打扰
2893
+ try {
2894
+ const knowledgeRoot = join(specBase, 'knowledge')
2895
+ let anyDistilled = false
2896
+ for (const linked of realChanges) {
2897
+ const changeDir = join(specBase, 'changes', linked)
2898
+ if (!existsSync(changeDir)) { out.warnings.push(`linked 变更目录缺失:${linked}`); continue }
2899
+ const hasReqs = existsSync(join(changeDir, 'requirements.md'))
2900
+ const hasDecisions = existsSync(join(changeDir, 'decisions.md'))
2901
+ if (!hasReqs && !hasDecisions) continue // quick/scale:small 无索引义务(fr-index 既有语义对齐)
2902
+ // 决策蒸馏(decisions.md → knowledge/decisions/,幂等;无文件内部自跳过)
2903
+ if (hasDecisions) {
2904
+ try {
2905
+ const { distillIntoKnowledge } = await import('../decision-distill.js')
2906
+ const r = distillIntoKnowledge(changeDir, knowledgeRoot, '')
2907
+ const writtenCount = r && Array.isArray(r.written) ? r.written.length : 0
2908
+ if (writtenCount > 0) { out.decisionCount += writtenCount; anyDistilled = true }
2909
+ } catch (e) { out.warnings.push(`decision-distill ${linked} 异常跳过:${e && e.message ? e.message : e}`) }
2910
+ }
2911
+ // FR 索引(requirements.md → knowledge/fr/,幂等:同变更名 no-op)
2912
+ if (hasReqs) {
2913
+ try {
2914
+ const { indexRequirements } = await import('../fr-index.js')
2915
+ const r = indexRequirements({ changeDir, knowledgeRoot, headHash: '' })
2916
+ if (r && Array.isArray(r.written) && r.written.length > 0) { out.frCount += r.written.length; anyDistilled = true }
2917
+ } catch (e) { out.warnings.push(`fr-index ${linked} 异常跳过:${e && e.message ? e.message : e}`) }
2918
+ }
2919
+ }
2920
+ out.distilled = anyDistilled
2921
+ // lite 归档(蒸馏后;仅四件套容器变更——requirements.md 在场=brainstorm 承接的薄通道形态;
2922
+ // 纯信号自动关联的 proposal-only 变更不归档〔quick-single-change-auto-link §3 契约钉死〕)
2923
+ for (const linked of realChanges) {
2924
+ if (!existsSync(join(specBase, 'changes', linked, 'requirements.md'))) continue
2925
+ try {
2926
+ const r = await liteArchiveChange({ pm, cwd, specBase, changeName: linked, platformOpts })
2927
+ if (r.archivedTo) out.archived = true
2928
+ else if (r.skipped) out.warnings.push(`lite 归档 ${linked}:${r.skipped}`)
2929
+ } catch (e) { out.warnings.push(`lite 归档 ${linked} 异常跳过:${e && e.message ? e.message : e}`) }
2930
+ }
2931
+ if (anyDistilled || out.archived) {
2932
+ console.log(`📚 quick 资产尾:${out.frCount} 条 FR 入索引、${out.decisionCount} 条决策入 knowledge${out.archived ? '、linked 变更 lite 归档' : ''}(门禁后自动,agent 零新增命令)`)
2933
+ }
2934
+ } catch (e) {
2935
+ out.warnings.push(`蒸馏尾异常(fail-open 不拦 quick 完成):${e && e.message ? e.message : e}`)
2936
+ }
2937
+ return out
2938
+ }
package/src/run/gates.js CHANGED
@@ -1905,22 +1905,27 @@ export function writeReconcileRunResult({ runtimeRoot, changeName, envelope, res
1905
1905
  */
1906
1906
  /**
1907
1907
  * 打印跨仓 per-repo 对账结果(cross-repo-reconcile.js 产出的 advisory 明细,verify 块接线)。
1908
- * 恒不阻断(返回 void):跨仓 diff 锚点=各仓最近提交窗口,锚点脆弱期(已 commit 多笔/全在
1909
- * working-tree)会产②类假信号——明细供人工裁决,不翻主仓对账状态。
1908
+ * 恒不阻断(返回 void):锚点档逐行携带(内核分级:A reviews 锡点区间 > B 最近提交窗口 >
1909
+ * C 未提交窗口,anchor.label 透传),锚点脆弱期(已 commit 多笔/全在 working-tree)会产②类
1910
+ * 假信号——明细供人工裁决,不翻主仓对账状态。
1910
1911
  * @param {Array<{repo:string,repoPath:string|null,declaredCount:number,actualCount:number,
1911
1912
  * matched:string[],missing:Array<{task:string,path:string,isNew?:boolean}>,
1912
- * undeclared:string[],scaffoldCount:number,degradedReason:string|null}>|undefined} crossRepo
1913
+ * undeclared:string[],scaffoldCount:number,degradedReason:string|null,
1914
+ * anchor?:{source:string,base:string|null,head:string|null,label:string}}>|undefined} crossRepo
1913
1915
  */
1914
1916
  function printCrossRepoReconcile(crossRepo) {
1915
1917
  if (!Array.isArray(crossRepo) || crossRepo.length === 0) return
1916
- console.log(`\n🔀 跨仓 per-repo 对账(advisory 不阻断,锚点=各仓最近提交窗口):`)
1918
+ console.log(`\n🔀 跨仓 per-repo 对账(advisory 不阻断):`)
1917
1919
  for (const r of crossRepo) {
1918
1920
  if (r.degradedReason) {
1919
1921
  console.warn(` ⚠️ ${r.repo}:${r.degradedReason}`)
1920
1922
  continue
1921
1923
  }
1922
1924
  const flag = (r.missing.length > 0 || r.undeclared.length > 0) ? '⚠️ ' : '✅ '
1923
- console.log(` ${flag}${r.repo}(${r.repoPath}):声明 ${r.declaredCount} / 实测 ${r.actualCount} / 对上 ${r.matched.length} / ②缺 ${r.missing.length} / ③多 ${r.undeclared.length}${r.scaffoldCount > 0 ? ` / 脚手架聚合 ${r.scaffoldCount}` : ''}`)
1925
+ // 锚点档标签(task-03):有 anchor.label 时追加(A/B/C 档人类可读描述);无 anchor(旧形态)
1926
+ // 零输出——逐字兼容
1927
+ const anchorTag = r.anchor && typeof r.anchor.label === 'string' && r.anchor.label ? `|锚点 ${r.anchor.label}` : ''
1928
+ console.log(` ${flag}${r.repo}(${r.repoPath}):声明 ${r.declaredCount} / 实测 ${r.actualCount} / 对上 ${r.matched.length} / ②缺 ${r.missing.length} / ③多 ${r.undeclared.length}${r.scaffoldCount > 0 ? ` / 脚手架聚合 ${r.scaffoldCount}` : ''}${anchorTag}`)
1924
1929
  for (const m of r.missing.slice(0, 10)) {
1925
1930
  console.warn(` ②缺 ${m.task}: ${m.path}${m.isNew ? '(NEW: 声明新建,该仓未见)' : ''}——锚点窗口外已提交或多笔时可能假信号,到该仓 git log 核实`)
1926
1931
  }
package/src/run/prompt.js CHANGED
@@ -1151,7 +1151,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1151
1151
  if (entries.length === 0) {
1152
1152
  promptText = promptText.replace(/\{FR_INDEX_DIGEST\}/g, '(触达域暂无 active FR 索引条目——本变更大概率是这些域的首批需求,照常写作)')
1153
1153
  } else {
1154
- const lines = entries.map((e) => `- ${e.id} ${e.title}(来源 ${e.change}${e.scenarios.length ? ';场景:' + e.scenarios.slice(0, 3).join(',') : ''}${(e.decisions || []).length ? ';依据:' + e.decisions.slice(0, 3).join('、') : ''})`)
1154
+ const lines = entries.map((e) => `- ${e.id} ${e.title}(来源 ${e.change}${e.scenarios.length ? ';场景:' + e.scenarios.slice(0, 3).join(',') : ''}${(e.decisions || []).length ? ';依据:' + e.decisions.slice(0, 3).join('、') : ''})${e.needsReview ? ` ⚠️ 待复核(${e.needsReview})——该 FR 覆盖的代码近期被 quick 触达,行为可能已变;本变更若触及同域先核对现状再决定承接/新写` : ''}`)
1155
1155
  lines.push('')
1156
1156
  lines.push('> 域解析自本变更 design.md 文件清单(漏域先核对清单)。改写/取代已有行为 → 对应 FR 块加承接行;新行为 → 新 FR 块。superseded 条目默认不列(历史回溯自行读 knowledge/fr/)。依据决策(L2)= 当年取舍锚——翻案须先读 knowledge/decisions/<域>.md 的否决理由,满足复潮条件走 D-xxx@vN+1,不得静默改行为。承接行可带退役理由:`承接: FR-<域>-NNN(退役理由:一句话)`——归档时写进被取代条目(理由内禁逗号);条目正文是截断摘要,全文锚(全文:<归档路径>#FR-NN)由 CLI 自动落。')
1157
1157
  promptText = promptText.replace(/\{FR_INDEX_DIGEST\}/g, lines.join('\n'))
package/src/run/shared.js CHANGED
@@ -854,6 +854,28 @@ async function raceWithAbort(op, timeoutMs = resolveSyncTotalTimeoutMs()) {
854
854
  export const QUICK_SID_RE = /^quick-[0-9a-f]{8}$/
855
855
 
856
856
  export async function triggerSync(cwd, changeName, platformOpts = {}, opts = {}) {
857
+ // ── 后台异步化(2026-09-20 用户反馈:--done 被网络尾巴拖住分钟级)──
858
+ // 默认把同步整体挪进 detached 后台子进程:17 个调用点全部 fire-and-forget,但进程
859
+ // 退出要等事件循环排空,在飞 fetch 拖住命令返回。回落 inline 的三条件:
860
+ // opts.inline(bg 子进程自身回环调用/测试断言进程内行为)
861
+ // env SILLYSPEC_SYNC_BG=0(逃生阀,回退旧行为)
862
+ // env SILLYSPEC_BG_SYNC_CWD 在场(本进程已是后台子进程,防 fork 链)
863
+ // spawn 决策(未连接预判/单飞锁/rerunQueued 合并/日志)见 bg-sync.js;bg 机制任何
864
+ // 异常降级 inline,绝不因后台化阻断主流程。
865
+ if (!opts.inline && changeName
866
+ && process.env.SILLYSPEC_SYNC_BG !== '0'
867
+ && !process.env.SILLYSPEC_BG_SYNC_CWD) {
868
+ try {
869
+ const bg = await import('./bg-sync.js')
870
+ const r = await bg.spawnBackgroundSync(cwd, changeName, platformOpts, opts)
871
+ if (r.status === 'spawned') {
872
+ console.log(`🔄 [spec-sync] 平台同步转后台执行(不阻塞本命令返回;输出与结果见 ${r.logPath})`)
873
+ return
874
+ }
875
+ if (r.status === 'coalesced') return // 活后台轮在跑,本轮状态已并入其下一轮
876
+ // not-connected → 落回 inline(内部静默 no-op,与旧行为一致)
877
+ } catch { /* 后台化失败降级 inline(best-effort 契约不变) */ }
878
+ }
857
879
  // 平台模式不再整体跳过(2026-08-26 用户决策):上行回传(进度 + 四件套 + spec 树)
858
880
  // 在平台模式同样执行,凭据经 env SILLYHUB_PLATFORM_URL/TOKEN(预留通道,daemon 未
859
881
  // 实现注入——需外部显式 export,sync.js _getPlatform,链路 D 同款)或 local.yaml
@@ -1815,6 +1837,16 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
1815
1837
  type: 'fr-rot-suspect', change: guard.changeName || null,
1816
1838
  domains: [...touchedDomains], count: frs.length, source: 'quick-done',
1817
1839
  })
1840
+ // 地图诚实升级(2026-09-20-quick-asset-tail FR-02/D-003):遥测之外给条目打
1841
+ // 待复核标记——下一次 brainstorm 注入行带 ⚠️,承接翻链时清除。fail-open 同块。
1842
+ const markRef = guard.changeName || 'recent-quick'
1843
+ try {
1844
+ const { markFrNeedsReview } = await import('../fr-index.js')
1845
+ const mr = markFrNeedsReview(join(specBase, 'knowledge'), frs.map(f => f.id), markRef)
1846
+ if (mr.marked > 0) {
1847
+ console.warn(`⚠️ [FR 待复核标记] ${mr.marked} 条 active FR 已标「待复核:${markRef}」——下次 brainstorm 注入可见,承接翻链时自动清除`)
1848
+ }
1849
+ } catch { /* 标记失败不影响遥测/审计 */ }
1818
1850
  console.warn(`⚠️ [FR 腐烂 suspect·advisory] 本次 quick 触达 ${[...touchedDomains].join('、')} 域的 ${frs.length} 条 active FR——quick 不走归档,若改动影响这些行为,索引不会自动 supersede(L3 声明义务;盲区:unmapped 文件/map 漂移不报警)`)
1819
1851
  }
1820
1852
  }