sillyspec 3.28.13 → 3.29.0

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 (41) hide show
  1. package/package.json +1 -1
  2. package/src/blast-surface.js +168 -0
  3. package/src/ceremony-tier.js +359 -0
  4. package/src/change-risk-profile.js +72 -199
  5. package/src/config-schema.js +21 -0
  6. package/src/decisions-io.js +32 -0
  7. package/src/design-facts.js +1 -0
  8. package/src/doctor-diagnostics.js +119 -1
  9. package/src/index.js +92 -5
  10. package/src/knowledge-stats.js +25 -0
  11. package/src/modules.js +28 -0
  12. package/src/prefill.js +491 -0
  13. package/src/probe7-anchor-check.js +8 -6
  14. package/src/quick-gate-profile.js +8 -4
  15. package/src/quick-session-owner.js +63 -0
  16. package/src/review-dispatch.js +473 -1
  17. package/src/review-material-pack.js +265 -0
  18. package/src/review-tier.js +185 -41
  19. package/src/run/archive-distill.js +6 -0
  20. package/src/run/command.js +158 -87
  21. package/src/run/complete-handlers.js +82 -1
  22. package/src/run/complete.js +37 -1
  23. package/src/run/gates.js +301 -19
  24. package/src/run/prompt.js +423 -21
  25. package/src/run/shared.js +36 -0
  26. package/src/run/verify-quality-scan.js +14 -6
  27. package/src/scope-audit.js +8 -1
  28. package/src/span-risk-surface.js +127 -0
  29. package/src/stage-contract-spec.js +2 -2
  30. package/src/stage-contract.js +102 -81
  31. package/src/stage-review.js +26 -2
  32. package/src/stages/brainstorm.js +28 -15
  33. package/src/stages/execute.js +10 -2
  34. package/src/stages/plan.js +29 -12
  35. package/src/stages/verify.js +7 -7
  36. package/src/taskcard.js +3 -0
  37. package/src/verify-postcheck.js +375 -20
  38. package/src/verify-probes.js +117 -10
  39. package/src/worktree.js +1 -1
  40. package/templates/prompts/taskcard-rules.md +1 -0
  41. package/templates/prompts/verify-probes.md +1 -1
@@ -36,6 +36,8 @@ import { READONLY_AUXILIARY_STAGES } from '../constants.js'
36
36
  import { stageRegistry, auxiliaryStages } from '../stages/index.js'
37
37
  import { definition as brainstormAutoDef } from '../stages/brainstorm-auto.js'
38
38
  import { setQuickFileNotes } from '../quicklog.js'
39
+ import { checkQuickSessionOwner, writeQuickSessionOwner } from '../quick-session-owner.js'
40
+ import { hasDecisionId } from '../decisions-io.js'
39
41
 
40
42
  // F2/F4: 哪些 flag 吃下一个 token 作「值」(其余 --flag 都是布尔,不吞值)。
41
43
  // 校验循环只对 VALUE_FLAGS 跳下一个 token(否则 --done 后跟 typo 会被当 --done 的值吞掉)。
@@ -49,6 +51,7 @@ const VALUE_FLAGS = new Set([
49
51
  '--ql', // quick --cancel 显式指定 qlId(缺省读会话 guard.json)
50
52
  '--base', // scan diff 基线 commit(吃值;只在 run scan --diff 转发路径消费)
51
53
  '--session', // 显式会话标识(吃值;2026-09-14-change-ownership-guards task-02 所有权三级解析最高优先级层,启动 claim 消费)
54
+ '--inherit-from', // wait 继承盖章锚点 <D-xxx@vN>(吃值;2026-09-18-preflight-slimming task-03 / D-003@v2:仅 --wait 场景合法,--wait 分发点校验消费)
52
55
  ])
53
56
 
54
57
  /**
@@ -225,6 +228,7 @@ sillyspec run ${stageName}${stage && stage.title ? ` — ${stage.title}` : ''}${
225
228
  通用参数: --done --output --input --status --skip --reset --reopen --from-step
226
229
  --wait --continue --answer --change --spec-dir --non-interactive
227
230
  --interactive --skip-approval --json(不支持)
231
+ --wait --inherit-from <D-xxx@vN> 决策继承盖章(仅 --wait 场景合法,ID 须存在于 decisions.md)
228
232
  quick 专属: --linked-changes none|a,b --files a.js,b.js --allow-new
229
233
  --allow-delete --force-baseline --confirm --file-notes
230
234
  scan 专属: --quick --standard --deep --force-rescan --diff [--base <commit>] [--full] [--report]
@@ -377,6 +381,18 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
377
381
  // 作为全局 flag 吞掉、不透传到此处,故 isInteractive 在此恒为 false)。
378
382
  const waitReason = getFlagValue('--reason')
379
383
  const waitOptions = getFlagValue('--options')
384
+ // ── --inherit-from <D-xxx@vN>(2026-09-18-preflight-slimming task-03 / D-003@v2 / FR-03)──
385
+ // wait 继承盖章锚点:照 --reason/--options 同款解析(值类 flag,getFlagValue F4 守卫)。
386
+ // 仅 --wait 场景合法——不带 --wait 时出现即用法错 exit 2(该参数语义是给 wait 补答案轮,
387
+ // 脱离 --wait 无消费点,静默吞会让 agent 误以为已盖章)。存在性校验(hasDecisionId
388
+ // fail-closed)在下方 --wait 分发点做(彼时 changeDir/specRoot 才解析完)。
389
+ const waitInheritFrom = getFlagValue('--inherit-from')
390
+ if (waitInheritFrom !== null && !isWait) {
391
+ console.error('❌ --inherit-from 仅在 --wait 场景合法(决策继承盖章:wait 答案轮由 decisions.md 既有决策盖印)')
392
+ console.error(` 用法: sillyspec run ${stageName} --wait --reason "..." --inherit-from D-xxx@vN`)
393
+ console.error(' 不需要继承盖章时去掉 --inherit-from,--wait 行为与现状一致')
394
+ process.exit(2) // 用法错 → exit 2
395
+ }
380
396
  const continueAnswer = getFlagValue('--answer')
381
397
  const resolvedSpecDir = specDir || getFlagValue('--spec-dir') || getFlagValue('--spec-root')
382
398
  const platformOpts = {
@@ -655,6 +671,83 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
655
671
  console.error(` 合法变更名示例:2026-07-27-add-login(仅字母/数字/._-,不含 / \\ ..)`)
656
672
  process.exit(2)
657
673
  }
674
+ // ── flag 校验 + --help 短路(2026-09-19 上移至 quick 会话生成之前)──
675
+ // 原位置在 quick sessionId 生成之后:--input 启动门上移(坑 quick-no-input-placeholder-title)
676
+ // 后,run quick --help 会先建会话/落 owner/上报 agent-log 再短路——run-help-shortcircuit
677
+ // 零副作用契约破坏(幻影会话实测复现)。上移后未知 flag 校验与帮助短路均先于一切副作用,
678
+ // 且未知 flag 先于 --input 语义门报错(用法错优先级:形态错 > 语义缺)。
679
+ // F10b(ql-20260818-010):语义别名定向提示。did-you-mean 按编辑距离猜形近 flag,猜中的常是
680
+ // 形近但语义错的(--title → --files,ql-20260818-003 负面③实证)。常见「语义别名」在此登记
681
+ // 定向指引:命中时替代 did-you-mean 打印,引导到真正承载该语义的 flag/机制。
682
+ // 2026-09-14-change-ownership-guards task-02:原 '--session' 条目(quick 会话名提示)随
683
+ // --session 升格为真实 flag(所有权会话标识)而移除——提示只对未知 flag 生效,已知 flag
684
+ // 挂提示是死代码;'--session-id' 是最常见形近拼法,改挂新语义指引(所有权三级标识)。
685
+ const FLAG_SEMANTIC_HINTS = {
686
+ '--title': 'QUICKLOG 条目标题无独立参数——从 --output 的「需求:」字段自动提取(写成一句语义化短标题即可)',
687
+ '--message': '结果摘要用 --output(quick 末步须含 需求:/根因:/方案:/结果: 四字段)',
688
+ '--summary': '结果摘要用 --output(quick 末步须含 需求:/根因:/方案:/结果: 四字段)',
689
+ '--result': '结果摘要用 --output(quick 末步须含 需求:/根因:/方案:/结果: 四字段)',
690
+ '--name': 'quick 会话名由 CLI 自动分配(quick-<hash>),恢复会话用 --change <quick-session-id>;关联变更用 --linked-changes',
691
+ '--session-id': '会话标识用 --session <id>(所有权三级解析最高优先级层;缺省 env SILLYSPEC_SESSION_ID,再缺省 anon@<host> 机器级降级)',
692
+ '--note': '文件括注用 --file-notes "path::注 || path::注";启动时任务描述用 --input',
693
+ '--notes': '文件括注用 --file-notes "path::注 || path::注"',
694
+ '--desc': '启动时任务描述用 --input;QUICKLOG 标题从 --output「需求:」自动提取',
695
+ '--description': '启动时任务描述用 --input;QUICKLOG 标题从 --output「需求:」自动提取',
696
+ }
697
+
698
+ // 未知参数 fail-fast
699
+ const knownFlags = new Set([
700
+ '--done', '--skip', '--status', '--reset', '--confirm', '--skip-approval', '--cancel', '--ql',
701
+ '--wait', '--continue', '--non-interactive', '--interactive',
702
+ '--wait-interactive', // auto 模式 TTY 直通(带值形态 `--wait-interactive true`,autoFlagValue 消费;ql-20260911-029 补注册——此前漏登记,进命令前即被未知参数 exit(2) 拦死,FR-03 整条死路)
703
+ '--reason', '--options', '--answer', '--confirm-mode',
704
+ '--output', '--input', '--change', '--linked-changes',
705
+ '--req', '--cause', '--solution', '--result',
706
+ '--spec-dir', '--spec-root', '--runtime-root', '--workspace-id', '--scan-run-id',
707
+ '--files', '--file-notes', '--allow-new', '--allow-delete', '--force-baseline', '--force-rescan',
708
+ '--no-docs', // L2 门禁文档认领显式豁免(task-02:布尔 flag 不吃值,audit 链 docClaim → exempt-no-docs)
709
+ '--json', '--dir', '--help',
710
+ '--reopen', '--from-step', '--mode',
711
+ '--deep', '--quick', '--standard', // scan profile 三档显式选择(scan-profile.js 从 argv 读;互斥见下方 PROFILE_FLAGS 检测)
712
+ '--adopt-branch', // execute 显式收编既有 sillyspec/<change> 分支为 worktree 工作分支(坑 worktree-user-branch-conflict)
713
+ '--diff', '--base', '--full', '--report', // scan diff(D-001:command.js 只补 flag,裸 token 解析归 index.js 子命令拦截)
714
+ '--meta', // P1-3:渲染 SS-META 机器块(布尔 flag,不吃值)
715
+ '--session', // 显式会话标识(吃值,VALUE_FLAGS 同步登记;2026-09-14-change-ownership-guards task-02:启动 claim 消费,所有权三级解析最高优先级层)
716
+ '--takeover', // 所有权护栏显式强制接管(task-02 / FR-01:本层只注册透传,实际消费在 index.js apply/cleanup 子命令层——run 侧带上不报未知参数,语义出口在 worktree 命令)
717
+ '--skip-apply', // 归档收口跳过 apply 校验(task-02 只注册透传不改行为,消费归 task-03 archive 接线)
718
+ '--inherit-from', // wait 继承盖章 <D-xxx@vN>(2026-09-18-preflight-slimming task-03:仅 --wait 场景合法,hasDecisionId 校验+盖章在 --wait 分发点/complete 层)
719
+ '-h',
720
+ ])
721
+ for (let i = 0; i < flags.length; i++) {
722
+ const f = flags[i]
723
+ if (f.startsWith('--')) {
724
+ if (!knownFlags.has(f)) {
725
+ // F10: flag 级 did-you-mean(此前只命令级有);F10b:语义别名定向提示优先于形近猜测
726
+ const semanticHint = FLAG_SEMANTIC_HINTS[f]
727
+ console.error(`❌ 未知参数: ${f}`)
728
+ if (semanticHint) {
729
+ console.error(` ${semanticHint}`)
730
+ } else {
731
+ const suggestion = didYouMean(f, [...knownFlags])
732
+ if (suggestion) console.error(` 你是想输入「${suggestion}」吗?`)
733
+ else console.error(`已知参数: ${[...knownFlags].sort().join(', ')}`)
734
+ }
735
+ process.exit(2) // 用法错 → exit 2
736
+ }
737
+ // F2: 只有吃值的 flag 才跳下一个 token。布尔 flag(--done 等)不能 i++——
738
+ // 否则会把紧跟的 typo flag 当成 --done 的「值」吞掉,既不校验也不生效(静默忽略)。
739
+ if (VALUE_FLAGS.has(f)) i++
740
+ }
741
+ }
742
+
743
+ // ── --help/-h 短路:flag 校验通过后、任何副作用(cwd 纠正/会话创建/QUICKLOG 落盘)之前 ──
744
+ // --help 此前在 knownFlags 白名单里被静默吞掉,run quick --help 会误开 quick 会话+写骨架
745
+ // 条目(查询意图不该有副作用);-h 更是被当未知参数 exit 2。帮助查询 = 用法展示 = 退出 0。
746
+ if (flags.includes('--help') || flags.includes('-h')) {
747
+ printStageUsage(stageName)
748
+ process.exit(0)
749
+ }
750
+
658
751
  // quick 会话隔离(D-001@v1 + D-003@v1 + §4.4 跨进程传递):每会话用 sessionId 作 changeName,
659
752
  // DB 分行 progress.quick-<uuid8>,避免并行 quick 会话共享单行 progress.default.quick 互相覆盖。
660
753
  // sessionId = crypto.randomUUID 前 8 hex(摒弃旧 quick-YYYYMMDD-HHMMSS 时间戳,同秒并发撞)。
@@ -698,8 +791,34 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
698
791
  } catch {}
699
792
  }
700
793
  if (!quickSessionId) {
794
+ // 坑 quick-no-input-placeholder-title(2026-08-24 二期①;2026-09-19 上移):全新 quick 会话
795
+ // (即将生成新 sessionId)缺 --input 且无可取标题的 --linked-changes → 拒绝启动(exit 2,
796
+ // 零沉没成本)。原位置在 flag 校验之后,但 sessionId 生成/「已建立」公告与 agent-log 平台
797
+ // 上报都在其之前——先宣告「已建立+续用提示」再 exit(2) 留幻影会话观感,且给平台报了一条
798
+ // 永不启动的 agent 日志(2026-09-19 实测踩);上移到 id 生成前,拒绝路径零输出副作用。
799
+ // --help/-h 豁免:帮助查询不该被拦——flag 校验+--help 短路块已于 2026-09-19 上移至本块
800
+ // 之前(见上方「flag 校验 + --help 短路」段注记),帮助路径到不了这里;本豁免保留为纵深
801
+ // 防御。done-like(--done/--status/--skip/--reset/--reopen)与
802
+ // --cancel 不受限(isDoneLike 含前五者);--linked-changes 非空可从关联变更提标题。
803
+ if (!(isDoneLike || isCancel) && !inputText
804
+ && !(Array.isArray(linkedChanges) && linkedChanges.length > 0)
805
+ && !flags.includes('--help') && !flags.includes('-h')) {
806
+ console.error('❌ 新 quick 会话必须带 --input "<一句话任务描述>"(或 --linked-changes <a,b> 从关联变更取标题)。')
807
+ console.error(' 不带描述启动会落「(quick 任务)」占位标题:平台「快速修复」列表隐藏占位条目、语义标题要拖到最终 --done 才回填,长任务全程不可见。')
808
+ console.error(' 用法: sillyspec run quick --input "<一句话任务描述>" [--linked-changes <a,b>] [--files <...>]')
809
+ process.exit(2) // 用法错(新会话无任务描述)→ exit 2
810
+ }
701
811
  quickSessionId = 'quick-' + randomUUID().slice(0, 8)
702
812
  quickSidFresh = true
813
+ // 归属首建(quick-shared-pointer-stomp,2026-09-19 实证修复):创建者 agent 身份落
814
+ // owner.json(与 guard.json 同目录,随 --cancel 目录清理销毁)。身份三级
815
+ // flag > env > anon@host——刻意不传 quickChangeName(resolveSessionIdentity 的
816
+ // quick 会话级会把身份解析成会话 id 本身、恒 self 失去区分度)。fail-open:
817
+ // 落盘失败不阻断启动(核验侧 owner 缺失即放行)。
818
+ try {
819
+ writeQuickSessionOwner(resolveRuntimeRoot(platformOpts, specRoot), quickSessionId,
820
+ resolveSessionIdentity({ flagSession: getFlagValue('--session'), warn: false }).session)
821
+ } catch { /* owner 落盘失败 → 核验侧按无 owner 放行 */ }
703
822
  // 坑 quick-sync-block-filenotes-and-quicklog-mixed-commit 坑1③:新会话 ID 生成即打印
704
823
  // 第一行——后续 agent-log push / spec-sync 等 best-effort 网络动作(平台慢时分钟级)
705
824
  // 若把命令拖过 exec 超时,调用方仍能从输出头部拿到 sessionId 续用(--status / guard.json
@@ -713,6 +832,25 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
713
832
  }
714
833
  }
715
834
 
835
+ // ── quick 会话归属核验(quick-shared-pointer-stomp,2026-09-19 实证修复)──
836
+ // 变更类操作(--done/--skip/--reset/--reopen/--cancel)在会话非本进程新建时核验 owner;
837
+ // --status 只读豁免;fresh 会话本进程刚落 owner 恒过。异主 exit 2——接管=显式
838
+ // --session <owner>(与 change 所有权护栏同语义;身份三级缺省 anon@host 机器级只拦他机)。
839
+ // 无 owner 文件的存量会话放行(brownfield 兼容)。
840
+ if (stageName === 'quick' && !quickSidFresh && quickSessionId
841
+ && (isDone || isSkip || isReset || isReopen || isCancel)) {
842
+ const actorSession = resolveSessionIdentity({ flagSession: getFlagValue('--session'), warn: false }).session
843
+ const ownerVerdict = checkQuickSessionOwner(resolveRuntimeRoot(platformOpts, specRoot), quickSessionId, actorSession)
844
+ if (!ownerVerdict.ok) {
845
+ console.error(`\n❌ quick 会话 ${quickSessionId} 属于其他会话(owner: ${ownerVerdict.owner}),当前会话标识 ${ownerVerdict.actor},拒绝操作。`)
846
+ console.error(' (多 agent 并行防互踩:共享指针 current-quick-run-id 是 last-writer-wins,2026-09-19 实证异会话 --done 覆写过他人 QUICKLOG 条目)')
847
+ console.error(' - 继续自己的会话:sillyspec run quick --status 查各会话,--change <自己的 quick-xxx> 精确指定')
848
+ console.error(` - 确需接管他者会话:--session ${ownerVerdict.owner}(显式接管,等同所有权护栏语义)`)
849
+ console.error(' - 会话标识三级:--session > SILLYSPEC_SESSION_ID > anon@<host>(缺省机器级只拦他机)')
850
+ process.exit(2)
851
+ }
852
+ }
853
+
716
854
  // ── 本地 agent 会话日志登记 + 上报(平台会话展示用,best-effort)──
717
855
  // 平台/daemon 模式下 SillyHub 只能看到 CLI 阶段信息,看不到本地 agent 的实际执行日志。
718
856
  // 在此探测当前 agent 环境(Claude Code / Codex / ZCode transcript / SILLYSPEC_AGENT_LOG
@@ -822,92 +960,8 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
822
960
  // 注册白名单透传,本行补 includes 解析进 completeStep options,isNoDocs 同款先例)。
823
961
  const isSkipApply = flags.includes('--skip-apply')
824
962
 
825
- // F10b(ql-20260818-010):语义别名定向提示。did-you-mean 按编辑距离猜形近 flag,猜中的常是
826
- // 形近但语义错的(--title → --files,ql-20260818-003 负面③实证)。常见「语义别名」在此登记
827
- // 定向指引:命中时替代 did-you-mean 打印,引导到真正承载该语义的 flag/机制。
828
- // 2026-09-14-change-ownership-guards task-02:原 '--session' 条目(quick 会话名提示)随
829
- // --session 升格为真实 flag(所有权会话标识)而移除——提示只对未知 flag 生效,已知 flag
830
- // 挂提示是死代码;'--session-id' 是最常见形近拼法,改挂新语义指引(所有权三级标识)。
831
- const FLAG_SEMANTIC_HINTS = {
832
- '--title': 'QUICKLOG 条目标题无独立参数——从 --output 的「需求:」字段自动提取(写成一句语义化短标题即可)',
833
- '--message': '结果摘要用 --output(quick 末步须含 需求:/根因:/方案:/结果: 四字段)',
834
- '--summary': '结果摘要用 --output(quick 末步须含 需求:/根因:/方案:/结果: 四字段)',
835
- '--result': '结果摘要用 --output(quick 末步须含 需求:/根因:/方案:/结果: 四字段)',
836
- '--name': 'quick 会话名由 CLI 自动分配(quick-<hash>),恢复会话用 --change <quick-session-id>;关联变更用 --linked-changes',
837
- '--session-id': '会话标识用 --session <id>(所有权三级解析最高优先级层;缺省 env SILLYSPEC_SESSION_ID,再缺省 anon@<host> 机器级降级)',
838
- '--note': '文件括注用 --file-notes "path::注 || path::注";启动时任务描述用 --input',
839
- '--notes': '文件括注用 --file-notes "path::注 || path::注"',
840
- '--desc': '启动时任务描述用 --input;QUICKLOG 标题从 --output「需求:」自动提取',
841
- '--description': '启动时任务描述用 --input;QUICKLOG 标题从 --output「需求:」自动提取',
842
- }
843
-
844
- // 未知参数 fail-fast
845
- const knownFlags = new Set([
846
- '--done', '--skip', '--status', '--reset', '--confirm', '--skip-approval', '--cancel', '--ql',
847
- '--wait', '--continue', '--non-interactive', '--interactive',
848
- '--wait-interactive', // auto 模式 TTY 直通(带值形态 `--wait-interactive true`,autoFlagValue 消费;ql-20260911-029 补注册——此前漏登记,进命令前即被未知参数 exit(2) 拦死,FR-03 整条死路)
849
- '--reason', '--options', '--answer', '--confirm-mode',
850
- '--output', '--input', '--change', '--linked-changes',
851
- '--req', '--cause', '--solution', '--result',
852
- '--spec-dir', '--spec-root', '--runtime-root', '--workspace-id', '--scan-run-id',
853
- '--files', '--file-notes', '--allow-new', '--allow-delete', '--force-baseline', '--force-rescan',
854
- '--no-docs', // L2 门禁文档认领显式豁免(task-02:布尔 flag 不吃值,audit 链 docClaim → exempt-no-docs)
855
- '--json', '--dir', '--help',
856
- '--reopen', '--from-step', '--mode',
857
- '--deep', '--quick', '--standard', // scan profile 三档显式选择(scan-profile.js 从 argv 读;互斥见下方 PROFILE_FLAGS 检测)
858
- '--adopt-branch', // execute 显式收编既有 sillyspec/<change> 分支为 worktree 工作分支(坑 worktree-user-branch-conflict)
859
- '--diff', '--base', '--full', '--report', // scan diff(D-001:command.js 只补 flag,裸 token 解析归 index.js 子命令拦截)
860
- '--meta', // P1-3:渲染 SS-META 机器块(布尔 flag,不吃值)
861
- '--session', // 显式会话标识(吃值,VALUE_FLAGS 同步登记;2026-09-14-change-ownership-guards task-02:启动 claim 消费,所有权三级解析最高优先级层)
862
- '--takeover', // 所有权护栏显式强制接管(task-02 / FR-01:本层只注册透传,实际消费在 index.js apply/cleanup 子命令层——run 侧带上不报未知参数,语义出口在 worktree 命令)
863
- '--skip-apply', // 归档收口跳过 apply 校验(task-02 只注册透传不改行为,消费归 task-03 archive 接线)
864
- '-h',
865
- ])
866
- for (let i = 0; i < flags.length; i++) {
867
- const f = flags[i]
868
- if (f.startsWith('--')) {
869
- if (!knownFlags.has(f)) {
870
- // F10: flag 级 did-you-mean(此前只命令级有);F10b:语义别名定向提示优先于形近猜测
871
- const semanticHint = FLAG_SEMANTIC_HINTS[f]
872
- console.error(`❌ 未知参数: ${f}`)
873
- if (semanticHint) {
874
- console.error(` ${semanticHint}`)
875
- } else {
876
- const suggestion = didYouMean(f, [...knownFlags])
877
- if (suggestion) console.error(` 你是想输入「${suggestion}」吗?`)
878
- else console.error(`已知参数: ${[...knownFlags].sort().join(', ')}`)
879
- }
880
- process.exit(2) // 用法错 → exit 2
881
- }
882
- // F2: 只有吃值的 flag 才跳下一个 token。布尔 flag(--done 等)不能 i++——
883
- // 否则会把紧跟的 typo flag 当成 --done 的「值」吞掉,既不校验也不生效(静默忽略)。
884
- if (VALUE_FLAGS.has(f)) i++
885
- }
886
- }
887
-
888
- // ── --help/-h 短路:flag 校验通过后、任何副作用(cwd 纠正/会话创建/QUICKLOG 落盘)之前 ──
889
- // --help 此前在 knownFlags 白名单里被静默吞掉,run quick --help 会误开 quick 会话+写骨架
890
- // 条目(查询意图不该有副作用);-h 更是被当未知参数 exit 2。帮助查询 = 用法展示 = 退出 0。
891
- if (flags.includes('--help') || flags.includes('-h')) {
892
- printStageUsage(stageName)
893
- process.exit(0)
894
- }
895
-
896
- // 坑 quick-no-input-placeholder-title(2026-08-24 用户反馈二期①):全新 quick 会话(上方
897
- // 刚生成新 sessionId)不带 --input 且无 --linked-changes 可取标题 → 拒绝启动(exit 2)。
898
- // 旧行为只 warn 提示「建议放弃重启」,占位标题「(quick 任务)」已落盘、只能 reset 重来——
899
- // 语义标题要拖到最终 --done 才回填,平台「快速修复」列表全程隐藏。此刻零沉没成本(progress
900
- // 行/QUICKLOG/guard 均未创建),直接拒掉最便宜。放在 --help 短路之后(help 查询不该被拦)、
901
- // 任何会话副作用之前。精确恢复(--change quick-<hex>)与 done-like(--done/--reset/--cancel/
902
- // --status/--skip/--reopen)不受限;--linked-changes 启动可从关联变更 proposal/design 提取标题。
903
- if (stageName === 'quick' && quickSidFresh
904
- && !(isDone || isStatus || isSkip || isReset || isReopen || isCancel)
905
- && !inputText && !(Array.isArray(linkedChanges) && linkedChanges.length > 0)) {
906
- console.error('❌ 新 quick 会话必须带 --input "<一句话任务描述>"(或 --linked-changes <a,b> 从关联变更取标题)。')
907
- console.error(' 不带描述启动会落「(quick 任务)」占位标题:平台「快速修复」列表隐藏占位条目、语义标题要拖到最终 --done 才回填,长任务全程不可见。')
908
- console.error(' 用法: sillyspec run quick --input "<一句话任务描述>" [--linked-changes <a,b>] [--files <...>]')
909
- process.exit(2) // 用法错(新会话无任务描述)→ exit 2
910
- }
963
+ // (--input 启动门原在此处,2026-09-19 上移到 quick sessionId 生成/公告之前——原位置会先
964
+ // 打印「会话已建立+续用提示」并上报 agent 日志再 exit(2),留幻影会话观感;见上方坑注。)
911
965
 
912
966
  const isAuxiliary = auxiliaryStages.includes(stageName)
913
967
  // scan 元数据追踪(存储在 stageData.scanMeta 中,completeStep 通过 progress 访问)
@@ -1521,7 +1575,24 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
1521
1575
 
1522
1576
  // --wait: 将 step 设为 waiting(独立于 --done)
1523
1577
  if (isWait) {
1524
- return await waitStep(pm, progress, stageName, cwd, outputText, waitReason, waitOptions, { changeName: effectiveChange, nonInteractive: isNonInteractive && !isInteractive, platformOpts })
1578
+ // ── wait 继承盖章锚点校验(2026-09-18-preflight-slimming task-03 / D-003@v2 / FR-03)──
1579
+ // --inherit-from <D-xxx@vN> 必须字面存在于变更 decisions.md 的 ## D-xxx@vN 标题
1580
+ // (hasDecisionId——task-01 契约,机械零语义);为假 fail-closed exit 2(R-04 防伪造
1581
+ // 锚点),且校验先于 waitStep 任何状态写入——不落任何 wait 状态。changeDir 走命令
1582
+ // 上下文既有解析(resolveChangeDir 同款:currentChange 优先、唯一目录兜底);
1583
+ // quick 会话无 changes/ 目录 → changeDir=null → hasDecisionId 恒 false → 同样 exit 2。
1584
+ let inheritFrom = null
1585
+ if (waitInheritFrom !== null) {
1586
+ const inheritChangeDir = resolveChangeDir(cwd, progress, specRoot)
1587
+ if (!hasDecisionId(inheritChangeDir, waitInheritFrom)) {
1588
+ console.error('❌ 决策 ID 不存在于 decisions.md——继承盖章 fail-closed(防伪造锚点)')
1589
+ console.error(` --inherit-from ${waitInheritFrom}:${inheritChangeDir ? `${join(inheritChangeDir, 'decisions.md')} 内无「## ${waitInheritFrom}」标题` : '当前上下文解析不到变更目录(quick 会话无 decisions.md,继承盖章不适用)'}`)
1590
+ console.error(' 核对决策编号与版本号(查看 decisions.md 的 ## D-xxx@vN 标题,或 sillyspec derive decisions)后原样重跑本命令')
1591
+ process.exit(2) // 用法错(伪造/漂移锚点)→ exit 2
1592
+ }
1593
+ inheritFrom = waitInheritFrom
1594
+ }
1595
+ return await waitStep(pm, progress, stageName, cwd, outputText, waitReason, waitOptions, { changeName: effectiveChange, nonInteractive: isNonInteractive && !isInteractive, platformOpts, inheritFrom })
1525
1596
  }
1526
1597
 
1527
1598
  // --continue: 从 waiting 恢复
@@ -160,6 +160,7 @@ function ownerFromReviewedFiles(reviewedFiles) {
160
160
  *
161
161
  * 归属 fail-closed(禁 mtime 猜、禁后缀匹配):
162
162
  * apply-pathspec-<change>.txt 精确文件名
163
+ * prompt-inject-<change>.json 精确文件名(2026-09-18-preflight-slimming:注入分叉账本回收登记)
163
164
  * execute-runs/<runId>/ 仅当 `change` 戳全等(无戳旧 run 留下,不按覆盖度启发式删)
164
165
  * stage-reviews/<dir>/ 仅当 review.json reviewedFiles 首段 === changeName
165
166
  * verify-runs/<ts>/ 仅当目录内 JSON 的 change 字段集合 size=1 且等于本变更
@@ -195,6 +196,15 @@ export function pruneArchivedChangeRuntime(runtimeRoot, changeName) {
195
196
  if (existsSync(pathspec)) gone(pathspec)
196
197
  } catch {}
197
198
 
199
+ // prompt-inject 账本(2026-09-18-preflight-slimming Phase 2 / D-002,Grill 评审 fail②):
200
+ // .runtime/prompt-inject-<change>.json 是 change 级派生文件(阶段感知注入分叉的账本),
201
+ // 变更归档/删除后无读者——不登记回收则按变更数累积孤儿。精确文件名,对齐上方
202
+ // apply-pathspec / 下方 friction-tally 同款形态(禁 glob/后缀匹配)。
203
+ try {
204
+ const ledger = join(runtimeRoot, `prompt-inject-${changeName}.json`)
205
+ if (existsSync(ledger)) gone(ledger)
206
+ } catch {}
207
+
198
208
  // friction tally(friction-signal-hint FR-05):摩擦计数随变更终态一并回收——真实变更的
199
209
  // friction-tally-<change>.json 落 runtimeRoot,变更归档/删除后无读者,残留即孤儿文件。
200
210
  // 删前残余先滚台账(tax-governance FR-02 兜底滚动点,见函数头注释)。
@@ -322,7 +332,7 @@ export async function archiveWorktreeCleanup(cwd, archiveChangeName, specBase, p
322
332
  // (apply 已 cleanup),漏接就会让 execute-runs/verify-runs 继续按变更数累积。
323
333
  const pruned = pruneArchivedChangeRuntime(runtimeRoot, archiveChangeName)
324
334
  if (pruned.removed > 0) {
325
- console.log(`🧹 归档回收 ${pruned.removed} 项 runtime 取证(execute-runs/stage-reviews/verify-runs/apply-pathspec,按 change 精确匹配)`)
335
+ console.log(`🧹 归档回收 ${pruned.removed} 项 runtime 取证(execute-runs/stage-reviews/verify-runs/apply-pathspec/prompt-inject,按 change 精确匹配)`)
326
336
  }
327
337
  }
328
338
  } catch (e) {
@@ -816,6 +826,8 @@ export async function handleArchiveConfirmStep({ stageName, steps, currentIdx, c
816
826
  console.warn(`⚠️ db 声明门异常降级放行(apply-manifest.json 读取/解析失败,fail-open): ${(e && e.message) || e}`)
817
827
  }
818
828
  }
829
+ // Ceremony 双跑收口·第二出口(task-04 / FR-03 / D-003):实现在文件尾 runArchiveCeremonyDualRunExit——mismatch 阻断级警告+记账+种子,不回滚归档(D-006)
830
+ await runArchiveCeremonyDualRunExit({ cwd, specBase, changeName, sessionFlag, platformOpts, progress })
819
831
  // ── 归档前 delta.md 自动生成(P3d task-02,fail-soft 零阻断)──
820
832
  // 目录移走前在 changes/<name>/ 落一份 Before/Delta/After 三段式快照(四源采集容缺,
821
833
  // buildDeltaReport 见 ../archive-delta.js),随归档包留存。失败只 console.error 留痕、
@@ -1449,6 +1461,40 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
1449
1461
  if (outputText) steps[currentIdx].output = null
1450
1462
  process.exit(1)
1451
1463
  }
1464
+ // ── 结果虚报提交核对(坑 quicklog-result-false-commit-claim,2026-09-18 实证:ql 条目
1465
+ // 写「已提交 <hash> 推送」但 git show 核查该提交不含本会话任何文件——代码滞留暂存区
1466
+ // 11 小时,台账账实不符)。advisory warn 不阻断(hash 可能是组合/merge 提交,命中判定
1467
+ // 模糊),但把 CLI 端能机械核实的部分摆上台面:结果文本声称提交(40 位 hex,或短 hash
1468
+ // 紧跟「已提交/已推送」类字样)且会话声明文件(guard.allowedFiles)无一命中该提交面 →
1469
+ // 醒目警告要求 git show 复核;确认是正常形态可照常落账(警告留痕即审计面)。
1470
+ try {
1471
+ const m40 = outputText.match(/\b[0-9a-f]{40}\b/)
1472
+ const mShort = outputText.match(/\b([0-9a-f]{7,12})\b(?=[^\n]{0,40}(?:已提交|已推送|推送|提交入库|落库))/)
1473
+ const claimed = m40 || mShort
1474
+ if (claimed) {
1475
+ const hash = m40 ? m40[0] : mShort[1]
1476
+ const declared = Array.isArray(guard?.allowedFiles) ? guard.allowedFiles : []
1477
+ let commitFiles = null
1478
+ try {
1479
+ const out = safeGit(cwd, ['show', '--name-only', '--pretty=format:', hash])
1480
+ const raw = typeof out === 'string' ? out : (out && out.value) || ''
1481
+ commitFiles = String(raw).split('\n').map(s => s.trim()).filter(Boolean)
1482
+ } catch { /* 引用不存在/解析失败 → commitFiles 保持 null 走提示分支 */
1483
+ }
1484
+ if (Array.isArray(commitFiles) && commitFiles.length > 0 && declared.length > 0) {
1485
+ const hit = declared.some(f => commitFiles.includes(f)
1486
+ || commitFiles.some(c => c.endsWith('/' + f) || f.endsWith('/' + c) || c === f.replace(/^\.?\//, '')))
1487
+ if (!hit) {
1488
+ console.warn('')
1489
+ console.warn(`⚠️ 结果声称已提交 ${String(hash).slice(0, 12)},但本会话声明的 ${declared.length} 个文件均不在该提交面内——疑似「计划提交的 hash」被当成「已提交的 hash」(git add 整体失败 / commit pathspec 漏文件的已知形态)。`)
1490
+ console.warn(` 核实:git show ${String(hash).slice(0, 12)} --stat 对照本会话文件;确未落库则补提交后重跑 --done 更正「结果:」。`)
1491
+ console.warn(` 组合/merge 提交等正常形态确认后照常落账(本警告留痕即审计面)。`)
1492
+ }
1493
+ } else if (commitFiles === null) {
1494
+ console.warn(`⚠️ 结果引用提交 ${String(hash).slice(0, 12)} 但该引用无法解析(git show 失败)——落账前请 git show 核实。`)
1495
+ }
1496
+ }
1497
+ } catch { /* 校验自身异常不阻断 --done(advisory) */ }
1452
1498
  }
1453
1499
 
1454
1500
  if (!qlId) {
@@ -2652,3 +2698,38 @@ export async function assertWaveTasksComplete({ steps, currentIdx, changeName, c
2652
2698
  console.log(`🛡️ 「${stepName}」完成度门通过:${waveTaskIds.length} 个 task checkbox 全勾(${waveTaskIds.join('、')})`)
2653
2699
  }
2654
2700
 
2701
+ /**
2702
+ * Ceremony 双跑收口·第二出口(2026-09-18-ceremony-risk-pricing task-04 / FR-03 / D-003):
2703
+ * archive confirm(--confirm 已过)目录移动前,与 verify --done 出口同款检查函数对账——
2704
+ * 实际 diff 经 resolveReconcileActualFiles 单点重跑 blast+span,vs .runtime/ceremony-tier-
2705
+ * <change>.json 开跑声明档。mismatch → 阻断级警告(⛔ console.error 列声明档/事实档/超阈
2706
+ * 分量)+ 摩擦账 + 事实面预价种子——但只警告不回滚归档步骤(archive=终态铁律,D-006 防复潮:
2707
+ * 不 reopen、不改归档终态语义,与上方 db 门「回 pending 阻断」刻意不同形)。显式降级被
2708
+ * 事实面支持 → 放行并在 warning 披露(D-004);skipped(档位文件缺失等)/ degraded
2709
+ * (git 不可用)零噪音放行;检查整体 fail-soft,异常降级放行不阻断归档。
2710
+ *
2711
+ * 检查实现 + 记账 + 种子全部收敛在 verify-postcheck.runCeremonyDualRunCheck 单点(两出口
2712
+ * 同一实现),本函数只做接线 + 会话标识(resolveSessionIdentity 同 :604 所有权护栏三级口径:
2713
+ * --session > env > anon@host;warn:false——机器消费路径,教学 warning 归所有权护栏面)。
2714
+ * 动态 import 是 gates.js 消费 verify-postcheck 的既有防环形态(verify-probes 闭包含
2715
+ * run/shared)。实现放文件尾与 assertWaveTasksCompleted 同一考量:platform-interface-map.md
2716
+ * 锚 handleScanStageCompleted 于 [2216,2223] 行窗(doc-ref-check 层2),handleArchiveConfirmStep
2717
+ * 内联大块会把锚推出窗口——调用点只留两行,注释与实现全落此处。
2718
+ */
2719
+ async function runArchiveCeremonyDualRunExit({ cwd, specBase, changeName, platformOpts = {}, sessionFlag = null, progress = null }) {
2720
+ try {
2721
+ const { runCeremonyDualRunCheck, printCeremonyDualRunCheck } = await import('../verify-postcheck.js')
2722
+ const ceremonyChangeName = (progress && progress.currentChange) || changeName
2723
+ if (!ceremonyChangeName) return
2724
+ const { session: ceremonySession } = resolveSessionIdentity({ flagSession: sessionFlag || null, cwd, warn: false })
2725
+ const ceremonyCheck = await runCeremonyDualRunCheck({
2726
+ cwd, specBase, changeName: ceremonyChangeName,
2727
+ runtimeRoot: resolveRuntimeRoot(platformOpts, specBase),
2728
+ session: ceremonySession,
2729
+ })
2730
+ printCeremonyDualRunCheck(ceremonyCheck)
2731
+ } catch (e) {
2732
+ console.warn(`⚠️ Ceremony 双跑对账异常降级放行(不阻断归档,fail-soft): ${(e && e.message) || e}`)
2733
+ }
2734
+ }
2735
+
@@ -1412,7 +1412,7 @@ function formatWaitHistory(step) {
1412
1412
  }
1413
1413
 
1414
1414
  export async function waitStep(pm, progress, stageName, cwd, outputText, waitReason, waitOptions, options = {}) {
1415
- const { changeName, nonInteractive = false, platformOpts = {} } = options
1415
+ const { changeName, nonInteractive = false, platformOpts = {}, inheritFrom = null } = options
1416
1416
  const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
1417
1417
  const stageData = progress.stages[stageName]
1418
1418
 
@@ -1483,10 +1483,46 @@ export async function waitStep(pm, progress, stageName, cwd, outputText, waitRea
1483
1483
  }
1484
1484
  }
1485
1485
 
1486
+ // ── wait 继承盖章(2026-09-18-preflight-slimming task-03 / D-003@v2 / FR-03)──
1487
+ // --wait --inherit-from <D-xxx@vN>:wait 记录(上方 waiting 字段)落成后,同命令追加盖章
1488
+ // 答案轮进 waitAnswers(照 continueStep 既有答案轮数据形态 {round, answer, question,
1489
+ // answeredAt}——协议通道不改结构,回放链照常回放),wait 语义从「等待用户」翻转为
1490
+ // 「已盖章待续跑」:清 waiting 标记字段 + 回 pending(照 continueStep 对 waiting 态的
1491
+ // 清理形态)。消费路径最小侵入:completeStep 的 waiting 前置守卫见无 waiting 步骤直过、
1492
+ // requiresWait 硬门见 waitAnswer 已置不阻断——后续普通 --done 不再要求 --answer。
1493
+ // 锚点存在性已由 command.js --wait 分发点 hasDecisionId fail-closed 校验(此处收到的
1494
+ // inheritFrom 必为 decisions.md 内真实存在的 ID),答案文本自带来源标注(R-04 可审计)。
1495
+ if (inheritFrom) {
1496
+ const step = stageData.steps[currentIdx]
1497
+ const prevOutput = step.output || ''
1498
+ const stampRound = (step.waitRound || 0) + 1
1499
+ const stampAnswer = `由 ${inheritFrom} 继承确认(CLI 盖章)`
1500
+ step.waitRound = stampRound
1501
+ step.waitAnswer = stampAnswer
1502
+ step.waitAnswers = Array.isArray(step.waitAnswers) ? step.waitAnswers : []
1503
+ step.waitAnswers.push({ round: stampRound, answer: stampAnswer, question: prevOutput || null, answeredAt: now })
1504
+ step.maxWaitRounds = stepDef.maxWaitRounds ?? step.maxWaitRounds
1505
+ step.output = prevOutput ? `${prevOutput} | ${stampAnswer}` : stampAnswer
1506
+ delete step.waitReason
1507
+ delete step.waitOptions
1508
+ delete step.waitedAt
1509
+ step.status = 'pending'
1510
+ step.completedAt = null
1511
+ }
1512
+
1486
1513
  progress.lastActive = now
1487
1514
  pm._write(cwd, progress, changeName)
1488
1515
  triggerSync(cwd, changeName, platformOpts)
1489
1516
 
1517
+ // 盖章路径的收尾提示独立分叉:wait 已在记录态完成(非等待用户),提示 --done 直接收尾;
1518
+ // 无 --inherit-from 路径输出与现状逐字节一致(兼容红线)。
1519
+ if (inheritFrom) {
1520
+ console.log(`✅ Step ${currentIdx + 1}/${stageData.steps.length} wait 已由决策继承盖章:${stageData.steps[currentIdx].name}`)
1521
+ console.log(` 盖章轮:由 ${inheritFrom} 继承确认(CLI 盖章)——wait 已完成记录,本步回到待执行`)
1522
+ console.log(` 后续 --done 不再要求 --answer,直接收尾:sillyspec run ${stageName} --done${changeName ? ` --change ${changeName}` : ''} --output "你的摘要"`)
1523
+ return
1524
+ }
1525
+
1490
1526
  console.log(`⏸️ Step ${currentIdx + 1}/${stageData.steps.length} 已暂停等待:${stageData.steps[currentIdx].name}`)
1491
1527
  if (waitReason) console.log(` 原因:${waitReason}`)
1492
1528
  if (waitOptions) console.log(` 选项:${formatWaitOptions(waitOptions)}`)