sillyspec 3.32.8 → 3.32.9

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/src/index.js CHANGED
@@ -3382,17 +3382,19 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
3382
3382
  // --from <域> --to <域> [--anchor <FR-id> 可重复];干跑缺省列条目,--write 落盘。
3383
3383
  // 身份铁律:迁域不换号(绑定/supersede 链靠 ID 寻址);平台伪域卡「一键迁移」的 CLI 端。
3384
3384
  if (has('--redomain')) {
3385
- const fromD = flag('--from')
3386
- const toD = flag('--to')
3385
+ const { planRedomain, redomainFrEntries, normalizeDomainArg } = await import('./redomain.js')
3386
+ // 入参归一化(2026-10-14-redomain-domain-arg-normalize):--from/--to 容忍
3387
+ // flow done 提示语原样形态(fr/auto-backend、auto-backend.md),回显/报错统一裸域名。
3388
+ const fromD = normalizeDomainArg(flag('--from'))
3389
+ const toD = normalizeDomainArg(flag('--to'))
3387
3390
  // --by-change(坑 fr-domain-suggest-typo-and-no-split-migration 缺陷2):unmapped 等
3388
3391
  // 混合池按「变更:<名>」字段分批迁移——整域迁移会错置大部分条目,来源变更才是分批键。
3389
3392
  const byChangeD = flag('--by-change')
3390
- if (!fromD || !toD) { fail('用法: sillyspec tests --redomain --from <源域> --to <目标域> [--anchor <FR-id>] [--by-change <变更名>] [--write]'); break }
3393
+ if (!fromD || !toD) { fail('用法: sillyspec tests --redomain --from <源域> --to <目标域> [--anchor <FR-id>] [--by-change <变更名>] [--write]\n 域名可带 fr/ 前缀与 .md 后缀(fr/auto-backend ≍ auto-backend.md ≍ auto-backend)'); break }
3391
3394
  // 多 --anchor 收集
3392
3395
  const anchorList = []
3393
3396
  for (let i = 0; i < filteredArgs.length - 1; i++) if (filteredArgs[i] === '--anchor') anchorList.push(filteredArgs[i + 1])
3394
3397
  try {
3395
- const { planRedomain, redomainFrEntries } = await import('./redomain.js')
3396
3398
  if (!has('--write')) {
3397
3399
  // 预览与落盘同口径透传 byChange(坑 redomain-plan-preview-by-change-ignored,
3398
3400
  // 2026-10-02 实证:76338b4d 收编在途工作时此行漏透传——预览列整域、落盘只迁
@@ -4397,7 +4399,15 @@ SillySpec platform — SillyHub 平台同步
4397
4399
  const syncChangeName = syncChangeIdx >= 0 && args[syncChangeIdx + 1] && !String(args[syncChangeIdx + 1]).startsWith("--") ? args[syncChangeIdx + 1] : null;
4398
4400
  if (syncChangeName) {
4399
4401
  // manual:用户显式同步,change_deleted 回执噪音闸旁路(显式动作的回执必须可见)
4400
- await syncModule.sync(syncChangeName, dir, { manual: true });
4402
+ // 失败必须 exit 1(2026-10-13-sync-ledger-tombstone,事故 2026-10-13-new-change-e5a27813):
4403
+ // 此前忽略返回值恒 exit 0——变更不存在/未连接平台都「假成功」,daemon RPC
4404
+ // (sillyspec.sync_change)据退出码回执,假成功掩盖行缺失。成功路径(含
4405
+ // change_deleted 预期回执、台账墓碑补推)errors 为空,exit 0 语义不变。
4406
+ const syncResult = await syncModule.sync(syncChangeName, dir, { manual: true });
4407
+ if (syncResult && Array.isArray(syncResult.errors) && syncResult.errors.length > 0) {
4408
+ console.error(`❌ platform sync 失败(${syncChangeName}): ${syncResult.errors.join(';')}`);
4409
+ process.exit(1);
4410
+ }
4401
4411
  } else {
4402
4412
  // 全树 spec 文件同步(2026-10-11-sync-conflict-tri-fix FR-03):不带 --change
4403
4413
  // 此前报「需要指定变更名称」——归档/历史变更无活跃会话标签时无从下手。现走
@@ -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/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 }
@@ -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
 
@@ -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) 只剩
@@ -22,22 +22,19 @@ import { writeAtomicSync } from '../fs-atomic.js'
22
22
  import { gitQuiet } from '../git-helper.js'
23
23
  import { writeCloseTraceArtifacts, projectTraceFaceRows } from '../flow-parity.js'
24
24
  import { withFileLock } from '../quicklog.js'
25
- import { triggerSync, WAIT_MARKER_RE, getStageSteps, formatWaitOptions, resolveRuntimeRoot, getOrCreateMultiRepoContext, resolveChangeDir, writePlatformDocsPointer } from './shared.js'
25
+ import { triggerSync, WAIT_MARKER_RE, getStageSteps, formatWaitOptions, resolveRuntimeRoot, getOrCreateMultiRepoContext, resolveChangeDir, writePlatformDocsPointer, isRetiredAutoBrainstormTable, printRetiredAutoBrainstormRefusal } from './shared.js'
26
26
  import { isExplicitReviewWrite, collectWorktreeChangedFiles } from '../task-review.js'
27
27
 
28
- // auto 模式 brainstorm 步骤表感知解析(2026-09-08 E2E 实证 bug:--done/--wait 推进主模式 8 步表,
29
- // 与 runAutoMode getAutoSteps 的 4 步 auto 表双轨互踩——ensureAutoStage 判非 auto 表重种清零,
30
- // 进度永远回 step1)。DB 现存 brainstorm 表为 auto 形态(4 步+首名「进度确认与上下文加载」)时,
31
- // complete 路径与渲染路径同源用 brainstormAutoDef.steps。
32
- async function getStageStepsAutoAware(stageName, cwd, progress, specDir) {
33
- const steps = await getStageSteps(stageName, cwd, progress, specDir)
34
- if (stageName !== 'brainstorm') return steps
35
- const existing = progress?.stages?.brainstorm?.steps
36
- const isAutoTable = Array.isArray(existing) && existing.length === 4
37
- && existing[0]?.name === '进度确认与上下文加载'
38
- if (!isAutoTable) return steps
39
- const { definition: brainstormAutoDef } = await import('../stages/brainstorm-auto.js')
40
- return brainstormAutoDef.steps
28
+ // 退役自动道守门(2026-10-14-brainstorm-auto-retire):brainstorm-auto 4 步表已随自动道删除。
29
+ // 2026-09-08 双轨互踩 bug 的兼容面(complete 路径感知 auto 表用 auto 定义推进)就此退役——
30
+ // 存量 auto 表变更在此明确拒绝(不静默重种清零进度);--reset/--reopen 走 resetStage 内联
31
+ // 重种主模式表 = 恢复出路。
32
+ async function getStageStepsAutoAware(stageName, cwd, progress, specDir, changeName = null) {
33
+ if (stageName === 'brainstorm' && isRetiredAutoBrainstormTable(progress?.stages?.brainstorm?.steps)) {
34
+ printRetiredAutoBrainstormRefusal(changeName)
35
+ process.exit(2)
36
+ }
37
+ return getStageSteps(stageName, cwd, progress, specDir)
41
38
  }
42
39
 
43
40
  import { executeScanPreflight, executeScanPostcheck, computeScanProfile, executeScanDetectProjects, executeScanResumeCheck, executeScanFinalize } from './scan-profile.js'
@@ -337,7 +334,7 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
337
334
  }
338
335
 
339
336
  // ── requiresWait 硬门控 ──
340
- const defStepsForCurrent = await getStageStepsAutoAware(stageName, cwd, progress, platformOpts?.specRoot || null)
337
+ const defStepsForCurrent = await getStageStepsAutoAware(stageName, cwd, progress, platformOpts?.specRoot || null, changeName)
341
338
  // def↔DB 一致性守卫(坑 execute-step-table-drift,2026-08-20 实证):runCommand 入口已按名
342
339
  // 重播种(ensureStageSteps),但同一命令进程内 plan.md 若再次被改(或绕过 runCommand 直调
343
340
  // completeStep 的路径),def 与 DB 步数错位会让 currentIdx 的门控/prompt 施加到错误的步骤上
@@ -1028,7 +1025,7 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
1028
1025
  }
1029
1026
  }
1030
1027
 
1031
- const defSteps = await getStageStepsAutoAware(stageName, cwd, progress, platformOpts?.specRoot || null)
1028
+ const defSteps = await getStageStepsAutoAware(stageName, cwd, progress, platformOpts?.specRoot || null, changeName)
1032
1029
  console.log(`✅ Step ${currentIdx + 1}/${steps.length} 完成:${steps[currentIdx].name}\n`)
1033
1030
 
1034
1031
  // Workflow post_check(W6 Step6 抽至 complete-handlers.js handleWorkflowPostCheck)
@@ -1888,7 +1885,7 @@ export async function waitStep(pm, progress, stageName, cwd, outputText, waitRea
1888
1885
 
1889
1886
  // maxWaitRounds 硬上限:达到后拒绝继续 --wait
1890
1887
  const currentStep = stageData.steps[currentIdx]
1891
- const defSteps = await getStageStepsAutoAware(stageName, cwd, progress, platformOpts?.specRoot || null)
1888
+ const defSteps = await getStageStepsAutoAware(stageName, cwd, progress, platformOpts?.specRoot || null, changeName)
1892
1889
  const stepDef = defSteps?.[currentIdx] || {}
1893
1890
  const maxWaitRounds = currentStep.maxWaitRounds ?? stepDef.maxWaitRounds
1894
1891
  const currentWaitRound = currentStep.waitRound || 0
@@ -2038,7 +2035,7 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
2038
2035
  } else {
2039
2036
  currentIdx = waitingSteps[0].idx
2040
2037
  }
2041
- const defSteps = await getStageStepsAutoAware(stageName, cwd, progress, platformOpts?.specRoot || null)
2038
+ const defSteps = await getStageStepsAutoAware(stageName, cwd, progress, platformOpts?.specRoot || null, changeName)
2042
2039
  const currentStepDef = defSteps?.[currentIdx] || {}
2043
2040
  const currentStep = stageData.steps[currentIdx]
2044
2041
  const isRepeatableWait = currentStepDef.repeatableWait === true || currentStep.repeatableWait === true
@@ -2247,7 +2244,7 @@ export async function skipStep(pm, progress, stageName, cwd, changeName, platfor
2247
2244
  console.log(`⚠️ Step "${steps[currentIdx].name}" 此前被门控阻断(blocked),本次 --skip 将跳过该步——若门控条件已修复,更宜直接 --done 推进。`)
2248
2245
  }
2249
2246
 
2250
- const defSteps = await getStageStepsAutoAware(stageName, cwd, progress, platformOpts?.specRoot || null)
2247
+ const defSteps = await getStageStepsAutoAware(stageName, cwd, progress, platformOpts?.specRoot || null, changeName)
2251
2248
  const stepDef = defSteps ? defSteps[currentIdx] : null
2252
2249
  if (stepDef && !stepDef.optional) {
2253
2250
  console.error(`❌ 步骤 "${steps[currentIdx].name}" 不可跳过`)
package/src/run/gates.js CHANGED
@@ -224,7 +224,7 @@ export function warnMissingUiPrototype(stageName, changeName, currentStepName, s
224
224
  const design = readFileSync(designPath, 'utf8')
225
225
  const feExts = [...new Set((design.match(/\.(vue|svelte|astro|tsx|jsx|css|scss|sass|less|styl|html)\b/gi) || []).map(s => s.replace('.', '').toLowerCase()))]
226
226
  if (feExts.length === 0) return
227
- console.warn(` ⚠️ design.md 命中前端文件类型(${feExts.join('/')})但变更目录无 prototype-*.html——轻微样式改动可忽略;布局/交互流程级变化应补原型(sillyspec run brainstorm --reopen 回到「分段展示设计」,或直接让 agent 补生成)`)
227
+ console.warn(` ⚠️ design.md 命中前端文件类型(${feExts.join('/')})但变更目录无 prototype-*.html——轻微样式改动可忽略;布局/交互流程级变化应补原型(sillyspec run brainstorm --reopen 回到「分段展示设计」,或直接让 agent 补生成;已有可复跑 HTML 原型则登记复用:复制进变更目录并命名 prototype-<名称>.html,execute 按此命名注入)`)
228
228
  }
229
229
 
230
230
  /**
package/src/run/prompt.js CHANGED
@@ -2247,6 +2247,16 @@ ${maskVolatileForGuide(guideTemplate)}
2247
2247
  }
2248
2248
  console.log('<!--SS-META:' + JSON.stringify(meta) + '-->')
2249
2249
  }
2250
+
2251
+ // 尾部全文锚(2026-10-14-guide-anchor-adopt-proto,sess_fb51d988 实证):首见全量渲染可能被
2252
+ // harness 工具结果截断保尾——长动态注入段(模块欠账/知识命中等)把决策关键节(原型分级/生成
2253
+ // 要求等)顶出可见窗口,agent 只见铁律半句就临场发挥。末尾一行带 step-guides 全文路径,截断后
2254
+ // 仍能 Read 回完整规则(complete.js task-08 锚定行的补全:带路径,不只报位置)。复入分支顶部
2255
+ // 已有 📄 行,但其动态附录同样可能很长把顶部顶掉——尾部统一再锚一次。guide 关闭/--json 劫持/
2256
+ // 文件不在场时零输出(stdout 确定性测试面不变)。
2257
+ if (guideEnabled && !jsonHijacked && existsSync(guideFile)) {
2258
+ console.log(`📄 指引全文锚:${guideFile}(本输出被截断或上下文丢失时 Read 此文件拿回完整步骤规则——决策关键节都在内)`)
2259
+ }
2250
2260
  }
2251
2261
  /**
2252
2262
  * 替换 prompt 文本中的路径根占位符 {SPEC_ROOT}/{DOCS_ROOT}/{PROJECTS_ROOT}/
package/src/run/shared.js CHANGED
@@ -2308,6 +2308,25 @@ export function _clearMultiRepoCtxCache(cwd, changeName) {
2308
2308
  }
2309
2309
 
2310
2310
 
2311
+ /**
2312
+ * 退役自动道步骤表形态判定(2026-10-14-brainstorm-auto-retire):brainstorm 4 步 auto 表
2313
+ *(首名「进度确认与上下文加载」——与主模式首步「进度确认」字面可区分)。检测到时调用方必须
2314
+ * 报退役指引并退出:防止 ensureStageSteps 把 auto 表判「漂移」静默重种清零存量进度。
2315
+ * --reset/--reopen 走 resetStage 内联重种(不经此判定)——即指引中的恢复出路。
2316
+ */
2317
+ export function isRetiredAutoBrainstormTable(steps) {
2318
+ return Array.isArray(steps) && steps.length === 4
2319
+ && steps[0]?.name === '进度确认与上下文加载'
2320
+ }
2321
+
2322
+ /** 退役拒绝文案(单一真相源:ensureStageSteps / getStageStepsAutoAware / ensureAutoStage 三入口共用)。 */
2323
+ export function printRetiredAutoBrainstormRefusal(changeName) {
2324
+ console.error(`⛔ 该变更的 brainstorm 步骤表来自已退役的自动道(4 步 auto 表,brainstorm-auto 已于 2026-10-14 删除——其设计产物流程无原型分级/真码口径,且双轨步骤表机制易互踩)。`)
2325
+ console.error(` 处置:① sillyspec run brainstorm --change ${changeName || '<变更名>'} --reset(重种主模式 8 步表从头开始——brainstorm 尚未收口时适用)② sillyspec doctor(诊断后处置)。`)
2326
+ console.error(` 进度未被修改(拒绝不写盘,不静默重种)。`)
2327
+ }
2328
+
2329
+
2311
2330
  /**
2312
2331
  * 获取阶段的步骤定义(execute 需要动态构建)
2313
2332
  */
@@ -1329,7 +1329,7 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, c
1329
1329
  * { key, repoPath, anchor, totals, degraded, degradedReason, patch?, patchSha256? };
1330
1330
  * notes 为调用方 note 链的增量行(跨仓计数行 + 降级行)。
1331
1331
  */
1332
- export function reconcileCrossRepoPlan({ cwd, specBase, runtimeRoot, changeName, planEntries, collectPatch = false }) {
1332
+ export function reconcileCrossRepoPlan({ cwd, specBase, runtimeRoot, changeName, planEntries, collectPatch = false, crossBaselines = null }) {
1333
1333
  const rows = []
1334
1334
  const repos = []
1335
1335
  const notes = []
@@ -1344,7 +1344,7 @@ export function reconcileCrossRepoPlan({ cwd, specBase, runtimeRoot, changeName,
1344
1344
  let repoActual = null
1345
1345
  let repoFailure = null
1346
1346
  try {
1347
- repoActual = collectRepoActual({ repoKey, specBase, cwd, runtimeRoot, changeName })
1347
+ repoActual = collectRepoActual({ repoKey, specBase, cwd, runtimeRoot, changeName, crossBaselines })
1348
1348
  } catch (e) {
1349
1349
  repoFailure = e && e.message ? String(e.message).split('\n')[0] : String(e)
1350
1350
  }
@@ -67,7 +67,7 @@ sillyspec flow start --change <YYYY-MM-DD-名> --input "<动机与背景>
67
67
  (2 调用协议收口)。用户同意则本阶段可在此收尾(\`--done\` 并提示转轻量变更),不必继续走完整设计流程。
68
68
  - **明显 large**(需要 Wave 并行编排/上下文分片/多阶段治理,或用户显式要求完整流程)→ 不要提示轻量变更,继续进入下一步「对话式探索与需求澄清」。
69
69
  - **拿不准 → 继续探索**(这正是头脑风暴的职责;澄清完成后 Step 8 按同轴精判——注意:经过头脑风暴澄清的需求通常已可单上下文吞吐,出口默认收编轻量道)。
70
- - 这是**粗判**,只为让明显的纯执行小变更免走设计流程;后续「生成规范文件」步骤做精判兜底。
70
+ - 这是**粗判**,只为让明显的纯执行小变更免走设计流程;后续「生成规范文件」步骤做精判兜底(方案在澄清中缩水后按定稿重判,本步印象不作数)。
71
71
 
72
72
  ### 输出
73
73
  项目现状理解摘要(3-5 句话,关键约定和架构决策)+ 可能涉及的模块列表 + 本次需求所属子项目 + (如命中)quick 建议
@@ -248,11 +248,15 @@ sillyspec flow start --change <YYYY-MM-DD-名> --input "<动机与背景>
248
248
  3. 展示完整设计方案(不要逐段停顿,一次性展示)
249
249
  4. 分段设计含新机制时同上检索(固定动作):设计的分段内容引入新机制词(例词为举例非机制,不构成封闭清单)时,先跑 \`sillyspec knowledge search --query "<机制词>"\`,命中条目必读(尤其 status=rejected 的防复潮条目)再定稿设计
250
250
  5. 确认变更名(格式:\`YYYY-MM-DD-<简短描述>\`,例如 \`2026-05-13-user-auth\`——CLI 已强制:新建/改名不带日期前缀直接 exit 2)
251
- 6. 按「HTML 原型分级」生成原型或写出跳过原因(二选一必填,见下)
251
+ 6. 按「HTML 原型分级」三选一必填:生成原型 / 登记复用既有原型 / 写出跳过原因(见下)
252
252
  7. 暂停等待用户确认或修改意见
253
253
 
254
254
  ### HTML 原型分级(按改动程度,不是"是否碰前端文件")
255
255
  判定对象是**改造后的界面相对现状的变化程度**:
256
+ - **免生成·登记复用**(用户已提供**可复跑**的 HTML 原型且覆盖本次 UI 面——先判本档再判改动程度):
257
+ - 不重画。把原型文件**复制**进 \`{SPEC_ROOT}/changes/<change-name>/\` 并**必须命名为 \`prototype-<名称>.html\`**——execute 阶段按此命名扫描注入原型引用,保留中文名/原名=没登记=白复用
258
+ - 输出复用声明:原型来源路径 + 覆盖面(覆盖哪些页面/端);只覆盖部分 UI 面时如实声明,未覆盖部分仍按下两档生成(变更目录允许多个 prototype-*.html 共存)
259
+ - 截图/静态设计稿**不适用本档**(不可复跑)——仍按「必须生成」产出可跑 HTML
256
260
  - **必须生成**(布局/结构/流程级变化):
257
261
  - 新增页面、视图、路由
258
262
  - 页面布局变化:区域增删、骨架重排、信息层级调整(如单栏改多栏、列表改看板)
@@ -265,19 +269,20 @@ sillyspec flow start --change <YYYY-MM-DD-名> --input "<动机与背景>
265
269
  - 文案、颜色、间距、字号、图标等样式微调
266
270
  - 纯后端逻辑/CLI/配置/文档,无任何界面变化
267
271
 
268
- ### 生成要求
269
- 生成一个独立的 HTML 文件(内联 CSS + JS),保存到:
270
- \`{SPEC_ROOT}/changes/<change-name>/prototype-<名称>.html\`(目录不存在则先创建)
271
- - 单文件,浏览器直接打开
272
- - 高保真:布局/组件/交互流程按真实效果呈现,后续 execute 阶段可直接对照复用
273
- - 与项目代码风格一致:优先复用项目现有技术栈/组件库/设计 token(先查 scan 文档与现有前端代码),不另起风格
272
+ ### 生成要求(定稿原型=真码产物,不是手绘图)
273
+ 入口文件保存到 \`{SPEC_ROOT}/changes/<change-name>/prototype-<名称>.html\`(目录不存在则先创建;该命名是 execute 注入契约,不改)。真码五要素:
274
+ - **与实现同方言**:用项目真实前端栈构建——组件库/设计 token/样式从项目源码或 node_modules 真实引入(例:React 项目挂真实组件而非仿制标签、Vue 项目用真实 SFC;例词非封闭清单),不手搓仿制样式
275
+ - **可复跑**:能被本项目工具链跑起来(dev server / 组件文档页 / 构建产物均可,入口 HTML 从本地 node_modules 引真实库也行)——先查 scan 文档与现有前端代码就近发现项目原型管线,已有则复用,没有则搭最小入口并留在项目惯例位置
276
+ - **高保真=组件即实现原料**:布局/组件/交互按真实效果呈现,execute 阶段直接对照搬组件,不经过"看图重新翻译"
277
+ - **手绘单文件(自造样式)仅限一次性粗选对比,不得作为定稿原型**(frontend-apple-style 实证:手绘定稿后每条意见都要反向翻译回真实代码);粗选定了方向,升级真码再收口
278
+ - **例外与降级**:项目本身即单文件 HTML 方言(纯静态站)时单文件即真码,不适用手绘限定;无法同方言真码化(如目标平台无 HTML 运行面)时显式声明降级与理由请用户裁决留痕——静默降级会被收口「UI 视觉证据」探针拦下
274
279
 
275
280
  ### 铁律 — 必须等待用户确认设计
276
281
  - 展示完整设计方案后必须暂停等用户确认,**不要自己说"设计已充分确认"然后推进**(命令由 CLI 在下方注入)
277
- - 原型生成或跳过声明必须在暂停**之前**完成——暂停是本步骤最后动作,用户确认后直接进入写 design.md,不会回头补
282
+ - 原型生成、复用登记或跳过声明必须在暂停**之前**完成(复用登记=原型文件已按 \`prototype-<名称>.html\` 落在变更目录)——暂停是本步骤最后动作,用户确认后直接进入写 design.md,不会回头补
278
283
 
279
284
  ### 输出
280
- 完整设计方案 + 变更名 + 原型文件路径 或 跳过原因(二选一必填;跳过时写明分级依据,用户可当场否决要求补)
285
+ 完整设计方案 + 变更名 + 原型文件路径 或 复用声明(来源+覆盖面+登记后路径)或 跳过原因(三选一必填;跳过时写明分级依据,用户可当场否决要求补)
281
286
 
282
287
  ### 注意
283
288
  - 不要一次输出大段文字,按模块/Phase 分段
@@ -531,9 +536,12 @@ status: passed | needs-user-input | blocked | skipped
531
536
  按**复杂度/上下文轴**判断本次变更规模(2026-09-29-brainstorm-exit-thin-default:此前文件数轴「≤2 文件才 small」与选道表第 3 行的复杂度措辞不一致,且模糊需求探索完很少只碰 2 文件——出口几乎必然五阶段,实测坐实):
532
537
  - **small(收编轻量道)**:单上下文可吞吐——预计无需 Wave 并行编排、无需上下文分片(单会话吃得下全部任务)、无多阶段治理/设计期人机对抗需求
533
538
  - **large(完整五阶段)**:需要 Wave 并行编排、或单任务规模大需上下文分片、或多阶段治理、或用户显式要求完整流程
539
+ - **反判据(2026-10-14-scale-large-evidence,deploy-release-notes 实证:零建表定稿被早期印象判 large 走了五阶段)**:跨多端/多仓/多文件**不是** large 依据——轻量道支持多文件与跨仓交付;任务间**顺序依赖也不是**(B 等 A 的契约/产物正是轻量道任务循环的天然形态,一波等一波不构成 Wave)。Wave 证据=存在**可真并行**的任务簇(互相不等)**且单会话上下文吃不下**,两条同时成立才够 large
540
+ - **Wave 证据落盘**:判 large 时必须在 design.md(总体方案/Phase-Wave 处)写明——哪些任务簇可真并行、单会话上下文为何吃不下;**写不出 Wave 证据 = 改判 small**
541
+ - **按定稿方案重判**:Step 2 早期筛查只是路由粗判,方案在澄清中缩水(砍建表/砍迁移/砍端点)后必须按**定稿实质**重判——沿用早期筛查印象会把已缩水的方案误判厚(实证:plan 阶段自判单会话执行〔execution_mode=main〕反证 Wave 并非真需求)
534
542
  - **拿不准 → small**(与 thin-default-flip 同哲学:预判反诱发误升厚——R16 实证;升厚留运行时证据:实测失败自动升厚+--upgrade-thick 用户决策)
535
543
  在 design.md frontmatter 写入 \`scale: small\` 或 \`scale: large\`(frontmatter 不存在则补 \`author\`/\`created_at\`/\`scale\`)。规模决定下面的产物范围和实现路径。
536
- (注:早期「加载项目上下文」步骤已做过一次粗判并可能建议过走 quick;此处基于 design 方案实质做精判兜底——判据同轴。)
544
+ (注:早期「加载项目上下文」步骤已做过一次粗判并可能建议过走 quick;此处基于 design 方案实质做精判兜底——判据同轴,方案缩水后按定稿重判、早期印象不作数。)
537
545
 
538
546
  ### 操作
539
547
  1. **按规模生成规范文件**:
@@ -1425,7 +1425,7 @@ ${worktreeNoticeSingle}
1425
1425
 
1426
1426
  **主代理直写(execution_mode: main)——你直接实现,不派子代理。**
1427
1427
 
1428
- 逐任务闭环(串行,完成一个再下一个):读 task 卡(路径见下方任务摘要)→ 在 worktree 内实现(TDD:先写测试再实现)→ 跑该 task 卡 verify 命令 → \`sillyspec wt-commit --change <change-name> -- <task-files>\` 逐任务提交 → 手动勾选 tasks.md 对应 checkbox(完成=实现+测试绿+wt-commit 即勾,2026-09-26 Task Review 退役起勾选回归 agent 手动,同 thin 工作单元语义)→ 下一任务。
1428
+ 逐任务闭环(串行,完成一个再下一个):读 task 卡(路径见下方任务摘要)→ 在 worktree 内实现(TDD 按层适用:纯逻辑先写测试再实现,行为契约断言用户可见行为,样式/纯配置不写——判据见「测试用例设计」)→ 跑该 task 卡 verify 命令 → \`sillyspec wt-commit --change <change-name> -- <task-files>\` 逐任务提交 → 手动勾选 tasks.md 对应 checkbox(完成=实现+测试绿+wt-commit 即勾,2026-09-26 Task Review 退役起勾选回归 agent 手动,同 thin 工作单元语义)→ 下一任务。
1429
1429
 
1430
1430
  worktree 隔离 / 写入守卫(allowed_paths)/ verify 门禁全部保留——只换执行宿主,不换任何防线。
1431
1431