sillyspec 3.32.8 → 3.32.10

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 (63) hide show
  1. package/.claude/skills/sillyspec-brainstorm/SKILL.md +2 -2
  2. package/.claude/skills/sillyspec-execute/SKILL.md +2 -2
  3. package/.claude/skills/sillyspec-flow/SKILL.md +1 -1
  4. package/.claude/skills/sillyspec-plan/SKILL.md +2 -2
  5. package/AGENTS.md +5 -4
  6. package/assets/command-cards/flow.md +9 -4
  7. package/package.json +1 -1
  8. package/src/change-list.js +13 -0
  9. package/src/commit-guard.js +70 -2
  10. package/src/commit-suggest.js +45 -4
  11. package/src/config-schema.js +14 -0
  12. package/src/cross-repo-reconcile.js +54 -1
  13. package/src/decision-distill.js +77 -21
  14. package/src/decision-renumber.js +157 -0
  15. package/src/deleted-ledger.js +75 -0
  16. package/src/design-facts.js +5 -3
  17. package/src/dispatch/backends/local-agent.js +1 -1
  18. package/src/dispatch/backends/sillyhub-mcp.js +1 -1
  19. package/src/docs-check.js +30 -15
  20. package/src/docs-debt.js +3 -2
  21. package/src/doctor-diagnostics.js +3 -1
  22. package/src/flow-draft.js +24 -2
  23. package/src/flow-parity.js +32 -2
  24. package/src/flow.js +427 -69
  25. package/src/fr-index.js +15 -1
  26. package/src/hooks/worktree-guard.js +3 -3
  27. package/src/index.js +22 -10
  28. package/src/knowledge-digest.js +23 -6
  29. package/src/knowledge-graph.js +92 -14
  30. package/src/knowledge-match.js +34 -9
  31. package/src/knowledge-stats.js +12 -13
  32. package/src/knowledge-vector.js +98 -7
  33. package/src/machine-interface.js +26 -0
  34. package/src/progress/change-registry.js +7 -0
  35. package/src/progress.js +67 -0
  36. package/src/quicklog.js +6 -1
  37. package/src/redomain.js +27 -0
  38. package/src/route-hindsight.js +34 -2
  39. package/src/run/archive-distill.js +3 -0
  40. package/src/run/command.js +40 -44
  41. package/src/run/complete-handlers.js +8 -3
  42. package/src/run/complete.js +129 -63
  43. package/src/run/gates.js +94 -90
  44. package/src/run/prompt.js +13 -3
  45. package/src/run/shared.js +19 -0
  46. package/src/run/stage.js +1 -1
  47. package/src/scope-audit.js +2 -2
  48. package/src/sentinel-assertions.js +43 -1
  49. package/src/stage-contract.js +26 -3
  50. package/src/stage-review.js +44 -11
  51. package/src/stage-templates.js +2 -0
  52. package/src/stages/brainstorm.js +24 -12
  53. package/src/stages/execute.js +1 -1
  54. package/src/stages/explore.js +3 -5
  55. package/src/stages/knowledge.js +56 -5
  56. package/src/sync.js +98 -2
  57. package/src/task-tick.js +12 -0
  58. package/src/timeline.js +60 -19
  59. package/src/verify-postcheck.js +83 -6
  60. package/src/verify-probes.js +61 -18
  61. package/templates/agents-instruction.md +5 -4
  62. package/templates/prompts/testcase-design.md +5 -0
  63. package/src/stages/brainstorm-auto.js +0 -286
@@ -18,20 +18,65 @@
18
18
  * "change": "2026-09-26-thin-agent-tasks", "score": 0.83 } ] }
19
19
  * anchor = 决策条目 id(## D-xxx@vN);change = 条目「变更:」字段——同号条目跨变更常见
20
20
  * (runtime.md 七个 D-001@v1 先例),change 在场时消歧;缺省时同号条目全量带回(候选诚实)。
21
+ * epoch 注记(2026-10-14-decision-global-id-epoch):新蒸馏条目 anchor 为全局号形态
22
+ * D-<域>-NNN@vN——等值匹配语义不变;平台重建索引窗口内向量召回对新形态条目降级
23
+ * (本地词面回退不受影响),重索引后恢复。
21
24
  *
22
25
  * 降级纪律:任何失败(未连接/开关关闭/网络/404/5xx/超时/响应形态不符)静默降级本地层
23
26
  * ——检索是 advisory 面且高频(每方案步 --done),失败刷屏即狼来了;debug 排障可设
24
27
  * SILLYSPEC_KNOWLEDGE_VECTOR_DEBUG=1 打单行stderr。
28
+ *
29
+ * 开关契约(2026-10-15-vector-capability-autodetect 终态;前身 vector-gate-tighten 缺省关是
30
+ * 「端点不存在+无发现机制」时代的正确补丁,平台端点落地后 auto 成为诚实缺省):
31
+ * 缺省(未写键/auto)跟随平台能力自动探测——能力缓存 .runtime/vector-capability.json
32
+ * (404→unavailable 重试窗 1h 内跳过;成功→available;按平台 URL 匹配;跨进程持久),
33
+ * 平台部署端点后最迟 1h 全仓自动启用,消费仓零配置。local.yaml on=强制开跳过探测、
34
+ * off=强制关(用户逃生阀);env SILLYSPEC_KNOWLEDGE_VECTOR 同语义强制。
35
+ * 404 另有进程级死信短路(端点死信不再空转探测)。
25
36
  */
26
- import { readFileSync } from 'fs'
37
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
27
38
  import { join, dirname } from 'path'
28
39
  import { readPlatformConfig } from './sync.js'
29
40
  import { matchByRouting, fallbackByQueryShingles, parseDecisionEntries } from './knowledge-match.js'
30
41
 
31
42
  const VECTOR_TIMEOUT_MS = 3000
32
43
  const ENDPOINT_PATH = '/api/spec/knowledge/vector-search'
44
+ /** 能力缓存重试窗:unavailable 记录 1h 内不再探测(平台部署后最迟 1h 自动启用)。 */
45
+ const CAPABILITY_RETRY_MS = 60 * 60 * 1000
46
+
47
+ // ── 平台能力探测缓存(2026-10-15-vector-capability-autodetect)────────────────
48
+ // .runtime/ 已 gitignored 且被 daemon spec 上传排除(UPLOAD_EXCLUDE_TOP_BASE),是仓内
49
+ // 机器态的标准落点(knowledge-hits 同款)。写盘全 fail-soft;按平台 URL 匹配(换平台即失效)。
50
+
51
+ function capabilityCachePath(cwd) {
52
+ return join(cwd, '.sillyspec', '.runtime', 'vector-capability.json')
53
+ }
54
+
55
+ function readCapabilityCache(cwd) {
56
+ try {
57
+ const obj = JSON.parse(readFileSync(capabilityCachePath(cwd), 'utf-8'))
58
+ const cfg = readPlatformConfig(cwd)
59
+ if (!cfg || !cfg.url) return null
60
+ if (obj.url !== String(cfg.url).replace(/\/+$/, '')) return null // 换平台 → 缓存失效
61
+ if (typeof obj.available !== 'boolean' || typeof obj.probedAt !== 'number') return null
62
+ return { available: obj.available, probedAt: obj.probedAt }
63
+ } catch { /* 无缓存/坏 JSON = 未探测 */ }
64
+ return null
65
+ }
66
+
67
+ function writeCapabilityCache(cwd, platformUrl, available) {
68
+ try {
69
+ const p = capabilityCachePath(cwd)
70
+ const cur = readCapabilityCache(cwd)
71
+ if (cur && cur.available === available && Date.now() - cur.probedAt < CAPABILITY_RETRY_MS) return // 值未变且未过期:零写盘
72
+ const dir = join(p, '..')
73
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
74
+ writeFileSync(p, JSON.stringify({ url: platformUrl, available, probedAt: Date.now() }), 'utf-8')
75
+ } catch { /* 缓存写失败不影响检索本体(fail-soft) */ }
76
+ }
33
77
 
34
- /** local.yaml knowledge.vector_search 开关(行级扫描 knowledge: 段下 vector_search: off)。 */
78
+ /** local.yaml knowledge.vector_search 开关(行级扫描 knowledge: 段下 vector_search 值;
79
+ * 未写键/无 yaml → null=未表态——2026-10-15-vector-gate-tighten 起缺省关,显式落盘才启用)。 */
35
80
  function vectorSwitchFromLocalYaml(cwd) {
36
81
  try {
37
82
  const lines = readFileSync(join(cwd, '.sillyspec', 'local.yaml'), 'utf-8').split(/\r?\n/)
@@ -43,16 +88,55 @@ function vectorSwitchFromLocalYaml(cwd) {
43
88
  const kv = line.match(/^\s+vector_search\s*:\s*(\S+)/)
44
89
  if (kv) return kv[1].toLowerCase()
45
90
  }
46
- } catch { /* 无 local.yaml = 缺省 auto */ }
47
- return 'auto'
91
+ } catch { /* 无 local.yaml = 未表态 */ }
92
+ return null
48
93
  }
49
94
 
95
+ /**
96
+ * 向量层开关(2026-10-15-vector-capability-autodetect 终态契约;前身 2026-10-15-vector-gate-tighten
97
+ * 缺省关是「端点不存在+无发现机制」时代的正确补丁,端点落地后 auto 成为诚实缺省):
98
+ * env SILLYSPEC_KNOWLEDGE_VECTOR=on|auto → 强制启用;off(其他值保守视为 off)→ 强制关;
99
+ * local.yaml knowledge.vector_search: on → 强制启用;off → 强制关(用户显式逃生阀);
100
+ * auto/未写键 → **跟随平台能力自动探测**:读能力缓存(.runtime/vector-capability.json,
101
+ * {url,available,probedAt})——unavailable 未过重试窗(1h)=关,否则乐观试(探测结果由
102
+ * platformVectorRecall 落缓存,404→unavailable / 成功→available;跨进程持久)。
103
+ * 「端点部署本身就是开关」——平台部署后最迟 1h 内全仓自动启用,消费仓零配置。
104
+ */
50
105
  function vectorEnabled(cwd) {
51
106
  const env = String(process.env.SILLYSPEC_KNOWLEDGE_VECTOR || '').toLowerCase()
52
- if (env === 'off' || env === 'auto') return env === 'auto'
53
- return vectorSwitchFromLocalYaml(cwd) !== 'off'
107
+ if (env === 'on' || env === 'auto') return true
108
+ if (env) return false
109
+ const sw = vectorSwitchFromLocalYaml(cwd)
110
+ if (sw === 'off') return false
111
+ if (sw === 'on') return true
112
+ // absent/auto → 能力缓存判定(无平台配置=没得探测=关)
113
+ if (!readPlatformConfig(cwd)) return false
114
+ const cap = readCapabilityCache(cwd)
115
+ if (cap && !cap.available && Date.now() - cap.probedAt < CAPABILITY_RETRY_MS) return false
116
+ return true
54
117
  }
55
118
 
119
+ /**
120
+ * 向量能力提示(2026-10-15-vector-capability-autodetect 语义随迁;前身 vector-switch-discoverability
121
+ * 的「未开请配置」提示随缺省 auto 退役——auto 模式下没有可执行配置,提示转为信息性):
122
+ * 仅当 auto 模式(无显式键/键=auto)且能力缓存记 unavailable 时给一行信息——「平台部署后
123
+ * 自动启用,无需本仓配置」。强制关(yaml off/env off=用户显式选择)与已启用 → ''(零噪音)。
124
+ * 平台未连接 → ''。
125
+ */
126
+ export function vectorSwitchHint(cwd) {
127
+ if (!readPlatformConfig(cwd)) return ''
128
+ if (vectorEnabled(cwd)) return ''
129
+ const cap = readCapabilityCache(cwd)
130
+ if (cap && !cap.available) {
131
+ return `ℹ️ 平台向量语义检索未就绪(自动探测缓存生效中)——平台部署 vector-search 端点后最迟 1 小时内本仓自动启用,无需配置`
132
+ }
133
+ return ''
134
+ }
135
+
136
+ /** 404 死信端点(进程级,按平台 URL 记):端点未实现=今天真实平台状态,标记后本进程不再
137
+ * 对同 URL 空转探测(CLI 短进程生命周期天然重置;测试各用独立端口互不污染)。 */
138
+ const vectorDeadEndpoints = new Set()
139
+
56
140
  /**
57
141
  * 平台向量召回(纯 HTTP 层)。未连接/失败/形态不符 → null(调用方降级)。
58
142
  * @returns {Promise<{results: Array<{spec_path: string, anchor: string, score: number}>}|null>}
@@ -60,19 +144,26 @@ function vectorEnabled(cwd) {
60
144
  export async function platformVectorRecall({ cwd, query, limit = 10, timeoutMs = VECTOR_TIMEOUT_MS }) {
61
145
  const cfg = readPlatformConfig(cwd)
62
146
  if (!cfg || !cfg.url || !cfg.token) return null
147
+ const base = String(cfg.url).replace(/\/+$/, '')
148
+ if (vectorDeadEndpoints.has(base)) return null
63
149
  const q = String(query || '').trim().slice(0, 4000)
64
150
  if (!q) return null
65
151
  try {
66
- const res = await fetch(`${String(cfg.url).replace(/\/+$/, '')}${ENDPOINT_PATH}`, {
152
+ const res = await fetch(`${base}${ENDPOINT_PATH}`, {
67
153
  method: 'POST',
68
154
  headers: { Authorization: `Bearer ${cfg.token}`, 'Content-Type': 'application/json' },
69
155
  body: JSON.stringify({ query: q, limit }),
70
156
  signal: AbortSignal.timeout(timeoutMs),
71
157
  })
72
158
  if (!res.ok) {
159
+ if (res.status === 404) {
160
+ vectorDeadEndpoints.add(base) // 端点未实现——本进程短路后续探测
161
+ writeCapabilityCache(cwd, base, false) // 跨进程持久:重试窗内不再探测(平台部署后自动启用)
162
+ }
73
163
  if (process.env.SILLYSPEC_KNOWLEDGE_VECTOR_DEBUG) console.error(`[knowledge-vector] ${res.status} ${ENDPOINT_PATH}——降级本地`)
74
164
  return null
75
165
  }
166
+ writeCapabilityCache(cwd, base, true) // 端点在场——缓存 available(后续进程零探测直用)
76
167
  const body = await res.json().catch(() => null)
77
168
  const results = body && Array.isArray(body.results)
78
169
  ? body.results.filter((r) => r && typeof r.spec_path === 'string' && typeof r.anchor === 'string')
@@ -187,10 +187,25 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot, s
187
187
  // 各 spawn 2 个 git 进程(gate execute 一次省 ≈ 60-200ms on Windows)。
188
188
  const sharedEvidence = stage === 'execute' ? checkExecuteCodeEvidence(cwd, changeName) : undefined;
189
189
 
190
+ // ── gate 预检实况注入(2026-10-14-gate-verify-apiface-preflight FR-01~FR-03):facts.apiFace
191
+ // 唯一生产者是 run verify --done 收尾前置 backfill(run/gates.js),gate 路径不回填且 --init
192
+ // 刻意不落键 → 锚点核对 parseSet 空/旧 → design接口表# 锚点恒空指(平台 6 轮 gate 实证)。
193
+ // 此处以同一解析器实况解析 design.md 经 context 注入 validator(与 --done backfill 产物同形
194
+ // 同源,零落盘守 D-002 只读契约);design.md 缺失/不可读 helper 返 null、解析异常 fail-soft
195
+ // ——两种态都不注入,退回 facts 口径(现状行为;评审 P1:空面注入会覆写有效 facts 反致误阻断)。──
196
+ let apiFaceLive = null;
197
+ if (stage === 'verify') {
198
+ try {
199
+ const { computeApiFaceFromDesign } = await import('./verify-probes.js');
200
+ apiFaceLive = computeApiFaceFromDesign(join(specRoot, 'changes', changeName, 'design.md'));
201
+ } catch { /* fail-soft:不注入退回 facts 口径 */ }
202
+ }
203
+
190
204
  // ── a. artifacts:阶段产物校验(全部阶段)──
191
205
  const r = runValidators(stage, cwd, changeName, {
192
206
  projectName: progress.project,
193
207
  specRoot,
208
+ ...(apiFaceLive ? { apiFaceLive } : {}),
194
209
  ...(sharedEvidence ? { evidence: sharedEvidence } : {}),
195
210
  });
196
211
  checks.push({
@@ -752,10 +767,21 @@ export async function runDerive(facet, changeName, { cwd, specBase, runtimeRoot,
752
767
 
753
768
  // ── d. artifacts:当前阶段产物校验 ──
754
769
  // runValidators 需要 stage:用 progress.currentStage(若为空串则校验器内部处理)。
770
+ // verify 阶段同款实况注入(2026-10-14-gate-verify-apiface-preflight):与 runGate 同因——
771
+ // facts.apiFace 唯一生产者是 --done backfill,derive 不回填 → 锚点核对恒空指;零落盘 +
772
+ // fail-soft(缺失返 null/异常两态都不注入退回 facts 口径),守 D-002 只读契约。
755
773
  case 'artifacts': {
774
+ let apiFaceLive = null;
775
+ if (currentStage === 'verify') {
776
+ try {
777
+ const { computeApiFaceFromDesign } = await import('./verify-probes.js');
778
+ apiFaceLive = computeApiFaceFromDesign(join(specRoot, 'changes', changeName, 'design.md'));
779
+ } catch { /* fail-soft:不注入退回 facts 口径 */ }
780
+ }
756
781
  const r = runValidators(currentStage, cwd, changeName, {
757
782
  projectName: progress.project,
758
783
  specRoot,
784
+ ...(apiFaceLive ? { apiFaceLive } : {}),
759
785
  });
760
786
  data = { ok: r.ok, errors: r.errors, warnings: r.warnings };
761
787
  ok = r.ok;
@@ -3,6 +3,7 @@
3
3
  // pm._changePath(persistence-core 留 facade 本体)。无共享常量依赖。
4
4
  import { existsSync, mkdirSync, readFileSync, renameSync } from 'fs';
5
5
  import { join } from 'path';
6
+ import { appendDeletedLedger } from '../deleted-ledger.js';
6
7
 
7
8
  // 所有权活跃心跳窗缺省(2026-09-14-change-ownership-guards task-02 / FR-01):owner 非本会话
8
9
  // 且 now-last_active < 窗口 → 拒绝接管;窗口外 → 自动接管。local.yaml change-ownership.
@@ -601,6 +602,9 @@ export class ChangeRegistry {
601
602
  // 本地脏度(D-013 / task-04):删除也是本地状态推进(triggerSync 据此推终态/墓碑上行)
602
603
  this.pm._touchLocalModified(cwd, changeName, now);
603
604
  });
605
+ // 删除台账(2026-10-13-sync-ledger-tombstone):DB 行被外部物理清除后墓碑链的唯一
606
+ // 终态凭证。事务提交后追加、fail-open(见 deleted-ledger.js 头注释)。
607
+ appendDeletedLedger(this.pm._getSpecDir(cwd), changeName, 'deleted');
604
608
  }
605
609
 
606
610
  /**
@@ -669,6 +673,9 @@ export class ChangeRegistry {
669
673
  // 本地脏度(D-013 / task-04):归档也是本地状态推进
670
674
  this.pm._touchLocalModified(cwd, changeName, now);
671
675
  });
676
+ // 删除台账(2026-10-13-sync-ledger-tombstone):归档终态同记(archived)——行被外部
677
+ // 清除后 sync 台账分支据此补推 archived 墓碑(平台 location='archive')。fail-open。
678
+ appendDeletedLedger(this.pm._getSpecDir(cwd), changeName, 'archived');
672
679
  }
673
680
  }
674
681
 
package/src/progress.js CHANGED
@@ -1217,6 +1217,73 @@ export class ProgressManager {
1217
1217
  return this.read(cwd, changeName);
1218
1218
  }
1219
1219
 
1220
+ /**
1221
+ * 原子抢占式建变更(2026-10-13-autocreate-atomic-claim,事故 2026-10-13-new-change-e5a27813)。
1222
+ *
1223
+ * 背景:run brainstorm / run auto 无 --change 的自动建名此前是「listChanges 查零活跃 →
1224
+ * initChange 插行」两步——检查与插入非原子,并行进程都能通过检查各建一行(当日
1225
+ * daemon-log-config-view 与 new-change-e5a27813 同秒落地实证)。本方法把「零活跃检查 +
1226
+ * 建行」收敛为单条 INSERT..SELECT(NOT EXISTS active / NOT EXISTS 同名 双守卫)——
1227
+ * SQLite 单语句原子;跨进程 WAL 快照过期(BUSY_SNAPSHOT)由 db.js transaction 的
1228
+ * BUSY 整体重跑收敛:重跑后守卫见到先行者已提交的 active 行即拒绝。
1229
+ *
1230
+ * 与 initChange 的差异(调用方仅 run/command.js 自动建名两处;显式 --change / 平台
1231
+ * 工具 / 测试 fixture 继续用 initChange,语义不动):
1232
+ * - 检查+插行原子化(败者零写入);
1233
+ * - 目录创建移到抢占成功之后(败者不留 changes/<自动名>/ 空目录);
1234
+ * - 失败返回原因而非静默创建。
1235
+ *
1236
+ * @param {string} cwd 项目根
1237
+ * @param {string} changeName 变更名(自动名恒为日期前缀名;quick 会话名与 default 不走本方法)
1238
+ * @param {{title?: string|null, quicklogId?: string|null}} [meta]
1239
+ * @returns {{created: true, progress: object} | {created: false, reason: 'active_exists'|'name_exists', activeChanges?: string[]}}
1240
+ */
1241
+ initChangeIfNoActive(cwd, changeName, meta = {}) {
1242
+ if (!changeName) {
1243
+ console.warn('⚠️ initChangeIfNoActive: changeName 为空,跳过');
1244
+ return { created: false, reason: 'name_exists' };
1245
+ }
1246
+ const db = this._ensureDB(cwd);
1247
+ let claimed = false;
1248
+ db.transaction(() => {
1249
+ const sqlDb = db.getDb();
1250
+ const now = new Date().toISOString();
1251
+ // 单语句原子抢占:守卫与插入同一语句,无跨语句竞态窗口
1252
+ const claim = sqlDb.prepare(
1253
+ `INSERT INTO changes (name, current_stage, status, created_at, last_active, title, quicklog_id)
1254
+ SELECT ?, 'brainstorm', 'active', ?, ?, ?, ?
1255
+ WHERE NOT EXISTS (SELECT 1 FROM changes WHERE status = 'active')
1256
+ AND NOT EXISTS (SELECT 1 FROM changes WHERE name = ?)`
1257
+ ).run(changeName, now, now, meta.title || null, meta.quicklogId || null, changeName);
1258
+ claimed = claim.changes === 1;
1259
+ if (!claimed) return; // 败者:事务内零后续写入,提交为空事务
1260
+ // 抢到后与 initChange 同款收尾(同事务):种全部合法阶段 + 本地脏度戳
1261
+ const changeRow = sqlDb.prepare('SELECT id FROM changes WHERE name = ?').get(changeName);
1262
+ for (const stage of VALID_STAGES) {
1263
+ sqlDb.prepare(
1264
+ `INSERT OR IGNORE INTO stages (change_id, stage, status)
1265
+ VALUES (?, ?, 'pending')`
1266
+ ).run(changeRow.id, stage);
1267
+ }
1268
+ this._touchLocalModified(cwd, changeName, now);
1269
+ });
1270
+ if (!claimed) {
1271
+ const sqlDb = db.getDb();
1272
+ const activeChanges = sqlDb.prepare("SELECT name FROM changes WHERE status = 'active'").all().map((r) => r.name);
1273
+ const sameName = sqlDb.prepare('SELECT id FROM changes WHERE name = ?').get(changeName);
1274
+ // 判序:同名行存在(任意 status)优先报 name_exists(重名与自动名随机碰撞兜底);
1275
+ // 否则按 active_exists 附清单(调用方走多活跃引导)
1276
+ return sameName && !activeChanges.includes(changeName)
1277
+ ? { created: false, reason: 'name_exists' }
1278
+ : { created: false, reason: 'active_exists', activeChanges };
1279
+ }
1280
+ // 抢占成功后才建目录(败者零残留);quick/default 豁免与 initChange 同款(防辅助阶段空目录物化)
1281
+ if (!/^quick-[0-9a-f]{8}$/.test(changeName) && changeName !== 'default') {
1282
+ this._ensureChangeDir(cwd, changeName);
1283
+ }
1284
+ return { created: true, progress: this.read(cwd, changeName) };
1285
+ }
1286
+
1220
1287
  setStage(cwd, stage, changeName = null) { return this._stepStore.setStage(cwd, stage, changeName); }
1221
1288
 
1222
1289
  addStep(cwd, stage, stepName, changeName = null) { return this._stepStore.addStep(cwd, stage, stepName, changeName); }
package/src/quicklog.js CHANGED
@@ -124,13 +124,18 @@ export function sanitizeDesc(description) {
124
124
  * 「需求:/动机:」类标签前缀,空白折叠单行,超长截断加 …;空输入返回 ''(调用方兜底
125
125
  * change 名)。agent 在 --input 首行写一句中文概括即成标题(flow start 横幅有指引),
126
126
  * --title 显式覆盖一切推导。
127
+ * 段头行防御(2026-10-15-title-label-skip):过门格式示例曾被读成段头形态——agent 把
128
+ * 「动机与背景:」独立成行照抄,白名单无复合词致整行成标题(进度库 6 变更实证)。两道
129
+ * 防御:白名单补复合词(动机与背景/背景与动机);任意短段头行(≤12 字非冒号内容+行尾
130
+ * 冒号,如「变更背景:」「详细说明:」)整行跳过取次非空行——冒号后无内容不成标题。
127
131
  */
128
132
  export function deriveChangeTitle(input, max = 50) {
129
133
  for (const l of String(input || '').replace(/\r\n/g, '\n').split('\n')) {
130
134
  const t = l.trim()
131
135
  if (!t) continue
132
136
  if (/^成功标准\s*[::]/.test(t)) break
133
- const stripped = t.replace(/^(?:需求|动机|背景|任务|标题)\s*[::]\s*/, '').trim()
137
+ if (/^[^::]{1,12}[::]$/.test(t)) continue
138
+ const stripped = t.replace(/^(?:需求|动机|背景|任务|标题|动机与背景|背景与动机)\s*[::]\s*/, '').trim()
134
139
  if (!stripped) continue
135
140
  const one = stripped.replace(/\s+/g, ' ')
136
141
  return one.length > max ? one.slice(0, max) + '…' : one
package/src/redomain.js CHANGED
@@ -21,6 +21,27 @@ import { writeAtomicSync } from './fs-atomic.js'
21
21
  /** 条目段头(## FR-xxx-NNN / ## FR-NNN)判定与 ID 提取 */
22
22
  const FR_HEADER_RE = /^## (FR-[\w.-]+-\d+|FR-\d+)\s*(.*)$/
23
23
 
24
+ /**
25
+ * 域名入参归一化(2026-10-14-redomain-domain-arg-normalize)。
26
+ *
27
+ * flow done 伪域告警提示语(fr-index.js)给出的是 `fr/auto-backend` 形态、手工
28
+ * 输入还可能带 `.md` 后缀,而本模块按裸域名拼 `fr/${from}.md`——两种口径曾打架
29
+ * 产生 `fr/fr/` 双前缀报错。本函数把入参统一收敛为裸域名:trim → 剥前导 `fr/`
30
+ * (不区分大小写)→ 剥尾随 `.md`;裸名原样返回,幂等。可能返回空串(如只传
31
+ * `fr/`)——空值拒绝由调用方负责(报错文案需含「fr/ 前缀」用法提示)。
32
+ * 域文件物理上都在 fr/ 目录内、裸名不含 `/`,`fr/xxx` 只可能是前缀形态,无歧义。
33
+ */
34
+ export function normalizeDomainArg(v) {
35
+ return String(v ?? '').trim().replace(/^fr\//i, '').replace(/\.md$/i, '').trim()
36
+ }
37
+
38
+ /** 归一化后空值拒绝(统一文案,调用方在一切 fs IO 之前调用) */
39
+ function assertDomainArgs({ from, to }) {
40
+ if (!from || !to) {
41
+ throw new Error('源域/目标域为空:域名可带 fr/ 前缀与 .md 后缀(fr/auto-backend ≍ auto-backend.md ≍ auto-backend)')
42
+ }
43
+ }
44
+
24
45
  function frEntryId(headerLine) {
25
46
  const m = FR_HEADER_RE.exec(headerLine)
26
47
  return m ? m[1] : null
@@ -64,6 +85,9 @@ function entrySourceChange(sectionLines) {
64
85
  * @throws 源域缺席 / 无匹配条目 / anchor 未命中
65
86
  */
66
87
  export function redomainFrEntries({ knowledgeRoot, from, to, anchors = null, byChange = null }) {
88
+ from = normalizeDomainArg(from)
89
+ to = normalizeDomainArg(to)
90
+ assertDomainArgs({ from, to })
67
91
  const frDir = join(knowledgeRoot, 'fr')
68
92
  const fromPath = join(frDir, `${from}.md`)
69
93
  const toPath = join(frDir, `${to}.md`)
@@ -134,6 +158,9 @@ export function redomainFrEntries({ knowledgeRoot, from, to, anchors = null, byC
134
158
 
135
159
  /** 干跑预览:列出将迁移条目,不落盘(byChange 同执行侧过滤) */
136
160
  export function planRedomain({ knowledgeRoot, from, to, anchors = null, byChange = null }) {
161
+ from = normalizeDomainArg(from)
162
+ to = normalizeDomainArg(to)
163
+ assertDomainArgs({ from, to })
137
164
  const frDir = join(knowledgeRoot, 'fr')
138
165
  const fromPath = join(frDir, `${from}.md`)
139
166
  if (!existsSync(fromPath)) throw new Error(`源域文件不存在:fr/${from}.md`)
@@ -32,7 +32,7 @@
32
32
  * flow-state substeps 无失败计数面(执行期裁决 D-006:数据源从卡面所述「flow-state
33
33
  * substeps」改读同为既有记录面的 verify-runs 时间线,封闭面语义不变)。
34
34
  */
35
- import { existsSync, readFileSync, readdirSync, mkdirSync } from 'node:fs'
35
+ import { existsSync, readFileSync, readdirSync, mkdirSync, rmSync } from 'node:fs'
36
36
  import { createHash } from 'node:crypto'
37
37
  import { join, basename, dirname } from 'node:path'
38
38
  import { writeAtomicSync } from './fs-atomic.js'
@@ -273,4 +273,36 @@ export function readHindsightHint({ specBase }) {
273
273
  }
274
274
  }
275
275
 
276
- export default { HINDSIGHT_THRESHOLDS, snapshotBaseline, computeHindsightMetrics, markHindsight, readHindsightHint }
276
+ /**
277
+ * 标记结构化读取(2026-10-14-prestage-consent-gate):同意门需要 change/reasons 组文案,
278
+ * 提示行渲染(resume 路径)仍走 readHindsightHint。无文件/损坏 → null(读不到=无门)。
279
+ * @returns {{ change: string, marked_at?: string, metrics?: object, reasons?: string[] } | null}
280
+ */
281
+ export function readHindsightMark({ specBase }) {
282
+ try {
283
+ const raw = readFileSync(join(specBase, '.runtime', MARK_FILE), 'utf8')
284
+ const j = JSON.parse(raw)
285
+ if (!j || typeof j !== 'object' || !j.change) return null
286
+ return j
287
+ } catch {
288
+ return null
289
+ }
290
+ }
291
+
292
+ /**
293
+ * 消费标记(声明放行 --no-prestage / adopt 头脑风暴收编时由 flow.js 调用):删除标记文件。
294
+ * 一次性反馈语义——声明只豁免下一次 start,不豁免之后所有(之后再被标记会重新落盘重新拦)。
295
+ * @returns {boolean} 是否真删(无标记/删除失败 = false,幂等零副作用)
296
+ */
297
+ export function clearHindsightMark({ specBase }) {
298
+ try {
299
+ const p = join(specBase, '.runtime', MARK_FILE)
300
+ if (!existsSync(p)) return false
301
+ rmSync(p)
302
+ return true
303
+ } catch {
304
+ return false
305
+ }
306
+ }
307
+
308
+ export default { HINDSIGHT_THRESHOLDS, snapshotBaseline, computeHindsightMetrics, markHindsight, readHindsightHint, readHindsightMark, clearHindsightMark }
@@ -39,6 +39,9 @@ export async function executeArchiveDistill({ cwd, specBase, changeName }) {
39
39
  console.warn(`\n⚠️ rejected 决策缺否决理由/复潮条件(needsWait)——这些条目未写盘,其余条目照常提炼。`)
40
40
  console.warn(` 裁决指引(确认归档 --confirm 前处理):`)
41
41
  console.warn(` ① 补录:把缺失字段补进 ${join(changeDir, 'decisions.md')} 后重跑提炼(幂等)——`)
42
+ console.warn(` 可复制补丁样例(在对应 D-xxx@vN 条目末尾追加两行,字段名与值之间用冒号+空格):`)
43
+ console.warn(` - 否决理由: <一句话核心劣势>`)
44
+ console.warn(` - 复潮条件: <何种信号下可重议本方案>`)
42
45
  console.warn(` node --input-type=module -e "import { distillIntoKnowledge } from '<sillyspec 源码路径>/src/decision-distill.js'; console.log(JSON.stringify(distillIntoKnowledge(${JSON.stringify(changeDir)}, ${JSON.stringify(knowledgeRoot)}, ${JSON.stringify(headHash)}), null, 2))"`)
43
46
  console.warn(` ② 跳过:经用户裁决不入库该条,并在「确认归档」--output 注记跳过原因。`)
44
47
  }
@@ -24,7 +24,7 @@ import { existsSync, readdirSync, mkdirSync, writeFileSync, readFileSync, rmSync
24
24
  import { randomBytes, randomUUID } from 'node:crypto'
25
25
  import { fileURLToPath } from 'node:url'
26
26
  import { writeAtomicSync } from '../fs-atomic.js'
27
- import { resolveSpecDir, countAncestorSpecDirs, ancestorSpecDirs, resolveAncestorCeiling, resolveChangeDir, triggerSync, getStageSteps, formatWaitOptions, checkApproval, warnApprovalUnknown, didYouMean, assertSafeChangeName, assertDatedChangeName, detectQuickSessionDrift, detectWorktreeSpecDrift, resolveRuntimeRoot, resolveQuickSessionsDir, writePlatformPointer, checkPlatformManaged, isSelfReferentialSpecRoot, isTempResidueSpecRoot, PLATFORM_MANAGED_FILENAME, warnSelfRefPointerOnce, readStageBurst, STAGE_BURST_STAGES } from './shared.js'
27
+ import { resolveSpecDir, countAncestorSpecDirs, ancestorSpecDirs, resolveAncestorCeiling, resolveChangeDir, triggerSync, getStageSteps, formatWaitOptions, checkApproval, warnApprovalUnknown, didYouMean, assertSafeChangeName, assertDatedChangeName, detectQuickSessionDrift, detectWorktreeSpecDrift, resolveRuntimeRoot, resolveQuickSessionsDir, writePlatformPointer, checkPlatformManaged, isSelfReferentialSpecRoot, isTempResidueSpecRoot, PLATFORM_MANAGED_FILENAME, warnSelfRefPointerOnce, readStageBurst, STAGE_BURST_STAGES, isRetiredAutoBrainstormTable, printRetiredAutoBrainstormRefusal } from './shared.js'
28
28
  import { resolveQuickLinkedChanges } from './quick-audit.js'
29
29
  import { outputStep, collectStageWaitHistory } from './prompt.js'
30
30
  import { completeStep, completeStepBurst, skipStep, waitStep, continueStep, synthesizeStepOutput } from './complete.js'
@@ -34,7 +34,7 @@ import { ProgressManager, resolveSessionIdentity } from '../progress.js'
34
34
  import { validateChangeExists, checkTransition } from '../stage-contract.js'
35
35
  import { READONLY_AUXILIARY_STAGES } from '../constants.js'
36
36
  import { stageRegistry, auxiliaryStages } from '../stages/index.js'
37
- import { definition as brainstormAutoDef } from '../stages/brainstorm-auto.js'
37
+
38
38
  import { setQuickFileNotes } from '../quicklog.js'
39
39
  import { checkQuickSessionOwner, writeQuickSessionOwner } from '../quick-session-owner.js'
40
40
  import { hasDecisionId } from '../decisions-io.js'
@@ -171,6 +171,14 @@ function resolveChangeName(cwd, progress, specDir = null) {
171
171
  export async function ensureStageSteps(progress, stageName, cwd, specDir = null) {
172
172
  if (!progress.stages) progress.stages = {}
173
173
 
174
+ // 退役自动道守门(2026-10-14-brainstorm-auto-retire):存量 brainstorm 4 步 auto 表在此明确
175
+ // 拒绝——否则下方「步骤表与当前定义漂移」分支会把 auto 表静默重种为主模式 8 步表并清零进度。
176
+ // --reset/--reopen 走 resetStage 内联重种(不经此函数)= 指引中的恢复出路。
177
+ if (stageName === 'brainstorm' && isRetiredAutoBrainstormTable(progress.stages.brainstorm?.steps)) {
178
+ printRetiredAutoBrainstormRefusal(progress?.change || null)
179
+ process.exit(2)
180
+ }
181
+
174
182
  const steps = await getStageSteps(stageName, cwd, progress, specDir)
175
183
  if (!steps) return false
176
184
 
@@ -224,7 +232,7 @@ export async function ensureStageSteps(progress, stageName, cwd, specDir = null)
224
232
  function emitReadonlyEmptyGuide() {
225
233
  console.log('ℹ️ 未找到进度数据(只读查询不建变更)')
226
234
  console.log(' → 可开始轻量变更(--input 引号内换行合法,下例可照抄):')
227
- console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<动机与背景>')
235
+ console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句中文概括(首行即变更标题);随后动机与背景')
228
236
  console.log('')
229
237
  console.log(' 成功标准:')
230
238
  console.log(' - <可验证标准>"')
@@ -850,7 +858,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
850
858
  quickFallbackUsed = true
851
859
  } else {
852
860
  console.log(' 本仓无近期 quick 会话记录(current marker 与 quick-sessions 均空)——新工作请走轻量变更(--input 引号内换行合法,可照抄):')
853
- console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<动机与背景>')
861
+ console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句中文概括(首行即变更标题);随后动机与背景')
854
862
  console.log('')
855
863
  console.log(' 成功标准:')
856
864
  console.log(' - <可验证标准>"')
@@ -1340,39 +1348,45 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
1340
1348
  process.exit(2) // 用法错(--done 找不到变更,未传 --change)→ exit 2
1341
1349
  }
1342
1350
  // task-03 (D-006@v1 / FR-05): 无 --change 仅当无已存在活跃变更时 auto-create;
1343
- // 多活跃变更仓强制 --change(exit 2 引导),防 B8 幽灵变更。活跃变更数用 pm.listChanges
1344
- // (与 progress show 同源,DB changes 表 status='active')。
1345
- const activeChanges = pm.listChanges(cwd)
1346
- if (activeChanges.length > 0) {
1351
+ // 多活跃变更仓强制 --change(exit 2 引导),防 B8 幽灵变更。
1352
+ // 2026-10-13-autocreate-atomic-claim:listChanges 检查 + initChange 两步非原子
1353
+ // (并行进程同过检查各建一行,2026-10-13-new-change-e5a27813 事故实证)——改
1354
+ // initChangeIfNoActive 单语句原子抢占,败者走同一引导。
1355
+ const date = new Date().toISOString().slice(0, 10)
1356
+ const autoName = `${date}-new-change-${randomBytes(4).toString('hex')}`
1357
+ const claim = pm.initChangeIfNoActive(cwd, autoName, { title: inputText ? (deriveChangeTitle(inputText) || sanitizeDesc(inputText)) : autoName })
1358
+ if (!claim.created) {
1359
+ const activeChanges = claim.activeChanges || []
1347
1360
  console.error(`❌ 已存在 ${activeChanges.length} 个活跃变更,run brainstorm 无 --change 不会自动创建新变更。`)
1348
1361
  console.error(` 活跃变更:${activeChanges.join('、')}`)
1349
1362
  console.error(' 请用 --change <变更名> 指定要操作的变更,')
1350
1363
  console.error(' 或先归档/收尾现有活跃变更后重试。')
1351
1364
  process.exit(2) // 用法错(多活跃变更未指定 --change)→ exit 2
1352
1365
  }
1353
- const date = new Date().toISOString().slice(0, 10)
1354
- const autoName = `${date}-new-change-${randomBytes(4).toString('hex')}`
1355
1366
  console.log(`🔄 自动创建变更:${autoName}`)
1356
1367
  console.log(` 提示:可以用 --change <名称> 指定自定义变更名`)
1357
1368
  console.log(` 或事后重命名:sillyspec change-rename ${autoName} <新名称>`)
1358
- // brainstorm 自动创建变更也写 title(--input 需求描述优先 / 自动名兜底),proposal 落盘后刷新。
1359
- progress = pm.initChange(cwd, autoName, { title: inputText ? (deriveChangeTitle(inputText) || sanitizeDesc(inputText)) : autoName })
1369
+ progress = claim.progress
1360
1370
  changeName = autoName
1361
1371
  } else if (stageName === 'auto') {
1362
1372
  // auto 零活跃建变更(2026-09-08-auto-driver D-003@v1,Grill P1-②):SKILL 启动命令即
1363
1373
  // run auto --input——此前此处 exit 2 是断头路。复用 brainstorm 命名(date-new-change-hex)。
1364
- // pm.read 对零/多活跃都返 null——先分流:多活跃报错列候选(不隐式选择),零活跃才建。
1365
- const activeCount = pm.listChanges(cwd)
1366
- if (activeCount.length > 1) {
1374
+ // pm.read 对零/多活跃都返 null——多活跃报错列候选(不隐式选择),零活跃才建。
1375
+ // 2026-10-13-autocreate-atomic-claim:同 brainstorm 改原子抢占;顺带收紧「恰好 1 个
1376
+ // 活跃也建第二个」的旧宽限(旧 >1 检查与其「零活跃才建」注释自相矛盾,且计数宽限
1377
+ // 正是并发双建的温床)——1 活跃现与多活跃同走 exit 2 引导。
1378
+ const date = new Date().toISOString().slice(0, 10)
1379
+ const autoName = `${date}-new-change-${randomBytes(4).toString('hex')}`
1380
+ const claim = pm.initChangeIfNoActive(cwd, autoName, { title: inputText ? (deriveChangeTitle(inputText) || sanitizeDesc(inputText)) : autoName })
1381
+ if (!claim.created) {
1382
+ const activeCount = claim.activeChanges || []
1367
1383
  console.error(`❌ 已存在 ${activeCount.length} 个活跃变更,run auto 无法自动定位目标。`)
1368
1384
  console.error(' 活跃变更:' + activeCount.join('、'))
1369
1385
  console.error(' 请用 --change <变更名> 显式指定,或先归档/收尾现有变更。')
1370
1386
  process.exit(2)
1371
1387
  }
1372
- const date = new Date().toISOString().slice(0, 10)
1373
- const autoName = `${date}-new-change-${randomBytes(4).toString('hex')}`
1374
1388
  console.log(`🔄 auto 模式自动创建变更:${autoName}(可用 --change 指定名称,或事后 change-rename)`)
1375
- progress = pm.initChange(cwd, autoName, { title: inputText ? (deriveChangeTitle(inputText) || sanitizeDesc(inputText)) : autoName })
1389
+ progress = claim.progress
1376
1390
  changeName = autoName
1377
1391
  } else {
1378
1392
  console.error('❌ 未找到进度数据,请先运行 sillyspec init 或指定 --change <变更名>')
@@ -2021,11 +2035,9 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
2021
2035
  }
2022
2036
  }
2023
2037
 
2024
- // Helper: 在 auto 模式下获取步骤定义
2038
+ // Helper: 在 auto 模式下获取步骤定义(brainstorm-auto 已删除 2026-10-14-brainstorm-auto-retire:
2039
+ // brainstorm 一律主模式定义,wait 步由 auto 驱动方按 SS-META requiresUser 自行推进)
2025
2040
  const getAutoSteps = async (stage) => {
2026
- if (stage === 'brainstorm') {
2027
- return brainstormAutoDef.steps
2028
- }
2029
2041
  return getStageSteps(stage, cwd, progress, platformOpts?.specRoot || null)
2030
2042
  }
2031
2043
 
@@ -2037,27 +2049,11 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
2037
2049
  const ensureAutoStage = async (stage) => {
2038
2050
  const stageChanged = progress.currentStage !== stage
2039
2051
  progress.currentStage = stage
2040
- // Auto 模式下 brainstorm 使用 artifact-first 步骤
2041
- if (stage === 'brainstorm') {
2042
- const existingSteps = progress.stages?.brainstorm?.steps
2043
- const isAutoModeSteps = existingSteps?.length === 4 && existingSteps?.[0]?.name === '进度确认与上下文加载'
2044
- // auto 表在位即早退(2026-09-08 E2E 实证 bug:此前仅 !isAutoModeSteps 重种后 return,
2045
- // auto 表存在时掉进下方通用 ensureStageSteps——主模式 8 步定义判 4 步 auto 表「漂移」
2046
- // 重种主模式表,与 getAutoSteps 渲染双轨互踩,进度永远回 step1)
2047
- if (isAutoModeSteps) return progress
2048
- if (!isAutoModeSteps) {
2049
- if (!progress.stages) progress.stages = {}
2050
- progress.stages.brainstorm = {
2051
- status: 'in-progress',
2052
- startedAt: new Date().toLocaleString('zh-CN', { hour12: false }),
2053
- completedAt: null,
2054
- steps: brainstormAutoDef.steps.map(s => ({ name: s.name, status: 'pending' }))
2055
- }
2056
- pm._write(cwd, progress, changeName)
2057
- triggerSync(cwd, changeName, platformOpts)
2058
- progress = pm.read(cwd, changeName)
2059
- return progress
2060
- }
2052
+ // 退役自动道守门(2026-10-14-brainstorm-auto-retire):存量 4 步 auto 表明确拒绝(下方通用
2053
+ // ensureStageSteps 同款拒绝兜底,此处先行给带变更名的文案)
2054
+ if (stage === 'brainstorm' && isRetiredAutoBrainstormTable(progress.stages?.brainstorm?.steps)) {
2055
+ printRetiredAutoBrainstormRefusal(changeName)
2056
+ process.exit(2)
2061
2057
  }
2062
2058
  // specRoot 必传(坑 auto-mode-specroot-miss):平台模式 specRoot 是外部目录,漏传时
2063
2059
  // getStageSteps 从 cwd 推导 miss 掉 plan/execute 动态步骤(buildPlanSteps(null) 只剩
@@ -2080,7 +2076,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
2080
2076
  if (classification.mode === 'quick') {
2081
2077
  console.log(`📊 auto 模式分类:${classification.reason} 判为小变更`)
2082
2078
  console.log(` 小变更请走轻量变更(quick 通道已退役,v3.30.0 起拒绝新会话;--input 引号内换行合法,可照抄):`)
2083
- console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<动机与背景>')
2079
+ console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句中文概括(首行即变更标题);随后动机与背景')
2084
2080
  console.log('')
2085
2081
  console.log(' 成功标准:')
2086
2082
  console.log(' - <可验证标准>"')
@@ -816,9 +816,14 @@ export async function bakeArchiveTimeline({ cwd, specBase, changeName, destDir,
816
816
  const { readWatcherEvents, watcherEventsPath } = await import('../watcher.js')
817
817
  const res = readWatcherEvents({ runtimeRoot, change: changeName })
818
818
  if (!res.exists) return { ok: true, skipped: true, reason: `无事件流(${watcherEventsPath(runtimeRoot, changeName)})` }
819
- const { parseTaskLines, renderBakedTimeline, resolveCommitAnchors, readBirthTs } = await import('../timeline.js')
819
+ // 侧车透传(2026-10-14-timeline-ticktime-anchor-scope):destDir 内 task-ticks.jsonl 已随归档包
820
+ // ——烤制面读包内侧车(与 tasks.md 同源同路径形态),精确勾选时刻跨机可读;锚收敛 changeName
821
+ // 进解析(消息不含变更名的提交不锚任务行)。
822
+ const { parseTaskLines, renderBakedTimeline, resolveCommitAnchors, parseTickLog, readBirthTs } = await import('../timeline.js')
820
823
  let tasks = null
821
824
  try { tasks = parseTaskLines(readRawImpl(join(destDir, 'tasks.md'), 'utf8')) } catch { /* 任务面缺失照烤(渲染层有降级段) */ }
825
+ let tickLog = []
826
+ try { tickLog = parseTickLog(readRawImpl(join(destDir, 'task-ticks.jsonl'), 'utf8')) } catch { /* 侧车缺失照烤(时刻退推断面) */ }
822
827
  let tier = null
823
828
  try { const m = /^tier:\s*(\S+)/m.exec(readRawImpl(join(destDir, 'flow-state.yaml'), 'utf8')); if (m) tier = m[1] } catch { /* 头脑风暴预段等无 flow-state 按未知 */ }
824
829
  const look = gitLook || ((hash) => {
@@ -829,12 +834,12 @@ export async function bakeArchiveTimeline({ cwd, specBase, changeName, destDir,
829
834
  const sep2 = rest.indexOf('\x1f')
830
835
  return { hash: out.slice(0, sep), dateISO: rest.slice(0, sep2), message: rest.slice(sep2 + 1) }
831
836
  })
832
- const anchors = resolveCommitAnchors(res.events, look)
837
+ const anchors = resolveCommitAnchors(res.events, look, changeName)
833
838
  let eventsRaw = null
834
839
  try { eventsRaw = readRawImpl(watcherEventsPath(runtimeRoot, changeName), 'utf8') } catch { /* 副本缺源只烤渲染面 */ }
835
840
  const oversize = eventsRaw != null && Buffer.byteLength(eventsRaw, 'utf8') > eventsCopyMaxBytes
836
841
  // 评审 P3-1 清偿:副本未落盘(读源失败或超帽)时头注记必须显式「未随包」,不虚报在场
837
- const timelineMd = renderBakedTimeline({ change: changeName, events: res.events, tasks, anchors, birthTs: readBirthTs(destDir), tier, bakedAtIso: nowIso || new Date().toISOString(), eventsCopySkipped: eventsRaw == null || oversize })
842
+ const timelineMd = renderBakedTimeline({ change: changeName, events: res.events, tasks, anchors, birthTs: readBirthTs(destDir), tier, bakedAtIso: nowIso || new Date().toISOString(), eventsCopySkipped: eventsRaw == null || oversize, tickLog })
838
843
  writeImpl(join(destDir, 'timeline.md'), timelineMd)
839
844
  const files = ['timeline.md']
840
845
  if (eventsRaw != null && !oversize) {