sillyspec 3.28.10 → 3.28.12

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 (52) hide show
  1. package/.claude/skills/sillyspec-execute/SKILL.md +223 -222
  2. package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
  3. package/.claude/skills/sillyspec-quick/SKILL.md +8 -10
  4. package/.claude/skills/sillyspec-verify/SKILL.md +91 -87
  5. package/.husky/pre-commit +4 -0
  6. package/.husky/pre-push +3 -0
  7. package/AGENTS.md +1 -1
  8. package/package.json +1 -1
  9. package/src/change-risk-profile.js +76 -6
  10. package/src/commit-guard.js +119 -0
  11. package/src/config-schema.js +23 -0
  12. package/src/design-facts.js +46 -9
  13. package/src/diagnostic-codes.js +95 -0
  14. package/src/docs-check.js +14 -10
  15. package/src/docs-debt.js +7 -2
  16. package/src/docs-gate.js +42 -15
  17. package/src/doctor-diagnostics.js +141 -1
  18. package/src/endpoint-extractor.js +50 -2
  19. package/src/foreign-declared.js +25 -16
  20. package/src/hooks/worktree-guard.js +75 -2
  21. package/src/index.js +129 -1
  22. package/src/machine-interface.js +63 -0
  23. package/src/probe7-anchor-check.js +20 -2
  24. package/src/progress/stage-machine.js +34 -5
  25. package/src/progress.js +1 -1
  26. package/src/quicklog.js +231 -1
  27. package/src/run/complete-handlers.js +172 -1
  28. package/src/run/complete.js +73 -3
  29. package/src/run/gate-snapshot.js +262 -0
  30. package/src/run/gates.js +41 -7
  31. package/src/run/prompt.js +79 -1
  32. package/src/run/shared.js +10 -0
  33. package/src/run/verify-quality-scan.js +151 -5
  34. package/src/stage-contract-spec.js +14 -0
  35. package/src/stage-contract.js +738 -8
  36. package/src/stage-review-checklist.js +16 -2
  37. package/src/stage-review.js +137 -2
  38. package/src/stages/archive.js +9 -5
  39. package/src/stages/brainstorm.js +7 -1
  40. package/src/stages/execute.js +62 -8
  41. package/src/stages/plan-postcheck.js +42 -5
  42. package/src/stages/plan.js +8 -1
  43. package/src/stages/quick.js +5 -5
  44. package/src/stages/verify.js +8 -3
  45. package/src/sync.js +69 -0
  46. package/src/task-review.js +9 -3
  47. package/src/verify-facts-schema.js +95 -10
  48. package/src/verify-postcheck.js +346 -28
  49. package/src/verify-probes.js +1221 -46
  50. package/src/worktree-apply.js +175 -3
  51. package/src/write-audit.js +55 -0
  52. package/.claude/full.json +0 -1
package/src/index.js CHANGED
@@ -76,6 +76,9 @@ SillySpec CLI — 规范驱动开发工具包
76
76
  sillyspec wt-commit [--change <名>] -m <信息> [--pathspec-from-file <f>] -- <path...>
77
77
  worktree 内 per-task 串行化提交(文件锁排队 + 强制显式 pathspec,
78
78
  防 add -A 卷入并行兄弟 WIP;worktree 内可省 --change,从 cwd 推断)
79
+ sillyspec quicklog commit [--change <quick会话ID>] -m <信息> [--ql <ql-id>]... [-- <额外pathspec...>]
80
+ 一键收编本会话 QUICKLOG 条目(切片提交:HEAD 基线+本会话条目块,
81
+ patches sidecar 一并带上;并行会话条目恢复为工作区未提交态,不夹带)
79
82
  sillyspec worktree <cmd> git worktree 隔离管理(execute 阶段相关)
80
83
  create <change> [--base <branch>] 创建隔离 worktree
81
84
  apply <change> [--check-only] 校验并应用变更到主工作区
@@ -167,6 +170,7 @@ SillySpec CLI — 规范驱动开发工具包
167
170
  sillyspec run plan --reopen --from-step 2 # 修订 plan,从第 2 步重做
168
171
  sillyspec run quick --non-interactive --done --output "CI 内的快修" # 脚本/CI
169
172
  sillyspec progress show
173
+ sillyspec progress show --all # 多变更汇总时无信号变更全量单行列出(默认折叠超 8 个)
170
174
  sillyspec progress show --json # 全局总览 envelope(面板/脚本消费)
171
175
  sillyspec worktree apply 2026-07-03-add-login
172
176
  `);
@@ -372,7 +376,7 @@ async function main() {
372
376
  process.exitCode = exitCode;
373
377
  break;
374
378
  }
375
- pm.show(progDir, progChangeName);
379
+ pm.show(progDir, progChangeName, { all: filteredArgs.includes('--all') });
376
380
  break;
377
381
  }
378
382
  case 'check': {
@@ -606,6 +610,84 @@ async function main() {
606
610
  // [--changed-files a,b](归属切片为空或纯验证任务时显式给出;""=空覆盖)
607
611
  // [--base <rev> --head <rev>](无 worktree meta 时显式锡点)[--force]
608
612
  const rwSub = filteredArgs[1];
613
+ // ── review status(C1 轻量硬机制,坑 review-liveness-opaque,2026-09-15 wp EHS 实证:
614
+ // 独立审查 94 分钟无收敛、用户三催——宿主/agent 无只读探针可查「审查到哪了、审完没有、
615
+ // 审完之后文档又改过几版」。agent-tool 通道的子代理进程归宿主管(CLI 无 kill 面),但
616
+ // 「状态可见」这一半可以闭环:各 stage review 的 verdict/通道/刷新序数/代落盘·降级标记/
617
+ // docHash 现势一行可见,供催问时一查即答、也供派发前查前序审查复用(C2 互补)。──
618
+ if (rwSub === 'status') {
619
+ const rsVal = (flag) => {
620
+ const i = filteredArgs.indexOf(flag);
621
+ return i >= 0 && filteredArgs[i + 1] && !String(filteredArgs[i + 1]).startsWith("--") ? filteredArgs[i + 1] : null;
622
+ };
623
+ const rsChange = rsVal('--change');
624
+ if (!rsChange) {
625
+ console.error('用法: sillyspec review status --change <名> [--json]\n 只读查询各阶段 stage review 状态:verdict/执行通道/刷新序数/代落盘·降级标记/docHash 现势(主文档改版未刷新时提示)');
626
+ process.exit(2);
627
+ }
628
+ assertSafeChangeName(rsChange, '--change 变更名');
629
+ const { getLatestStageReviewRunId, computeDocHash, isDegradedSelfReview, hasDelegatedWriteDisclosure, classifyReviewerChannel } = await import('./stage-review.js');
630
+ const { resolveRuntimeRoot } = await import('./run/shared.js');
631
+ const rsPlatformOpts = {};
632
+ const rsResolved = resolvePlatformOpts(dir, specDir);
633
+ if (rsResolved) {
634
+ rsPlatformOpts.specRoot = rsResolved.specRoot;
635
+ if (rsResolved.runtimeRoot) rsPlatformOpts.runtimeRoot = rsResolved.runtimeRoot;
636
+ }
637
+ const rsSpecBase = rsPlatformOpts.specRoot || join(dir, '.sillyspec');
638
+ const rsRuntimeRoot = resolveRuntimeRoot(rsPlatformOpts, rsSpecBase);
639
+ const rows = [];
640
+ for (const st of ['brainstorm', 'plan', 'execute']) {
641
+ let runId = null;
642
+ try { runId = getLatestStageReviewRunId(rsRuntimeRoot, st, rsChange) } catch {}
643
+ if (!runId) { rows.push({ stage: st, status: 'none' }); continue }
644
+ const rjPath = join(rsRuntimeRoot, 'stage-reviews', `${st}-${runId}`, 'review.json');
645
+ let rev = null;
646
+ try { rev = JSON.parse(readFileSync(rjPath, 'utf8')) } catch {
647
+ rows.push({ stage: st, status: 'unreadable', runId, reviewPath: rjPath }); continue
648
+ }
649
+ const notes = typeof rev.reviewerNotes === 'string' ? rev.reviewerNotes : '';
650
+ const refreshCount = (notes.match(/docHash auto-refreshed at /g) || []).length;
651
+ // docHash 现势:主文档在 [specBase, changeDir, cwd] 基准解析后重算对比
652
+ let docState = 'unknown';
653
+ const primaryRel = Array.isArray(rev.reviewedFiles) ? rev.reviewedFiles[0] : null;
654
+ if (primaryRel) {
655
+ for (const base of [rsSpecBase, join(rsSpecBase, 'changes', rsChange), dir]) {
656
+ const abs = join(base, primaryRel);
657
+ if (existsSync(abs)) {
658
+ docState = computeDocHash(abs) === rev.docHash ? 'match' : 'stale(改版未刷新——gate 将自动重算放行,verdict 续用需人工确认)';
659
+ break
660
+ }
661
+ }
662
+ if (docState === 'unknown') docState = '主文档缺失';
663
+ }
664
+ rows.push({
665
+ stage: st, status: 'present', runId, reviewPath: rjPath,
666
+ specVerdict: rev.specVerdict, qualityVerdict: rev.qualityVerdict,
667
+ channel: classifyReviewerChannel(rev),
668
+ degradedSelf: isDegradedSelfReview(rev), delegatedWrite: hasDelegatedWriteDisclosure(rev),
669
+ refreshCount, docState,
670
+ });
671
+ }
672
+ if (json) {
673
+ process.stdout.write(JSON.stringify({ ok: true, command: 'review status', change: rsChange, stages: rows }) + '\n');
674
+ } else {
675
+ console.log(`📋 stage review 状态 [${rsChange}]:`);
676
+ for (const r of rows) {
677
+ if (r.status === 'none') { console.log(` ⬜ ${r.stage}: 无 review 记录`); continue }
678
+ if (r.status === 'unreadable') { console.log(` ⚠️ ${r.stage}: review.json 不可读(${r.reviewPath})`); continue }
679
+ const flags = [
680
+ r.channel !== 'agent-tool' && r.channel !== 'platform' ? `通道=${r.channel}` : null,
681
+ r.degradedSelf ? '⚠️降级自审' : null,
682
+ r.delegatedWrite ? '⚠️代落盘' : null,
683
+ r.refreshCount > 0 ? `已刷新×${r.refreshCount}${r.refreshCount >= 2 ? '(反复改版,人工确认结论续用)' : ''}` : null,
684
+ r.docState !== 'match' && r.docState !== 'unknown' ? `docHash=${r.docState}` : null,
685
+ ].filter(Boolean).join(',');
686
+ console.log(` ${r.specVerdict === 'pass' && r.qualityVerdict === 'pass' ? '✅' : '❌'} ${r.stage}: spec=${r.specVerdict} quality=${r.qualityVerdict}(${r.channel}${flags ? ';' + flags : ''})`);
687
+ }
688
+ }
689
+ break;
690
+ }
609
691
  if (rwSub !== 'write') {
610
692
  console.error('用法: sillyspec review write --change <名> --task task-NN --spec <pass|fail|cannot_verify> --quality <verdict>\n [--notes "评审备注"] [--evidence "cannot_verify 证据说明"] [--changed-files a,b] [--base <rev> --head <rev>] [--force] [--json]\n 单 task review.json 命令式写入:verdict/notes 由你给,executeRunId/base/head/changedFiles/diffPaths 由 CLI 从 git + task 卡代算;已存在默认拒覆盖(--force 越过)');
611
693
  process.exit(2);
@@ -1979,6 +2061,8 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
1979
2061
  console.log(` marker → ${result.markerPath}`);
1980
2062
  if (result.mode === 'refreshed') {
1981
2063
  console.log(` docHash 已按当前 ${result.mainDoc} 重算(verdict 结论是否仍适用需人工确认)`);
2064
+ } else if (result.mode === 'noop-unchanged') {
2065
+ console.log(` docHash 已与当前 ${result.mainDoc} 一致,跳过重写(幂等 no-op,不动文件)`);
1982
2066
  } else {
1983
2067
  console.log(` 下一步:独立审查子代理对照 ${result.mainDoc} 填 verdict/checklist 后重跑 --done`);
1984
2068
  }
@@ -2697,6 +2781,50 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
2697
2781
  }
2698
2782
  break;
2699
2783
  }
2784
+ case 'quicklog': {
2785
+ // known-issues ④ v1(2026-09-16):QUICKLOG 多会话条目交织——本会话提交 git add 整文件
2786
+ // 会夹带并行会话未完成条目,只能手工四步舞(备份→剥离→commit→恢复)。本命令一键机制化:
2787
+ // 切片(HEAD 基线+本会话条目块)→ 显式 pathspec 提交(QUICKLOG 文件+patches sidecar+额外
2788
+ // pathspec)→ 恢复工作区(并行条目留未提交态)。参数面照抄 wt-commit 先例(--change/-m/--
2789
+ // pathspec)+ 可重复 --ql(guard 缺失/损坏时的显式逃生门;含已取消条目)。
2790
+ if (filteredArgs[1] !== 'commit') {
2791
+ console.error(`❌ 未知 quicklog 子命令: ${filteredArgs[1] || '(空)'}——当前支持: sillyspec quicklog commit [--change <quick会话ID>] -m <信息> [--ql <ql-id>]... [-- <额外pathspec...>]`);
2792
+ process.exit(1);
2793
+ }
2794
+ const qlRest = filteredArgs.slice(2);
2795
+ const qlDd = qlRest.indexOf('--');
2796
+ const qlFlags = qlDd === -1 ? qlRest : qlRest.slice(0, qlDd);
2797
+ const qlExtra = qlDd === -1 ? [] : qlRest.slice(qlDd + 1);
2798
+ let qlChange = null;
2799
+ let qlMessage = null;
2800
+ const qlIds = [];
2801
+ for (let i = 0; i < qlFlags.length; i++) {
2802
+ if (qlFlags[i] === '--change' && qlFlags[i + 1]) qlChange = qlFlags[++i];
2803
+ else if ((qlFlags[i] === '-m' || qlFlags[i] === '--message') && qlFlags[i + 1]) qlMessage = qlFlags[++i];
2804
+ else if (qlFlags[i] === '--ql' && qlFlags[i + 1]) qlIds.push(qlFlags[++i]);
2805
+ }
2806
+ try {
2807
+ const { resolvePlatformSpecDir, resolvePlatformOpts } = await import('./progress.js');
2808
+ const qlSpecBase = resolvePlatformSpecDir(dir, specDir);
2809
+ const { resolveQuickSessionsDir } = await import('./run/shared.js');
2810
+ const qlSessionsDir = resolveQuickSessionsDir(resolvePlatformOpts(dir, specDir), qlSpecBase);
2811
+ let qlUser = 'unknown';
2812
+ try { qlUser = (await import('./git-helper.js')).git(dir, ['config', 'user.name']) || 'unknown'; } catch {}
2813
+ const { runQuicklogCommit } = await import('./quicklog.js');
2814
+ const r = await runQuicklogCommit({
2815
+ specBase: qlSpecBase, cwd: dir, gitUser: qlUser, qlIds,
2816
+ changeName: qlChange, sessionsDir: qlSessionsDir,
2817
+ message: qlMessage, extraPathspecs: qlExtra,
2818
+ });
2819
+ if (json) console.log(JSON.stringify({ command: 'quicklog commit', ...r }, null, 2));
2820
+ else if (r.skipped) console.log(`⏭️ quicklog commit 无新内容跳过(HEAD 不动):${r.shortHead}(条目均已收编:${(r.skippedQlIds || []).join(', ') || '(无)'})`);
2821
+ else console.log(`✅ quicklog commit 完成:${r.shortHead}(${r.files.length} 文件;收编 ${(r.committedQlIds || []).join(', ')})\n 并行会话条目已恢复为工作区未提交态;备份:${(r.backupPaths || []).join(' , ')}`);
2822
+ } catch (e) {
2823
+ console.error(`❌ quicklog commit 失败:${e.message}`);
2824
+ process.exit(1);
2825
+ }
2826
+ break;
2827
+ }
2700
2828
  case 'worktree': {
2701
2829
  const { WorktreeManager } = await import('./worktree.js');
2702
2830
  const { ProgressManager, resolveSessionIdentity } = await import('./progress.js');
@@ -22,6 +22,7 @@ import { resolveRuntimeRoot } from './run/shared.js';
22
22
  import { runValidators, checkTransition, checkExecuteCodeEvidence } from './stage-contract.js';
23
23
  import { validateTaskReviews, resolveLatestExecuteRunId } from './task-review.js';
24
24
  import { runVerifyTestCheck } from './verify-postcheck.js';
25
+ import { checkCode } from './diagnostic-codes.js';
25
26
 
26
27
  // ============ 退出码常量(D-004@v1)============
27
28
 
@@ -52,6 +53,9 @@ export const SCHEMA_VERSION = 1;
52
53
  * @param {string[]} [fields.warnings=[]]
53
54
  * @param {Array<object>|undefined} [fields.checks]
54
55
  * @param {object|undefined} [fields.data]
56
+ * @param {string[]|undefined} [fields.codes] - 诊断码聚合(2026-09-17-mi-diagnostic-codes
57
+ * 加法式:失败 check 的 code 按出现序去重;信封级错误路径单码。可选键遵循 optional-once:
58
+ * !== undefined 才挂。errors 仍 string[],中文 message 不变,schema_version 仍 1。
55
59
  * @returns {object} 可直接 JSON.stringify 的 envelope 对象
56
60
  */
57
61
  export function buildEnvelope({
@@ -64,6 +68,7 @@ export function buildEnvelope({
64
68
  warnings = [],
65
69
  checks,
66
70
  data,
71
+ codes,
67
72
  }) {
68
73
  const envelope = {
69
74
  schema_version: SCHEMA_VERSION,
@@ -80,6 +85,7 @@ export function buildEnvelope({
80
85
  if (facet !== undefined) envelope.facet = facet;
81
86
  if (checks !== undefined) envelope.checks = checks;
82
87
  if (data !== undefined) envelope.data = data;
88
+ if (codes !== undefined) envelope.codes = codes;
83
89
 
84
90
  return envelope;
85
91
  }
@@ -130,6 +136,7 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot, s
130
136
  change: changeName,
131
137
  ok: false,
132
138
  errors: [`无法核验:进度库不存在(${dbPath})——变更 ${changeName} 尚未运行过任何阶段,只读 gate 不会为其建库`],
139
+ codes: ['db_missing'],
133
140
  });
134
141
  return { envelope, exitCode: EXIT_UNKNOWN };
135
142
  }
@@ -143,6 +150,7 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot, s
143
150
  change: changeName,
144
151
  ok: false,
145
152
  errors: [`变更不存在: ${changeName}`],
153
+ codes: ['change_not_found'],
146
154
  });
147
155
  return { envelope, exitCode: EXIT_UNKNOWN };
148
156
  }
@@ -163,11 +171,40 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot, s
163
171
  });
164
172
  checks.push({
165
173
  id: 'artifacts',
174
+ code: checkCode('artifacts'),
166
175
  ok: r.ok,
167
176
  errors: r.errors || [],
168
177
  warnings: r.warnings || [],
169
178
  });
170
179
 
180
+ // ── a2. design 文件清单行级核验(brainstorm;坑 design-file-ref-late-feedback,2026-09-15
181
+ // wp EHS 会话实证:41 条 design_file_ref_invalid 只在 --done 末步爆出,gate 预检全绿——
182
+ // 前移进 gate 让写作期预检即可见,免「末步拦→改文档→重跑 --done」整轮往返。与 complete.js
183
+ // --done 硬门同源实现(design-facts.js validateDesignFileList),gate 只读不写)──
184
+ if (stage === 'brainstorm') {
185
+ try {
186
+ const { validateDesignFileList } = await import('./design-facts.js');
187
+ const dfl = validateDesignFileList({ changeDir: join(specRoot, 'changes', changeName), cwd });
188
+ checks.push({
189
+ id: 'design-file-list',
190
+ code: checkCode('design-file-list'),
191
+ ok: (dfl.errors || []).length === 0,
192
+ // errors 元素是 {path, message} 对象(与 complete.js --done 打印侧同源),gate check 面拍平为 message 字符串
193
+ errors: (dfl.errors || []).map(e => e && e.message ? e.message : String(e)),
194
+ warnings: dfl.warnings || [],
195
+ });
196
+ } catch (e) {
197
+ // fail-open 与 complete.js 同口径:核验自身异常不误拦,warning 留痕
198
+ checks.push({
199
+ id: 'design-file-list',
200
+ code: checkCode('design-file-list'),
201
+ ok: true,
202
+ errors: [],
203
+ warnings: [`design 清单核验自身异常,跳过(${e && e.message ? e.message : e})`],
204
+ });
205
+ }
206
+ }
207
+
171
208
  // ── b. transition:状态转换合法性(参与综合 ok,与 completeStep 硬阻断一致)──
172
209
  // 与 run.js runStage 同源:传 fromStageData 触发 failed_post_check 门控。
173
210
  // transition 必须参与 ok:否则 gate 返回 ok=true、exit 0,Agent 据此 --done 却被
@@ -176,6 +213,7 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot, s
176
213
  const t = checkTransition(currentStage, stage, fromStageData ? { fromStageData } : {});
177
214
  checks.push({
178
215
  id: 'transition',
216
+ code: checkCode('transition'),
179
217
  ok: t.allowed,
180
218
  errors: t.allowed ? [] : [t.reason].filter(Boolean),
181
219
  warnings: [],
@@ -188,6 +226,7 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot, s
188
226
  const ev = sharedEvidence;
189
227
  checks.push({
190
228
  id: 'execute-evidence',
229
+ code: checkCode('execute-evidence'),
191
230
  ok: ev.status !== 'unchanged',
192
231
  errors: ev.status === 'unchanged' ? [`base..head 无代码变更: ${ev.detail}`] : [],
193
232
  warnings: ev.status === 'unknown' ? [`无法判定代码变更: ${ev.detail}`] : [],
@@ -241,6 +280,7 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot, s
241
280
  });
242
281
  checks.push({
243
282
  id: 'task-reviews',
283
+ code: checkCode('task-reviews'),
244
284
  ok: tr.ok,
245
285
  errors: tr.errors || [],
246
286
  warnings: tr.warnings || [],
@@ -265,6 +305,7 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot, s
265
305
  }
266
306
  checks.push({
267
307
  id: 'verify-test',
308
+ code: checkCode('verify-test'),
268
309
  ok: vt.status !== 'failed',
269
310
  errors: vt.status === 'failed' ? [`测试失败: ${vt.reason || ''}`] : [],
270
311
  warnings: vtWarnings,
@@ -283,6 +324,10 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot, s
283
324
  const ok = checks.filter((c) => !c.informational).every((c) => c.ok);
284
325
  const exitCode = ok ? EXIT_OK : EXIT_BLOCKED;
285
326
 
327
+ // 诊断码聚合(加法式):失败 check 的 code 按出现序(push 序)去重;成功为空数组。
328
+ // 非 1:1:多条 errors 可共享一个 check 级码,逐条归因请读 checks[].code。
329
+ const codes = [...new Set(checks.filter((c) => !c.ok && c.code).map((c) => c.code))];
330
+
286
331
  const errors = [];
287
332
  const warnings = [];
288
333
  for (const c of checks) {
@@ -298,6 +343,7 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot, s
298
343
  errors,
299
344
  warnings,
300
345
  checks,
346
+ codes,
301
347
  });
302
348
 
303
349
  return { envelope, exitCode };
@@ -309,6 +355,7 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot, s
309
355
  change: changeName,
310
356
  ok: false,
311
357
  errors: [`internal: ${e.message}`],
358
+ codes: ['internal_error'],
312
359
  });
313
360
  return { envelope, exitCode: EXIT_UNKNOWN };
314
361
  }
@@ -353,6 +400,7 @@ export async function runDerive(facet, changeName, { cwd, specBase, runtimeRoot,
353
400
  change: changeName,
354
401
  ok: false,
355
402
  errors: [`非法 facet: ${facet},合法值: ${FACETS.join(', ')}`],
403
+ codes: ['unknown_facet'],
356
404
  });
357
405
  return { envelope, exitCode: EXIT_UNKNOWN };
358
406
  }
@@ -371,6 +419,7 @@ export async function runDerive(facet, changeName, { cwd, specBase, runtimeRoot,
371
419
  change: changeName,
372
420
  ok: false,
373
421
  errors: [`无法核验:进度库不存在(${dbPath})——变更 ${changeName} 尚未运行过任何阶段,只读 derive 不会为其建库`],
422
+ codes: ['db_missing'],
374
423
  });
375
424
  return { envelope, exitCode: EXIT_UNKNOWN };
376
425
  }
@@ -384,6 +433,7 @@ export async function runDerive(facet, changeName, { cwd, specBase, runtimeRoot,
384
433
  change: changeName,
385
434
  ok: false,
386
435
  errors: [`变更不存在: ${changeName}`],
436
+ codes: ['change_not_found'],
387
437
  });
388
438
  return { envelope, exitCode: EXIT_UNKNOWN };
389
439
  }
@@ -511,12 +561,20 @@ export async function runDerive(facet, changeName, { cwd, specBase, runtimeRoot,
511
561
  change: changeName,
512
562
  ok: false,
513
563
  errors: [`非法 facet: ${facet},合法值: ${FACETS.join(', ')}`],
564
+ codes: ['unknown_facet'],
514
565
  });
515
566
  return { envelope, exitCode: EXIT_UNKNOWN };
516
567
  }
517
568
  }
518
569
 
519
570
  // stage 仅 artifacts 时出现(产物校验绑定阶段语义);其余 facet 不传 stage。
571
+ // 诊断码(加法式):facet 失败面单码(execute-test 类 facet 失败即对应码),成功为空数组。
572
+ const FACET_FAILURE_CODE = {
573
+ 'execute-evidence': 'execute_evidence_unchanged',
574
+ 'verify-test': 'verify_test_failed',
575
+ 'task-reviews': 'task_reviews_invalid',
576
+ artifacts: 'artifacts_invalid',
577
+ };
520
578
  const envelope = buildEnvelope({
521
579
  command: 'derive',
522
580
  facet,
@@ -525,6 +583,7 @@ export async function runDerive(facet, changeName, { cwd, specBase, runtimeRoot,
525
583
  errors,
526
584
  warnings,
527
585
  data,
586
+ codes: ok ? [] : [FACET_FAILURE_CODE[facet]].filter(Boolean),
528
587
  stage: facet === 'artifacts' ? currentStage : undefined,
529
588
  });
530
589
 
@@ -537,6 +596,7 @@ export async function runDerive(facet, changeName, { cwd, specBase, runtimeRoot,
537
596
  change: changeName,
538
597
  ok: false,
539
598
  errors: [`internal: ${e.message}`],
599
+ codes: ['internal_error'],
540
600
  });
541
601
  return { envelope, exitCode: EXIT_UNKNOWN };
542
602
  }
@@ -571,6 +631,7 @@ export function runStatusOverview({ cwd, specBase } = {}) {
571
631
  command: 'progress show',
572
632
  ok: false,
573
633
  errors: [`无法核验:进度库不存在(${dbPath})——项目尚未初始化进度,只读总览不会为其建库`],
634
+ codes: ['db_missing'],
574
635
  });
575
636
  return { envelope, exitCode: EXIT_UNKNOWN };
576
637
  }
@@ -592,6 +653,7 @@ export function runStatusOverview({ cwd, specBase } = {}) {
592
653
  ok: true,
593
654
  warnings,
594
655
  data,
656
+ codes: [],
595
657
  });
596
658
  return { envelope, exitCode: EXIT_OK };
597
659
  } catch (e) {
@@ -600,6 +662,7 @@ export function runStatusOverview({ cwd, specBase } = {}) {
600
662
  command: 'progress show',
601
663
  ok: false,
602
664
  errors: [`internal: ${e.message}`],
665
+ codes: ['internal_error'],
603
666
  });
604
667
  return { envelope, exitCode: EXIT_UNKNOWN };
605
668
  }
@@ -4,7 +4,18 @@
4
4
  * 坑 probe7-covered-anchor-missing(2026-09-15 复盘实证:探针7 矩阵第一轮 7 行 covered 判定的
5
5
  * 证据列缺 file:line 锚点被审查打回——预填说明写着「证据列给首命中 file:line 锚点」但模板要求
6
6
  * 没细读就交,人工往返一轮)。本模块把该口径机器化:verify --done 时点解析正文探针7 段,
7
- * 判定列=covered 的行必须含 `path:line` 形态锚点,缺则 advisory 提示回补。
7
+ * 判定列=covered 的行必须含 file:line / `.test.` 测试文件名 / 反引号包裹路径或测试名三形态
8
+ * 之一,缺则 advisory 提示回补。
9
+ *
10
+ * 口径对齐(2026-09-16-friction5-hardening FR-05 / D-005@v1,用户 2026-09-16 驾驭小结⑤):
11
+ * 锚点认 file:line 或 `.test.` 文件名两形态(对齐 stage-contract 硬门三形态中的两形态)——
12
+ * 行号随提交漂移场景 `.test.` 文件名锚过硬门仍被 advisory 提示回补是无效摩擦。file:line
13
+ * 侧本判定(/:\d+\b/)宽于 stage-contract 硬门形态集,advisory 宽于硬门是设计容差。
14
+ *
15
+ * 口径再对齐(2026-09-17-feedback-hardening FR-02 / D-005@v2,supersedes D-005@v1 的
16
+ * 「裸反引号不收」,用户 2026-09-17 驾驭小结②):反引号包裹的路径/测试名与硬门
17
+ * matrixEvidenceHasAnchor 同权收认——硬门已保证锚点存在,advisory 多要行号在行号漂移
18
+ * 场景零正确性收益纯摩擦。
8
19
  *
9
20
  * 定位与边界:
10
21
  * - **advisory 不阻断**:covered 行缺锚点只 warn(证据可以是合法的人工核验形态,但 covered=
@@ -20,6 +31,10 @@
20
31
  // file:line 锚点:冒号后跟数字(`test/x.test.mjs:42`、`src/a.py:12-18` 均命中)
21
32
  const ANCHOR_RE = /:\d+\b/
22
33
 
34
+ // 反引号包裹的路径/测试名(`test/x.mjs`、`src/plain.js` 等——D-005@v2 第三形态,
35
+ // 与 stage-contract 硬门 matrixEvidenceHasAnchor 的 /`[^`]+`/ 同权)
36
+ const BACKTICK_RE = /`[^`]+`/
37
+
23
38
  // 判定枚举纯值(骨架四枚举;容忍反引号包裹——门禁层 extractAcceptanceMatrixSlots 另有纯值校验)
24
39
  function normalizeVerdict(cell) {
25
40
  return String(cell || '').trim().replace(/^`|`$/g, '').toLowerCase()
@@ -57,7 +72,10 @@ export function checkProbe7AnchorCoverage(reportText) {
57
72
  if (verdict !== 'covered') continue
58
73
  out.coveredRows++
59
74
  const evidence = cells[5] || ''
60
- if (!ANCHOR_RE.test(evidence)) {
75
+ // 三形态锚点(FR-05 / D-005@v2):file:line(ANCHOR_RE,本体不动)、`.test.` 测试文件名、
76
+ // 反引号包裹路径/测试名(BACKTICK_RE)——三者对齐 stage-contract 硬门形态,消灭行号漂移
77
+ // 场景的无效回补提示
78
+ if (!ANCHOR_RE.test(evidence) && !/\.test\./.test(evidence) && !BACKTICK_RE.test(evidence)) {
61
79
  out.missingAnchors.push({
62
80
  task: task || '(未知 task)',
63
81
  acceptance: (cells[1] || '').slice(0, 80),
@@ -185,7 +185,7 @@ export class StageMachine {
185
185
  console.log(`✅ 阶段 ${stage} 已标记为完成(不自动推进,下一步由你决定)`);
186
186
  }
187
187
 
188
- show(cwd, changeName = null) {
188
+ show(cwd, changeName = null, opts = {}) {
189
189
  // 如果指定了变更名,只显示该变更
190
190
  if (changeName) {
191
191
  return this._showChange(cwd, changeName);
@@ -218,20 +218,36 @@ export class StageMachine {
218
218
  const changesRoot = join(this.pm._getSpecDir(cwd), 'changes');
219
219
  // P2-2-①:未决同步冲突标红(overview 同源 _listPendingConflicts;该变更命中 → 🔴 行)
220
220
  const pendingConflicts = this._listPendingConflicts(this.pm._getSpecDir(cwd));
221
+ // 折叠显示(2026-09-17 用户反馈⑥-③,ql-20260917-005):29 个滞留变更的全量逐条输出
222
+ // (每条 3-5 行 + 空行)把真正需要看的信号淹掉。口径:**有信号的变更**(未决冲突/滞留/
223
+ // 疑似完成/目录缺失/无法读取)保持原有详情渲染——它们就是该看的东西;**无信号变更**压成
224
+ // 单行,且超过 CLEAN_SHOW_CAP 个时折叠为计数行(--all 展开全量单行;单变更详情恒走
225
+ // --change <名>)。--json envelope(机器面)不受影响。
226
+ const CLEAN_SHOW_CAP = 8;
227
+ const withSignals = [];
228
+ const clean = [];
221
229
  for (const cn of changes) {
222
230
  const data = this.pm.read(cwd, cn);
223
231
  const dirMissing = _isGhostChange(changesRoot, cn);
232
+ const cnConflicts = pendingConflicts.filter(c => c.change === cn);
233
+ const stall = data ? this._stallSignal(cwd, data, cn) : null;
234
+ if (!data || dirMissing || cnConflicts.length > 0 || stall) {
235
+ withSignals.push({ cn, data, dirMissing, cnConflicts, stall });
236
+ } else {
237
+ clean.push({ cn, data });
238
+ }
239
+ }
240
+
241
+ for (const { cn, data, dirMissing, cnConflicts, stall } of withSignals) {
224
242
  if (!data) {
225
243
  console.log(` 📂 ${cn} — (无法读取)${dirMissing ? ' ⚠️ 目录缺失(残留记录,sillyspec doctor --cleanup-ghosts --confirm 可归档清理)' : ''}`);
226
244
  continue;
227
245
  }
228
- const currentStage = data.currentStage || '(无)';
229
- const stageLabel = STAGE_LABELS[data.currentStage] || currentStage;
246
+ const stageLabel = STAGE_LABELS[data.currentStage] || data.currentStage || '(无)';
230
247
  const lastActive = data.lastActive ? this._timeAgo(data.lastActive) : '未知';
231
248
 
232
249
  console.log(` 📂 ${cn}${dirMissing ? ' ⚠️ 目录缺失(残留记录,sillyspec doctor --cleanup-ghosts --confirm 可归档清理)' : ''}`);
233
250
  // 变更级未决冲突标红(冲突可见性,2026-09-02 P2-2-①):不再靠 agent 自己撞上才发现
234
- const cnConflicts = pendingConflicts.filter(c => c.change === cn);
235
251
  if (cnConflicts.length > 0) {
236
252
  for (const cf of cnConflicts) {
237
253
  console.log(` 🔴 未决同步冲突(${cf.type === 'progress' ? '进度' : 'spec 树'}${cf.created_at ? `,${cf.created_at}` : ''})——请 sillyspec platform resolve 处理后再推进`);
@@ -239,7 +255,6 @@ export class StageMachine {
239
255
  }
240
256
  console.log(` 当前阶段: ${stageLabel} 最近活跃: ${lastActive}`);
241
257
  // 滞留/疑似完成信号(2026-08-30 用户反馈②):多变更汇总逐行透出,无需逐个 --change 看详情
242
- const stall = this._stallSignal(cwd, data, cn);
243
258
  if (stall) {
244
259
  console.log(` ⏳ ${stall.text}`);
245
260
  console.log(stall.kind === 'likely-complete'
@@ -249,6 +264,20 @@ export class StageMachine {
249
264
  console.log('');
250
265
  }
251
266
 
267
+ // 无信号变更:单行压缩(名 + 阶段 + 最近活跃),超帽折叠(--all 展开)
268
+ clean.sort((a, b) => String(b.data?.lastActive || '').localeCompare(String(a.data?.lastActive || '')));
269
+ const shown = opts.all ? clean : clean.slice(0, CLEAN_SHOW_CAP);
270
+ for (const { cn, data } of shown) {
271
+ const stageLabel = STAGE_LABELS[data.currentStage] || data.currentStage || '(无)';
272
+ const lastActive = data.lastActive ? this._timeAgo(data.lastActive) : '未知';
273
+ console.log(` 📂 ${cn} — ${stageLabel} · 最近活跃 ${lastActive}`);
274
+ }
275
+ if (!opts.all && clean.length > shown.length) {
276
+ console.log(` … 另有 ${clean.length - shown.length} 个无信号活跃变更已折叠(progress show --change <名> 看单变更详情;全量单行列出加 --all)`);
277
+ } else if (clean.length > 0) {
278
+ console.log('');
279
+ }
280
+
252
281
  // P2-2-① 补丁(2026-09-02 晚):全局未决冲突汇总段——逐变更行只显示「对上活跃变更」的冲突,
253
282
  // 挂在 ghost/非活跃变更上的冲突会漏(huber 面板联调实证 11 条只显示 0)。此处按
254
283
  // pending_conflicts 全量兜底显示,与 --json envelope 顶层字段同口径。
package/src/progress.js CHANGED
@@ -1163,7 +1163,7 @@ export class ProgressManager {
1163
1163
 
1164
1164
  completeStage(cwd, stage, changeName = null, opts = {}) { return this._stageMachine.completeStage(cwd, stage, changeName, opts); }
1165
1165
 
1166
- show(cwd, changeName = null) { return this._stageMachine.show(cwd, changeName); }
1166
+ show(cwd, changeName = null, opts = {}) { return this._stageMachine.show(cwd, changeName, opts); }
1167
1167
 
1168
1168
  // 只读全局总览(show 多变更汇总的机器版,2026-09-02 单一状态源):纯数据,machine-interface 包装
1169
1169
  overview(cwd) { return this._stageMachine.overview(cwd); }