codehere 0.7.1 → 1.0.1

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 (139) hide show
  1. package/README.md +22 -13
  2. package/dist/application/services/verdict-service.js +154 -0
  3. package/dist/application/use-cases/assess-cross-vendor.js +126 -0
  4. package/dist/application/use-cases/build-cross-vendor-verdict.js +118 -0
  5. package/dist/application/use-cases/export-aat.js +107 -0
  6. package/dist/application/use-cases/generate-acceptance-criteria.js +223 -0
  7. package/dist/application/use-cases/generate-final-report.js +140 -15
  8. package/dist/application/use-cases/generate-narration.js +5 -3
  9. package/dist/application/use-cases/generate-thread-title.js +5 -2
  10. package/dist/application/use-cases/narrator-prompt-template.js +1 -1
  11. package/dist/application/use-cases/plan-auto-review.js +57 -0
  12. package/dist/application/use-cases/reconcile-audit.js +100 -0
  13. package/dist/application/use-cases/rerun-tests.js +226 -0
  14. package/dist/application/use-cases/search-sessions.js +70 -0
  15. package/dist/application/use-cases/should-auto-review.js +71 -0
  16. package/dist/application/use-cases/thread-export.js +90 -0
  17. package/dist/application/use-cases/verify-claim.js +187 -5
  18. package/dist/domain/audit.js +11 -0
  19. package/dist/domain/services/codebase-detector.js +0 -6
  20. package/dist/domain/task.js +91 -0
  21. package/dist/error-handler.js +3 -115
  22. package/dist/index.js +279 -31
  23. package/dist/infrastructure/completion/bash-completion.js +7 -3
  24. package/dist/infrastructure/completion/fish-completion.js +7 -12
  25. package/dist/infrastructure/completion/zsh-completion.js +7 -12
  26. package/dist/infrastructure/config/config-manager.js +4 -3
  27. package/dist/infrastructure/config/product-context.js +1 -19
  28. package/dist/infrastructure/context/adapters/figma-source.js +37 -2
  29. package/dist/infrastructure/context/adapters/github-issue-source.js +3 -2
  30. package/dist/infrastructure/context/github-api.js +110 -0
  31. package/dist/infrastructure/context/github-device-flow.js +65 -0
  32. package/dist/infrastructure/context/oauth-store.js +5 -2
  33. package/dist/infrastructure/context/registry-bootstrap.js +0 -4
  34. package/dist/infrastructure/cost/budget-config.js +4 -3
  35. package/dist/infrastructure/cost/cost-router.js +16 -23
  36. package/dist/infrastructure/cost/spend-aggregator.js +91 -16
  37. package/dist/infrastructure/cost/team-ledger.js +355 -0
  38. package/dist/infrastructure/discovery/discovery-log.js +85 -0
  39. package/dist/infrastructure/hooks/audit-tip-signature.js +336 -0
  40. package/dist/infrastructure/hooks/claude-sdk-hook.js +139 -3
  41. package/dist/infrastructure/hooks/hook-mode.js +20 -4
  42. package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +8 -2
  43. package/dist/infrastructure/hooks/pre-prompt-scan.js +3 -3
  44. package/dist/infrastructure/observability/execution-tracer.js +1 -1
  45. package/dist/infrastructure/orchestration/agents-config-seed.js +23 -11
  46. package/dist/infrastructure/orchestration/aider-adapter.js +188 -24
  47. package/dist/infrastructure/orchestration/artifacts-extractor.js +60 -2
  48. package/dist/infrastructure/orchestration/cassette-recorder.js +1 -1
  49. package/dist/infrastructure/orchestration/claude-code-adapter.js +117 -3
  50. package/dist/infrastructure/orchestration/codex-adapter.js +52 -16
  51. package/dist/infrastructure/orchestration/conductor-adapter.js +46 -7
  52. package/dist/infrastructure/orchestration/context-handoff.js +32 -14
  53. package/dist/infrastructure/orchestration/detect-ollama.js +75 -0
  54. package/dist/infrastructure/orchestration/failover-config.js +1 -11
  55. package/dist/infrastructure/orchestration/gemini-adapter.js +92 -12
  56. package/dist/infrastructure/orchestration/generic-cli-adapter.js +134 -3
  57. package/dist/infrastructure/orchestration/graphify-context.js +14 -59
  58. package/dist/infrastructure/orchestration/memory-injector.js +4 -1
  59. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +167 -9
  60. package/dist/infrastructure/orchestration/opencode-adapter.js +84 -14
  61. package/dist/infrastructure/orchestration/progress-aggregator.js +275 -23
  62. package/dist/infrastructure/orchestration/session-manager.js +593 -94
  63. package/dist/infrastructure/orchestration/session-persistence.js +177 -41
  64. package/dist/infrastructure/orchestration/thread-key-store.js +1 -1
  65. package/dist/infrastructure/orchestration/write-pass-store.js +0 -8
  66. package/dist/infrastructure/protocols/cloud-sync.js +2 -2
  67. package/dist/infrastructure/protocols/mcp-helpers.js +1 -1
  68. package/dist/infrastructure/protocols/mcp-server-impl.js +1498 -354
  69. package/dist/infrastructure/protocols/mcp-tools.js +401 -0
  70. package/dist/infrastructure/protocols/ui/app-script-artifacts.js +121 -0
  71. package/dist/infrastructure/protocols/ui/app-script-audit-footer.js +44 -0
  72. package/dist/infrastructure/protocols/ui/app-script-audit-verify.js +56 -0
  73. package/dist/infrastructure/protocols/ui/app-script-composer-cost.js +588 -0
  74. package/dist/infrastructure/protocols/ui/app-script-context-sources.js +168 -0
  75. package/dist/infrastructure/protocols/ui/app-script-conversation.js +67 -0
  76. package/dist/infrastructure/protocols/ui/app-script-drawer.js +115 -0
  77. package/dist/infrastructure/protocols/ui/app-script-failover.js +133 -0
  78. package/dist/infrastructure/protocols/ui/app-script-github.js +113 -0
  79. package/dist/infrastructure/protocols/ui/app-script-observer.js +104 -0
  80. package/dist/infrastructure/protocols/ui/app-script-onboarding.js +80 -0
  81. package/dist/infrastructure/protocols/ui/app-script-overlay.js +203 -0
  82. package/dist/infrastructure/protocols/ui/app-script-recovery.js +136 -0
  83. package/dist/infrastructure/protocols/ui/app-script-render.js +166 -0
  84. package/dist/infrastructure/protocols/ui/app-script-share.js +78 -0
  85. package/dist/infrastructure/protocols/ui/app-script-sidebar.js +194 -0
  86. package/dist/infrastructure/protocols/ui/app-script-tour.js +78 -0
  87. package/dist/infrastructure/protocols/ui/app-script-wpass.js +312 -0
  88. package/dist/infrastructure/protocols/ui/app-script.js +2998 -0
  89. package/dist/infrastructure/protocols/ui/font-faces.js +7 -0
  90. package/dist/infrastructure/protocols/ui/styles.js +2147 -0
  91. package/dist/infrastructure/protocols/ui/tokens.js +96 -0
  92. package/dist/infrastructure/protocols/ui-page-v2.js +116 -4126
  93. package/dist/infrastructure/protocols/verify-page.js +884 -148
  94. package/dist/infrastructure/security/fix-code.js +144 -0
  95. package/dist/infrastructure/security/fix-secret.js +89 -0
  96. package/dist/infrastructure/security/permissions-config.js +3 -2
  97. package/dist/infrastructure/security/scan-types.js +10 -0
  98. package/dist/infrastructure/security/ship-scan.js +195 -0
  99. package/dist/infrastructure/security/trust-scorer.js +70 -3
  100. package/dist/infrastructure/security/vibe-checks.js +99 -0
  101. package/dist/infrastructure/storage/atomic-write.js +86 -0
  102. package/dist/infrastructure/storage/project-thread-mirror.js +151 -4
  103. package/dist/infrastructure/storage/project-thread-reader.js +262 -25
  104. package/dist/infrastructure/storage/project-thread-title-backfill.js +1 -1
  105. package/dist/monitoring.js +2 -2
  106. package/dist/presentation/cli/commands/audit-command.js +79 -4
  107. package/dist/presentation/cli/commands/budget-command.js +2 -2
  108. package/dist/presentation/cli/commands/check-command.js +96 -0
  109. package/dist/presentation/cli/commands/context-source-command.js +20 -1
  110. package/dist/presentation/cli/commands/help-command.js +5 -5
  111. package/dist/presentation/cli/commands/hook-command.js +5 -5
  112. package/dist/presentation/cli/commands/init-command.js +89 -66
  113. package/dist/presentation/cli/commands/memory-command.js +28 -28
  114. package/dist/presentation/cli/commands/permission-command.js +1 -12
  115. package/dist/presentation/cli/commands/protocols-builder.js +2 -2
  116. package/dist/presentation/cli/commands/protocols-command.js +4 -4
  117. package/dist/presentation/cli/commands/repo-map-builder.js +1 -1
  118. package/dist/presentation/cli/commands/report-command.js +48 -32
  119. package/dist/presentation/cli/commands/scan-command.js +52 -0
  120. package/dist/presentation/cli/commands/scan-tool-command.js +24 -8
  121. package/dist/presentation/cli/commands/search-command.js +51 -0
  122. package/dist/presentation/cli/commands/setup-command.js +31 -8
  123. package/dist/presentation/cli/commands/status-command.js +39 -86
  124. package/dist/presentation/cli/commands/team-command.js +256 -0
  125. package/dist/presentation/cli/commands/thread-command.js +87 -5
  126. package/dist/presentation/cli/commands/trust-command.js +53 -31
  127. package/dist/presentation/cli/commands/verify-command.js +74 -9
  128. package/dist/presentation/cli/error-display.js +0 -8
  129. package/dist/presentation/cli/repl.js +15 -7
  130. package/dist/presentation/cli/task-deliverable.js +36 -10
  131. package/dist/testing/replay-adapter.js +0 -19
  132. package/dist/ui.js +51 -239
  133. package/dist/utils/version.js +0 -6
  134. package/package.json +30 -10
  135. package/dist/domain/entities/task.js +0 -61
  136. package/dist/infrastructure/protocols/ui-page.js +0 -2377
  137. package/dist/infrastructure/security/input-validator.js +0 -152
  138. package/dist/infrastructure/storage/embeddings-path.js +0 -37
  139. package/dist/infrastructure/storage/task-repository.js +0 -206
@@ -22,122 +22,131 @@
22
22
  * highlight ring + caption (the visibleAct pattern) and can record one .webm
23
23
  * per fixture in-browser, or be driven headless by scripts/record-replay.mjs.
24
24
  */
25
+ import { violatesGitInvariant } from '../../application/use-cases/verify-claim.js';
26
+ import { DESIGN_TOKENS } from './ui/tokens.js';
27
+ import { FONT_FACES } from './ui/font-faces.js';
25
28
  export const VERIFY_HTML = `<!doctype html>
26
29
  <html lang="en">
27
30
  <head>
28
31
  <meta charset="utf-8" />
29
32
  <meta name="viewport" content="width=device-width, initial-scale=1" />
33
+ <link rel="icon" type="image/svg+xml" href="/icon.svg" />
30
34
  <title>codehere · /verify</title>
31
- <style>
32
- :root{
33
- --bg:hsl(0 0% 3.9%); --bg-1:hsl(0 0% 6%); --bg-2:hsl(0 0% 9%);
34
- --fg:hsl(0 0% 98%); --fg-70:rgba(255,255,255,.70); --fg-50:rgba(255,255,255,.50);
35
- --fg-40:rgba(255,255,255,.40); --fg-30:rgba(255,255,255,.30);
36
- --border:rgba(255,255,255,.10); --border-soft:rgba(255,255,255,.06);
37
- --accent:#ff9a76; --accent-wash:rgba(255,154,118,.10);
38
- --allow:#28c840; --block:#ff5f57; --warn:#febc2e; --info:#76b7ff;
39
- --radius:8px;
40
- --sans:Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
41
- --mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;
42
- }
35
+ <style>${FONT_FACES}${DESIGN_TOKENS}
36
+ /* /verify used to declare its own :root here: cool-gray surfaces from the
37
+ pre-v1 palette, Inter as the voice face, and macOS traffic lights for the
38
+ verdict colours (--allow was var(--verified)). That made three different "verified"
39
+ greens in one product, on a page linked directly from codehere start.
40
+ It now takes the same tokens and the same self-hosted faces as the
41
+ dashboard. DESIGN.md §4.5 always said it should. */
43
42
  *{box-sizing:border-box}
44
43
  body{margin:0;background:var(--bg);color:var(--fg);font-family:var(--sans);font-size:13px;line-height:1.55;-webkit-font-smoothing:antialiased;
45
44
  background-image:radial-gradient(circle,rgba(74,144,226,.045) 1px,transparent 1px);background-size:24px 24px;}
46
45
  a{color:inherit}
47
46
  header.top{display:flex;align-items:center;gap:14px;padding:10px 20px;border-bottom:1px solid var(--border-soft);background:rgba(0,0,0,.55);backdrop-filter:blur(8px);position:sticky;top:0;z-index:5;flex-wrap:wrap}
48
- .logo{font-size:15px;font-weight:700;display:inline-flex;align-items:center;gap:7px;letter-spacing:-.01em}
49
- .logo .dot{width:7px;height:7px;border-radius:50%;background:var(--accent);box-shadow:0 0 8px var(--accent);animation:pulse 2s infinite}
50
- @keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
47
+ .logo{font-size:14.5px;font-weight:700;display:inline-flex;align-items:center;gap:7px;letter-spacing:-.01em}
48
+ /* S16 brand-sync: gradient name only, no dot — the landing nav wordmark carries none */
51
49
  .logo .name{background:linear-gradient(135deg,#ff9a76 0%,#4a90e2 100%);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:transparent}
52
50
  .route{color:var(--fg-40);font-family:var(--mono);font-size:12px}
53
51
  .pill{font-size:10.5px;padding:3px 8px;border:1px solid var(--border);border-radius:999px;color:var(--fg-50)}
54
- .pill.local{border-color:rgba(40,200,64,.3);color:var(--allow)}
52
+ .pill.local{border-color:var(--border);color:var(--fg-70)}
55
53
  .spacer{flex:1}
56
- .hint{color:var(--fg-40);font-size:11.5px}
54
+ .hint{color:var(--fg-40);font-size:12px}
57
55
  .hint code{font-family:var(--mono);color:var(--info)}
58
56
  .wrap{max-width:1180px;margin:0 auto;padding:22px 20px 80px}
59
57
  .actions{display:flex;align-items:center;gap:12px;margin:18px 0 8px;flex-wrap:wrap}
60
- button.run{background:linear-gradient(135deg,#ff9a76,#ff7a4d);color:#1a0f08;border:0;font-weight:700;font-size:13px;padding:9px 18px;border-radius:var(--radius);cursor:pointer;box-shadow:0 0 0 1px rgba(255,154,118,.3),0 8px 24px -8px rgba(255,154,118,.5)}
61
- button.run:disabled{opacity:.6;cursor:default}
62
- .linkbtn{color:var(--fg-70);text-decoration:none;border:1px solid var(--border);padding:8px 14px;border-radius:var(--radius);font-size:12.5px;cursor:pointer;background:transparent}
58
+ /* S17 · same token system as the product page: ONE flat accent fill per
59
+ action row (no gradient, no glow), calm hover, visible focus ring.
60
+ Buttons don't inherit type by default — pull every control into the system. */
61
+ button,select,input{font:inherit}
62
+ button.run{background:var(--accent);color:#1a0f08;border:0;font-weight:600;font-size:13px;padding:9px 18px;border-radius:var(--radius);cursor:pointer;transition:filter var(--dur) var(--ease)}
63
+ button.run:hover{filter:brightness(1.06)}
64
+ button.run:disabled{opacity:.6;cursor:default;filter:none}
65
+ button.run:focus-visible,.linkbtn:focus-visible,.cell:focus-visible,.steprow:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
66
+ .linkbtn{color:var(--fg-70);text-decoration:none;border:1px solid var(--border);padding:8px 14px;border-radius:var(--radius);font-size:12px;cursor:pointer;background:transparent}
63
67
  .linkbtn:hover{border-color:var(--accent);color:var(--fg)}
64
68
  .tally{display:inline-flex;gap:14px;align-items:center;font-family:var(--mono);font-size:13px;margin-left:6px}
65
- .tally .p{color:var(--allow)} .tally .f{color:var(--block)} .tally .b{color:var(--fg-40)} .tally .t{color:var(--fg-50)}
69
+ .tally .p{color:var(--verified)} .tally .f{color:var(--caught)} .tally .b{color:var(--fg-40)} .tally .t{color:var(--fg-50)}
66
70
  .verdict-line{font-size:12px;margin:4px 0 0}
67
- .verdict-line.green{color:var(--allow)} .verdict-line.red{color:var(--block)}
68
- .legend{display:flex;gap:18px;margin:8px 0 0;font-size:11.5px;color:var(--fg-50);flex-wrap:wrap}
71
+ .verdict-line.green[data-evidence='measured']{color:var(--verified)} .verdict-line.red{color:var(--caught)}
72
+ .legend{display:flex;gap:18px;margin:8px 0 0;font-size:12px;color:var(--fg-50);flex-wrap:wrap}
69
73
  .legend b{font-family:var(--mono)}
70
- h2.section{font-size:11px;text-transform:uppercase;letter-spacing:.12em;color:var(--fg-40);margin:26px 0 10px;font-weight:600}
74
+ h2.section{font-size:12px;text-transform:uppercase;letter-spacing:.12em;color:var(--fg-40);margin:26px 0 10px;font-weight:600}
71
75
  .verifiers{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:6px}
72
- .vchip{border:1px solid var(--border);border-radius:999px;padding:4px 11px;font-size:11.5px;color:var(--fg-70)}
76
+ .vchip{border:1px solid var(--border);border-radius:999px;padding:4px 11px;font-size:12px;color:var(--fg-70)}
73
77
  .vchip code{font-family:var(--mono);color:var(--accent)}
74
- table.matrix{width:100%;border-collapse:collapse;font-size:12.5px;border:1px solid var(--border-soft);border-radius:var(--radius);overflow:hidden}
78
+ table.matrix{width:100%;border-collapse:collapse;font-size:12px;border:1px solid var(--border-soft);border-radius:var(--radius);overflow:hidden}
75
79
  table.matrix th,table.matrix td{padding:9px 11px;text-align:left;border-bottom:1px solid var(--border-soft)}
76
80
  table.matrix thead th{font-size:10.5px;text-transform:uppercase;letter-spacing:.08em;color:var(--fg-40);background:var(--bg-1);font-weight:600;vertical-align:bottom}
77
81
  table.matrix tbody tr:hover{background:rgba(255,255,255,.02)}
78
82
  .unit-id{font-family:var(--mono);color:var(--fg)}
79
- .unit-desc{color:var(--fg-50);font-size:11.5px}
80
- .needs{font-family:var(--mono);color:var(--fg-30);font-size:11px}
83
+ .unit-desc{color:var(--fg-50);font-size:12px}
84
+ .needs{font-family:var(--mono);color:var(--fg-30);font-size:12px}
81
85
  .cell{display:inline-flex;align-items:center;justify-content:center;width:26px;height:22px;border-radius:5px;font-size:12px;cursor:pointer;border:0;background:transparent;color:var(--fg-30);font-family:var(--mono)}
82
- .cell.pass{background:rgba(40,200,64,.12);color:var(--allow)}
83
- .cell.fail{background:rgba(255,95,87,.16);color:var(--block);box-shadow:0 0 0 1px rgba(255,95,87,.5)}
86
+ .cell.pass{background:rgba(127,195,154,.12);color:var(--verified)}
87
+ .cell.fail{background:rgba(226,130,127,.16);color:var(--caught);box-shadow:0 0 0 1px rgba(226,130,127,.5)}
84
88
  .cell.fail.expected{animation:flag 1.6s infinite}
85
89
  .cell.blocked{background:rgba(255,255,255,.05);color:var(--fg-40)}
86
- @keyframes flag{0%,100%{box-shadow:0 0 0 1px rgba(255,95,87,.5)}50%{box-shadow:0 0 0 1px rgba(255,95,87,1),0 0 12px rgba(255,95,87,.5)}}
90
+ @keyframes flag{0%,100%{box-shadow:0 0 0 1px rgba(226,130,127,.5)}50%{box-shadow:0 0 0 1px rgba(226,130,127,1),0 0 12px rgba(226,130,127,.5)}}
87
91
  .fixhdr{font-family:var(--mono);font-size:10.5px;color:var(--fg-50);writing-mode:vertical-rl;transform:rotate(180deg);white-space:nowrap;height:96px}
88
- .fixhdr.probe{color:var(--block)}
89
- .probe-row td{background:rgba(255,95,87,.04)}
90
- .probe-tag{font-size:9.5px;color:var(--block);border:1px solid rgba(255,95,87,.4);border-radius:4px;padding:1px 5px;margin-left:8px;font-family:var(--mono)}
91
- .note{color:var(--fg-40);font-size:11.5px;margin-top:8px}
92
+ .fixhdr.probe{color:var(--caught)}
93
+ .probe-row td{background:rgba(226,130,127,.04)}
94
+ .probe-tag{font-size:10.5px;color:var(--caught);border:1px solid rgba(226,130,127,.4);border-radius:4px;padding:1px 5px;margin-left:8px;font-family:var(--mono)}
95
+ .note{color:var(--fg-40);font-size:12px;margin-top:8px}
92
96
  .note code{font-family:var(--mono);color:var(--info)}
93
97
  /* replay */
94
98
  .grid2{display:grid;grid-template-columns:1.4fr 1fr;gap:18px;margin-top:6px}
95
99
  .stage-wrap{border:1px solid var(--border);border-radius:10px;overflow:hidden;background:var(--bg-1);position:relative}
96
- .stage-wrap.v-pass{box-shadow:0 0 0 1px rgba(40,200,64,.4)}
97
- .stage-wrap.v-fail{box-shadow:0 0 0 1px rgba(255,95,87,.55)}
100
+ .stage-wrap.v-pass{box-shadow:0 0 0 1px rgba(127,195,154,.4)}
101
+ .stage-wrap.v-fail{box-shadow:0 0 0 1px rgba(226,130,127,.55)}
98
102
  .stage-wrap.v-blocked{box-shadow:0 0 0 1px rgba(255,255,255,.12)}
99
103
  .titlebar{display:flex;align-items:center;gap:7px;padding:8px 12px;background:var(--bg-2);border-bottom:1px solid var(--border-soft)}
100
- .tl-dot{width:11px;height:11px;border-radius:50%}.tl-dot.r{background:#ff5f57}.tl-dot.y{background:#febc2e}.tl-dot.g{background:#28c840}
101
- .tl-title{margin-left:8px;font-size:11.5px;color:var(--fg-70)}.tl-title .dim{color:var(--fg-30);font-family:var(--mono)}
104
+ .tl-dot{width:11px;height:11px;border-radius:50%}.tl-dot.r{background:var(--caught)}.tl-dot.y{background:var(--warn)}.tl-dot.g{background:var(--verified)}
105
+ .tl-title{margin-left:8px;font-size:12px;color:var(--fg-70)}.tl-title .dim{color:var(--fg-30);font-family:var(--mono)}
102
106
  iframe#surface{width:100%;height:560px;border:0;display:block;background:var(--bg)}
103
- .checklist{position:absolute;top:46px;right:14px;width:226px;background:rgba(0,0,0,.82);border:1px solid var(--border);border-radius:10px;padding:10px 12px;z-index:52;font-size:11.5px;backdrop-filter:blur(6px);opacity:0;transition:opacity .2s}
107
+ .checklist{position:absolute;top:46px;right:14px;width:226px;background:rgba(0,0,0,.82);border:1px solid var(--border);border-radius:10px;padding:10px 12px;z-index:52;font-size:12px;backdrop-filter:blur(6px);opacity:0;transition:opacity .2s}
104
108
  .checklist.on{opacity:1}
105
- .checklist .cl-h{font-family:var(--mono);font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--fg-40);margin-bottom:7px}
109
+ .checklist .cl-h{font-family:var(--mono);font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--fg-40);margin-bottom:7px}
106
110
  .checklist .cl-row{display:flex;align-items:flex-start;gap:7px;padding:3px 0;color:var(--fg-50);transition:color .2s}
107
111
  .checklist .cl-row.run{color:var(--fg)}
108
112
  .checklist .cl-row .cl-id{font-family:var(--mono);font-weight:700;width:22px}
109
113
  .checklist .cl-row .cl-st{width:14px;text-align:center}
110
- .checklist .cl-row.pass .cl-id{color:var(--allow)} .checklist .cl-row.fail .cl-id{color:var(--block)} .checklist .cl-row.blocked .cl-id{color:var(--fg-40)}
114
+ .checklist .cl-row.pass .cl-id{color:var(--verified)} .checklist .cl-row.fail .cl-id{color:var(--caught)} .checklist .cl-row.blocked .cl-id{color:var(--fg-40)}
111
115
  .ring{position:fixed;border:2px solid var(--accent);border-radius:8px;box-shadow:0 0 0 4px rgba(255,154,118,.25),0 0 18px rgba(255,154,118,.6);pointer-events:none;z-index:50;animation:ringpulse 1s infinite;transition:all .25s ease}
112
116
  @keyframes ringpulse{0%,100%{opacity:1}50%{opacity:.55}}
113
- .caption{position:absolute;bottom:14px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,.88);border:1px solid var(--border);border-radius:999px;padding:7px 16px;font-size:12.5px;color:var(--fg);font-family:var(--mono);white-space:nowrap;z-index:51;opacity:0;transition:opacity .2s}
117
+ .caption{position:absolute;bottom:14px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,.88);border:1px solid var(--border);border-radius:999px;padding:7px 16px;font-size:12px;color:var(--fg);font-family:var(--mono);white-space:nowrap;z-index:51;opacity:0;transition:opacity .2s}
114
118
  .caption.on{opacity:1}
115
119
  .framestatus{padding:10px 14px;border-top:1px solid var(--border-soft);font-size:12px;display:flex;align-items:center;gap:12px;flex-wrap:wrap}
116
120
  .framestatus .badge{font-family:var(--mono);font-weight:700;padding:2px 9px;border-radius:5px}
117
- .badge.pass{color:var(--allow);background:rgba(40,200,64,.12)} .badge.fail{color:var(--block);background:rgba(255,95,87,.14)} .badge.blocked{color:var(--fg-40);background:rgba(255,255,255,.05)}
118
- .framestatus .issues{color:var(--block);font-size:11.5px}
121
+ .badge.pass{color:var(--verified);background:rgba(127,195,154,.12)} .badge.fail{color:var(--caught);background:rgba(226,130,127,.14)} .badge.blocked{color:var(--fg-40);background:rgba(255,255,255,.05)}
122
+ .framestatus .issues{color:var(--caught);font-size:12px}
119
123
  .framestatus .issues.blk{color:var(--fg-40)}
120
124
  .replay-side h3{font-size:12px;color:var(--fg-70);margin:0 0 10px}
121
125
  .ctrlrow{display:flex;gap:8px;align-items:center;margin-bottom:12px;flex-wrap:wrap}
122
126
  .progress{height:5px;background:var(--bg-2);border-radius:3px;overflow:hidden;margin:8px 0 14px}
123
- .progress>div{height:100%;width:0;background:linear-gradient(90deg,#ff9a76,#4a90e2);transition:width .3s}
127
+ .progress>div{height:100%;width:0;background:var(--accent);transition:width var(--dur-max) var(--ease)}
124
128
  .steplist{font-size:12px}
125
129
  .steprow{display:flex;align-items:center;gap:8px;padding:5px 7px;border-radius:6px;cursor:pointer}
126
130
  .steprow.active{background:var(--accent-wash)}
127
131
  .steprow .st{width:16px;text-align:center}
128
- .steprow .nm{font-family:var(--mono);font-size:11.5px;color:var(--fg-70)}
129
- .steprow.fail .nm{color:var(--block)}
130
- .clipnote{margin-top:14px;font-size:11.5px;color:var(--fg-40)}
132
+ .steprow .nm{font-family:var(--mono);font-size:12px;color:var(--fg-70)}
133
+ .steprow.fail .nm{color:var(--caught)}
134
+ .clipnote{margin-top:14px;font-size:12px;color:var(--fg-40)}
131
135
  .clipnote code{font-family:var(--mono);color:var(--info)}
132
136
  details.manifest{margin-top:26px;border:1px solid var(--border-soft);border-radius:var(--radius);padding:10px 14px}
133
137
  details.manifest summary{cursor:pointer;color:var(--fg-50);font-size:12px}
134
- details.manifest pre{font-family:var(--mono);font-size:11px;color:var(--fg-50);overflow:auto;margin:10px 0 0;max-height:340px}
138
+ details.manifest pre{font-family:var(--mono);font-size:12px;color:var(--fg-50);overflow:auto;margin:10px 0 0;max-height:340px}
135
139
  .hidden{display:none !important}
140
+ /* S17 · prefers-reduced-motion: ALL motion collapses, information stays
141
+ (same rule the product page ships). */
142
+ @media (prefers-reduced-motion: reduce){
143
+ *,*::before,*::after{animation-duration:.01ms !important;animation-iteration-count:1 !important;transition-duration:.01ms !important;scroll-behavior:auto !important}
144
+ }
136
145
  </style>
137
146
  </head>
138
147
  <body data-verify-page="dashboard">
139
148
  <header class="top">
140
- <span class="logo"><span class="dot"></span><span class="name">codehere</span></span>
149
+ <span class="logo"><span class="name">codehere</span></span>
141
150
  <span class="route">/verify</span>
142
151
  <span class="pill local">&#9679; local &middot; zero telemetry</span>
143
152
  <span class="spacer"></span>
@@ -152,10 +161,10 @@ export const VERIFY_HTML = `<!doctype html>
152
161
  </div>
153
162
  <div class="verdict-line" id="verdict-line"></div>
154
163
  <div class="legend">
155
- <span><b style="color:var(--allow)">PASS</b> observed &amp; honest</span>
156
- <span><b style="color:var(--block)">FAIL</b> observed &amp; lying</span>
164
+ <span><b style="color:var(--verified)">PASS</b> observed &amp; honest</span>
165
+ <span><b style="color:var(--caught)">FAIL</b> observed &amp; lying</span>
157
166
  <span><b style="color:var(--fg-40)">BLOCKED</b> 0 units to observe &mdash; never a silent green</span>
158
- <span><b>&#128269; probe</b> designed-to-fail self-test</span>
167
+ <span><b>probe</b> designed-to-fail self-test</span>
159
168
  </div>
160
169
 
161
170
  <h2 class="section">Pluggable verifiers</h2>
@@ -169,16 +178,16 @@ export const VERIFY_HTML = `<!doctype html>
169
178
  <table class="matrix" id="matrix"><thead></thead><tbody></tbody></table>
170
179
  <p class="note" id="probe-note"></p>
171
180
 
172
- <h2 class="section">Feature coverage &middot; v0 &rarr; v1 MVP &mdash; does each PRODUCT_STATE claim actually render?</h2>
181
+ <h2 class="section">Feature coverage &middot; v0 &#8250; v1 MVP &mdash; does each PRODUCT_STATE claim actually render?</h2>
173
182
  <div class="actions">
174
183
  <button class="run" id="run-cov">&#9654; Run coverage</button>
175
184
  <span class="tally" id="cov-tally"><span class="t">not run yet</span></span>
176
185
  </div>
177
186
  <div class="verdict-line" id="cov-verdict"></div>
178
187
  <div class="legend">
179
- <span><b style="color:var(--allow)">OBSERVED</b> claimed shipped &amp; rendered</span>
180
- <span><b style="color:var(--block)">MISSING</b> claimed shipped, NOT rendered (doc lies)</span>
181
- <span><b style="color:var(--warn)">PARTIAL-GAP</b> 🟡 not yet visible</span>
188
+ <span><b style="color:var(--verified)">OBSERVED</b> claimed shipped &amp; rendered</span>
189
+ <span><b style="color:var(--caught)">MISSING</b> claimed shipped, NOT rendered (doc lies)</span>
190
+ <span><b style="color:var(--warn)">PARTIAL-GAP</b> ◐ honest, not yet visible</span>
182
191
  <span><b style="color:var(--fg-40)">SKIP</b> roadmap / future (correctly absent)</span>
183
192
  </div>
184
193
  <table class="matrix" id="cov-matrix"><thead><tr><th style="width:52%">Feature (PRODUCT_STATE id)</th><th class="needs" style="width:12%">tier</th><th class="needs" style="width:12%">surface</th><th>state</th></tr></thead><tbody></tbody></table>
@@ -200,13 +209,13 @@ export const VERIFY_HTML = `<!doctype html>
200
209
  <h3>Replay controls</h3>
201
210
  <div class="ctrlrow">
202
211
  <button class="linkbtn" id="play">&#9654; Play</button>
203
- <button class="linkbtn" id="skip">&#9197; Skip</button>
204
- <button class="linkbtn" id="record">&#9210; Record clips</button>
212
+ <button class="linkbtn" id="skip">Skip &#8250;</button>
213
+ <button class="linkbtn" id="record">&#9679; Record clips</button>
205
214
  <span class="hint" id="dwell-label">dwell 1500ms</span>
206
215
  </div>
207
216
  <div class="progress"><div id="prog"></div></div>
208
217
  <div class="steplist" id="steplist"></div>
209
- <div class="clipnote">Each fixture &rarr; its own <code>.webm</code> (in-browser, pick this tab). Headless: <code>node scripts/record-replay.mjs</code> hands you one webm, no setup.</div>
218
+ <div class="clipnote">Each fixture gets its own <code>.webm</code> (in-browser, pick this tab). Headless: <code>node scripts/record-replay.mjs</code> hands you one webm, no setup.</div>
210
219
  </div>
211
220
  </div>
212
221
  </div>
@@ -221,7 +230,11 @@ export const VERIFY_HTML = `<!doctype html>
221
230
  (function(){
222
231
  "use strict";
223
232
 
224
- /* -------- fixtures: the seeded sessions (the product's real surfaces) -------- */
233
+ /* -------- fixtures: the seeded sessions (the product's real surfaces) --------
234
+ S20 · the two lens columns re-observe the FAILED fixture with the Review /
235
+ Status lens active (sid = the session, lens = applied via the page's own
236
+ applyLens before reading): every invariant — H9/H13/H14/H15 included —
237
+ must hold under each lens. Honesty must survive the recomposition. */
225
238
  var FIXTURES = [
226
239
  { id:"demo-rate-limit-shipped-v0.9", label:"rate-limit-shipped" },
227
240
  { id:"demo-recovery-test-failed-v0.9", label:"recovery-failed" },
@@ -230,17 +243,25 @@ export const VERIFY_HTML = `<!doctype html>
230
243
  { id:"demo-stalled-opencode-hung-v1", label:"stalled-hung" },
231
244
  { id:"demo-non-git-honest-v1", label:"non-git-honest" },
232
245
  { id:"demo-running-streaming-v0.9", label:"running-stream" },
246
+ { id:"lens-review-failed", sid:"demo-recovery-test-failed-v0.9", lens:"review", label:"review lens \\u00b7 failed" },
247
+ { id:"lens-status-failed", sid:"demo-recovery-test-failed-v0.9", lens:"status", label:"status lens \\u00b7 failed" },
233
248
  { id:"demo-probe-lie-v1", label:"probe-lie", probe:true, expectFail:"H5" }
234
249
  ];
235
250
 
236
251
  /* -------- invariants: the honesty contracts (the "units") --------
237
252
  needs(s) -> can this invariant observe anything here? (else BLOCKED)
238
253
  check(s) -> [] when honest, or violation strings when lying (FAIL) */
254
+ /* LOW #23 (atomic audit) · there is no H4 — it was never built; numbering
255
+ jumps H3->H5 and is kept STABLE because H-ids are referenced by docs,
256
+ fixtures (expectFail) and evidence bundles. Do not renumber. */
239
257
  var INVARIANTS = [
240
258
  { id:"H1", title:"no-verified-without-git", needs:"verifier",
241
259
  desc:"verdict is not 'verified' when git is untracked",
242
260
  can:function(s){ return s.verifiers.length>0; },
243
- check:function(s){ return s.verifiers.filter(function(v){ return v.gittracked==="false" && v.verdict==="verified"; }).map(function(){ return "verifier verdict=verified while git untracked (overclaim)"; }); } },
261
+ /* R11/M2 · the rule itself is the VerdictService's, interpolated at
262
+ build time — this harness EXECUTES verify-claim's own predicate
263
+ instead of re-encoding it, so engine and harness can't drift. */
264
+ check:function(s){ var violates=(${violatesGitInvariant.toString()}); return s.verifiers.filter(function(v){ return violates(v.verdict, v.gittracked!=="false"); }).map(function(){ return "verifier verdict=verified while git untracked (overclaim)"; }); } },
244
265
  { id:"H2", title:"say-never-bare", needs:"say",
245
266
  desc:"every reasoning bubble has real words, not the bare label",
246
267
  can:function(s){ return s.says.length>0; },
@@ -256,7 +277,97 @@ export const VERIFY_HTML = `<!doctype html>
256
277
  { id:"H6", title:"fail-has-honest-reason", needs:"terminal-fail",
257
278
  desc:"a failed / killed / crashed run shows a plain reason, never a bare dot",
258
279
  can:function(s){ return s.terminalFail; },
259
- check:function(s){ return s.hasExplanation ? [] : ["failed session shows no recovery / crash / stall explanation"]; } }
280
+ check:function(s){ return s.hasExplanation ? [] : ["failed session shows no recovery / crash / stall explanation"]; } },
281
+ { id:"H8", title:"observer-honest", needs:"observer rail",
282
+ desc:"the observer never shows a stale session as live, never renders a blank box, never seeds fake rows (Thariq loop-2 design B)",
283
+ can:function(s){ return !!s.observer; },
284
+ check:function(s){
285
+ var o=s.observer, out=[];
286
+ if(o.mode==='empty'){
287
+ if(o.rowStates.length>0) out.push("empty observer rendered "+o.rowStates.length+" row(s) · should seed NO fake rows");
288
+ if(o.teachLen<20) out.push("empty observer is a blank box · it must teach the 60-second path");
289
+ } else if(o.mode==='active'){
290
+ var liveRows=o.rowStates.filter(function(st){ return st==='live'; }).length;
291
+ if(o.liveClaimed!==liveRows) out.push("header claims "+o.liveClaimed+" live but "+liveRows+" row(s) are marked live (stale-as-live)");
292
+ if(o.textLen<10) out.push("active observer rendered with no content (empty container)");
293
+ }
294
+ return out;
295
+ } },
296
+ { id:"H12", title:"follow-up-never-lies", needs:"detail composer",
297
+ desc:"the follow-up box is enabled only when the server can deliver the message; a disabled box carries a plain reason \u00b7 never an invitation to send into a void (S1)",
298
+ can:function(s){ return !!s.followUp && !!s.followUp.state; },
299
+ check:function(s){
300
+ var f=s.followUp, out=[];
301
+ if(f.state==='disabled-honest' && f.placeholderLen<20) out.push("follow-up disabled but the reason placeholder is missing/too thin");
302
+ if(f.state==='disabled-honest' && !f.disabled) out.push("composer claims disabled-honest but the box is still enabled (the invitation lie)");
303
+ if(f.state==='enabled' && f.disabled) out.push("composer stamped enabled but the box is disabled (silent dead composer)");
304
+ return out;
305
+ } },
306
+ { id:"H11", title:"running-task-stoppable", needs:"a running session",
307
+ desc:"a running task shows a Stop control on its detail · you can stop an agent the moment you stop trusting it (S2)",
308
+ can:function(s){ return s.status==='running'; },
309
+ check:function(s){ return s.hasStopControl ? [] : ["running task renders no Stop control on the detail (unstoppable runaway)"]; } },
310
+ { id:"H10", title:"cost-estimate-honest", needs:"composer cost chip",
311
+ desc:"the cost chip is computed (free / recent-real / typical), labeled as what it is, and its tooltip never claims a computation that didn't happen (S7)",
312
+ can:function(s){ return s.costEst && s.costEst.length>0; },
313
+ check:function(s){
314
+ var out=[];
315
+ s.costEst.forEach(function(c){
316
+ if(!c.mode) out.push("cost chip rendered with no computed mode (estimator never ran) \\u00b7 text: '"+c.text+"'");
317
+ else if(c.mode==='free' && c.text.indexOf('$0')===-1) out.push("free-mode chip does not say $0");
318
+ else if(c.mode==='typical' && c.text.indexOf('typical')===-1) out.push("flat-fallback chip not labeled 'typical' \\u00b7 a flat figure dressed as a computation");
319
+ else if(c.mode==='typical' && /prompt length/i.test(c.title)) out.push("typical-mode tooltip claims prompt-length computation that did not happen");
320
+ else if(c.mode==='recent' && c.text.indexOf('~$')===-1) out.push("recent-mode chip missing the computed ~$ figure");
321
+ });
322
+ return out;
323
+ } },
324
+ { id:"H9", title:"no-green-on-failed", needs:"verifier on a failed run",
325
+ desc:"a failed / killed / crashed task never wears the green 'verified' block · an honest report about a failure is not a success (S5)",
326
+ can:function(s){ return s.terminalFail && s.verifiers.length>0; },
327
+ check:function(s){ return s.verifiers.filter(function(v){ return v.verdict==="verified"; }).map(function(){ return "status=failed task renders a green verdict=verified block (fake-green on the proof surface)"; }); } },
328
+ { id:"H13", title:"hero-numbers-honest", needs:"home hero",
329
+ desc:"the home hero's claimed counts (agents/verified/caught/running) equal the counts derivable from the task list the page itself holds; the first-run hero never hides real work (S12)",
330
+ can:function(s){ return !!(s.hero && s.hero.expected); },
331
+ check:function(s){
332
+ var h=s.hero, e=h.expected, out=[];
333
+ if(h.mode==="first-run"){
334
+ if(e.today>0) out.push("hero shows the first-run story while "+e.today+" task(s) started today (real work hidden)");
335
+ return out;
336
+ }
337
+ ["agents","verified","caught","stopped","running"].forEach(function(k){
338
+ if(h[k]!==e[k]) out.push("hero claims "+k+"="+h[k]+" but the task list derives "+e[k]);
339
+ });
340
+ return out;
341
+ } },
342
+ { id:"H14", title:"decisions-honest", needs:"decision-queue state",
343
+ desc:"the NEEDS-YOUR-DECISION queue's claimed N equals the count derivable from the page's own state (pending write-passes + verifier-broken verdicts today + failed runs with retry available today); zero derivable decisions means the section is absent entirely (S13)",
344
+ can:function(s){ return !!(s.decisions && s.decisions.expected!==null); },
345
+ check:function(s){
346
+ var d=s.decisions, out=[];
347
+ if(d.expected===0){
348
+ if(d.present) out.push("decision queue renders ("+d.rows+" row(s), claims N="+d.n+") while 0 decisions are derivable \\u00b7 the section must be absent when nothing needs you");
349
+ return out;
350
+ }
351
+ if(!d.present){ out.push(d.expected+" decision(s) derivable from page state but the queue is absent (real decisions hidden)"); return out; }
352
+ if(d.n!==d.expected) out.push("queue claims N="+d.n+" but the page state derives "+d.expected);
353
+ if(d.rows!==d.n) out.push("queue claims N="+d.n+" but renders "+d.rows+" row(s)");
354
+ return out;
355
+ } },
356
+ { id:"H15", title:"glyph-discipline", needs:"rendered surface",
357
+ desc:"no emoji/pictograph codepoint anywhere in the rendered CHROME, observed in the default AND expanded history/practice states \\u00b7 status and meaning are carried by the sanctioned geometric glyph set (\\u2713 \\u2715 \\u25CF \\u25CB \\u2022 \\u2298 \\u203A), words and color, never emoji-as-icon; user/run content ([data-user-content]) is the user's own text, not chrome, and is excluded (S19)",
358
+ can:function(s){ return !!(s.glyphScan && s.glyphScan.scanned); },
359
+ check:function(s){ return s.glyphScan.hits; } },
360
+ { id:"H7", title:"recovery-buttons-must-not-lie", needs:"recovery card",
361
+ desc:"every ENABLED recovery action does a real thing; a disabled one carries its reason · no button narrates an action it doesn't perform (Thariq loop-1)",
362
+ can:function(s){ return s.recoveryButtons && s.recoveryButtons.length>0; },
363
+ check:function(s){
364
+ var out = [];
365
+ s.recoveryButtons.forEach(function(b){
366
+ if(b.bareToast) out.push("recovery '"+b.action+"' is enabled but only fires a toast · it lies about acting");
367
+ if(b.disabled && !b.hasReason) out.push("recovery '"+b.action+"' is disabled with no reason (silent dead button)");
368
+ });
369
+ return out;
370
+ } }
260
371
  ];
261
372
 
262
373
  var PROTO_VERSION = "codehere-verify/1";
@@ -264,75 +375,344 @@ export const VERIFY_HTML = `<!doctype html>
264
375
  /* -------- feature coverage: every granular PRODUCT_STATE claim, v0->v1 --------
265
376
  Turns the ~150-row inventory's claims into watchable checks against the REAL
266
377
  surface. status drives the verdict:
267
- shipped (✅/🟢) -> element MUST render -> OBSERVED, else MISSING (the claim is a lie)
268
- partial (🟡) -> render if present -> OBSERVED, else PARTIAL-GAP (honest, not a lie)
269
- roadmap (⛔) -> SKIP-ROADMAP (planned, not built — correctly absent)
270
- future (🟦/cut) -> SKIP-FUTURE (deferred by design / cut)
378
+ shipped ([shipped]/[green]) -> element MUST render -> OBSERVED, else MISSING (the claim is a lie)
379
+ partial ([partial]) -> render if present -> OBSERVED, else PARTIAL-GAP (honest, not a lie)
380
+ roadmap ([roadmap]) -> SKIP-ROADMAP (planned, not built — correctly absent)
381
+ future ([future]/cut) -> SKIP-FUTURE (deferred by design / cut)
271
382
  surface: 'list' = dashboard · 'detail' = opens demo-rate-limit-shipped-v0.9.
272
383
  sel = real CSS selector confirmed against the live DOM (not invented). */
273
384
  var COV_FIXTURE = "demo-rate-limit-shipped-v0.9";
385
+ /* Inject a synthetic 3-task batch into the page's task STATE (the banner-
386
+ drive honesty bar: state occurs -> the real render path must produce the
387
+ surface; never DOM injection). Restored by __unseedCovBatch. Without this,
388
+ a home that never ran the batch seed reported the two v1.0 rows MISSING
389
+ even though the render path is real (found in the It-4 joint ceremony). */
390
+ function __seedCovBatch(w){
391
+ try{
392
+ var tk = w._cached && w._cached.tasks; if(!tk) return false;
393
+ if (w.__covSavedPersisted !== undefined) return true; // already seeded this run
394
+ w.__covSavedPersisted = tk.persisted;
395
+ var now = Date.now()-300000;
396
+ var mk = function(id, ag, st){ return { sessionId:id, agent:ag, prompt:"coverage probe "+id, cwd:"/cov-batch-probe", startedAt:now, status:st, batchId:"cov-batch-1", steps:[], artifacts:{}, provenance:"persisted", durationMs:1000, completedAt: st==="running"?0:now+1000 }; };
397
+ tk.persisted = (tk.persisted||[]).concat([ mk("cov-b1","claude-code","completed"), mk("cov-b2","codex","running"), mk("cov-b3","gemini","failed") ]);
398
+ return true;
399
+ }catch(e){ return false; }
400
+ }
401
+ function __unseedCovBatch(w){
402
+ try{
403
+ if (w.__covSavedPersisted === undefined) return;
404
+ if (w._cached && w._cached.tasks) w._cached.tasks.persisted = w.__covSavedPersisted;
405
+ delete w.__covSavedPersisted;
406
+ }catch(e){}
407
+ }
408
+
409
+ /* It-4 V1-MULTITASK/V1-FANOUT-ALL drive helper. Finds a cwd in the
410
+ /api/tasks data (real or coverage-injected) whose turns include a batchId
411
+ shared by >=2 tasks, then drives the real openRoom() so renderRoom()
412
+ produces the live .project-rollup + .fanout-all surface. Returns the cwd
413
+ it opened, or null if no batch is present. */
414
+ function __openBatchRoom(w){
415
+ try{
416
+ var all = (w.allS ? w.allS(w._cached && w._cached.tasks || {}) : []);
417
+ var byBatch = {};
418
+ for(var i=0;i<all.length;i++){
419
+ /* S18 · practice (mockRun) batches collapse behind the practice row in
420
+ rooms — they no longer render a rollup, so the drive must open a
421
+ REAL batch room (the seeded cov-batch qualifies). Same honest rule
422
+ as renderRoom, applied to both sides. */
423
+ var t=all[i]; if(!t.batchId || t.mockRun) continue;
424
+ (byBatch[t.batchId]=byBatch[t.batchId]||[]).push(t);
425
+ }
426
+ for(var bid in byBatch){
427
+ if(byBatch[bid].length>=2){
428
+ var cwd=byBatch[bid][0].cwd; if(!cwd) continue;
429
+ if(w.openRoom){ w.openRoom(encodeURIComponent(cwd)); return cwd; }
430
+ }
431
+ }
432
+ }catch(e){}
433
+ return null;
434
+ }
274
435
  var FEATURES = [
275
436
  { id:"F-AUDIT-FOOTER", name:"Audit chain footer", surface:"list", sel:".audit-chain-panel", status:"shipped", tier:"v0.7" },
276
- { id:"F-BUDGET-CHIP", name:"Budget pill", surface:"list", sel:".budget", status:"shipped", tier:"v0.7" },
277
- { id:"F-BUDGET-WARN-BANNER", name:"Budget warning banner", surface:"list", sel:".budget-banner", status:"partial", tier:"v0.6" },
278
- { id:"F-CARD-AGENT-BORDER", name:"Per-vendor left-border color on task-card", surface:"list", sel:".task-card.agent-claude-code", status:"shipped", tier:"v0.9" },
279
- { id:"F-CARD-CLICK-DETAIL", name:"Click card → detail view", surface:"list", sel:".task-card", status:"shipped", tier:"v0.6" },
280
- { id:"F-CARD-OUTCOME-CHIP", name:"Outcome chip right-aligned", surface:"list", sel:".card-outcome.ok", status:"shipped", tier:"v0.7" },
281
- { id:"F-CUSTOM-VIEW-BANNER", name:"Custom view banner", surface:"list", sel:".custom-view-banner", status:"shipped", tier:"v0.8" },
437
+ { id:"F-BUDGET-CHIP", name:"Budget pill", surface:"list", sel:".budget", status:"shipped", tier:"v0.7",
438
+ /* .budget fills its text only when a daily budget is set (upBud). Real
439
+ ~/.codehere often has no cap set, so it flaps empty. Drive the REAL
440
+ upBud() over a set-budget pricing state (60% spent · safe), restore. */
441
+ drive:function(w,d){ try{ w.__covBud=w._cached.pricing; w._cached.pricing={dailyBudgetCents:500,spentTodayCents:300}; w.upBud(); }catch(e){} },
442
+ undrive:function(w,d){ try{ w._cached.pricing=w.__covBud; w.upBud(); }catch(e){} } },
443
+ { id:"F-BUDGET-WARN-BANNER", name:"Budget warning banner", surface:"list", sel:".budget-banner", status:"partial", tier:"v0.6",
444
+ /* drive the REAL upBud() over an over-budget pricing state, observe the
445
+ banner, restore. Same legitimacy as F-CUSTOM-VIEW-BANNER's class drive. */
446
+ drive:function(w,d){ try{ w.__covPricing=w._cached.pricing; w._cached.pricing={dailyBudgetCents:500,spentTodayCents:500}; w.upBud(); }catch(e){} },
447
+ undrive:function(w,d){ try{ w._cached.pricing=w.__covPricing; w.upBud(); }catch(e){} } },
448
+ /* S13 · at rest the home shows TODAY one-line rows only; demos + earlier
449
+ work collapse behind the history row. Card rows are observed by
450
+ expanding history through the REAL toggle (window.__histExpand), the
451
+ same path the user's click takes — never DOM injection. */
452
+ { id:"F-CARD-AGENT-BORDER", name:"Per-vendor class on task row (S12: identity is the monochrome name, no hue)", surface:"list", sel:".task-card.agent-claude-code", status:"shipped", tier:"v0.9",
453
+ drive:function(w,d){ try{ w.__histExpand(); }catch(e){} },
454
+ undrive:function(w,d){ try{ w.__histCollapse(); }catch(e){} } },
455
+ { id:"F-CARD-CLICK-DETAIL", name:"Click card/row -> detail view", surface:"list", sel:".task-card", status:"shipped", tier:"v0.6",
456
+ drive:function(w,d){ try{ w.__histExpand(); }catch(e){} },
457
+ undrive:function(w,d){ try{ w.__histCollapse(); }catch(e){} } },
458
+ { id:"F-CARD-OUTCOME-CHIP", name:"Outcome on the card (the .tc-stat pill: verdict word + checks, right-aligned with cost)", surface:"list", sel:".tcard .tc-stat", status:"shipped", tier:"v0.7",
459
+ drive:function(w,d){ try{ w.__histExpand(); }catch(e){} },
460
+ undrive:function(w,d){ try{ w.__histCollapse(); }catch(e){} } },
461
+ { id:"F-CUSTOM-VIEW-BANNER", name:"Custom view banner", surface:"list", sel:".custom-view-banner", status:"shipped", tier:"v0.8",
462
+ drive:function(w,d){ d.body.classList.add("custom-view"); },
463
+ undrive:function(w,d){ d.body.classList.remove("custom-view"); } },
282
464
  { id:"F-DAEMON-HEARTBEAT", name:"Local pill auto-update", surface:"list", sel:".local-pill", status:"shipped", tier:"v0.6" },
283
- { id:"F-FAILOVER-BANNER", name:"Failover banner", surface:"list", sel:".failover-banner", status:"partial", tier:"v0.7" },
284
- { id:"F-FILTER-PILLS", name:"All/Running/Done/Failed segmented filters", surface:"list", sel:".filter-tab", status:"shipped", tier:"v0.7" },
285
- { id:"F-INDEXING-BANNER", name:"First-run indexing banner", surface:"list", sel:".indexing-banner", status:"partial", tier:"v0.7" },
465
+ { id:"F-FAILOVER-BANNER", name:"Failover banner", surface:"list", sel:".failover-banner.show", status:"partial", tier:"v0.7",
466
+ /* drive the banner's OWN render (window.__fbRender) with a real offer
467
+ shape — same object /api/failover-offers returns — so we observe the
468
+ live render path, not a fake DOM injection. */
469
+ drive:function(w,d){ try{ w.__fbRender([{ sessionId:"cov-failover", target:"codex", signal:{ fromAgent:"claude-code", errorCode:"rate_limited" } }]); }catch(e){} },
470
+ undrive:function(w,d){ try{ w.__fbRender([]); }catch(e){} } },
471
+ /* S13 · filters moved into the ⌘K palette (rule 9). Observed by opening
472
+ the REAL palette (window.openPalette — the same function ⌘K runs) and
473
+ asserting the filter commands render; the command's Enter runs the
474
+ same setFilter() the tabs ran. */
475
+ { id:"F-FILTER-PILLS", name:"All/Running/Done/Failed filters (S13: \u2318K command EXECUTED \u203A the removable token is the observed effect)", surface:"list", sel:".ftoken", status:"shipped", tier:"v0.7",
476
+ /* Refuter (S13) · presence-only re-pointing was vacuous: a lobotomized
477
+ command element would still pass. This drive runs the REAL command
478
+ (palette open \u2192 click filter-failed) and the row observes its
479
+ CONSEQUENCE (.ftoken) \u2014 a broken run closure now reads MISSING. */
480
+ drive:function(w,d){ try{ w.openPalette(); var c=d.querySelector("[data-verify-cmd='filter-failed']"); if(c) c.click(); }catch(e){} },
481
+ undrive:function(w,d){ try{ var x=d.querySelector(".ftoken .x"); if(x) x.click(); w.closePalette(); }catch(e){} } },
482
+ { id:"F-INDEXING-BANNER", name:"First-run indexing banner", surface:"list", sel:".indexing-banner", status:"partial", tier:"v0.7",
483
+ /* partial by nature: this banner only shows on a genuinely fresh home
484
+ (<3 sessions). The drive reproduces that fresh-home state — empties
485
+ _cached.tasks + clears the sessionStorage dismissal — runs the REAL
486
+ upIdx(), observes, then restores. */
487
+ drive:function(w,d){ try{ w.__covTasks=w._cached.tasks; w._cached.tasks={spawned:[],persisted:[],sessions:[]}; try{w.sessionStorage.removeItem("codehere_indexing_dismissed");}catch(e){} w.upIdx(); }catch(e){} },
488
+ undrive:function(w,d){ try{ w._cached.tasks=w.__covTasks; w.upIdx(); }catch(e){} } },
286
489
  { id:"F-LOCAL-PILL", name:"Local-first / zero-telemetry pill", surface:"list", sel:".local-pill", status:"shipped", tier:"v0.7" },
287
490
  { id:"F-LOGO", name:"Brand wordmark", surface:"list", sel:".logo .name", status:"shipped", tier:"v0.7" },
288
- { id:"F-NEW-TASK-BTN", name:"+ New Task primary button", surface:"list", sel:".new-btn", status:"shipped", tier:"v0.7" },
289
- { id:"F-OBSERVED-THREAD", name:"Observed session as thread", surface:"list", sel:".obs-row", status:"shipped", tier:"v0.7" },
491
+ { id:"F-NEW-TASK-BTN", name:"New task (S13: first ⌘K command, focuses the composer)", surface:"list", sel:"[data-verify-cmd='new-task']", status:"shipped", tier:"v0.7",
492
+ drive:function(w,d){ try{ w.openPalette(); }catch(e){} },
493
+ undrive:function(w,d){ try{ w.closePalette(); }catch(e){} } },
494
+ { id:"F-OBSERVED-THREAD", name:"Observed session as thread", surface:"list", sel:".obs-row", status:"shipped", tier:"v0.7",
495
+ /* S9 made practice mode honest: the mock chain starts EMPTY, so the
496
+ observer correctly renders its teach-state and this row has no .obs-row
497
+ to observe — it used to pass only because past mock runs had POLLUTED
498
+ the real chain with host sessions. Banner-drive honesty bar (same as
499
+ __seedCovBatch): seed the STATE through the REAL render path (the
500
+ exact function /api/host-sessions feeds), clearly labeled as a
501
+ coverage probe, restored from the real API after. Never DOM. */
502
+ /* S13 · the observer rests as one calm line; .obs-row renders in the
503
+ EXPANDED rail — drive the real expand flag with the probe session. */
504
+ drive:function(w,d){ w._obsOpen=true; w.renderHostSessions({ sessions:[{ sessionId:"coverage-probe-host-session", shortId:"cov-probe", lastSeen:new Date().toISOString(), toolMix:{ Bash:3, Read:2 }, blockedCalls:0, criticalCalls:0, totalCalls:5, durationMs:60000, risk:"low" }] }); },
505
+ undrive:function(w,d){ try{ w._obsOpen=false; fetch("/api/host-sessions").then(function(r){ return r.json(); }).then(function(h){ w.renderHostSessions(h); }); }catch(e){} } },
290
506
  { id:"F-OSS-FOOTER", name:"OSS footer", surface:"list", sel:".oss-footer", status:"shipped", tier:"v0.7" },
291
- { id:"F-PERSONA-SELECTOR", name:"Header role selector", surface:"list", sel:".persona-select", status:"shipped", tier:"v0.7" },
292
- { id:"F-PIN-DEMO", name:"Demo-shape sessions pinned to FEATURED", surface:"list", sel:".task-card.tc-pinned", status:"shipped", tier:"v0.9" },
293
- { id:"F-PORT-FAILOVER-NOTICE", name:"Port-collision UI surface", surface:"list", sel:".failover-banner", status:"shipped", tier:"v0.7" },
294
- { id:"F-RESHAPE-BTN", name:"Reshape view button", surface:"list", sel:".reshape-btn", status:"shipped", tier:"v0.8" },
295
- { id:"F-SETTINGS-COG", name:"Settings drawer trigger", surface:"list", sel:".settings-btn", status:"shipped", tier:"v0.8" },
296
- { id:"F-SHARE-BTN", name:"Share thread button", surface:"list", sel:".share-btn-hdr", status:"shipped", tier:"v0.8" },
297
- { id:"F-SQUAD-ROW", name:"Agent picker chips + presence dots", surface:"list", sel:".squad-row", status:"shipped", tier:"v0.7" },
298
- { id:"F-TASK-CARD", name:"Task card", surface:"list", sel:".task-card", status:"shipped", tier:"v0.7" },
299
- { id:"F-TIME-GAP-PILL", name:"Brand pill + relative-time sub", surface:"list", sel:".time-gap", status:"shipped", tier:"v0.7" },
300
- { id:"F-WORKTREE-ALERT", name:"Worktree alert", surface:"list", sel:".worktree-alert", status:"partial", tier:"v0.7" },
301
- { id:"M-OVERLAY-LINEAR", name:"Overlay-renderer Linear", surface:"list", sel:".or-overlay", status:"shipped", tier:"v0.9" },
302
- { id:"M-PRIMITIVES-NOTE", name:"Primitives note", surface:"list", sel:".primitives-note .prims .p", status:"shipped", tier:"v0.8" },
303
- { id:"M-RESHAPE-AGENT-COMPOSE", name:"Agent-compose animation", surface:"list", sel:".agent-compose .line", status:"shipped", tier:"v0.8" },
507
+ /* S20 · the five personas died; three lenses replace them. Each row is
508
+ EFFECT-observing (the F-FILTER-PILLS bar): the drive EXECUTES the real
509
+ ⌘K lens command (palette open -> click -> applyLens) and the sel pins
510
+ the RECOMPOSITION that command must produce — a lobotomized command, a
511
+ missing body[data-lens], or a lens that failed to recompose all read
512
+ MISSING. State captured + restored in undrive. */
513
+ // F-LENS-DEFAULT/REVIEW/STATUS removed 2026-06-20 — the lens mode-switch was
514
+ // cut for ONE consistent conversation-first view (no more UI reshaping
515
+ // between visits). The detail's honest verdict + evidence disclosure carry
516
+ // what Review/Status used to surface.
517
+ { id:"F-PIN-DEMO", name:"Demo-shape sessions pinned to FEATURED (S13: inside expanded history)", surface:"list", sel:".task-card.tc-pinned", status:"shipped", tier:"v0.9",
518
+ drive:function(w,d){ try{ w.__histExpand(); }catch(e){} },
519
+ undrive:function(w,d){ try{ w.__histCollapse(); }catch(e){} } },
520
+ { id:"F-PORT-FAILOVER-NOTICE", name:"Port-collision UI surface (banner render path shared with failover; the PORT-collision trigger itself is not simulated)", surface:"list", sel:".failover-banner.show", status:"partial", tier:"v0.7", // shares the failover-banner surface; driven via window.__fbRender (see F-FAILOVER-BANNER). Honest = partial: the port-collision PATH itself isn't simulated, only the shared banner render.
521
+ drive:function(w,d){ try{ w.__fbRender([{ sessionId:"cov-port", target:"codex", signal:{ fromAgent:"claude-code", errorCode:"port_in_use" } }]); }catch(e){} },
522
+ undrive:function(w,d){ try{ w.__fbRender([]); }catch(e){} } },
523
+ // F-RESHAPE-BTN removed 2026-06-20 — Reshape mode cut for one consistent view.
524
+ { id:"F-SETTINGS-COG", name:"Settings drawer trigger (S13: ⌘K Settings command)", surface:"list", sel:"[data-verify-cmd='settings']", status:"shipped", tier:"v0.8",
525
+ drive:function(w,d){ try{ w.openPalette(); }catch(e){} },
526
+ undrive:function(w,d){ try{ w.closePalette(); }catch(e){} } },
527
+ { id:"F-SHARE-BTN", name:"Share thread (S13: ⌘K Share command -> same signed-link modal)", surface:"list", sel:"[data-verify-cmd='share']", status:"shipped", tier:"v0.8",
528
+ drive:function(w,d){ try{ w.openPalette(); }catch(e){} },
529
+ undrive:function(w,d){ try{ w.closePalette(); }catch(e){} } },
530
+ { id:"F-SQUAD-ROW", name:"Agent readiness (S13: strip died · ⌘K Agents command carries the real readiness)", surface:"list", sel:"[data-verify-cmd='agents']", status:"shipped", tier:"v0.7",
531
+ drive:function(w,d){ try{ w.openPalette(); }catch(e){} },
532
+ undrive:function(w,d){ try{ w.closePalette(); }catch(e){} } },
533
+ { id:"F-TASK-CARD", name:"Task row (one-line, keeps .task-card contract)", surface:"list", sel:".task-card", status:"shipped", tier:"v0.7",
534
+ drive:function(w,d){ try{ w.__histExpand(); }catch(e){} },
535
+ undrive:function(w,d){ try{ w.__histCollapse(); }catch(e){} } },
536
+ { id:"F-TIME-GAP-PILL", name:"Brand pill + relative-time sub", surface:"list", sel:".time-gap", status:"shipped", tier:"v0.7",
537
+ /* Data-dependent (same hazard verify-page already calls out for goal groups):
538
+ .time-gap renders only BETWEEN tasks >2h apart (renderL). Real ~/.codehere
539
+ can legitimately have no such gap — a busy session clusters every task into
540
+ one <2h window (this exact flap, 2026-06-08) — so the live-DOM check flaps
541
+ MISSING on honest data. Observe it DETERMINISTICALLY instead, the same way
542
+ F-WORKTREE-ALERT does: inject two same-goal tasks 6h apart, render, observe,
543
+ restore. The render path is the REAL renderL — not a faked element. */
544
+ /* S12 · both fixtures sit 3 days back so they land in ONE chapter
545
+ (EARLIER) deterministically — the home now splits NOW/TODAY/EARLIER
546
+ and a now/now-6h pair could straddle midnight and split chapters. */
547
+ /* S13 · the gap rows live inside the EARLIER goal groups, which sit
548
+ behind the history row now — expand through the REAL toggle. */
549
+ /* S21 · same-title runs now collapse into ONE thread (groupByTitle), so a
550
+ time-gap between two runs of the SAME task no longer renders (they're one
551
+ thread) — correct. The .time-gap feature renders between DISTINCT tasks
552
+ >2h apart; the fixture uses two distinct-title tasks 6h apart to exercise
553
+ it (still same goal/cwd, so still one goal group). */
554
+ drive:function(w,d){ try{ w.__covTG=w._cached.tasks; var now=Date.now(); var mk=function(id,ts,p){return{sessionId:id,status:"completed",cwd:"/repo/cov-tg",startedAt:ts,prompt:p,agent:"claude-code",steps:[]};}; var base=now-3*24*3600*1000; w._cached.tasks={spawned:[],persisted:[mk("cov-tg-new",base,"coverage fixture new"),mk("cov-tg-old",base-6*3600*1000,"coverage fixture old")],sessions:[]}; w.__histExpand(); w.renderL(); }catch(e){} },
555
+ undrive:function(w,d){ try{ w._cached.tasks=w.__covTG; w.__histCollapse(); w.renderL(); }catch(e){} } },
556
+ { id:"F-WORKTREE-ALERT", name:"Worktree alert", surface:"list", sel:".worktree-alert", status:"partial", tier:"v0.7",
557
+ /* upWk() fires when >=2 running sessions span different cwds and the odd
558
+ one has been running >5min. Drive that exact predicate over the REAL
559
+ upWk() with a synthetic 2-running-session state, observe, restore. */
560
+ drive:function(w,d){ try{ w.__covTasks=w._cached.tasks; var old=Date.now()-10*60*1000; w._cached.tasks={spawned:[],persisted:[{sessionId:"cov-wk-main",status:"running",cwd:"/repo/main",startedAt:old},{sessionId:"cov-wk-odd",status:"running",cwd:"/repo/feature-x",startedAt:old}],sessions:[]}; w.upWk(); }catch(e){} },
561
+ undrive:function(w,d){ try{ w._cached.tasks=w.__covTasks; w.upWk(); }catch(e){} } },
562
+ /* goal-first home + goal room (v1 spec · shared-room hand-off). Goal groups
563
+ render from REAL non-demo tasks (same data dependency as F-TIME-GAP-PILL);
564
+ room features are observed by driving the REAL openRoom() on the first
565
+ goal-head's data-goal-cwd, then closeRoom() — same pattern as showD(). */
566
+ /* S13 · goal groups own the EXPANDED history (today is flat one-line
567
+ rows); observed deterministically with the same injected-state drive
568
+ F-TIME-GAP-PILL uses — real renderL, real toggle, restored after. */
569
+ { id:"F-GOAL-GROUP", name:"Goal grouping (S13: inside expanded history; heads still open rooms)", surface:"list", sel:".goal-group", status:"shipped", tier:"v1",
570
+ drive:function(w,d){ try{ w.__covGG=w._cached.tasks; var now=Date.now(); var mk=function(id,ts){return{sessionId:id,status:"completed",cwd:"/repo/cov-gg",startedAt:ts,prompt:"coverage fixture",agent:"claude-code",steps:[]};}; var base=now-3*24*3600*1000; w._cached.tasks={spawned:[],persisted:[mk("cov-gg-a",base),mk("cov-gg-b",base-3600*1000)],sessions:[]}; w.__histExpand(); w.renderL(); }catch(e){} },
571
+ undrive:function(w,d){ try{ w._cached.tasks=w.__covGG; w.__histCollapse(); w.renderL(); }catch(e){} } },
572
+ { id:"F-GOAL-HEAD-OPEN", name:"Goal head opens its room (carries cwd)", surface:"list", sel:".goal-head[data-goal-cwd]", status:"shipped", tier:"v1",
573
+ drive:function(w,d){ try{ w.__covGH=w._cached.tasks; var now=Date.now(); var mk=function(id,ts){return{sessionId:id,status:"completed",cwd:"/repo/cov-gh",startedAt:ts,prompt:"coverage fixture",agent:"claude-code",steps:[]};}; var base=now-3*24*3600*1000; w._cached.tasks={spawned:[],persisted:[mk("cov-gh-a",base),mk("cov-gh-b",base-3600*1000)],sessions:[]}; w.__histExpand(); w.renderL(); }catch(e){} },
574
+ undrive:function(w,d){ try{ w._cached.tasks=w.__covGH; w.__histCollapse(); w.renderL(); }catch(e){} } },
575
+ { id:"F-AGENT-PICK", name:"Composer agent picker (Auto + 5 agents)", surface:"list", sel:"button#agentpick-btn", status:"shipped", tier:"v1" },
576
+ /* S13 · the two new surfaces. The palette is driven through the REAL
577
+ opener (the same function ⌘K runs); the decision queue renders ONLY
578
+ when real decisions exist, so it's observed by injecting a failed-task
579
+ state through the real renderDecisions(), then restored. */
580
+ { id:"F-CMDK-PALETTE", name:"⌘K command palette · grouped Act/View/Trust/Go, Enter runs the real function", surface:"list", sel:"[data-verify-unit='palette'] .prow[data-verify-cmd]", status:"shipped", tier:"v1",
581
+ drive:function(w,d){ try{ w.openPalette(); }catch(e){} },
582
+ undrive:function(w,d){ try{ w.closePalette(); }catch(e){} } },
583
+ { id:"F-DECISION-QUEUE", name:"Needs-your-decision queue · absent when nothing needs you (H14 pins the count; this drive pins the PRESENT branch deterministically through the real renderDecisions)", surface:"list", sel:"#decision-queue .dq-row", status:"shipped", tier:"v1",
584
+ drive:function(w,d){ try{ w.__covDQ=w._cached.tasks; var now=Date.now()-600000; w._cached.tasks={spawned:[],persisted:[{sessionId:"cov-dq-fail",status:"failed",cwd:"/repo/cov-dq",startedAt:now,prompt:"coverage decision probe",agent:"claude-code",steps:[]}],sessions:[]}; w.renderDecisions(); }catch(e){} },
585
+ undrive:function(w,d){ try{ w._cached.tasks=w.__covDQ; w.renderDecisions(); }catch(e){} } },
586
+ /* S18 · the practice group's own regression net (the board must be able to
587
+ catch a regression in the exact story it ships). Two assertions in ONE
588
+ drive, both through the REAL render path:
589
+ (1) absent <=> 0 practice — with zero practice fixtures renderL must
590
+ render NO .prac-row; a phantom row aborts the drive, sel stays
591
+ unmatched, the row reads MISSING;
592
+ (2) with 2 mockRun fixtures, the REAL window.__pracExpand must render a
593
+ row whose claimed data-verify-practice-n=2 AND exactly 2 rows inside
594
+ .practice-rows (sel pins both via :nth-child(2):last-child) — a
595
+ "practice runs · 2" label over 0 rows, or practice flooding back
596
+ into the real list, reads MISSING. State restored in undrive. */
597
+ { id:"F-PRACTICE-ROW", name:"Practice-runs group · quiet collapsed row, claimed N == rendered rows, absent when no practice exists (S18)", surface:"list", sel:"#task-list .prac-row[data-verify-practice-n='2'][aria-expanded='true'] + .practice-rows > .task-card:nth-child(2):last-child", status:"shipped", tier:"v1",
598
+ drive:function(w,d){ try{ w.__covPRAC=w._cached.tasks; w.__pracCollapse(); w._cached.tasks={spawned:[],persisted:[],sessions:[]}; w.renderL(); if(d.querySelector("#task-list .prac-row")) return; var base=Date.now()-3*24*3600*1000; var mkp=function(id,ts){return{sessionId:id,status:"completed",cwd:"/repo/cov-prac",startedAt:ts,prompt:"coverage practice fixture "+id,agent:"claude-code",steps:[],mockRun:true,provenance:"persisted",durationMs:1000,completedAt:ts+1000};}; w._cached.tasks={spawned:[],persisted:[mkp("cov-prac-a",base),mkp("cov-prac-b",base-3600*1000)],sessions:[]}; w.__pracExpand(); }catch(e){} },
599
+ undrive:function(w,d){ try{ w._cached.tasks=w.__covPRAC; w.__pracCollapse(); w.renderL(); }catch(e){} } },
600
+ /* S18 · H13's aggregation path, pinned. Under CODEHERE_MOCK every live run
601
+ is practice, so the board's hero compare is honestly 0==0 — vacuous.
602
+ This drive injects deterministic NON-practice fixtures (one running ·
603
+ one completed-verified · one caught, three distinct agents, started
604
+ today) through the REAL upHero(); sel pins every claimed
605
+ data-verify-hero-* attr to the fixture-derived counts. A double-counted
606
+ agent or mis-bucketed running session reads MISSING. */
607
+ { id:"F-HERO-COUNTS", name:"Day-line hero · real aggregation path pinned (agents=3 · verified=1 · caught=1 · stopped=0 · running=1) (S18)", surface:"list", sel:"[data-verify-unit='home-hero'][data-verify-hero-mode='counts'][data-verify-hero-agents='3'][data-verify-hero-verified='1'][data-verify-hero-caught='1'][data-verify-hero-stopped='0'][data-verify-hero-running='1']", status:"shipped", tier:"v1",
608
+ drive:function(w,d){ try{ w.__covHERO=w._cached.tasks; var hnow=Date.now(); var mkh=function(id,ag,st,vd){ var t={sessionId:id,status:st,cwd:"/repo/cov-hero",startedAt:hnow,prompt:"coverage hero fixture "+id,agent:ag,steps:[],provenance:"persisted"}; if(vd){ t.verdict={claim:{status:vd}}; t.durationMs=1000; t.completedAt=hnow; } return t; }; w._cached.tasks={spawned:[],persisted:[mkh("cov-hero-run","claude-code","running",null),mkh("cov-hero-ok","codex","completed","verified"),mkh("cov-hero-bad","gemini","completed","broken")],sessions:[]}; w.upHero(); }catch(e){} },
609
+ undrive:function(w,d){ try{ w._cached.tasks=w.__covHERO; w.upHero(); }catch(e){} } },
610
+ /* room selectors are scoped to #task-list: the hidden detail view's DOM
611
+ persists (display:none still matches querySelectorAll) and also has an
612
+ .audit-line — unscoped, a dead room would false-OBSERVE off it. */
613
+ { id:"F-ROOM-HEAD", name:"Goal room header (title + outcome sub)", surface:"room", sel:"#task-list .room-head .room-title", status:"shipped", tier:"v1" },
614
+ // F-ROOM-BACK removed 2026-07-07 — the '← Projects' back-link died for the
615
+ // rail's Today row (nav option B); the rail + Esc + the logo are the way out.
616
+ { id:"F-ROOM-TURNS", name:"Turns in order (how agents worked the goal)", surface:"room", sel:"#task-list .turns-label", status:"partial", tier:"v1" },
617
+ { id:"F-ROOM-AUDIT", name:"Room audit line (steps in local chain)", surface:"room", sel:"#task-list .audit-line", status:"partial", tier:"v1" },
618
+ /* partial by nature: renders ONLY when the daemon reports project memory
619
+ OFF for the opened room (initialized projects correctly show nothing). */
620
+ { id:"F-ROOM-MEM-NUDGE", name:"Project-memory init nudge (turn on memory)", surface:"room", sel:"#task-list .mem-nudge", status:"partial", tier:"v1" },
621
+ /* on-demand surfaces: hidden until a state class the product itself
622
+ toggles. The harness DRIVES that exact state (same honesty bar as the
623
+ seeded fixtures: state occurs -> surface renders), then restores it.
624
+ Before AUD-05 these were "observed" while display:none — a blind spot. */
625
+ { id:"M-OVERLAY-LINEAR", name:"Overlay-renderer Linear", surface:"list", sel:".or-overlay", status:"shipped", tier:"v0.9",
626
+ drive:function(w,d){ d.getElementById("or-overlay").classList.add("open"); },
627
+ undrive:function(w,d){ d.getElementById("or-overlay").classList.remove("open"); } },
628
+ { id:"M-PRIMITIVES-NOTE", name:"Primitives note", surface:"list", sel:".primitives-note .prims .p", status:"shipped", tier:"v0.8",
629
+ drive:function(w,d){ w.openReshape(); },
630
+ undrive:function(w,d){ var m=d.getElementById("reshape-modal"); if(m) m.classList.remove("open"); } },
631
+ { id:"M-RESHAPE-AGENT-COMPOSE", name:"Agent-compose animation", surface:"list", sel:".agent-compose .line", status:"shipped", tier:"v0.8",
632
+ drive:function(w,d){ w.openReshape(); d.getElementById("agent-compose").classList.add("show"); },
633
+ undrive:function(w,d){ d.getElementById("agent-compose").classList.remove("show"); var m=d.getElementById("reshape-modal"); if(m) m.classList.remove("open"); } },
304
634
  { id:"M-SETTINGS-DRAWER", name:"Settings drawer", surface:"list", sel:".drawer", status:"shipped", tier:"v0.8" },
305
635
  { id:"M-SHARE-TOAST", name:"Share toast notification", surface:"list", sel:".share-toast", status:"shipped", tier:"v0.8" },
306
- { id:"M-WPASS-APPROVAL", name:"W-PASS approval modal", surface:"list", sel:".wp-modal", status:"shipped", tier:"v0.9" },
307
- { id:"P-AB-BADGE", name:"Agent identity badge", surface:"list", sel:".ab", status:"shipped", tier:"v0.6" },
308
- { id:"P-STATUS-PILL", name:"Per-status pill", surface:"list", sel:".status-icon.running", status:"shipped", tier:"v0.7" },
636
+ { id:"M-WPASS-APPROVAL", name:"W-PASS approval modal", surface:"list", sel:".wp-modal", status:"shipped", tier:"v0.9",
637
+ drive:function(w,d){ d.getElementById("wp-modal").classList.add("open"); },
638
+ undrive:function(w,d){ d.getElementById("wp-modal").classList.remove("open"); } },
639
+ { id:"P-AB-BADGE", name:"Agent identity badge (S13: on history cards + detail; rows carry the mono name)", surface:"list", sel:".ab", status:"shipped", tier:"v0.6",
640
+ drive:function(w,d){ try{ w.__histExpand(); }catch(e){} },
641
+ undrive:function(w,d){ try{ w.__histCollapse(); }catch(e){} } },
642
+ { id:"P-STATUS-PILL", name:"Per-status pill on the card (.tc-stat: running pulse / verified / reported / caught)", surface:"list", sel:".tcard .tc-stat", status:"shipped", tier:"v0.7",
643
+ drive:function(w,d){ try{ w.__histExpand(); }catch(e){} },
644
+ undrive:function(w,d){ try{ w.__histCollapse(); }catch(e){} } },
309
645
  { id:"F-DETAIL-HEADER", name:"Detail view header (back button + title + agent badge + status line)", surface:"detail", sel:".back-btn", status:"shipped", tier:"v0.7" },
310
646
  { id:"F-DETAIL-KICKER", name:"Detail kicker + failover context", surface:"detail", sel:".detail-kicker", status:"shipped", tier:"v0.7" },
311
647
  { id:"F-OUTCOME-PANEL", name:"Outcome section (files/steps/Final Report)", surface:"detail", sel:"[data-verify-unit='outcome']", status:"shipped", tier:"v0.7" },
312
648
  { id:"F-SHIMMER-TITLE", name:"Animated gradient title", surface:"detail", sel:".detail-title", status:"shipped", tier:"v0.7" },
313
649
  { id:"P-CODE-CARD", name:"Inline syntax-tinted code preview", surface:"detail", sel:".code-card", status:"shipped", tier:"v0.7" },
314
650
  { id:"P-CTX-URL-CHIP", name:"Clickable context-source URL preview chip", surface:"detail", sel:".ctx-url-chip", status:"shipped", tier:"v0.8" },
315
- { id:"P-VERIFIED-BLOCK", name:"Centered verification celebration card", surface:"detail", sel:".verified-block", status:"shipped", tier:"v0.9" },
316
- { id:"P-WRITE-PASS", name:"Write-pass inline card (blue-accented)", surface:"detail", sel:".write-pass .head + h4", status:"shipped", tier:"v0.9" },
651
+ /* S14 · the celebration card died with the calm redesign — the verified
652
+ state is now the dot+word verdict line (the only green). Honest
653
+ re-point: observe the SAME state (verdict=verified) on its new dress. */
654
+ { id:"P-VERIFIED-BLOCK", name:"Verified verdict line (S14: dot + word, the only green)", surface:"detail", sel:".vline[data-verify-verdict='verified']", status:"shipped", tier:"v0.9",
655
+ /* the verified verdict line renders in the detail for a completed,
656
+ git-tracked, non-mock task with a verified claim (verdictState==='verified').
657
+ Mock seeds are practice/reported. Mutate the ALREADY-OPEN detail task
658
+ in place (no navigate/no _cached swap — that corrupts the shared detail
659
+ other detail probes observe), re-render, observe, restore. */
660
+ drive:function(w,d){ try{ var t=w.findS?w.findS(COV_FIXTURE):null; if(t){ w.__covVB={v:t.verdict,m:t.mockRun,s:t.status,g:(t.artifacts?t.artifacts.noGitTracking:undefined),r:t.finalReport}; t.verdict={claim:{status:"verified"}}; t.mockRun=false; t.status="completed"; if(t.artifacts)t.artifacts.noGitTracking=false; t.finalReport={summary:"coverage fixture \\u00b7 claims match reality",source:"agent-summary"}; w.renderD(t); } }catch(e){} },
661
+ undrive:function(w,d){ try{ var t=w.findS?w.findS(COV_FIXTURE):null; if(t&&w.__covVB){ t.verdict=w.__covVB.v; t.mockRun=w.__covVB.m; t.status=w.__covVB.s; if(t.artifacts)t.artifacts.noGitTracking=w.__covVB.g; t.finalReport=w.__covVB.r; w.renderD(t); } }catch(e){} } },
662
+ { id:"P-WRITE-PASS", name:"Write-pass inline card (blue-accented)", surface:"detail", sel:".write-pass .head + h4", status:"shipped", tier:"v0.9",
663
+ drive:function(w,d){ try{ var c=d.querySelector("details.conv-wrap"); if(c) c.open=true; }catch(e){} },
664
+ undrive:function(w,d){ try{ var c=d.querySelector("details.conv-wrap"); if(c) c.open=false; }catch(e){} } },
317
665
  { id:"F-RECOVERY-CARD", name:"failed-task recovery card", surface:"detail", sel:".recovery-card", status:"shipped", tier:"v0.7", fixture:"demo-recovery-test-failed-v0.9" },
318
666
  { id:"F-CRASH-RECOVERY", name:"orphaned-session banner", surface:"detail", sel:".crash-recovery", status:"shipped", tier:"v0.7", fixture:"demo-crash-orphaned-mid-write-v0.9" },
319
667
  { id:"F-HOOK-BLOCK-CARD", name:"hook-block card + override", surface:"detail", sel:".hook-block-card", status:"shipped", tier:"v0.7", fixture:"demo-hook-block-bulk-rm-v0.9" },
320
668
  { id:"F-OVERRIDE-FLOW", name:"override + log reason", surface:"detail", sel:".hook-block-actions", status:"shipped", tier:"v0.7", fixture:"demo-hook-block-bulk-rm-v0.9" },
321
- { id:"F-GIT-NOTE", name:"non-git honest note", surface:"detail", sel:".git-note,.verifier-panel.partial", status:"shipped", tier:"v0.7", fixture:"demo-non-git-honest-v1" },
322
- { id:"F-CONVERSATION", name:"multi-speaker conversation", surface:"detail", sel:".conv-turn,.turn,.speaker", status:"shipped", tier:"v0.7", fixture:"demo-rate-limit-shipped-v0.9" },
669
+ { id:"F-GIT-NOTE", name:"non-git honest note (S14: .verifier-panel died · the partial verdict line carries it)", surface:"detail", sel:".git-note,.vline[data-verify-verdict='partial']", status:"shipped", tier:"v0.7", fixture:"demo-non-git-honest-v1" },
670
+ /* S14 · the conversation rests as one collapsed row ("conversation · N
671
+ turns · every tool call recorded"); these rows drive the REAL expand
672
+ (details.conv-wrap.open — the same toggle the user's click flips),
673
+ observe inside, then collapse back. */
674
+ { id:"F-CONVERSATION", name:"multi-speaker conversation (S14: behind the collapsed conversation row)", surface:"detail", sel:".conv-turn,.turn,.speaker", status:"shipped", tier:"v0.7", fixture:"demo-rate-limit-shipped-v0.9",
675
+ drive:function(w,d){ try{ var c=d.querySelector("details.conv-wrap"); if(c) c.open=true; }catch(e){} },
676
+ undrive:function(w,d){ try{ var c=d.querySelector("details.conv-wrap"); if(c) c.open=false; }catch(e){} } },
323
677
  { id:"P-ARTIFACTS-STRUCTURED", name:"structured artifacts", surface:"detail", sel:".art-cmp,.artifact,.art-card,.struct-art", status:"partial", tier:"v0.7", fixture:"demo-rate-limit-shipped-v0.9" },
324
678
  { id:"P-PANEL-TITLE", name:"uppercase panel title", surface:"detail", sel:".panel-title", status:"shipped", tier:"v0.7", fixture:"demo-rate-limit-shipped-v0.9" },
325
- { id:"F-THREAD-PHASED", name:"phased narrative timeline", surface:"detail", sel:".tl-div,.chapter-card,.phase-pos", status:"shipped", tier:"v0.7", fixture:"demo-rate-limit-shipped-v0.9" },
679
+ { id:"F-THREAD-PHASED", name:"phased narrative timeline", surface:"detail", sel:".tl-div,.chapter-card,.phase-pos", status:"shipped", tier:"v0.7", fixture:"demo-rate-limit-shipped-v0.9",
680
+ drive:function(w,d){ try{ var c=d.querySelector("details.conv-wrap"); if(c) c.open=true; }catch(e){} },
681
+ undrive:function(w,d){ try{ var c=d.querySelector("details.conv-wrap"); if(c) c.open=false; }catch(e){} } },
326
682
  { id:"F-FILES-TESTS", name:"files & tests panel", surface:"detail", sel:".file-tree", status:"shipped", tier:"v0.7", fixture:"demo-rate-limit-shipped-v0.9" },
327
- { id:"P-TOOL-CARD", name:"tool-call activity card", surface:"detail", sel:".tl-acts", status:"shipped", tier:"v0.7", fixture:"demo-rate-limit-shipped-v0.9" },
328
- { id:"F-THINKING-EMITTER", name:"live thinking dots (needs SSE token stream)", surface:"detail", sel:".thinking", status:"partial", tier:"v0.7", fixture:"demo-running-streaming-v0.9" },
329
- { id:"P-REPL-PRIMITIVES", name:"streaming repl-beat (needs SSE token stream)", surface:"detail", sel:".repl-beat", status:"partial", tier:"v0.7", fixture:"demo-running-streaming-v0.9" },
330
- { id:"P-AUDIT-LINE", name:"inline audit beat (host-thread / live audit)", surface:"detail", sel:".audit-line", status:"partial", tier:"v0.7", fixture:"demo-rate-limit-shipped-v0.9" },
683
+ { id:"P-TOOL-CARD", name:"tool-call activity card", surface:"detail", sel:".tl-acts", status:"shipped", tier:"v0.7", fixture:"demo-rate-limit-shipped-v0.9",
684
+ drive:function(w,d){ try{ var c=d.querySelector("details.conv-wrap"); if(c) c.open=true; }catch(e){} },
685
+ undrive:function(w,d){ try{ var c=d.querySelector("details.conv-wrap"); if(c) c.open=false; }catch(e){} } },
686
+ { id:"F-THINKING-EMITTER", name:"live thinking dots (needs SSE token stream)", surface:"detail", sel:".thinking", status:"partial", tier:"v0.7", fixture:"demo-running-streaming-v0.9",
687
+ drive:function(w,d){ try{ var c=d.querySelector("details.conv-wrap"); if(c) c.open=true; }catch(e){} },
688
+ undrive:function(w,d){ try{ var c=d.querySelector("details.conv-wrap"); if(c) c.open=false; }catch(e){} } },
689
+ { id:"P-REPL-PRIMITIVES", name:"streaming repl-beat (needs SSE token stream)", surface:"detail", sel:".repl-beat", status:"partial", tier:"v0.7", fixture:"demo-running-streaming-v0.9",
690
+ drive:function(w,d){ try{ var c=d.querySelector("details.conv-wrap"); if(c) c.open=true; }catch(e){} },
691
+ undrive:function(w,d){ try{ var c=d.querySelector("details.conv-wrap"); if(c) c.open=false; }catch(e){} } },
692
+ { id:"P-AUDIT-LINE", name:"inline audit beat (host-thread / live audit)", surface:"detail", sel:".audit-line", status:"partial", tier:"v0.7", fixture:"demo-rate-limit-shipped-v0.9",
693
+ drive:function(w,d){ try{ var c=d.querySelector("details.conv-wrap"); if(c) c.open=true; }catch(e){} },
694
+ undrive:function(w,d){ try{ var c=d.querySelector("details.conv-wrap"); if(c) c.open=false; }catch(e){} } },
331
695
  { id:"F-TOKEN-TABLE", name:"per-agent token table (needs artifacts.usage)", surface:"detail", sel:".token-table", status:"partial", tier:"v0.7", fixture:"demo-rate-limit-shipped-v0.9" },
332
- { id:"M-OVERLAY-FIGMA", name:"overlay-renderer · Figma", surface:"detail", sel:".overlay-figma", status:"future", tier:"v1.0" },
333
- { id:"M-OVERLAY-DRAWIO", name:"overlay-renderer · draw.io", surface:"detail", sel:".overlay-drawio", status:"future", tier:"v1.0" },
334
- { id:"V1-MULTITASK", name:"multi-task projects · parent/child", surface:"list", sel:".project-rollup", status:"roadmap", tier:"v1.0" },
335
- { id:"V1-FANOUT-ALL", name:"@all fanout to ready adapters", surface:"list", sel:".fanout-all", status:"roadmap", tier:"v1.0" },
696
+ /* overlay-renderer per-source cards · the drive renders the REAL per-source
697
+ render branch over a fixture payload (same shape the adapter unit fixtures
698
+ produce) so .overlay-figma/.overlay-drawio are observable without a live
699
+ Figma token or network fetch. */
700
+ { id:"M-OVERLAY-FIGMA", name:"overlay-renderer · Figma", surface:"list", sel:".overlay-figma", status:"shipped", tier:"v1.0",
701
+ drive:function(w,d){ w.codehereRenderOverlayPayload({ source:"figma", url:"figma://file/abc123", text:"Figma file · API Design\\nCANVAS · Mobile\\n FRAME · Home 375×812\\n TEXT · Title · \\"Welcome to the app\\"", metadata:{ fileKey:"abc123", name:"API Design", lastModified:"2026-05-21T09:00:00Z", frames:[{ name:"Home", width:375, height:812, text:"Welcome to the app" }] } }); },
702
+ undrive:function(w,d){ var o=d.getElementById("or-overlay"); if(o) o.classList.remove("open"); } },
703
+ { id:"M-OVERLAY-DRAWIO", name:"overlay-renderer · draw.io", surface:"list", sel:".overlay-drawio", status:"shipped", tier:"v1.0",
704
+ drive:function(w,d){ w.codehereRenderOverlayPayload({ source:"drawio", url:"drawio://gh/owner/repo/docs/arch.drawio", text:"draw.io diagram · github.com/owner/repo/docs/arch.drawio\\n\\nstructure: 5 cells · 1 edges · 1 page\\n--- raw xml (truncated) ---\\n<mxfile><diagram><mxGraphModel>...", metadata:{ label:"github.com/owner/repo/docs/arch.drawio", bytes:842, cellCount:5, edgeCount:1, pageCount:1 } }); },
705
+ undrive:function(w,d){ var o=d.getElementById("or-overlay"); if(o) o.classList.remove("open"); } },
706
+ { id:"V1-MULTITASK", name:"multi-task projects · parent/child", surface:"list", sel:".project-rollup", status:"shipped", tier:"v1.0",
707
+ /* It-4 · banner-drive precedent: inject a known 3-task batch STATE
708
+ (__seedCovBatch, never DOM), then the REAL openRoom() -> renderRoom()
709
+ derives the .project-rollup row live. Restored in undrive. */
710
+ drive:function(w,d){ try{ __seedCovBatch(w); w.__covBatchCwd=__openBatchRoom(w); }catch(e){} },
711
+ undrive:function(w,d){ try{ if(w.closeRoom) w.closeRoom(); __unseedCovBatch(w); }catch(e){} } },
712
+ { id:"V1-FANOUT-ALL", name:"@all fanout to ready adapters", surface:"list", sel:".fanout-all", status:"shipped", tier:"v1.0",
713
+ /* Same injected batch room renders the .fanout-all grid (one column/adapter). */
714
+ drive:function(w,d){ try{ __seedCovBatch(w); __openBatchRoom(w); }catch(e){} },
715
+ undrive:function(w,d){ try{ if(w.closeRoom) w.closeRoom(); __unseedCovBatch(w); }catch(e){} } },
336
716
  { id:"F-AGENT-SWITCHER", name:"'reply as' agent picker", surface:"detail", sel:".agent-switcher", status:"future", tier:"cut" }
337
717
  ];
338
718
 
@@ -347,13 +727,230 @@ export const VERIFY_HTML = `<!doctype html>
347
727
  return { verdict: el.getAttribute("data-verify-verdict"), gittracked: el.getAttribute("data-verify-gittracked") };
348
728
  });
349
729
  var says = Array.prototype.slice.call(doc.querySelectorAll('.tl-say')).map(function(el){
350
- var t = (el.querySelector(".tl-say-text") ? el.querySelector(".tl-say-text").textContent : "") || "";
730
+ // A glance turn carries its words in .gl-line (the one-sentence glance);
731
+ // a plain reasoning bubble in .tl-say-text. First match in document
732
+ // order wins — an actually-wordless bubble still reads len 0 and FAILS.
733
+ var w = el.querySelector(".tl-say-text, .gl-line");
734
+ var t = (w ? w.textContent : "") || "";
351
735
  t = t.replace(/^\\s+|\\s+$/g, "");
352
736
  return { len:t.length, text:t };
353
737
  });
738
+ // Thariq loop-1 · recovery-card buttons. Capture each action button's
739
+ // enabled state + whether its onclick is a bare al() toast — so the
740
+ // recovery-buttons-must-not-lie probe can FAIL on a relapse.
741
+ var recoveryButtons = Array.prototype.slice.call(doc.querySelectorAll('.recovery-card [data-verify-action]')).map(function(el){
742
+ var oc = el.getAttribute("onclick") || "";
743
+ return {
744
+ action: el.getAttribute("data-verify-action"),
745
+ disabled: el.hasAttribute("disabled"),
746
+ // a "lie" = an ENABLED button whose only behavior is a toast
747
+ bareToast: !el.hasAttribute("disabled") && /^\\s*al\\(/.test(oc),
748
+ hasReason: !!el.getAttribute("title"),
749
+ };
750
+ });
751
+ // Thariq loop-2 · the observer rail. Capture its mode (empty/active), the
752
+ // claimed live count, per-row states, and whether the empty state actually
753
+ // teaches (non-blank) — so H8 can pin: never a stale "live", never a blank
754
+ // empty box, never fake rows.
755
+ var obsRail = doc.querySelector('[data-verify-unit="observer"]');
756
+ var observer = obsRail ? {
757
+ mode: obsRail.getAttribute("data-verify-observer"),
758
+ liveClaimed: Number(obsRail.getAttribute("data-verify-observer-live") || 0),
759
+ rowStates: Array.prototype.slice.call(obsRail.querySelectorAll('[data-verify-obs-state]')).map(function(r){ return r.getAttribute("data-verify-obs-state"); }),
760
+ teachLen: (obsRail.querySelector('.obs-teach') ? (obsRail.querySelector('.obs-teach').textContent||"").trim().length : 0),
761
+ textLen: (obsRail.textContent||"").trim().length
762
+ } : null;
763
+ // S7 · the composer cost chips. Capture each chip's computed mode + what
764
+ // it says, so H10 can pin: the chip never claims a computation that
765
+ // didn't happen (the pre-S7 chip was a hardcoded ~$0.02 under a
766
+ // "based on prompt length + pricing" tooltip).
767
+ var costEst = Array.prototype.slice.call(doc.querySelectorAll('[data-verify-unit="cost-est"]')).map(function(el){
768
+ return {
769
+ mode: el.getAttribute("data-verify-mode"),
770
+ text: (el.textContent||"").trim(),
771
+ title: el.getAttribute("title") || ""
772
+ };
773
+ });
354
774
  var found = !!(meta && visible);
775
+ // S12 · H13 — the home hero's CLAIMED counts (stamped data-verify-hero-*)
776
+ // vs an INDEPENDENT re-derivation from the same task state the page holds
777
+ // (_cached via the iframe window). The hero element persists in the DOM
778
+ // while a detail is open, so the check is readable from any fixture.
779
+ var heroEl = doc.querySelector('[data-verify-unit="home-hero"]');
780
+ var hero = null;
781
+ if (heroEl){
782
+ hero = {
783
+ mode: heroEl.getAttribute("data-verify-hero-mode"),
784
+ agents: Number(heroEl.getAttribute("data-verify-hero-agents")||0),
785
+ verified: Number(heroEl.getAttribute("data-verify-hero-verified")||0),
786
+ caught: Number(heroEl.getAttribute("data-verify-hero-caught")||0),
787
+ stopped: Number(heroEl.getAttribute("data-verify-hero-stopped")||0),
788
+ running: Number(heroEl.getAttribute("data-verify-hero-running")||0),
789
+ expected: null
790
+ };
791
+ try {
792
+ var hw = doc.defaultView;
793
+ var hAll = (hw && typeof hw.allS==="function") ? hw.allS((hw._cached && hw._cached.tasks)||{}) : [];
794
+ /* S18 · PAIRED with ui-page-v2 heroCounts' top filter: practice runs
795
+ (mockRun) never feed the day-line. Both sides exclude them
796
+ identically — the honest-rule update, not a weakened probe. */
797
+ var hReal = hAll.filter(function(t){ return !/^demo-/.test(t.sessionId||"") && !t.mockRun; });
798
+ var hSod = new Date(); hSod.setHours(0,0,0,0);
799
+ var hToday = hReal.filter(function(t){ return (t.startedAt||0) >= hSod.getTime(); });
800
+ var hLive = ["running","queued","paused","blocked"];
801
+ // Refuter C2 · this used to copy heroCounts' own (then-broken)
802
+ // expressions — a probe that re-derives the claim pipeline is probe
803
+ // theater. Derive from the VERDICT ground truth: verified = the
804
+ // verdict says claims match reality, on a real (non-mock) completed
805
+ // git-tracked run; caught = the verdict says broken; stopped =
806
+ // terminal-unclean statuses; running ignores the day boundary.
807
+ var hLiveAll = hReal.filter(function(t){ return hLive.indexOf(t.status)>=0; });
808
+ var hAg = {};
809
+ hToday.concat(hLiveAll).forEach(function(t){ var a=String(t.agent||""); if(a) hAg[a]=1; });
810
+ hero.expected = {
811
+ today: hToday.length,
812
+ agents: Object.keys(hAg).length,
813
+ // PAIRED with heroCounts' composite-grade rule (2.2): green needs an
814
+ // INDEPENDENT signal when a grade is attached; the legacy claim check
815
+ // applies only to verdicts without one. The sides drifted when the
816
+ // hero adopted grades and this stayed on claim-status — first exposed
817
+ // 2026-07-02 by a real --ship run (claim verified, self-report only):
818
+ // hero said 1, this derived 2. Change BOTH sides identically.
819
+ verified: hToday.filter(function(t){
820
+ return !!(t.verdict && (t.verdict.grade ? t.verdict.grade.green : (t.verdict.claim && t.verdict.claim.status==="verified")))
821
+ && !t.mockRun
822
+ && ["completed","done"].indexOf(t.status)>=0
823
+ && !(t.artifacts && t.artifacts.noGitTracking);
824
+ }).length,
825
+ caught: hToday.filter(function(t){ var v=t.verdict; return !!(v && (v.grade ? v.grade.headline==="broken" : (v.claim && v.claim.status==="broken"))); }).length,
826
+ stopped: hToday.filter(function(t){ return ["failed","error","crashed","killed"].indexOf(t.status)>=0; }).length,
827
+ running: hLiveAll.length
828
+ };
829
+ } catch(e){ /* state unreachable — H13 reports BLOCKED, never a silent green */ }
830
+ }
831
+ // S13 · H14 — the decision queue's CLAIMED N (data-verify-decisions-n)
832
+ // vs an INDEPENDENT re-derivation from the same state the page holds:
833
+ // pending write-passes (window._wpPendingAll, the list wpPoll feeds the
834
+ // queue AND the modal from) + verifier-broken verdicts today + failed
835
+ // runs with the prompt recorded (Retry available), non-demo, today.
836
+ // Like the hero, the container persists in the DOM while a detail is
837
+ // open, so the check is readable from any fixture.
838
+ var dqEl = doc.querySelector('[data-verify-unit="decisions"]');
839
+ var decisions = {
840
+ present: !!dqEl,
841
+ n: dqEl ? Number(dqEl.getAttribute("data-verify-decisions-n")||0) : 0,
842
+ rows: dqEl ? dqEl.querySelectorAll(".dq-row").length : 0,
843
+ expected: null
844
+ };
845
+ try {
846
+ var dw = doc.defaultView;
847
+ if (dw && typeof dw.allS === "function"){
848
+ /* S18 · PAIRED with ui-page-v2 decisionItems' top filter: a practice
849
+ (mockRun) failure never demands a real decision. Both sides exclude
850
+ them identically — the honest-rule update, not a weakened probe. */
851
+ var dAll = dw.allS((dw._cached && dw._cached.tasks)||{}).filter(function(t){ return !/^demo-/.test(t.sessionId||"") && !t.mockRun; });
852
+ var dSod = new Date(); dSod.setHours(0,0,0,0);
853
+ var dToday = function(t){ return (t.startedAt||0) >= dSod.getTime(); };
854
+ var dBrokenIs = function(t){ return !!(t.verdict && t.verdict.claim && t.verdict.claim.status==="broken"); };
855
+ var dPasses = (dw._wpPendingAll||[]).length;
856
+ var dBroken = dAll.filter(function(t){ return dToday(t) && dBrokenIs(t); }).length;
857
+ var dFailed = dAll.filter(function(t){ return dToday(t) && ["failed","error","crashed","killed"].indexOf(t.status)>=0 && t.prompt && !dBrokenIs(t); }).length;
858
+ decisions.expected = dPasses + dBroken + dFailed;
859
+ }
860
+ } catch(e){ /* state unreachable — H14 reports BLOCKED, never a silent green */ }
861
+ // S12 · the detail verdict hero stamps which staging it chose; it ALSO
862
+ // carries data-verify-unit="verifier" so H1/H9 already read it above.
863
+ var vhEl = doc.querySelector("[data-verify-verdict-hero]");
864
+ var verdictHero = vhEl ? vhEl.getAttribute("data-verify-verdict-hero") : null;
865
+ // S2 · a running task must carry a Stop control on its detail.
866
+ var hasStopControl = !!doc.querySelector('.detail-meta [data-verify-action="stop"]');
867
+ // S1 · the detail composer's follow-up honesty: enabled means the server
868
+ // says the message can be DELIVERED; disabled must carry a plain reason.
869
+ var dta = doc.getElementById('detail-ta');
870
+ var followUp = dta ? {
871
+ state: dta.getAttribute('data-verify-followup'),
872
+ disabled: dta.disabled,
873
+ placeholderLen: (dta.getAttribute('placeholder')||'').trim().length
874
+ } : null;
875
+ // S19 · glyph discipline — scan the REAL rendered surface (visible text
876
+ // nodes + human-facing attributes, script/style excluded) for emoji /
877
+ // pictograph codepoints outside the sanctioned geometric glyph set
878
+ // (\\u2713 \\u2715 \\u25CF \\u25CB \\u2022 \\u2298 \\u203A \\u26A0 \\u2014 \\u00B7, \\u2191 send, \\u2190 back,
879
+ // \\u2318/\\u23CE keyboard hints). Effect-observing: a re-introduced emoji
880
+ // anywhere in the driven chrome FAILS the board via H15.
881
+ var glyphScan = { scanned:false, hits:[] };
882
+ try {
883
+ // S19 R2 · the token contract bans emoji-AS-ICON (chrome), never emoji
884
+ // a user typed: [data-user-content] containers (prompts, say/think
885
+ // text, reports, step labels/outputs) are run CONTENT and excluded —
886
+ // an honest "fix the bug" prompt with an emoji must not void the board.
887
+ var gIsContent = function(el){
888
+ while (el && el.nodeType===1){
889
+ if (el.hasAttribute && el.hasAttribute("data-user-content")) return true;
890
+ el = el.parentNode;
891
+ }
892
+ return false;
893
+ };
894
+ var gParts = [];
895
+ var gCollect = function(){
896
+ var gWalk = doc.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT, { acceptNode: function(nd){
897
+ var pn = nd.parentNode && nd.parentNode.nodeName;
898
+ if (pn==="SCRIPT"||pn==="STYLE") return NodeFilter.FILTER_REJECT;
899
+ return gIsContent(nd.parentNode) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT;
900
+ } });
901
+ while (gWalk.nextNode()) gParts.push(gWalk.currentNode.nodeValue);
902
+ Array.prototype.slice.call(doc.body.querySelectorAll("*")).forEach(function(el){
903
+ if (el.nodeName==="SCRIPT"||el.nodeName==="STYLE") return;
904
+ if (gIsContent(el)) return;
905
+ ["title","placeholder","aria-label","alt","value"].forEach(function(at){
906
+ var v = el.getAttribute && el.getAttribute(at);
907
+ if (v) gParts.push(v);
908
+ });
909
+ });
910
+ };
911
+ gCollect();
912
+ // S19 R2 · the chrome claim must be OBSERVED in the one-click-away
913
+ // collapse states too: drive the REAL history/practice toggles (the
914
+ // same render path the S18 coverage drives use), collect again, then
915
+ // restore whichever state the surface was in. Pure UI state — no data
916
+ // is changed, and the duplicate text just re-feeds the dedup'd regex.
917
+ var gw = doc.defaultView;
918
+ var gHistBtn = doc.querySelector(".hist-row:not(.prac-row)");
919
+ var gPracBtn = doc.querySelector(".prac-row");
920
+ var gHistWas = gHistBtn ? gHistBtn.getAttribute("aria-expanded")==="true" : false;
921
+ var gPracWas = gPracBtn ? gPracBtn.getAttribute("aria-expanded")==="true" : false;
922
+ try {
923
+ if (gw && gw.__histExpand) gw.__histExpand();
924
+ if (gw && gw.__pracExpand) gw.__pracExpand();
925
+ gCollect();
926
+ } finally {
927
+ try { if (gw && !gHistWas && gw.__histCollapse) gw.__histCollapse(); } catch(e2){}
928
+ try { if (gw && !gPracWas && gw.__pracCollapse) gw.__pracCollapse(); } catch(e2){}
929
+ }
930
+ var gText = gParts.join("\\n");
931
+ var gRe = /[\\u2192-\\u21FF]|[\\u2300-\\u2317]|[\\u2319-\\u23CD]|[\\u23CF-\\u23FF]|[\\u2600-\\u269F]|[\\u26A1-\\u26FF]|[\\u2700-\\u2712]|\\u2714|[\\u2716-\\u27BF]|[\\u2B00-\\u2BFF]|\\uFE0F|[\\uD83A-\\uD83F][\\uDC00-\\uDFFF]|[\\uD83C-\\uD83E][\\uDC00-\\uDFFF]/g;
932
+ var gm, gSeen = {};
933
+ while ((gm = gRe.exec(gText))){
934
+ var gc = gm[0];
935
+ if (gSeen[gc]) continue;
936
+ gSeen[gc] = 1;
937
+ var gcp = gc.codePointAt(0).toString(16).toUpperCase();
938
+ glyphScan.hits.push("U+"+gcp+" ('"+gc+"') rendered in the chrome \\u00b7 emoji/pictograph outside the sanctioned glyph set");
939
+ }
940
+ glyphScan.scanned = true;
941
+ } catch(e){ /* unscannable -> H15 reports BLOCKED, never a silent green */ }
355
942
  return {
943
+ glyphScan: glyphScan,
356
944
  found: found,
945
+ hero: hero,
946
+ decisions: decisions,
947
+ verdictHero: verdictHero,
948
+ observer: observer,
949
+ costEst: costEst,
950
+ hasStopControl: hasStopControl,
951
+ followUp: followUp,
952
+ recoveryButtons: recoveryButtons,
953
+ recoveryState: (doc.querySelector('.recovery-card') ? doc.querySelector('.recovery-card').getAttribute('data-verify-recovery') : null),
357
954
  status: meta ? meta.getAttribute("data-verify-status") : null,
358
955
  terminalFail: meta ? meta.getAttribute("data-verify-terminal-fail")==="true" : false,
359
956
  hasExplanation: meta ? meta.getAttribute("data-verify-has-explanation")==="true" : false,
@@ -385,9 +982,18 @@ export const VERIFY_HTML = `<!doctype html>
385
982
  tries++;
386
983
  try {
387
984
  var w = frameWin(), d = frameDoc();
388
- if (w && typeof w.showD === "function" && d && d.querySelectorAll(".task-card, .obs-row").length > 0) return resolve();
985
+ /* S13 · at rest the home may legitimately have NO task-cards (today
986
+ empty -> first-run hero; demos collapsed behind the history row).
987
+ Ready = task state loaded AND renderL produced a real at-rest
988
+ surface (rows, observer, the history row, or the empty state). */
989
+ if (w && typeof w.showD === "function" && w._cached && w._cached.tasks && d && d.querySelectorAll(".task-card, .obs-row, .hist-row, .empty-state").length > 0) return resolve();
990
+ // The dashboard auto-opens a running task's detail on load, which
991
+ // hides the list and left runAll dead-on-arrival ("surface never
992
+ // became ready") whenever anything was running — found cold by the
993
+ // S5-S8 acceptance walk. Close the auto-opened detail and re-poll.
994
+ if (w && typeof w.hideD === "function" && d && d.body && d.body.classList.contains("detail-open")) { try { w.hideD(); } catch(e2){} }
389
995
  } catch(e){ /* not loaded yet */ }
390
- if (tries > 80) return reject(new Error("surface never became ready (no showD / no task cards) — is the daemon serving seeded sessions?"));
996
+ if (tries > 80) return reject(new Error("surface never became ready (no showD / no task cards) · is the daemon serving seeded sessions?"));
391
997
  setTimeout(poll, 150);
392
998
  })();
393
999
  });
@@ -395,11 +1001,23 @@ export const VERIFY_HTML = `<!doctype html>
395
1001
 
396
1002
  // Navigate the REAL surface to a session and read its contract. Honest about
397
1003
  // the not-seeded case: returns found=false rather than inventing a result.
398
- function observe(sid){
1004
+ // S20 · a fixture may carry { sid, lens }: the lens is applied through the
1005
+ // page's OWN applyLens (the same function the ⌘K command runs — a real
1006
+ // recomposition, never harness CSS), observed, then restored to default.
1007
+ function observe(fx){
1008
+ var sid = (fx && typeof fx === "object") ? (fx.sid || fx.id) : fx;
1009
+ var lens = (fx && typeof fx === "object") ? fx.lens : null;
399
1010
  var w = frameWin(), d = frameDoc();
400
1011
  try { if (typeof w.hideD === "function") w.hideD(); } catch(e){}
401
- try { w.showD(sid); } catch(e){ return Promise.resolve({ found:false, error:String(e), verifiers:[], says:[] }); }
402
- return sleep(140).then(function(){ return readSurface(frameDoc()); });
1012
+ try {
1013
+ if (lens && typeof w.applyLens === "function") w.applyLens(lens);
1014
+ w.showD(sid);
1015
+ } catch(e){ return Promise.resolve({ found:false, error:String(e), verifiers:[], says:[] }); }
1016
+ return sleep(140).then(function(){
1017
+ var s = readSurface(frameDoc());
1018
+ if (lens){ try { var w2 = frameWin(); if (typeof w2.applyLens === "function") w2.applyLens("default"); } catch(e){} }
1019
+ return s;
1020
+ });
403
1021
  }
404
1022
 
405
1023
  /* -------- run one (invariant × fixture) into a structured result -------- */
@@ -432,7 +1050,7 @@ export const VERIFY_HTML = `<!doctype html>
432
1050
  var tbody = document.querySelector("#matrix tbody");
433
1051
  var hrow = "<tr><th style=\\"width:40%\\">Invariant (unit)</th><th class=\\"needs\\" style=\\"width:13%\\">needs</th>";
434
1052
  FIXTURES.forEach(function(fx){
435
- hrow += "<th><div class=\\"fixhdr"+(fx.probe?" probe":"")+"\\">"+(fx.probe?"&#128269; ":"")+fx.label+"</div></th>";
1053
+ hrow += "<th><div class=\\"fixhdr"+(fx.probe?" probe":"")+"\\">"+(fx.probe?"probe \\u00b7 ":"")+fx.label+"</div></th>";
436
1054
  });
437
1055
  hrow += "</tr>";
438
1056
  thead.innerHTML = hrow;
@@ -487,17 +1105,17 @@ export const VERIFY_HTML = `<!doctype html>
487
1105
 
488
1106
  function paintTally(c){
489
1107
  document.getElementById("tally").innerHTML =
490
- "<span class=\\"p\\">\\u2705 "+c.pass+"</span><span class=\\"f\\">\\u274c "+c.fail+"</span><span class=\\"b\\">\\u26d4 "+c.blocked+"</span><span class=\\"t\\">/ "+c.total+"</span>";
1108
+ "<span class=\\"p\\">\\u2713 "+c.pass+"</span><span class=\\"f\\">\\u2715 "+c.fail+"</span><span class=\\"b\\">\\u2298 "+c.blocked+"</span><span class=\\"t\\">/ "+c.total+"</span>";
491
1109
  var line = document.getElementById("verdict-line");
492
1110
  if (c.ok){
493
- line.className = "verdict-line green";
1111
+ line.className = "verdict-line green"; line.setAttribute("data-evidence","measured");
494
1112
  line.textContent = "\\u2713 honest: every PASS was observed, and the probe FAILED exactly as designed (the harness can see a lie).";
495
1113
  } else {
496
- line.className = "verdict-line red";
1114
+ line.className = "verdict-line red"; line.setAttribute("data-evidence","measured");
497
1115
  var parts = [];
498
1116
  if (c.unexpectedFail.length) parts.push("unexpected FAIL: "+c.unexpectedFail.join(", "));
499
- if (c.probeMissed.length) parts.push("PROBE DID NOT FAIL ("+c.probeMissed.join(", ")+") \\u2014 the harness is blind, treat the whole board as void");
500
- line.textContent = "\\u2717 " + parts.join(" \\u00b7 ");
1117
+ if (c.probeMissed.length) parts.push("PROBE DID NOT FAIL ("+c.probeMissed.join(", ")+") \\u00b7 the harness is blind, treat the whole board as void");
1118
+ line.textContent = "\\u2715 " + parts.join(" \\u00b7 ");
501
1119
  }
502
1120
  }
503
1121
 
@@ -510,15 +1128,35 @@ export const VERIFY_HTML = `<!doctype html>
510
1128
  });
511
1129
  }
512
1130
 
1131
+ // S20 · the board observes the DEFAULT lens unless a fixture says otherwise:
1132
+ // pin it before driving (the iframe shares localStorage with the dashboard,
1133
+ // so a user's saved lens would silently reshape every fixture) and restore
1134
+ // the user's saved lens after.
1135
+ function pinLensDefault(){
1136
+ try {
1137
+ var w = frameWin(), d = frameDoc();
1138
+ if (window.__verifyLensSaved === undefined) window.__verifyLensSaved = (d.body.getAttribute("data-lens")) || "default";
1139
+ if (typeof w.applyLens === "function") w.applyLens("default");
1140
+ } catch(e){}
1141
+ }
1142
+ function restoreLens(){
1143
+ try {
1144
+ var w = frameWin();
1145
+ if (typeof w.applyLens === "function") w.applyLens(window.__verifyLensSaved || "default");
1146
+ delete window.__verifyLensSaved;
1147
+ } catch(e){}
1148
+ }
1149
+
513
1150
  function runAll(){
514
1151
  var btn = document.getElementById("run-all");
515
1152
  btn.disabled = true; btn.textContent = "Running\\u2026";
516
1153
  var results = [];
517
1154
  return surfaceReady().then(function(){
1155
+ pinLensDefault();
518
1156
  // iterate fixtures, observe each once, run all invariants against it
519
1157
  return FIXTURES.reduce(function(p, fx){
520
1158
  return p.then(function(){
521
- return observe(fx.id).then(function(s){
1159
+ return observe(fx).then(function(s){
522
1160
  INVARIANTS.forEach(function(inv){
523
1161
  var r = runOne(inv, fx, s);
524
1162
  resultMap[key(inv.id, fx.id)] = r;
@@ -529,6 +1167,7 @@ export const VERIFY_HTML = `<!doctype html>
529
1167
  });
530
1168
  }, Promise.resolve());
531
1169
  }).then(function(){
1170
+ restoreLens();
532
1171
  var c = classify(results);
533
1172
  paintTally(c);
534
1173
  lastSingle = results[results.length-1] || null;
@@ -537,9 +1176,10 @@ export const VERIFY_HTML = `<!doctype html>
537
1176
  btn.disabled = false; btn.textContent = "\\u25b6 Run all";
538
1177
  return results;
539
1178
  }).catch(function(err){
1179
+ restoreLens();
540
1180
  btn.disabled = false; btn.textContent = "\\u25b6 Run all";
541
1181
  document.getElementById("verdict-line").className = "verdict-line red";
542
- document.getElementById("verdict-line").textContent = "\\u26d4 BLOCKED: "+String(err && err.message || err);
1182
+ document.getElementById("verdict-line").textContent = "\\u2298 BLOCKED: "+String(err && err.message || err);
543
1183
  throw err;
544
1184
  });
545
1185
  }
@@ -571,7 +1211,7 @@ export const VERIFY_HTML = `<!doctype html>
571
1211
  var tb = document.querySelector("#cov-matrix tbody");
572
1212
  if (!tb) return;
573
1213
  tb.innerHTML = FEATURES.map(function(f){
574
- var statusTag = f.status==="shipped"?"":f.status==="partial"?"<span class=\\"probe-tag\\" style=\\"border-color:rgba(254,188,46,.5);color:var(--warn)\\">partial</span>":f.status==="roadmap"?"<span class=\\"probe-tag\\" style=\\"border-color:var(--border);color:var(--fg-40)\\">roadmap</span>":"<span class=\\"probe-tag\\" style=\\"border-color:var(--border);color:var(--fg-40)\\">"+(f.tier==="cut"?"cut":"future")+"</span>";
1214
+ var statusTag = f.status==="shipped"?"":f.status==="partial"?"<span class=\\"probe-tag\\" style=\\"border-color:rgba(217,164,65,.5);color:var(--warn)\\">partial</span>":f.status==="roadmap"?"<span class=\\"probe-tag\\" style=\\"border-color:var(--border);color:var(--fg-40)\\">roadmap</span>":"<span class=\\"probe-tag\\" style=\\"border-color:var(--border);color:var(--fg-40)\\">"+(f.tier==="cut"?"cut":"future")+"</span>";
575
1215
  return "<tr><td><span class=\\"unit-id\\">"+f.id+"</span> <span class=\\"unit-desc\\">"+f.name+"</span>"+statusTag+"</td>"+
576
1216
  "<td class=\\"needs\\">"+f.tier+"</td>"+
577
1217
  "<td class=\\"needs\\">"+f.surface+"</td>"+
@@ -605,12 +1245,36 @@ export const VERIFY_HTML = `<!doctype html>
605
1245
  var line = document.getElementById("cov-verdict");
606
1246
  if (!line) return;
607
1247
  if (c.ok){
608
- line.className = "verdict-line green";
1248
+ line.className = "verdict-line green"; line.setAttribute("data-evidence","measured");
609
1249
  line.textContent = "\\u2713 every feature claimed shipped was OBSERVED on the real surface · "+c.shipped+" shipped checked · "+(c.roadmap+c.future)+" correctly absent (roadmap/future).";
610
1250
  } else {
611
- line.className = "verdict-line red";
612
- line.textContent = "\\u2717 "+c.missing+" feature(s) claimed shipped but NOT rendered: "+c.missingIds.join(", ")+" — PRODUCT_STATE overclaims, fix the doc or the code.";
1251
+ line.className = "verdict-line red"; line.setAttribute("data-evidence","measured");
1252
+ line.textContent = "\\u2715 "+c.missing+" feature(s) claimed shipped but NOT rendered: "+c.missingIds.join(", ")+" · PRODUCT_STATE overclaims, fix the doc or the code.";
1253
+ }
1254
+ }
1255
+
1256
+ // OBSERVED requires a VISIBLE element, not just a DOM match — display:none
1257
+ // matched querySelectorAll and green-lit four dead state banners for weeks
1258
+ // (2026-06-07 audit, AUD-05). NOTE: layout-based checks (getClientRects /
1259
+ // offsetParent) cannot work here — the observed iframe sits inside the
1260
+ // harness's own hidden replay panel, so the product surface never has
1261
+ // layout. Instead walk computed styles WITHIN the iframe document: the
1262
+ // product hiding its own element is what we test; harness chrome isn't.
1263
+ function visibleInDoc(el, d){
1264
+ var node = el;
1265
+ while (node && node !== d.documentElement) {
1266
+ var cs = (d.defaultView || window).getComputedStyle(node);
1267
+ if (cs.display === "none" || cs.visibility === "hidden") return false;
1268
+ node = node.parentElement;
613
1269
  }
1270
+ return true;
1271
+ }
1272
+ function anyVisible(d, sel){
1273
+ var els = d.querySelectorAll(sel);
1274
+ for (var i = 0; i < els.length; i++) {
1275
+ if (visibleInDoc(els[i], d)) return true;
1276
+ }
1277
+ return false;
614
1278
  }
615
1279
 
616
1280
  function runCoverage(){
@@ -619,17 +1283,32 @@ export const VERIFY_HTML = `<!doctype html>
619
1283
  covResults = [];
620
1284
  return surfaceReady().then(function(){
621
1285
  var w = frameWin();
1286
+ pinLensDefault();
622
1287
  try { if (typeof w.hideD==="function") w.hideD(); } catch(e){}
623
1288
  return sleep(140);
624
1289
  }).then(function(){
625
1290
  // list-surface features (dashboard visible)
626
1291
  var d = frameDoc();
627
- FEATURES.filter(function(f){ return f.surface==="list" && f.status!=="roadmap" && f.status!=="future"; }).forEach(function(f){
628
- var present = d.querySelectorAll(f.sel).length > 0;
1292
+ var listFeats = FEATURES.filter(function(f){ return f.surface==="list" && f.status!=="roadmap" && f.status!=="future"; });
1293
+ listFeats.filter(function(f){ return !f.drive; }).forEach(function(f){
1294
+ var present = anyVisible(d, f.sel);
629
1295
  var r = { id:f.id, present:present, verdict:covVerdict(f, present) };
630
1296
  covResults.push(r); paintCov(r);
631
1297
  });
632
- return sleep(40);
1298
+ // driven on-demand surfaces: enter the state, observe, restore.
1299
+ return listFeats.filter(function(f){ return !!f.drive; }).reduce(function(p, f){
1300
+ return p.then(function(){
1301
+ var w = frameWin(), dd = frameDoc();
1302
+ try { f.drive(w, dd); } catch(e){}
1303
+ return sleep(120).then(function(){
1304
+ var present = anyVisible(frameDoc(), f.sel);
1305
+ var r = { id:f.id, present:present, verdict:covVerdict(f, present) };
1306
+ covResults.push(r); paintCov(r);
1307
+ try { f.undrive(w, frameDoc()); } catch(e){}
1308
+ return sleep(60);
1309
+ });
1310
+ });
1311
+ }, Promise.resolve());
633
1312
  }).then(function(){
634
1313
  // detail-surface features, grouped by the fixture that TRIGGERS them.
635
1314
  // Conditional surfaces (recovery/crash/hook-block/...) only render on the
@@ -644,25 +1323,70 @@ export const VERIFY_HTML = `<!doctype html>
644
1323
  return sleep(200).then(function(){
645
1324
  var d = frameDoc();
646
1325
  byFix[fx].forEach(function(f){
647
- var present = d.querySelectorAll(f.sel).length > 0;
1326
+ /* S14 · detail rows gained drive/undrive support (same contract
1327
+ as list rows): the conversation rests collapsed, so timeline
1328
+ rows drive the REAL expand (the user's click on the row),
1329
+ observe, then restore. Never DOM injection of content. */
1330
+ var wd = frameWin();
1331
+ if (f.drive) { try { f.drive(wd, d); } catch(e){} }
1332
+ var present = anyVisible(d, f.sel);
648
1333
  var r = { id:f.id, present:present, verdict:covVerdict(f, present), fixture:fx };
649
1334
  covResults.push(r); paintCov(r);
1335
+ if (f.undrive) { try { f.undrive(wd, d); } catch(e){} }
650
1336
  });
651
1337
  });
652
1338
  });
653
1339
  }, Promise.resolve());
1340
+ }).then(function(){
1341
+ // room-surface features · drive the REAL openRoom() (same pattern as
1342
+ // showD above) on the first goal-head's data-goal-cwd, observe, close.
1343
+ // No goal-head on the home = no real tasks; the room genuinely cannot
1344
+ // be opened, so room features go MISSING/PARTIAL-GAP per their status —
1345
+ // honest, and F-GOAL-GROUP fails alongside so the cause is visible.
1346
+ var room = FEATURES.filter(function(f){ return f.surface==="room" && f.status!=="roadmap" && f.status!=="future"; });
1347
+ if (!room.length) return;
1348
+ var w0 = frameWin();
1349
+ /* S13 · goal-heads sit inside the EXPANDED history now — expand through
1350
+ the real toggle first. If the home has no earlier real tasks at all,
1351
+ fall back to the seeded coverage batch (same precedent as
1352
+ V1-MULTITASK) so the room render path is still observed for real. */
1353
+ try { if (w0.__histExpand) w0.__histExpand(); } catch(e){}
1354
+ var gh = frameDoc().querySelector(".goal-head[data-goal-cwd]");
1355
+ var check = function(){
1356
+ var d = frameDoc();
1357
+ room.forEach(function(f){
1358
+ var present = anyVisible(d, f.sel);
1359
+ var r = { id:f.id, present:present, verdict:covVerdict(f, present) };
1360
+ covResults.push(r); paintCov(r);
1361
+ });
1362
+ };
1363
+ var seededRoom = false;
1364
+ if (gh){ try { w0.openRoom(gh.getAttribute("data-goal-cwd")); } catch(e){} }
1365
+ else {
1366
+ try { if (__seedCovBatch(w0) && __openBatchRoom(w0)) seededRoom = true; } catch(e){}
1367
+ if (!seededRoom){ check(); try{ if(w0.__histCollapse) w0.__histCollapse(); }catch(e){} return; }
1368
+ }
1369
+ return sleep(200).then(function(){
1370
+ check();
1371
+ try { if (w0.closeRoom) w0.closeRoom(); } catch(e){}
1372
+ try { if (seededRoom) __unseedCovBatch(w0); } catch(e){}
1373
+ try { if (w0.__histCollapse) w0.__histCollapse(); } catch(e){}
1374
+ return sleep(120);
1375
+ });
654
1376
  }).then(function(){
655
1377
  // roadmap / future: SKIP (correctly absent), no observation
656
1378
  FEATURES.filter(function(f){ return f.status==="roadmap" || f.status==="future"; }).forEach(function(f){
657
1379
  var r = { id:f.id, present:false, verdict:covVerdict(f, false) };
658
1380
  covResults.push(r); paintCov(r);
659
1381
  });
1382
+ restoreLens();
660
1383
  var c = classifyCov(covResults);
661
1384
  paintCovTally(c);
662
1385
  window.__verify_coverage = { results:covResults, summary:c };
663
1386
  if (btn){ btn.disabled = false; btn.textContent = "\\u25b6 Run coverage"; }
664
1387
  return covResults;
665
1388
  }).catch(function(err){
1389
+ restoreLens();
666
1390
  if (btn){ btn.disabled = false; btn.textContent = "\\u25b6 Run coverage"; }
667
1391
  throw err;
668
1392
  });
@@ -695,7 +1419,7 @@ export const VERIFY_HTML = `<!doctype html>
695
1419
  function openReplay(){ replayOpen = true; document.getElementById("replay").classList.remove("hidden"); buildStepList(); }
696
1420
  function buildStepList(){
697
1421
  document.getElementById("steplist").innerHTML = FIXTURES.map(function(fx,i){
698
- return "<div class=\\"steprow"+(fx.probe?" fail":"")+"\\" data-i=\\""+i+"\\"><span class=\\"st\\">\\u25cb</span><span class=\\"nm\\">"+(fx.probe?"&#128269; ":"")+fx.label+"</span></div>";
1422
+ return "<div class=\\"steprow"+(fx.probe?" fail":"")+"\\" data-i=\\""+i+"\\"><span class=\\"st\\">\\u25cb</span><span class=\\"nm\\">"+(fx.probe?"probe \\u00b7 ":"")+fx.label+"</span></div>";
699
1423
  }).join("");
700
1424
  Array.prototype.slice.call(document.querySelectorAll(".steprow")).forEach(function(row){
701
1425
  row.addEventListener("click", function(){ jumpTo(Number(row.getAttribute("data-i"))); });
@@ -706,7 +1430,7 @@ export const VERIFY_HTML = `<!doctype html>
706
1430
  if (!row) return;
707
1431
  Array.prototype.slice.call(document.querySelectorAll(".steprow")).forEach(function(r){ r.classList.remove("active"); });
708
1432
  if (verdict==null){ row.classList.add("active"); row.querySelector(".st").textContent="\\u25b8"; return; }
709
- row.querySelector(".st").textContent = verdict==="PASS"?"\\u2705":verdict==="FAIL"?"\\u274c":"\\u26d4";
1433
+ row.querySelector(".st").textContent = verdict==="PASS"?"\\u2713":verdict==="FAIL"?"\\u2715":"\\u2298";
710
1434
  }
711
1435
 
712
1436
  var caption = document.getElementById("caption");
@@ -738,7 +1462,7 @@ export const VERIFY_HTML = `<!doctype html>
738
1462
  var pass = results.filter(function(r){ return r.verdict==="PASS"; });
739
1463
  var verdict = fails.length ? "FAIL" : (pass.length ? "PASS" : "BLOCKED");
740
1464
  var html = "<span class=\\"badge "+verdict.toLowerCase()+"\\">"+verdict+"</span>";
741
- html += "<span class=\\"sub\\" style=\\"color:var(--fg-50)\\">\\u2705 "+pass.length+" \\u00b7 \\u274c "+fails.length+" \\u00b7 \\u26d4 "+blocked.length+"</span>";
1465
+ html += "<span class=\\"sub\\" style=\\"color:var(--fg-50)\\">\\u2713 "+pass.length+" \\u00b7 \\u2715 "+fails.length+" \\u00b7 \\u2298 "+blocked.length+"</span>";
742
1466
  if (fails.length) html += "<span class=\\"issues\\">"+fails.map(function(f){ return f.unitId+": "+(f.checks[0]?f.checks[0].label:""); }).join(" \\u00b7 ")+"</span>";
743
1467
  else if (!pass.length && blocked.length) html += "<span class=\\"issues blk\\">"+blocked[0].blockedReason+"</span>";
744
1468
  fs.innerHTML = html;
@@ -751,7 +1475,7 @@ export const VERIFY_HTML = `<!doctype html>
751
1475
  var d = frameDoc(); if (!d || staged) return;
752
1476
  var st = d.createElement("style");
753
1477
  st.textContent =
754
- ".header,.custom-view-banner,.failover-banner,.indexing-banner,.budget-banner,.squad-row,.bar,#task-detail-view .input-area{display:none!important}" +
1478
+ ".header,.custom-view-banner,.failover-banner,.indexing-banner,.budget-banner,#task-detail-view .input-area{display:none!important}" +
755
1479
  "body{overflow:auto!important;height:auto!important}" +
756
1480
  ".content{padding:0!important}#task-detail-view{padding:18px 22px!important}";
757
1481
  d.head.appendChild(st);
@@ -770,7 +1494,7 @@ export const VERIFY_HTML = `<!doctype html>
770
1494
  var row = document.getElementById("cl-"+invId);
771
1495
  if (!row) return;
772
1496
  row.className = "cl-row run " + verdict.toLowerCase();
773
- row.querySelector(".cl-st").textContent = verdict==="PASS"?"\\u2705":verdict==="FAIL"?"\\u274c":"\\u26d4";
1497
+ row.querySelector(".cl-st").textContent = verdict==="PASS"?"\\u2713":verdict==="FAIL"?"\\u2715":"\\u2298";
774
1498
  if (text) row.querySelector(".cl-tx").textContent = text;
775
1499
  }
776
1500
 
@@ -781,6 +1505,8 @@ export const VERIFY_HTML = `<!doctype html>
781
1505
  if (invId==="H3") return d.querySelector('[data-verify-unit="outcome"]');
782
1506
  if (invId==="H5") return d.querySelector('[data-verify-unit="filetree"]') || d.querySelector('[data-verify-unit="outcome"]');
783
1507
  if (invId==="H6") return d.querySelector('.recovery-card,.crash-recovery,.hook-block-card') || d.querySelector('[data-verify-unit="detail"]');
1508
+ if (invId==="H13") return d.querySelector('[data-verify-unit="home-hero"]') || d.querySelector('[data-verify-unit="detail"]');
1509
+ if (invId==="H14") return d.querySelector('[data-verify-unit="decisions"]') || d.querySelector('[data-verify-unit="home-hero"]') || d.querySelector('[data-verify-unit="detail"]');
784
1510
  return d.querySelector('[data-verify-unit="detail"]');
785
1511
  }
786
1512
  // A granular, human-readable line for what THIS check observed.
@@ -791,6 +1517,8 @@ export const VERIFY_HTML = `<!doctype html>
791
1517
  if (inv.id==="H3") return "H3 \\u00b7 outcome renders "+s.outcomeItems+" item(s)";
792
1518
  if (inv.id==="H5") return "H5 \\u00b7 claims "+s.filesClaimed+" files, renders "+s.filetreeRendered+" row(s)";
793
1519
  if (inv.id==="H6") return "H6 \\u00b7 status='"+s.status+"', reason shown="+s.hasExplanation;
1520
+ if (inv.id==="H13" && s.hero && s.hero.expected) return "H13 \\u00b7 hero claims a="+s.hero.agents+" v="+s.hero.verified+" c="+s.hero.caught+" r="+s.hero.running+" \\u00b7 derived a="+s.hero.expected.agents+" v="+s.hero.expected.verified+" c="+s.hero.expected.caught+" r="+s.hero.expected.running;
1521
+ if (inv.id==="H14" && s.decisions) return "H14 \\u00b7 queue "+(s.decisions.present?("claims N="+s.decisions.n+", renders "+s.decisions.rows+" row(s)"):"absent")+" \\u00b7 derived "+s.decisions.expected+" decision(s)";
794
1522
  return inv.desc;
795
1523
  }
796
1524
 
@@ -803,19 +1531,19 @@ export const VERIFY_HTML = `<!doctype html>
803
1531
  var d = frameDoc();
804
1532
  if (r.verdict==="BLOCKED"){
805
1533
  // brief, dim — honest "nothing to observe", no theatrics
806
- clSet(inv.id, "BLOCKED", inv.title+" \\u2014 n/a");
807
- showCaption("\\u26d4 " + narrate(inv, s, r));
1534
+ clSet(inv.id, "BLOCKED", inv.title+" \\u00b7 n/a");
1535
+ showCaption("\\u2298 " + narrate(inv, s, r));
808
1536
  return sleep(Math.round(KEY_HOLD*0.45)).then(function(){ showCaption(null); return r; });
809
1537
  }
810
1538
  var el = targetFor(inv.id, d);
811
1539
  if (el && el.scrollIntoView) try { el.scrollIntoView({block:"center"}); } catch(e){}
812
1540
  return sleep(120).then(function(){
813
1541
  drawRing(el);
814
- showCaption("\\ud83d\\udd0d " + narrate(inv, s, r));
1542
+ showCaption("\\u25CF " + narrate(inv, s, r));
815
1543
  return sleep(KEY_HOLD);
816
1544
  }).then(function(){
817
1545
  removeRing();
818
- var verdictWord = r.verdict==="FAIL" ? " \\u2192 \\u274c FAIL" : " \\u2192 \\u2705 PASS";
1546
+ var verdictWord = r.verdict==="FAIL" ? " \\u203A \\u2715 FAIL" : " \\u203A \\u2713 PASS";
819
1547
  showCaption((r.verdict==="FAIL"?"\\u26a0 ":"") + narrate(inv, s, r) + verdictWord);
820
1548
  clSet(inv.id, r.verdict, inv.title);
821
1549
  return sleep(Math.round(KEY_HOLD*0.7));
@@ -829,7 +1557,7 @@ export const VERIFY_HTML = `<!doctype html>
829
1557
  idx = i;
830
1558
  markStep(i, null);
831
1559
  clReset();
832
- document.getElementById("stage-title").innerHTML = "case " + (i+1) + "/" + FIXTURES.length + " &middot; <strong>" + (fx.probe?"&#128269; ":"") + fx.label + "</strong> <span class=\\"dim\\">/ "+fx.id+"</span>";
1560
+ document.getElementById("stage-title").innerHTML = "case " + (i+1) + "/" + FIXTURES.length + " &middot; <strong>" + (fx.probe?"probe \\u00b7 ":"") + fx.label + "</strong> <span class=\\"dim\\">/ "+fx.id+"</span>";
833
1561
  document.getElementById("framestatus").innerHTML = "<span class=\\"sub\\" style=\\"color:var(--fg-40)\\">opening case\\u2026</span>";
834
1562
  setStageVerdict(null);
835
1563
  document.getElementById("prog").style.width = ((i)/FIXTURES.length*100)+"%";
@@ -839,8 +1567,15 @@ export const VERIFY_HTML = `<!doctype html>
839
1567
  return sleep(120);
840
1568
  }).then(function(){
841
1569
  stageClean();
842
- showCaption("\\ud83d\\udc46 open case \\u2192 showD('"+fx.label+"')");
843
- try { frameWin().showD(fx.id); } catch(e){}
1570
+ showCaption("open case \\u203A showD('"+fx.label+"')");
1571
+ // S20 · lens fixtures replay through the page's own applyLens — the
1572
+ // human watches the recomposed surface, not the default one.
1573
+ try {
1574
+ var w = frameWin();
1575
+ if (fx.lens && typeof w.applyLens === "function") w.applyLens(fx.lens);
1576
+ else pinLensDefault();
1577
+ w.showD(fx.sid || fx.id);
1578
+ } catch(e){}
844
1579
  return sleep(Math.round(KEY_HOLD*0.7));
845
1580
  }).then(function(){
846
1581
  showCaption(null);
@@ -863,15 +1598,16 @@ export const VERIFY_HTML = `<!doctype html>
863
1598
 
864
1599
  function jumpTo(i){ playing=false; openReplay(); playFixture(i); }
865
1600
  function advance(){
866
- if (idx+1 >= FIXTURES.length){ playing=false; document.getElementById("play").innerHTML="&#8635; Replay again"; finishReplay(); return; }
1601
+ if (idx+1 >= FIXTURES.length){ playing=false; document.getElementById("play").innerHTML="&#9654; Replay again"; finishReplay(); return; }
867
1602
  playFixture(idx+1).then(function(){ if(playing) setTimeout(advance, DWELL); });
868
1603
  }
869
1604
  function startReplay(){
870
1605
  replayResults = []; idx = 0; playing = true;
871
- document.getElementById("play").innerHTML = "&#10074;&#10074; Pause";
1606
+ document.getElementById("play").innerHTML = "&#9646;&#9646; Pause";
872
1607
  playFixture(0).then(function(){ if(playing) setTimeout(advance, DWELL); });
873
1608
  }
874
1609
  function finishReplay(){
1610
+ restoreLens();
875
1611
  var c = classify(replayResults);
876
1612
  window.__verify_replay = { done:true, results:replayResults, summary:c };
877
1613
  }
@@ -887,11 +1623,11 @@ export const VERIFY_HTML = `<!doctype html>
887
1623
  var recStream=null, recorder=null, recChunks=[], recClips=[];
888
1624
  document.getElementById("record").addEventListener("click", function(){
889
1625
  var btn = this;
890
- if (recStream){ stopRecording(); btn.innerHTML="&#9210; Record clips"; return; }
1626
+ if (recStream){ stopRecording(); btn.innerHTML="&#9679; Record clips"; return; }
891
1627
  navigator.mediaDevices.getDisplayMedia({ video:{frameRate:30}, audio:false, preferCurrentTab:true })
892
1628
  .then(function(stream){
893
- recStream = stream; btn.innerHTML="&#9210; Recording \\u2014 stop";
894
- stream.getVideoTracks()[0].addEventListener("ended", function(){ stopRecording(); btn.innerHTML="&#9210; Record clips"; });
1629
+ recStream = stream; btn.innerHTML="&#9679; Recording \\u00b7 stop";
1630
+ stream.getVideoTracks()[0].addEventListener("ended", function(){ stopRecording(); btn.innerHTML="&#9679; Record clips"; });
895
1631
  // record one continuous clip across the whole replay (simple + reliable);
896
1632
  // download offered on stop.
897
1633
  recChunks = [];
@@ -905,7 +1641,7 @@ export const VERIFY_HTML = `<!doctype html>
905
1641
  recorder.start(200);
906
1642
  startReplay();
907
1643
  })
908
- .catch(function(e){ btn.innerHTML="&#9210; Record clips"; alert("recording cancelled: "+e); });
1644
+ .catch(function(e){ btn.innerHTML="&#9679; Record clips"; alert("recording cancelled: "+e); });
909
1645
  });
910
1646
  function stopRecording(){
911
1647
  try { if(recorder && recorder.state!=="inactive") recorder.stop(); } catch(e){}