@chrono-meta/fh-gate 2.15.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/.claude/regression/probes_live.yaml +137 -0
  2. package/.claude/rules/.residency-patterns.defaults +7 -0
  3. package/.claude/rules/fh_4axis_gate.md +88 -1
  4. package/.claude-plugin/marketplace.json +8 -2
  5. package/AGENTS.md +27 -0
  6. package/CATALOG.md +17 -0
  7. package/CLAUDE.md +13 -3
  8. package/README.ja.md +51 -7
  9. package/README.ko.md +48 -7
  10. package/README.md +43 -6
  11. package/README.zh.md +45 -8
  12. package/docs/STANDARDS_ALIGNMENT.md +61 -0
  13. package/docs/USER_GUIDE.md +3 -0
  14. package/docs/USE_CASES.md +50 -0
  15. package/docs/model_tier_expectations.md +60 -0
  16. package/knowledge/shared/harness-core/fh_three_layer_canon.md +77 -10
  17. package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +18 -0
  18. package/knowledge/shared/harness-core/harness_incubator_doctrine.md +9 -0
  19. package/knowledge/shared/harness-core/iso_ai_standards_crosswalk.md +139 -0
  20. package/knowledge/shared/harness-core/measurement-integrity-checklist.md +35 -1
  21. package/knowledge/shared/harness-core/ship_readiness_gate.md +19 -2
  22. package/knowledge/shared/learnings/subagent_invocations_log.yaml +489 -3
  23. package/package.json +48 -2
  24. package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
  25. package/plugins/fh-commons/skills/preprep/README.md +4 -1
  26. package/plugins/fh-commons/skills/preprep/SKILL.md +99 -3
  27. package/plugins/fh-commons/skills/preprep/diagram_from_json.py +154 -0
  28. package/plugins/fh-commons/skills/preprep/fixtures/fixture_R3_negative.pptx +0 -0
  29. package/plugins/fh-commons/skills/preprep/fixtures/fixture_R3_positive.pptx +0 -0
  30. package/plugins/fh-commons/skills/preprep/fixtures/mk_slide_fixtures.py +179 -0
  31. package/plugins/fh-commons/skills/preprep/interslide_deps.py +98 -9
  32. package/plugins/fh-commons/skills/preprep/lane_adjacent_dup.py +4 -1
  33. package/plugins/fh-commons/skills/preprep/lane_diagram.py +95 -0
  34. package/plugins/fh-commons/skills/preprep/lane_geometry.py +181 -0
  35. package/plugins/fh-commons/skills/preprep/lane_promise.py +4 -1
  36. package/plugins/fh-commons/skills/preprep/lane_slide_refs.py +134 -0
  37. package/plugins/fh-commons/skills/preprep/lane_slide_relations.py +300 -0
  38. package/plugins/fh-commons/skills/preprep/preprep.py +129 -8
  39. package/plugins/fh-commons/skills/preprep/preprep_wire.py +210 -0
  40. package/plugins/fh-commons/skills/preprep/presentation_checklist.md +4 -0
  41. package/plugins/fh-commons/skills/preprep/surfaces.example.yaml +18 -0
  42. package/plugins/fh-commons/skills/preprep/test_preprep_lanes_rp.py +184 -0
  43. package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
  44. package/plugins/fh-meta/CHANGELOG.md +67 -0
  45. package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +87 -2
  46. package/plugins/fh-meta/skills/frontier-digest/SKILL.md +2 -2
  47. package/plugins/fh-meta/skills/frontier-digest/SKILL_detail.md +25 -5
  48. package/plugins/fh-meta/skills/hub-cc-pr-reviewer/SKILL.md +11 -0
  49. package/plugins/fh-meta/skills/hub-cc-pr-reviewer/SKILL_detail.md +3 -0
  50. package/plugins/fh-qp/.claude-plugin/plugin.json +22 -0
  51. package/plugins/fh-qp/README.md +71 -0
  52. package/plugins/fh-qp/fixtures/evidence_known_clean.txt +3 -0
  53. package/plugins/fh-qp/fixtures/evidence_known_dirty.txt +6 -0
  54. package/plugins/fh-qp/fixtures/reach_known_partial.tsv +4 -0
  55. package/plugins/fh-qp/fixtures/reach_known_reached.tsv +4 -0
  56. package/plugins/fh-qp/fixtures/reach_known_wall.tsv +3 -0
  57. package/plugins/fh-qp/fixtures/verdicts_known_bad_branch.tsv +2 -0
  58. package/plugins/fh-qp/fixtures/verdicts_known_bad_vacuous_machine.tsv +2 -0
  59. package/plugins/fh-qp/fixtures/verdicts_known_good.tsv +4 -0
  60. package/plugins/fh-qp/fixtures/verdicts_verify_only.tsv +3 -0
  61. package/plugins/fh-qp/qp_profile.example.yaml +29 -0
  62. package/plugins/fh-qp/scripts/qp_tools.sh +216 -0
  63. package/plugins/fh-qp/skills/qp/SKILL.md +65 -0
  64. package/plugins/fh-qp/skills/qp-plan/SKILL.md +42 -0
  65. package/plugins/fh-qp/skills/qp-regress/SKILL.md +45 -0
  66. package/plugins/fh-qp/skills/qp-run/SKILL.md +49 -0
  67. package/scripts/backtick_guard.sh +194 -0
  68. package/scripts/chamber_run.sh +14 -5
  69. package/scripts/com.forge-harness.live-eval.plist +84 -0
  70. package/scripts/compaction_probe.sh +9 -35
  71. package/scripts/context_continuity_score.sh +49 -7
  72. package/scripts/directional_diff_gate.sh +14 -2
  73. package/scripts/fh-gate.sh +3 -3
  74. package/scripts/files_manifest_shipping_check.sh +19 -0
  75. package/scripts/frontier_digest_autopilot.sh +4 -1
  76. package/scripts/gate_pathspec_check.sh +1 -1
  77. package/scripts/map_postprocess.py +90 -0
  78. package/scripts/outbound_query_guard.sh +131 -0
  79. package/scripts/outbound_query_hook.sh +373 -0
  80. package/scripts/package_coverage_check.sh +79 -18
  81. package/scripts/pipe_verdict_guard.sh +41 -1
  82. package/scripts/probe_live_eval.sh +240 -0
  83. package/scripts/probe_live_eval_lib.py +579 -0
  84. package/scripts/proposal_hook.sh +192 -0
  85. package/scripts/public_surface_scan_files.sh +11 -2
  86. package/scripts/push_zone_check.sh +78 -0
  87. package/scripts/residency_closure_scan.py +252 -0
  88. package/scripts/revert_probe.sh +250 -0
  89. package/scripts/selfcheck.sh +106 -3
  90. package/scripts/session_close_check.sh +100 -0
  91. package/scripts/sim_isolated_run.sh +195 -9
  92. package/scripts/test_action_yml_lanes.sh +97 -0
  93. package/scripts/test_backtick_guard_lanes.sh +115 -0
  94. package/scripts/test_degrade_scan_shell_probes.sh +7 -7
  95. package/scripts/test_fh_qp_lanes.sh +105 -0
  96. package/scripts/test_files_manifest_shipping_lanes.sh +5 -5
  97. package/scripts/test_gate_two_verdicts_lanes.sh +139 -0
  98. package/scripts/test_heavy_classifier_lanes.sh +1 -1
  99. package/scripts/test_lane_runner_lanes.sh +59 -33
  100. package/scripts/test_map_postprocess_lanes.sh +143 -0
  101. package/scripts/test_mapped_tracks_lanes.sh +1 -1
  102. package/scripts/test_marker_affected_lanes.sh +93 -0
  103. package/scripts/test_marker_crossfamily_lanes.sh +90 -6
  104. package/scripts/test_marker_oracle_lanes.sh +136 -0
  105. package/scripts/test_marker_soul_check_lanes.sh +24 -0
  106. package/scripts/test_node_check_lanes.sh +34 -34
  107. package/scripts/test_outbound_query_hook_lanes.sh +433 -0
  108. package/scripts/test_outbound_query_lanes.sh +87 -0
  109. package/scripts/test_package_coverage_lanes.sh +53 -27
  110. package/scripts/test_pipe_verdict_guard_lanes.sh +31 -5
  111. package/scripts/test_precommit_pointer_index_lanes.sh +33 -0
  112. package/scripts/test_preprep_diagram_lanes.sh +87 -0
  113. package/scripts/test_preprep_drift_anchor.sh +16 -7
  114. package/scripts/test_preprep_drift_anchor_lanes.sh +23 -0
  115. package/scripts/test_preprep_slide_refs_lanes.sh +169 -0
  116. package/scripts/test_probe_live_eval_lanes.sh +437 -0
  117. package/scripts/test_proposal_hook_lanes.sh +57 -0
  118. package/scripts/test_push_zone_lanes.sh +304 -0
  119. package/scripts/test_residency_closure_lanes.sh +70 -0
  120. package/scripts/test_revert_probe_lanes.sh +146 -0
  121. package/scripts/test_session_close_lanes.sh +3 -5
  122. package/scripts/test_sim_isolated_run_lanes.sh +136 -0
  123. package/scripts/test_utterance_intake_lanes.sh +414 -0
  124. package/scripts/test_worktree_reclaim_lanes.sh +70 -0
  125. package/scripts/transcript_utterances.py +222 -0
  126. package/scripts/utterance_intake.sh +424 -0
  127. package/scripts/utterance_landing_check.sh +2 -2
  128. package/scripts/validate_yaml.sh +27 -0
  129. package/scripts/worktree_reclaim.sh +95 -0
  130. package/templates/.git-hooks/pre-commit +380 -5
  131. package/templates/.git-hooks/pre-push +91 -0
  132. package/templates/RED_TEAM_REPORT.md +49 -0
  133. package/templates/settings.PreToolUse.snippet.json +101 -1
  134. package/plugins/fh-commons/README.md +0 -38
@@ -193,6 +193,63 @@ diversity vs the orchestrator** (orchestrator = Claude/opus → recruit GPT or G
193
193
  succeeded where reading-files-itself hung) — or degrade to single-session with a recorded note.
194
194
  This is the sidecar twin of `mcp-circuit-breaker` (stop a stuck external call instead of hanging on it).
195
195
 
196
+ ## Step 4.5 — Residency screen (mandatory, immediately before ANY external-family send)
197
+
198
+ **Every** payload this skill sends outward — the diff, the marker (Step 5's payload), the prompt
199
+ text itself — crosses a family boundary the moment a codex/agy/gemini sidecar is recruited.
200
+ "Sanitize before external-family dispatch" used to be a prose reminder with no consumer reading it
201
+ back (`[[feedback_half_externalization_slot_without_consumer]]` shape — a slot that exists but
202
+ nothing downstream checks). This step makes it mechanical:
203
+
204
+ ```bash
205
+ python3 scripts/residency_closure_scan.py --files src/a.py docs/b.md # every file you are about to send, as separate args (zsh: ${=var}, never an unquoted "$var")
206
+ ```
207
+
208
+ `--files` (not the default closure-walk) because what matters is **what you are actually
209
+ sending**, not this skill's own dependency graph. Read the exit code, not the words:
210
+
211
+ | Exit | Meaning | What to do |
212
+ |---|---|---|
213
+ | 0 | CLEAN — no organizational identifier found **within the scanner's own stated limits** (see its header — this is a screening result, not a safety guarantee) | Send. Record `residency=CLEAN(files=N)` in the crossfamily grounds (Step 6). |
214
+ | 1 | TAINTED — at least one payload file matched a residency pattern | Do not send as-is. Either strip/redact the tainted file(s) from the payload and re-scan, or route to an in-boundary panel member. If it truly cannot be cleaned, record `residency=TAINTED(N files, stripped=no)` and do NOT dispatch — the crossfamily value becomes a `DEGRADED_*` rung, never `panel(...)`. |
215
+ | 10 | HARNESS_ERROR — the scan itself could not run (commonly: no operator-private `.claude/rules/.residency-patterns` override on this machine — the shipped `.defaults` file alone is deliberately insufficient to claim CLEAN, per the scanner's own header) | Do not send. Record `residency=NOT_SCANNED(<reason>)` — same as exit 1's outcome (no dispatch), different reason (*could not screen* vs *screened and found something*). |
216
+
217
+ 🟥 **On a fresh install with no operator pattern file, this WILL be exit 10 every time** — that is
218
+ the scanner's own documented fail-closed default (`residency_closure_scan.py`'s header: "오버라이드
219
+ 파일이 없으면 exit 10 이다"), not a bug introduced here. It means: until an operator configures
220
+ their own residency patterns, this skill cannot claim a panel dispatch was screened, so it honestly
221
+ degrades to a `DEGRADED_*` rung with `residency=NOT_SCANNED(...)` instead of silently sending. That
222
+ is not a new block on fresh installs — the crossfamily field already tolerates `DEGRADED_*` without
223
+ hard-failing the commit; it is one more honest reason to be in that state, not a novel one.
224
+
225
+ **Where the result lands**: `templates/.git-hooks/pre-commit`'s `validate_crossfamily_leg` now
226
+ requires a `residency=CLEAN(...)` token inside the grounds of any `panel(...)` value (grace date
227
+ `RESIDENCY_TOKEN_GRACE_DATE`, no retroactivity on markers dated earlier), and accepts an optional,
228
+ format-checked `residency=(CLEAN|TAINTED|NOT_SCANNED)(...)` token on `DEGRADED_*`/`UNKNOWN`/
229
+ `declined`. Fixtures: `scripts/test_marker_crossfamily_lanes.sh` (`r1`–`r13`). This is the SAME
230
+ typed field Step 6 already emits into — one channel, not a second marker line.
231
+
232
+ 🟥 **A stripped file is invisible to the reviewer, and a reviewer's default read of invisible is
233
+ "absent," not "redacted"** (governor dogfood, 2026-09-05, same-day live use of this Step): two
234
+ TAINTED files were stripped from a real payload and the CLEAN remainder sent cross-family — the
235
+ reviewer came back with a false-positive S-finding, *"runner wiring is missing,"* because the
236
+ wiring actually lived in one of the two files that had just been stripped. Stripping fixes
237
+ residency; it does nothing about the reviewer's next inference, and silence there reads as a hole.
238
+ **So**: whenever a payload is sent with `stripped > 0`, the review PROMPT itself must name the
239
+ omission explicitly — the list of what was left out, and one line per file on what it is
240
+ responsible for (not its content, just its role) — so the reviewer can discount that surface
241
+ instead of flagging it as missing. Record whether this was done: when the token carries
242
+ `stripped=M` with `M > 0`, the same grounds line adds one phrase on whether the omission list
243
+ reached the review prompt, e.g. `residency=CLEAN(files=3, stripped=2) · omission-list: given to
244
+ reviewer`. This is evidence-of-doing, not proof — same shape as Step 5's "quote one marker line it
245
+ checked," and it does not become a new hard-blocking hook check (channel discipline stays a
246
+ grounds phrase here, per the same Mechanization Boundary this whole field already respects).
247
+
248
+ ⚠️ **Scope, same as everywhere else in this file**: CLEAN is a screening result, not a safety
249
+ guarantee (dynamic imports, non-Python artifacts, encoding tricks, and TOCTOU are named, unclosed
250
+ gaps in the scanner's own header). Recording `residency=CLEAN(...)` asserts the scan ran and found
251
+ nothing **within those limits** — it does not assert the payload is safe in some absolute sense.
252
+
196
253
  ## Step 5 — Role split + source-grounded acceptance (S-2)
197
254
 
198
255
  CC = **governor**, terminal verdict, source-closes. Sidecars = decorrelated **challengers** feeding
@@ -202,6 +259,11 @@ re-checks** it against the artifact (phantom-quench back-trace) before accepting
202
259
  source-grounded is **dropped, not judged**. No sidecar-only verdict (no weak-local-judge regression).
203
260
  Local 4090 = **canary tier** (evidence-of, never terminal verdict).
204
261
 
262
+ **Report shape (2026-09-05)**: when the findings are written up (PR body · marker `crossfamily:` expansion · chamber record),
263
+ use `templates/RED_TEAM_REPORT.md` — the five sections mirror ISO/IEC AWI TS 42119-7 (risk identification · objectives & attack
264
+ vectors · method · findings with reproducing input · life-cycle integration). It is a name tag on what this skill already
265
+ produces, not a new check; crosswalk: `knowledge/shared/harness-core/iso_ai_standards_crosswalk.md §2`.
266
+
205
267
  🟥 **What you SEND decides which axis you get — put the MARKER in the payload, not just the diff**
206
268
  (2026-08-19). Sidecars have been receiving the *diff* only. That buys a review of the **code** and
207
269
  buys nothing on the **record**: the Axes 2+3 marker is self-attested, and this repo has already
@@ -238,8 +300,10 @@ floor-tier sims), which is why it is stated here instead of left to the reader t
238
300
 
239
301
  ⚠️ **Prose, not a check** — measured recurrence is 2, below this repo's own N≥3 bar
240
302
  (`[[feedback_mechanize_at_repetition_prose_before]]`). On the third occurrence, mechanize it here.
241
- ⚠️ **Residency still governs**: a marker can name company assets. Sanitize before any external-family
242
- dispatch, exactly as with the diff — the marker is not exempt because it is metadata.
303
+ ⚠️ **Residency still governs**: a marker can name company assets. Run Step 4.5's screen against the
304
+ marker file too, not just the diff — the marker is not exempt because it is metadata. (This line was
305
+ pure prose until 2026-09-05; Step 4.5 above is what closes it — the scan itself still carries the
306
+ named, unclosed gaps in its own header, so "screened" is not "guaranteed safe".)
243
307
 
244
308
  *Origin*: sister-asset read of `raphaelchristi/harness-evolver`'s `harness-critic` agent, whose whole
245
309
  role is auditing the **evaluator** rather than the artifact. Its detection signatures (score jumps,
@@ -290,6 +354,22 @@ rejected at commit time.
290
354
  | — | change is not load-bearing — decorrelation not required | `single-family` | — |
291
355
  | — | **panel never probed** | `UNKNOWN` | **required** |
292
356
 
357
+ 🟥 **Rungs 1–2's `panel(<families>)` additionally require a `residency=CLEAN(...)` token inside the
358
+ same grounds string, since 2026-09-05** (Step 4.5 above; `pre-commit`'s `validate_crossfamily_leg`
359
+ blocks a bare `panel(...)` with no such token on a marker dated on/after
360
+ `RESIDENCY_TOKEN_GRACE_DATE`). Full grammar:
361
+
362
+ ```
363
+ crossfamily: panel(codex) — residency=CLEAN(files=7) · R1..R2, 4 findings
364
+ crossfamily: DEGRADED_SINGLE_FAMILY — residency=TAINTED(2 files, stripped=no) → not sent, probed codex/agy, 0 reachable
365
+ crossfamily: DEGRADED_SINGLE_FAMILY — residency=NOT_SCANNED(no operator pattern file) → not sent, probed codex/agy, 0 reachable
366
+ ```
367
+
368
+ The token is **optional** on `DEGRADED_*`/`UNKNOWN`/`declined` (format-checked only when present —
369
+ those rungs never claimed anything left the boundary, so nothing to require) and **rejected as a
370
+ contradiction** if `panel(...)` carries `residency=TAINTED(` or `residency=NOT_SCANNED(` on the same
371
+ line — a sent payload and an unscreened/tainted one cannot both be true.
372
+
293
373
  **The three degrade values are the load-bearing split**: *could not* (`DEGRADED_SINGLE_FAMILY`) ·
294
374
  *did not* (`DEGRADED_PANEL_UNUSED`) · *did not look* (`UNKNOWN`). Free prose merges all three, and
295
375
  each merge hides a different thing — an unrun probe renders as a zero finding
@@ -388,6 +468,11 @@ at N per run (no loop).
388
468
  is NOT accepted on a load-bearing change — that block only runs there, so "decorrelation not
389
469
  required" is a contradiction, and as a no-ack pass it was a free bypass of the lane.
390
470
  *[mandatory-pass — enforced by pre-commit, fixtures `scripts/test_marker_crossfamily_lanes.sh`]*
471
+ - Step 4.5's residency screen ran against the actual send list before any `panel(...)` claim, and
472
+ its result is recorded as a `residency=CLEAN|TAINTED|NOT_SCANNED(...)` token inside the same
473
+ `crossfamily:` grounds — required when the value is `panel(...)`, format-checked when present on
474
+ a degrade value. *[mandatory-pass — enforced by pre-commit since RESIDENCY_TOKEN_GRACE_DATE,
475
+ fixtures `scripts/test_marker_crossfamily_lanes.sh` `r1`–`r13`]*
391
476
  - Panel members are review-capable: embedding/reranker/OCR/safeguard classes are excluded **before**
392
477
  family-diversity is counted, never after. *[mandatory-pass — same fixtures, ordering anchor c4/c5]*
393
478
  - Paid recruit was per-run spend-gated (or `paid_auto` set); free local tier may auto-fire. *[mandatory-pass]*
@@ -69,7 +69,7 @@ Collect from five sources (bash per source in §Collection-Bash):
69
69
 
70
70
  | Source | Method | Cap |
71
71
  |---|---|---|
72
- | HackerNews | Algolia API, score > 10, keyword-relevant | 15 items |
72
+ | HackerNews | Algolia API `search_by_date` (date-sorted, never `/search`), score > 30, keyword-relevant | 15 items |
73
73
  | arxiv | export API, latest by submittedDate | 6 items |
74
74
  | TLDR AI | RSS, title + link | 5 items |
75
75
  | The Batch (deeplearning.ai) | HTML scraping, title + issue slug | 5 items |
@@ -215,7 +215,7 @@ Present Step 4 menu options [1]–[5]. Do not skip to [5] silently — surface t
215
215
  ## Simplification Guards
216
216
 
217
217
  - Video Tier-3 probe fails (any of `yt-dlp` / `curl_cffi` / `ffmpeg` missing, or timedtext returns 429) → fall through to operator summary; never assume `yt-dlp` works
218
- - If 3+ arxiv queries fail, proceed with HN only (do not abort)
218
+ - If 3+ arxiv queries fail (HTTP 429): back off once → WebFetch the date-sorted `arxiv.org/list/cs.SE/recent` listing → only then HN-only. Never substitute WebSearch (recall channel, 5/5 REPEATs measured 2026-09-04); report `arxiv FAILED (429)`, never `0 items`. Detail: `SKILL_detail.md §Execution form`
219
219
  - On curl timeout, skip that item and continue with the rest
220
220
  - If synthesis result exceeds 400 characters, retain top 3 items and truncate the rest
221
221
  - Without `--save`, do not create files (conversation output only)
@@ -21,20 +21,40 @@ load: on-demand
21
21
  > through WebFetch on the same URLs. If an environment *does* allow the curl form, it is equivalent —
22
22
  > the transport is not the contract, the endpoints and criteria are.
23
23
  >
24
- > **arXiv HTTP 429 is a separate item, not part of this revision**: it is a remote rate limit
25
- > (N=11 consecutive, transport-independent). On 429: back off once, then fall back to per-item
26
- > `https://arxiv.org/abs/{id}` reads; record `FAILED (429)`, never 0 items.
24
+ > **arXiv HTTP 429 — fallback is a TRANSPORT change, never a channel change (revised 2026-09-04).**
25
+ > The export API rate-limits this runner (N=11 consecutive by 08-10; 3 consecutive days 09-02·03·04
26
+ > = past the `N≥3` mechanization bar). The 08-10 prescription («fall back to per-item `abs/{id}`
27
+ > reads») was unrunnable in practice — a 429 on the *query* leaves you with no IDs to read — so runs
28
+ > fell back to **WebSearch**, and the 09-04 run measured what that does: 5/5 papers it returned were
29
+ > REPEATs. Search sorts by *canonicity*, the export API by *submission date* — a search fallback is
30
+ > a **recall channel, not a discovery channel**, and swapping it in silently converts «unrun» into
31
+ > «nothing new» (`not found ≠ 0`). Order on 429:
32
+ > 1. back off once (≥30s), retry the same query;
33
+ > 2. still 429 → WebFetch the **date-sorted HTML listing** `https://arxiv.org/list/cs.SE/recent`
34
+ > (and `https://arxiv.org/list/cs.AI/recent` if the first is empty of agent/harness items), keep
35
+ > items whose title/abstract match the three query phrases, cap 6 — same sort axis as the API;
36
+ > 3. only if that also fails → HN-only, and the progress line says `arxiv FAILED (429)`, never `0 items`.
37
+ > 🟥 WebSearch is **not** on this ladder. If a run used it anyway, the arxiv leg is reported as
38
+ > `RECALL-ONLY (WebSearch)` and its items are excluded from the NEW/REPEAT count.
27
39
 
28
40
  ### HackerNews (Algolia API)
29
41
 
30
42
  ```bash
31
43
  for KW in "AI agent" "LLM harness" "Claude" "multi-agent" "context engineering"; do
32
44
  curl -s --max-time 8 \
33
- "https://hn.algolia.com/api/v1/search?query=$(echo $KW | tr ' ' '+')&tags=story&hitsPerPage=5&numericFilters=points>10"
45
+ "https://hn.algolia.com/api/v1/search_by_date?query=$(echo $KW | tr ' ' '+')&tags=story&hitsPerPage=5&numericFilters=points>30"
34
46
  done
35
47
  ```
36
48
 
37
- Collection criteria: score > 10, keyword-relevant items only. Max 15 items.
49
+ Collection criteria: score > 30, keyword-relevant items only. Max 15 items.
50
+
51
+ > **`/search_by_date`, not `/search` (revised 2026-09-04; prescribed 2026-09-01, unapplied for 3 runs).**
52
+ > `/search` ranks by relevance and returned items dated 2025-11→2026-06 on 2026-09-01 — a *relevant-but-stale*
53
+ > leg, the mirror image of the 2026-07-26 arXiv *fresh-but-off-axis* failure, and the HN leg carried neither
54
+ > refresh trigger. `/search_by_date` sorts by submission time; the points floor rises 10→30 to keep the
55
+ > date-sorted window from filling with noise (measured 2026-09-01: `points>30` gave a clean 08-27→08-31
56
+ > window). Same rule as the arXiv leg: a leg that silently falls back to a relevance/recall channel converts
57
+ > «unrun» into «nothing new».
38
58
 
39
59
  ### arxiv
40
60
 
@@ -98,6 +98,17 @@ verdict NOT_CONFIGURED, or the
98
98
  returned, what was found — never the matched lines themselves. Same rule as the marker: the file is
99
99
  a local artifact, the capsule is what crosses the boundary.
100
100
 
101
+ **Noise control — two rules that decide what reaches the comment (absorbed 2026-09-04 from the
102
+ AI-Native SDLC playbook's `REVIEW.md`; FH already tiers findings M/S/R, but had no cap and no
103
+ exclusion list, so a long PR produced a long comment that buried the one finding that mattered):**
104
+ - **Nit cap = 5.** A nit is any finding that would not change behavior, leak data, or breach a
105
+ baseline rule (style · naming · wording · ordering). Report at most five nits, then one line:
106
+ `+N more nits (not listed)`. M-tier and S-tier findings are never capped.
107
+ - **Do-not-report list.** Skip anything a mechanical gate already enforces on this repo (the
108
+ pre-commit 4-axis gate, `validate` CI, `regression_guard`, public-surface scan) and generated or
109
+ vendored files. Reporting what a gate already blocks is double coverage that reads as noise; if
110
+ a gate *should* have caught it and did not, that is an S-tier finding about the gate, not a nit.
111
+
101
112
  Then attach the review comment (8-matrix results + self-catch + refinement suggestions + merge
102
113
  recommendation) via `gh pr comment`.
103
114
 
@@ -28,6 +28,9 @@ gh pr comment "$PR_NUMBER" --body "$(cat <<'EOF'
28
28
  ### Refinement Suggestions (following simplification guard / areas for subsequent rounds outside this PR)
29
29
  {Subsequent round areas / omit if 0 items}
30
30
 
31
+ ### Nits (max 5; +N more not listed)
32
+ {At most five style/naming/wording items; omit the section if 0. Never list what a gate already enforces.}
33
+
31
34
  ### Admin Override Merge Recommendation
32
35
  {User decision delegation / beta stage policy adherence}
33
36
  EOF
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "fh-qp",
3
+ "version": "3.1.0",
4
+ "engines": {
5
+ "claudeCode": ">=1.0.0"
6
+ },
7
+ "description": "QP (Quality Platform) — 4 skills (qp · qp-plan · qp-run · qp-regress). Plan→Automation→Regression loop for web and desktop apps with zero domain constants. Calls a registered qasp typed capability when one is present (strictest-wins); otherwise drives the target through the session's Playwright MCP (web) or computer-use MCP (desktop). Mobile deferred.",
8
+ "author": {
9
+ "name": "chrono-meta",
10
+ "email": "chrono-meta@users.noreply.github.com"
11
+ },
12
+ "keywords": [
13
+ "qa",
14
+ "quality-platform",
15
+ "test-planning",
16
+ "e2e",
17
+ "regression",
18
+ "surface-reach",
19
+ "mtm",
20
+ "domain-agnostic"
21
+ ]
22
+ }
@@ -0,0 +1,71 @@
1
+ # fh-qp — QP (Quality Platform)
2
+
3
+ QP(Quality Platform) — qasp 의 PAR 를 도메인 상수 0 으로 옮긴 FH 동봉 플러그인.
4
+
5
+ > Namespace note: in the field harness's own documents «QP» has meant *QA-Prism* (a roadmap label). This plugin's QP is **Quality Platform** — a different name space; nothing here refers to that roadmap.
6
+
7
+ **What it is for**: you are inside Claude Code, you have a web page or a desktop app in front of you, and you want a repeatable **plan → run → regress** loop with a QA lens — not a one-off "click around for me".
8
+
9
+ ## Install / prerequisites (read this first — the first run fails without them)
10
+
11
+ | you test | you need connected in this session | how QP detects it |
12
+ |---|---|---|
13
+ | a **web** page | Playwright MCP (`mcp__playwright__*`) — or the Claude-in-Chrome extension | `qp_tools.sh adapter-probe --need web` |
14
+ | a **desktop** app | computer-use MCP (`mcp__computer-use__*`) | `qp_tools.sh adapter-probe --need desktop` |
15
+ | a **mobile** app | — deferred; not supported | — |
16
+
17
+ No adapter → every stage stops with `HARNESS_ERROR` (exit 10, plus a `qp/HARNESS_ERROR.txt`). That is a typed non-pass, never a skip.
18
+ Nothing else to install: the plugin is four skills + one bash script; no Python, no npm packages.
19
+
20
+ ## First command
21
+
22
+ ```
23
+ /qp https://app.example.com/ # plan → run → regress, stops at the first HARNESS_ERROR
24
+ /qp plan https://app.example.com/ # one stage
25
+ /qp app:"Example App" # desktop (needs a profile — see below)
26
+ ```
27
+ You get files under `qp/`: `plan/inventory.md` · `plan/tcs.tsv` · `run/<ts>/verdicts.tsv` + masked evidence · `regress/<ts>/surface_reach.txt` + `delta.tsv` + `report.md`. You succeeded when `report.md` exists and its first 8 lines name the engine, the MTM state and a surface_reach line.
28
+
29
+ ## Public vs profile-required targets (zero domain constants)
30
+
31
+ The plugin ships **no** host names, app names, credentials or organisation words. A target that is not public (private IP, `localhost`, `*.internal`/`*.lan`/`*.local`, a bare hostname, or any desktop app) is `PROFILE_REQUIRED` (exit 4) until you supply a profile:
32
+
33
+ ```
34
+ cp plugins/fh-qp/qp_profile.example.yaml <a gitignored path>/qp_profile.yaml # fill hosts/apps, reference env vars for secrets
35
+ /qp https://private.example.internal/ --profile <that path>
36
+ ```
37
+ The profile is the only place a target-specific constant may live, and it is yours, never the plugin's.
38
+
39
+ ## Engine — form C (skills are canonical; machinery is pluggable)
40
+
41
+ 1. **Typed capability** — if a QA harness registers a capability whose id ends in `:par`, QP calls its entry and merges constraints strictest-wins (`knowledge/shared/harness-core/capability_composition_contract.md`). **Status 2026-09-05: no such capability is registered anywhere** (FH registry and the field harness both checked — zero cap files). Registration is the field harness's move; until then this bridge is a declared slot. Asking for it explicitly (`--engine capability`) returns `HARNESS_ERROR reason=capability-not-registered`, not a silent fallback.
42
+ 2. **MCP fallback** — the path that exists today. Every artifact header says `engine=mcp-fallback adapter=<name> evidence=dom|pixel`, so a reader can tell which edition produced the report.
43
+ 3. Desktop through computer-use is the **weaker edition** (pixel evidence, no DOM): `verify` steps close by readable screenshot text only, surface_reach hashes screenshots. The report header says so.
44
+
45
+ ## What is mechanical vs what is prose (honest boundary)
46
+
47
+ | check | where | closed by |
48
+ |---|---|---|
49
+ | target public / profile-required | `scripts/qp_tools.sh target-class` | exit enum 0/4/10 |
50
+ | adapter present | `adapter-probe` | exit 0/10 (unknown ≠ present) |
51
+ | evidence masking, residue 0 | `mask` | exit 0/5 |
52
+ | surface_reach (TCs beyond entry / all TCs) | `surface-reach` | REACHED · PARTIAL · NOT_REACHED · UNMEASURED |
53
+ | verdict record shape — status/branch/closure/verb enums, `MACHINE` needs an assertion, branch↔`expected_source` binding | `mtm-check` | exit 0/10 |
54
+ | ≥1 state-changing verb executed | `run-verbs` | exit 0/5 |
55
+ | **whether a label is TRUE** (is this really *code differs*?) | prose in `qp-run` | judgment — paired, not mechanised (a frozen rule would be tomorrow's ceiling) |
56
+
57
+ The last row is the honest residual: QP types *that* a verdict is attributable and non-vacuous, not *that* it is correct.
58
+
59
+ ## Skills
60
+ | skill | stage | Done When (summary) |
61
+ |---|---|---|
62
+ | `qp` | router | target classified · adapter probed · every artifact carries `engine=`/`evidence=` |
63
+ | `qp-plan` | Prepare | inventory ≥1 observed surface · `tcs.tsv` 8 fields with `expected_source` · ≥1 click/input TC |
64
+ | `qp-run` | Automation | `verdicts.tsv` passes `mtm-check` + `run-verbs` · evidence masked, residue 0 |
65
+ | `qp-regress` | Regression | `surface_reach.txt` from the tool · one delta row per TC · NOT_REACHED sentence present |
66
+
67
+ ## Lane
68
+ `bash scripts/test_fh_qp_lanes.sh` — known pairs for the three negatives (profile-required → refuse · no adapter → HARNESS_ERROR · dirty evidence → masked, residue 0), surface_reach four states, mtm-check good/bad, run-verbs, and a residency scan of the whole plugin (fail-closed if the scanner cannot run).
69
+
70
+ ## Prior art this is built on (curated, not reinvented)
71
+ Playwright Test Agents (planner / generator / healer) and Playwright MCP do the exploring and the driving; Cypress UI Coverage and mabl cover page-level coverage; `page.screenshot({mask})` covers web masking. QP adds only the judgment layer on top: the verdict record contract (`mtm-check`), first-step BLOCKED attribution, surface_reach as a ratio over all TCs, and profile-gated targets.
@@ -0,0 +1,3 @@
1
+ step 1 open home
2
+ step 2 click About
3
+ plain text stays
@@ -0,0 +1,6 @@
1
+ step 1 login as qa.tester@example.com
2
+ Authorization: Bearer abcdefghijklmnopqrstuvwxyz0123456789
3
+ token=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJVadQssw5c
4
+ password: hunter2hunter2
5
+ sha=0123456789abcdef0123456789abcdef01234567
6
+ plain text stays
@@ -0,0 +1,4 @@
1
+ TC-1 1 h_entry
2
+ TC-1 2 h_entry
3
+ TC-2 1 h_entry
4
+ TC-2 2 h_about
@@ -0,0 +1,4 @@
1
+ TC-1 1 h_entry
2
+ TC-1 2 h_about
3
+ TC-2 1 h_entry
4
+ TC-2 2 h_docs
@@ -0,0 +1,3 @@
1
+ TC-1 1 h_entry
2
+ TC-2 1 h_entry
3
+ TC-3 2 h_entry
@@ -0,0 +1,2 @@
1
+ #mtm: UNAVAILABLE
2
+ TC-1 FAIL DIFFERS_FROM_PLAN JUDGMENT verify NONE
@@ -0,0 +1,2 @@
1
+ #mtm: ACTIVE
2
+ TC-1 FAIL CODE_DIFFERS MACHINE verify CODE
@@ -0,0 +1,4 @@
1
+ #mtm: UNAVAILABLE
2
+ TC-1 PASS AS_PLANNED MACHINE navigate url==https://example.com/ INVENTORY
3
+ TC-2 PASS AS_PLANNED MACHINE click text-visible:About INVENTORY
4
+ TC-3 BLOCKED NONE JUDGMENT input NONE
@@ -0,0 +1,3 @@
1
+ #mtm: UNAVAILABLE
2
+ TC-1 PASS AS_PLANNED MACHINE verify text-visible:x INVENTORY
3
+ TC-2 PASS AS_PLANNED MACHINE verify text-visible:y INVENTORY
@@ -0,0 +1,29 @@
1
+ # qp_profile.example.yaml — the ONLY place a target-specific constant may live.
2
+ # Copy to a gitignored path (e.g. tracks/_meta/qp_profile.yaml) and pass it with --profile.
3
+ # The plugin itself ships ZERO domain constants; without a profile, only public targets run.
4
+ #
5
+ # Every value below is a placeholder. Nothing in this file names a real organisation.
6
+ profile_name: example
7
+ targets:
8
+ web:
9
+ - id: example-web
10
+ base_url: https://app.example.com # non-public hosts (10.x, *.internal, *.lan, bare names) REQUIRE this file
11
+ route_prefix: "" # e.g. "#/" for hash routers
12
+ entry_route: /
13
+ desktop:
14
+ - id: example-desktop
15
+ app_name: Example App # window title / bundle name as the OS shows it
16
+ credentials:
17
+ # Never put secrets here. Reference an env var; qp-run reads it at type-time and masks it in evidence.
18
+ example-web:
19
+ username_env: QP_EXAMPLE_USER
20
+ password_env: QP_EXAMPLE_PASS
21
+ tc_vocabulary:
22
+ # Optional overrides for the step-verb keywords qp-run recognises (defaults are English + Korean).
23
+ click: []
24
+ input: []
25
+ navigate: []
26
+ verify: []
27
+ masking:
28
+ # Extra evidence-masking regexes (appended to the built-in email/token/password set).
29
+ extra_patterns: []
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/env bash
2
+ # qp_tools.sh — the MECHANICAL half of fh-qp (QP · Quality Platform).
3
+ #
4
+ # The four qp-* skills are prose; a prose rule is muscle (a strong model carrying it), not skeleton.
5
+ # Everything a floor-tier session must NOT be trusted to "just do" lives here as a typed check with a
6
+ # closed exit enum, so a report can be read without trusting the narrator:
7
+ #
8
+ # target-class <url|app:NAME> [--profile FILE] → PUBLIC | PROFILE_REQUIRED | PROFILE_OK | UNKNOWN
9
+ # adapter-probe --need web|desktop --tools "a,b,c" → ADAPTER=<name> | HARNESS_ERROR
10
+ # mask <in> <out> → MASKED (residue 0) | RESIDUE
11
+ # surface-reach <evidence.tsv> → REACHED | PARTIAL | NOT_REACHED | UNMEASURED
12
+ # mtm-check <verdicts.tsv> → OK | INVALID
13
+ # run-verbs <verdicts.tsv> → OK (≥1 state-changing verb) | VERIFY_ONLY
14
+ # screen-id <snapshot-file> → 12-hex stable id of a DOM/pixel observation
15
+ #
16
+ # Exit enum (shared): 0 = pass/positive · 4 = PROFILE_REQUIRED · 5 = negative-but-typed (RESIDUE ·
17
+ # NOT_REACHED · PARTIAL · VERIFY_ONLY · INVALID) · 10 = HARNESS_ERROR / UNMEASURED (fail-closed:
18
+ # never read 10 as "clean" or "reached").
19
+ #
20
+ # Domain constants: ZERO. Every host/app name comes from the caller or the profile file.
21
+ # bash 3.2 compatible (macOS default). No python required.
22
+ set -uo pipefail
23
+
24
+ usage() { sed -n '2,20p' "$0" >&2; exit 10; }
25
+ [ $# -ge 1 ] || usage
26
+ CMD="$1"; shift
27
+
28
+ # ── helpers ────────────────────────────────────────────────────────────────────
29
+ _host_of() { # strip scheme, path, port, creds
30
+ printf '%s' "$1" | sed -E 's#^[a-zA-Z][a-zA-Z0-9+.-]*://##; s#^[^/@]*@##; s#[/?].*$##; s#:[0-9]+$##' | tr 'A-Z' 'a-z'
31
+ }
32
+ _is_private_host() { # RFC1918 / loopback / link-local / non-public suffix / bare name
33
+ local h="$1"
34
+ case "$h" in
35
+ localhost|127.*|10.*|192.168.*|169.254.*|0.0.0.0|::1) return 0 ;;
36
+ 172.1[6-9].*|172.2[0-9].*|172.3[01].*) return 0 ;;
37
+ *.local|*.internal|*.lan|*.intra|*.home|*.test|*.localhost|*.private) return 0 ;;
38
+ esac
39
+ case "$h" in *.*) return 1 ;; *) return 0 ;; esac # bare hostname (no dot) → not public
40
+ }
41
+ _profile_lists() { # $1 = profile file, $2 = needle (host or app name). YAML-lite: grep the value.
42
+ [ -f "$1" ] || return 1
43
+ /usr/bin/grep -qiF -- "$2" "$1"
44
+ }
45
+
46
+ # ── target-class ───────────────────────────────────────────────────────────────
47
+ cmd_target_class() {
48
+ local target="" profile=""
49
+ while [ $# -gt 0 ]; do case "$1" in --profile) profile="${2:-}"; shift 2 ;; *) target="$1"; shift ;; esac; done
50
+ [ -n "$target" ] || { echo "target-class: missing target" >&2; exit 10; }
51
+ case "$target" in
52
+ app:*)
53
+ local app="${target#app:}"
54
+ if [ -n "$profile" ] && _profile_lists "$profile" "$app"; then echo "PROFILE_OK kind=desktop app=$app"; exit 0; fi
55
+ echo "PROFILE_REQUIRED kind=desktop app=$app reason=desktop-targets-always-need-a-profile"; exit 4 ;;
56
+ http://*|https://*)
57
+ local h; h="$(_host_of "$target")"
58
+ [ -n "$h" ] || { echo "UNKNOWN reason=no-host"; exit 10; }
59
+ if _is_private_host "$h"; then
60
+ if [ -n "$profile" ] && _profile_lists "$profile" "$h"; then echo "PROFILE_OK kind=web host=$h"; exit 0; fi
61
+ echo "PROFILE_REQUIRED kind=web host=$h reason=non-public-host"; exit 4
62
+ fi
63
+ echo "PUBLIC kind=web host=$h"; exit 0 ;;
64
+ *) echo "UNKNOWN reason=unrecognised-target-form (want http(s)://… or app:NAME)"; exit 10 ;;
65
+ esac
66
+ }
67
+
68
+ # ── adapter-probe ──────────────────────────────────────────────────────────────
69
+ # bash cannot introspect the session's MCP servers; the SESSION passes the tool names it actually has.
70
+ # Omitting --tools is HARNESS_ERROR: unknown is not present.
71
+ cmd_adapter_probe() {
72
+ local need="" tools=""
73
+ while [ $# -gt 0 ]; do case "$1" in --need) need="${2:-}"; shift 2 ;; --tools) tools="${2:-}"; shift 2 ;; *) shift ;; esac; done
74
+ [ "$need" = web ] || [ "$need" = desktop ] || { echo "HARNESS_ERROR reason=--need must be web|desktop"; exit 10; }
75
+ [ -n "$tools" ] || { echo "HARNESS_ERROR reason=tool-list-not-supplied (unknown ≠ present)"; exit 10; }
76
+ local t; for t in $(printf '%s' "$tools" | tr ',' ' '); do
77
+ case "$need:$t" in
78
+ web:mcp__playwright__browser_navigate) echo "ADAPTER=playwright-mcp evidence=dom"; exit 0 ;;
79
+ web:mcp__claude-in-chrome__navigate) echo "ADAPTER=claude-in-chrome evidence=dom"; exit 0 ;;
80
+ desktop:mcp__computer-use__screenshot) echo "ADAPTER=computer-use-mcp evidence=pixel"; exit 0 ;;
81
+ esac
82
+ done
83
+ echo "HARNESS_ERROR reason=no-$need-adapter-among-supplied-tools"; exit 10
84
+ }
85
+
86
+ # ── mask ───────────────────────────────────────────────────────────────────────
87
+ # Record-layer masking (the typing path still sends the real value). Residue 0 is asserted AFTER writing.
88
+ _MASK_EMAIL='[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}'
89
+ _MASK_JWT='eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}'
90
+ _MASK_BEARER='[Bb]earer[[:space:]]+[A-Za-z0-9._~+/=-]{16,}'
91
+ _MASK_KEYLIKE='(sk|pk|ghp|gho|xox[abp]|AKIA)[-_][A-Za-z0-9_-]{12,}'
92
+ _MASK_HEX='[A-Fa-f0-9]{40,}'
93
+ _MASK_PWKV='([Pp]ass(word|wd)?|[Ss]ecret|[Tt]oken)[[:space:]]*[:=][[:space:]]*"?[^"[:space:],}]{4,}'
94
+ cmd_mask() {
95
+ local in="${1:-}" out="${2:-}"
96
+ [ -f "$in" ] || { echo "HARNESS_ERROR reason=input-missing"; exit 10; }
97
+ [ -n "$out" ] || { echo "HARNESS_ERROR reason=output-missing"; exit 10; }
98
+ sed -E \
99
+ -e "s/$_MASK_JWT/__REDACTED_TOKEN__/g" \
100
+ -e "s/$_MASK_BEARER/Bearer __REDACTED_TOKEN__/g" \
101
+ -e "s/$_MASK_KEYLIKE/__REDACTED_TOKEN__/g" \
102
+ -e "s/$_MASK_HEX/__REDACTED_TOKEN__/g" \
103
+ -e "s/$_MASK_PWKV/\1=__REDACTED_SECRET__/g" \
104
+ -e "s/$_MASK_EMAIL/__REDACTED_EMAIL__/g" \
105
+ "$in" > "$out" || { echo "HARNESS_ERROR reason=sed-failed"; exit 10; }
106
+ local residue
107
+ # Strip our own markers before re-scanning — the Bearer pattern matches "__REDACTED_TOKEN__" itself
108
+ # (18 word-chars). Known-pair caught this on first run: an instrument matching its own output is
109
+ # [[feedback_lane_vocabulary_blind_to_its_own_fix]] in miniature.
110
+ residue=$(sed -E 's/__REDACTED_[A-Z]+__//g' "$out" | /usr/bin/grep -cE "$_MASK_EMAIL|$_MASK_JWT|$_MASK_BEARER|$_MASK_HEX" 2>/dev/null); residue="${residue:-0}"
111
+ if [ "$residue" -eq 0 ]; then echo "MASKED residue=0 out=$out"; exit 0; fi
112
+ echo "RESIDUE residue=$residue out=$out"; exit 5
113
+ }
114
+
115
+ # ── surface-reach ──────────────────────────────────────────────────────────────
116
+ # evidence.tsv: tc_id <TAB> step <TAB> screen_hash (one row per observed screen; hash = any stable id)
117
+ # Entry screen = the modal hash across the batch. A TC is "beyond entry" iff any of its hashes ≠ entry.
118
+ # Denominator = ALL tc_ids in the file. An empty file is UNMEASURED (10), never NOT_REACHED.
119
+ cmd_surface_reach() {
120
+ local f="${1:-}"
121
+ [ -f "$f" ] || { echo "UNMEASURED reason=evidence-missing"; exit 10; }
122
+ [ -s "$f" ] || { echo "UNMEASURED reason=evidence-empty (empty set is not reached)"; exit 10; }
123
+ LC_ALL=C awk -F'\t' '
124
+ NF < 3 || $1 ~ /^#/ { next }
125
+ { tc[$1]=1; n_rows++; cnt[$3]++; seen[$1 SUBSEP $3]=1 }
126
+ END {
127
+ if (n_rows == 0) { print "UNMEASURED reason=no-rows"; exit 10 }
128
+ best=""; bestc=-1
129
+ for (h in cnt) if (cnt[h] > bestc) { bestc=cnt[h]; best=h }
130
+ total=0; beyond=0
131
+ for (t in tc) { total++; b=0; for (k in seen) { split(k, p, SUBSEP); if (p[1]==t && p[2]!=best) b=1 } beyond+=b }
132
+ st = (beyond==total) ? "REACHED" : (beyond==0 ? "NOT_REACHED" : "PARTIAL")
133
+ printf "%s tcs_beyond_entry=%d tcs_total=%d entry_screen=%s\n", st, beyond, total, best
134
+ exit (st=="REACHED") ? 0 : 5
135
+ }' "$f"
136
+ }
137
+
138
+ # ── mtm-check ──────────────────────────────────────────────────────────────────
139
+ # verdicts.tsv: first non-comment line MUST be #mtm: ACTIVE|UNAVAILABLE|FAILED|DISABLED
140
+ # rows: tc_id <TAB> status <TAB> branch <TAB> closure <TAB> verb <TAB> assertion <TAB> expected_source
141
+ # status ∈ PASS FAIL BLOCKED AMBIGUOUS
142
+ # branch ∈ AS_PLANNED DIFFERS_FROM_PLAN CODE_DIFFERS NONE
143
+ # closure ∈ MACHINE JUDGMENT (MACHINE requires a non-empty assertion — the record that closed it)
144
+ # verb ∈ navigate click input verify
145
+ # expected_source ∈ PLAN_DOC INVENTORY CODE HUMAN NONE — WHERE the expected value came from.
146
+ # The branch→source binding is the mechanical discriminator (challenger #9, governor 2026-09-05):
147
+ # DIFFERS_FROM_PLAN ⇒ expected_source = PLAN_DOC and #mtm: ACTIVE (no plan doc → this branch is unreachable)
148
+ # CODE_DIFFERS ⇒ expected_source ∈ {CODE, PLAN_DOC}
149
+ # AS_PLANNED ⇒ expected_source ≠ NONE
150
+ # NONE ⇒ status ∈ {BLOCKED, AMBIGUOUS} (a verdict with no branch is a non-verdict)
151
+ # Any violation → INVALID (10). The check asserts the RECORD's properties (typed · attributable ·
152
+ # non-vacuous), never whether the label is TRUE — that stays judgment (CLAUDE.md §Mechanization Boundary).
153
+ cmd_mtm_check() {
154
+ local f="${1:-}"
155
+ [ -f "$f" ] || { echo "INVALID reason=file-missing"; exit 10; }
156
+ local mtm; mtm=$(/usr/bin/grep -m1 -E '^#mtm:' "$f" | sed -E 's/^#mtm:[[:space:]]*//' | tr -d '[:space:]')
157
+ case "$mtm" in ACTIVE|UNAVAILABLE|FAILED|DISABLED) ;; *) echo "INVALID reason=mtm-state-missing-or-bad (#mtm: ACTIVE|UNAVAILABLE|FAILED|DISABLED)"; exit 10 ;; esac
158
+ LC_ALL=C awk -F'\t' -v mtm="$mtm" '
159
+ /^#/ || NF==0 { next }
160
+ { rows++
161
+ if (NF < 7) { bad++; why="row-has-" NF "-fields-need-7"; next }
162
+ if ($2 !~ /^(PASS|FAIL|BLOCKED|AMBIGUOUS)$/) { bad++; why="status:" $2; next }
163
+ if ($3 !~ /^(AS_PLANNED|DIFFERS_FROM_PLAN|CODE_DIFFERS|NONE)$/) { bad++; why="branch:" $3; next }
164
+ if ($3 == "DIFFERS_FROM_PLAN" && mtm != "ACTIVE") { bad++; why="DIFFERS_FROM_PLAN-needs-mtm-ACTIVE"; next }
165
+ if ($4 !~ /^(MACHINE|JUDGMENT)$/) { bad++; why="closure:" $4; next }
166
+ if ($4 == "MACHINE" && $6 == "") { bad++; why="MACHINE-closure-without-assertion"; next }
167
+ if ($5 !~ /^(navigate|click|input|verify)$/) { bad++; why="verb:" $5; next }
168
+ if ($7 !~ /^(PLAN_DOC|INVENTORY|CODE|HUMAN|NONE)$/) { bad++; why="expected_source:" $7; next }
169
+ if ($3 == "DIFFERS_FROM_PLAN" && $7 != "PLAN_DOC") { bad++; why="DIFFERS_FROM_PLAN-needs-expected_source=PLAN_DOC"; next }
170
+ if ($3 == "CODE_DIFFERS" && $7 !~ /^(CODE|PLAN_DOC)$/) { bad++; why="CODE_DIFFERS-needs-expected_source=CODE|PLAN_DOC"; next }
171
+ if ($3 == "AS_PLANNED" && $7 == "NONE") { bad++; why="AS_PLANNED-with-expected_source=NONE"; next }
172
+ if ($3 == "NONE" && $2 !~ /^(BLOCKED|AMBIGUOUS)$/) { bad++; why="branch=NONE-only-for-BLOCKED|AMBIGUOUS"; next }
173
+ if ($4=="MACHINE") m++; else j++
174
+ }
175
+ END {
176
+ if (rows == 0) { print "INVALID reason=no-rows"; exit 10 }
177
+ if (bad > 0) { printf "INVALID rows=%d bad=%d first=%s\n", rows, bad, why; exit 10 }
178
+ printf "OK mtm=%s rows=%d machine_closed=%d judgment_left=%d\n", mtm, rows, m+0, j+0; exit 0
179
+ }' "$f"
180
+ }
181
+
182
+ # ── screen-id ──────────────────────────────────────────────────────────────────
183
+ # First-use finding (2026-09-05, gh-pages target): Playwright MCP accessibility snapshots carry per-navigation
184
+ # ref tokens (`[ref=e12]`, then `[ref=f2e12]`, `[ref=f3e12]` …) — the SAME page hashed to three different ids
185
+ # across three visits, so surface_reach credited two TCs for "leaving the entry screen" before they clicked.
186
+ # The screen id must be a CONTENT hash: strip ref/active tokens and whitespace runs before hashing.
187
+ # Binary files (screenshots) are hashed as-is.
188
+ cmd_screen_id() {
189
+ local f="${1:-}"
190
+ [ -f "$f" ] || { echo "HARNESS_ERROR reason=file-missing"; exit 10; }
191
+ if LC_ALL=C /usr/bin/grep -qI . "$f"; then
192
+ sed -E 's/\[ref=[A-Za-z0-9]+\]//g; s/\[active\]//g; s/[[:space:]]+/ /g' "$f" | shasum -a 256 | cut -c1-12
193
+ else
194
+ shasum -a 256 "$f" | cut -c1-12
195
+ fi
196
+ }
197
+
198
+ # ── run-verbs (challenger #13: verify-only evidence never exercised the runner) ─
199
+ cmd_run_verbs() {
200
+ local f="${1:-}"
201
+ [ -f "$f" ] || { echo "HARNESS_ERROR reason=file-missing"; exit 10; }
202
+ local n; n=$(LC_ALL=C awk -F'\t' '!/^#/ && NF>=5 && ($5=="click" || $5=="input"){c++} END{print c+0}' "$f")
203
+ if [ "$n" -ge 1 ]; then echo "OK state_changing_verbs=$n"; exit 0; fi
204
+ echo "VERIFY_ONLY state_changing_verbs=0 (runner not exercised)"; exit 5
205
+ }
206
+
207
+ case "$CMD" in
208
+ target-class) cmd_target_class "$@" ;;
209
+ adapter-probe) cmd_adapter_probe "$@" ;;
210
+ mask) cmd_mask "$@" ;;
211
+ surface-reach) cmd_surface_reach "$@" ;;
212
+ mtm-check) cmd_mtm_check "$@" ;;
213
+ run-verbs) cmd_run_verbs "$@" ;;
214
+ screen-id) cmd_screen_id "$@" ;;
215
+ *) usage ;;
216
+ esac