@chrono-meta/fh-gate 1.4.95 → 1.4.97

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 (76) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/AGENTS.md +18 -0
  3. package/CHEATSHEET.md +1 -1
  4. package/CLAUDE.md +11 -4
  5. package/README.md +19 -0
  6. package/knowledge/shared/harness-core/fh_detail_protocols.md +12 -0
  7. package/knowledge/shared/harness-core/ship_readiness_gate.md +7 -4
  8. package/knowledge/shared/learnings/subagent_invocations_log.yaml +113 -1
  9. package/package.json +10 -2
  10. package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
  11. package/plugins/fh-commons/agents/quench-challenger.md +49 -23
  12. package/plugins/fh-commons/skills/convergence-loop/SKILL.md +14 -0
  13. package/plugins/fh-commons/skills/deliberation/SKILL.md +14 -0
  14. package/plugins/fh-commons/skills/mcp-circuit-breaker/SKILL.md +10 -1
  15. package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
  16. package/plugins/fh-meta/CHANGELOG.md +36 -0
  17. package/plugins/fh-meta/agents/beginner.md +4 -1
  18. package/plugins/fh-meta/agents/challenger.md +7 -1
  19. package/plugins/fh-meta/agents/expert.md +1 -1
  20. package/plugins/fh-meta/agents/fact-checker.md +7 -1
  21. package/plugins/fh-meta/agents/hub-persona-auditor.md +2 -1
  22. package/plugins/fh-meta/agents/main-player.md +4 -1
  23. package/plugins/fh-meta/agents/persona-innovator.md +10 -2
  24. package/plugins/fh-meta/skills/agent-composer/SKILL.md +2 -2
  25. package/plugins/fh-meta/skills/apex-review/SKILL.md +5 -0
  26. package/plugins/fh-meta/skills/asset-placement-gate/SKILL.md +38 -8
  27. package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +16 -2
  28. package/plugins/fh-meta/skills/context-doctor/SKILL_detail.md +45 -10
  29. package/plugins/fh-meta/skills/corpus-grounding-expander/SKILL.md +14 -5
  30. package/plugins/fh-meta/skills/cross-ecosystem-synergy-detection/SKILL.md +93 -30
  31. package/plugins/fh-meta/skills/deep-clarify/SKILL.md +28 -9
  32. package/plugins/fh-meta/skills/fh/SKILL.md +4 -0
  33. package/plugins/fh-meta/skills/frontier-digest/SKILL.md +64 -8
  34. package/plugins/fh-meta/skills/frontier-digest/SKILL_detail.md +20 -7
  35. package/plugins/fh-meta/skills/goal-quench/SKILL.md +48 -15
  36. package/plugins/fh-meta/skills/goal-quench/SKILL_detail.md +58 -11
  37. package/plugins/fh-meta/skills/harness-doctor/SKILL_detail.md +109 -33
  38. package/plugins/fh-meta/skills/harvest-loop/SKILL.md +6 -1
  39. package/plugins/fh-meta/skills/hub-cc-pr-reviewer/SKILL.md +126 -17
  40. package/plugins/fh-meta/skills/install-doctor/SKILL.md +50 -14
  41. package/plugins/fh-meta/skills/install-wizard/SKILL.md +26 -7
  42. package/plugins/fh-meta/skills/install-wizard/SKILL_detail.md +68 -21
  43. package/plugins/fh-meta/skills/memory-hygiene/SKILL.md +64 -17
  44. package/plugins/fh-meta/skills/meta-prompt-builder/SKILL.md +38 -4
  45. package/plugins/fh-meta/skills/persona-roster-expander/SKILL.md +15 -7
  46. package/plugins/fh-meta/skills/plugin-recommender/SKILL.md +39 -11
  47. package/plugins/fh-meta/skills/plugin-recommender/SKILL_detail.md +24 -7
  48. package/plugins/fh-meta/skills/prompt-regression/SKILL.md +54 -11
  49. package/plugins/fh-meta/skills/salience-splitter/SKILL.md +120 -7
  50. package/plugins/fh-meta/skills/salience-splitter/SKILL_detail.md +46 -13
  51. package/plugins/fh-meta/skills/sim-conductor/SKILL_detail.md +28 -3
  52. package/plugins/fh-meta/skills/steel-quench/SKILL.md +3 -1
  53. package/plugins/fh-meta/skills/verify-bidirectional/SKILL.md +72 -14
  54. package/scripts/compaction_probe.sh +153 -10
  55. package/scripts/consent_registry_check.sh +63 -0
  56. package/scripts/count_check.sh +47 -1
  57. package/scripts/degrade_direction_scan.sh +276 -6
  58. package/scripts/degrade_probe_capability.sh +105 -0
  59. package/scripts/lane_runner_check.sh +425 -0
  60. package/scripts/package_coverage_check.sh +190 -35
  61. package/scripts/psa_probe_capability.sh +78 -0
  62. package/scripts/psa_scan_lib.sh +164 -0
  63. package/scripts/public_surface_scan_files.sh +38 -0
  64. package/scripts/selfcheck.sh +346 -28
  65. package/scripts/test_capability_entrypoint_shipping.sh +132 -0
  66. package/scripts/test_consent_registry.sh +54 -0
  67. package/scripts/test_count_check_readme_format_lanes.sh +75 -0
  68. package/scripts/test_degrade_scan_shell_probes.sh +415 -0
  69. package/scripts/test_marker_crossfamily_lanes.sh +10 -1
  70. package/scripts/test_marker_floor_lanes.sh +25 -1
  71. package/scripts/test_psa_singlefile_lanes.sh +288 -0
  72. package/scripts/test_reviewer_capability_conformance.sh +10 -1
  73. package/scripts/test_selfcheck_state_lanes.sh +173 -0
  74. package/scripts/validate_yaml.sh +146 -0
  75. package/templates/degrade_direction_scan.sh +276 -6
  76. package/templates/goal-quench-settings-merged.json +30 -0
@@ -222,13 +222,23 @@ echo "CC_HUB_DIR=${CC_HUB_DIR:-not set}"
222
222
  basename "$(pwd)"
223
223
  ls .claude/ 2>/dev/null
224
224
 
225
- # CC settings (handle both dict and list for plugins)
226
- cat .claude/settings.json 2>/dev/null | python3 -c "
227
- import json,sys
228
- d=json.load(sys.stdin)
229
- p=d.get('plugins',{})
230
- print('plugins:', list(p.keys()) if isinstance(p,dict) else p)
231
- " 2>/dev/null || echo "settings.json not found"
225
+ # CC settings (handle both dict and list for plugins).
226
+ # Split existence from parseability FIRST. The old `cat file | python3 || echo "not found"` read
227
+ # $? from python, so a CORRUPT settings.json printed "settings.json not found" — the wizard would
228
+ # then happily create a fresh one and silently clobber the user's real (broken) config.
229
+ if [ ! -f .claude/settings.json ]; then
230
+ echo "settings.json: ABSENT"
231
+ else
232
+ python3 - <<'PY' || echo "settings.json: UNPARSEABLE (present but unreadable — NOT the same as absent; do NOT overwrite, ask the user)"
233
+ import json, sys
234
+ try:
235
+ d = json.load(open('.claude/settings.json'))
236
+ except Exception as e:
237
+ print(f' parse error: {e}', file=sys.stderr); sys.exit(2)
238
+ p = d.get('plugins', {})
239
+ print('settings.json: OK — plugins:', list(p.keys()) if isinstance(p, dict) else p)
240
+ PY
241
+ fi
232
242
 
233
243
  # MCP plugin connection status
234
244
  python3 -c "import json,os; d=json.load(open(os.path.expanduser('~/.claude.json'))); print('MCP:', list(d.get('mcpServers',{}).keys()))" 2>/dev/null || echo "MCP config not found"
@@ -319,9 +329,18 @@ Reduction targets are only meta operation rules (PR procedures, commit guides, F
319
329
  **Detection bash:**
320
330
 
321
331
  ```bash
322
- # Detect existing harness scale
323
- CLAUDE_MD_LINES=$(wc -l < CLAUDE.md 2>/dev/null || echo 0)
324
- RULES_COUNT=$(ls .claude/rules/*.md 2>/dev/null | wc -l || echo 0)
332
+ # Detect existing harness scale.
333
+ # `| wc -l || echo 0` is a conditional disarm, not a harmless idiom: with `set -o pipefail` active
334
+ # (which the caller's shell may well have) a failing upstream stage makes the pipeline exit non-zero
335
+ # AFTER wc has already printed its count, so the fallback appends a SECOND line, the value becomes
336
+ # "0\n0", and the `-ge 3` test below dies with "integer expression expected" — on stderr only —
337
+ # leaving the guard silent. Measured both directions 2026-08-12: single line without pipefail,
338
+ # two lines with it. Sanitize instead of falling back.
339
+ CLAUDE_MD_LINES=$(wc -l < CLAUDE.md 2>/dev/null || true)
340
+ RULES_COUNT=$(ls .claude/rules/*.md 2>/dev/null | wc -l || true)
341
+ # head -n1 FIRST, then strip: `tr -dc '0-9'` alone would fuse a two-line "12\n0" into "120".
342
+ CLAUDE_MD_LINES=$(printf '%s\n' "$CLAUDE_MD_LINES" | head -n1 | tr -dc '0-9'); CLAUDE_MD_LINES=${CLAUDE_MD_LINES:-0}
343
+ RULES_COUNT=$(printf '%s\n' "$RULES_COUNT" | head -n1 | tr -dc '0-9'); RULES_COUNT=${RULES_COUNT:-0}
325
344
 
326
345
  echo "CLAUDE.md: ${CLAUDE_MD_LINES} lines"
327
346
  echo ".claude/rules/: ${RULES_COUNT} files"
@@ -493,6 +512,15 @@ propose adding rows to `agent-composer/SKILL.md` Step 1 mapping table in this fo
493
512
  | {skill name} related task | {skill name} (S) | — |
494
513
  ```
495
514
 
515
+ > **Verify this block against a temp file, never against `~/.zshrc`.** Set `ZSHRC_TARGET` to a
516
+ > scratch path and run the five arms: FH_DIR unset → rc=1, nothing written · CC_HUB_DIR unset →
517
+ > rc=1, nothing written · `$FH_DIR` without the script → ABORT, nothing written · consent absent →
518
+ > SKIPPED, nothing written · consent Y with both vars → block appended with values **substituted**
519
+ > and `$HOME`/`$FH_DIR` left literal, then `zsh -c 'source <temp>'` exits 0. Re-running must not
520
+ > duplicate the block. **Beware a contaminated control**: if your own shell already exports
521
+ > `FH_DIR`, the "unset" arm is not actually negative — isolate with `env -u FH_DIR`. That mistake
522
+ > made the first pass of this very verification report a false PASS.
523
+
496
524
  Output preview before execution:
497
525
  ```
498
526
  ▶ agent-composer mapping update
@@ -507,18 +535,37 @@ Output preview before execution:
507
535
 
508
536
  ```bash
509
537
  # zshrc hook — preview then confirm. The wizard is AI-mediated: SHOW the user the exact block
510
- # below and ask in-chat "Append this to ~/.zshrc? (Y/N)" BEFORE running the append. An earlier
511
- # revision's comment promised preview-then-confirm while the bash appended unconditionally —
512
- # the decline-integrity sim (2026-08-10) caught the mismatch; the gate is the instruction, the
513
- # bash below runs ONLY on Y.
514
- if ! grep -q "fh_audit_check.zsh" ~/.zshrc 2>/dev/null; then
515
- # (run only after an explicit in-chat Y)
516
- cat >> ~/.zshrc << 'EOF'
517
- export FH_DIR="{FH_DIR}"
518
- export CC_HUB_DIR="{CC_HUB_DIR}"
519
- export CC_SENTINELS_DIR="$HOME/.cc_sentinels"
520
- source "$FH_DIR/templates/fh_audit_check.zsh"
538
+ # below and ask in-chat "Append this to ~/.zshrc? (Y/N)" BEFORE running the append.
539
+ #
540
+ # TWO defects were fixed here (2026-08-12), both of which corrupt every future shell:
541
+ # (1) The heredoc was QUOTED (<< 'EOF'), so the literal text `{FH_DIR}` was written to ~/.zshrc.
542
+ # The resulting `source "$FH_DIR/templates/fh_audit_check.zsh"` then resolved to
543
+ # `{FH_DIR}/templates/...` and every subsequent shell start failed with
544
+ # `no such file or directory` (rc=127, reproduced in a sandbox against a temp file).
545
+ # Fix: unquoted heredoc so FH_DIR/CC_HUB_DIR expand AT WRITE TIME, with `\$HOME` and
546
+ # `\$FH_DIR` escaped so THOSE stay literal and resolve at shell-start.
547
+ # (2) The consent gate was a COMMENT ("run only after an explicit in-chat Y"). A comment does
548
+ # not gate anything — pasted or scripted, the block appended unconditionally. It is now a
549
+ # real conditional on FH_WIZARD_ZSHRC_CONSENT, default N (fail-closed).
550
+ #
551
+ # Set FH_WIZARD_ZSHRC_CONSENT=Y only after the user answers Y in chat.
552
+ ZSHRC="${ZSHRC_TARGET:-$HOME/.zshrc}"
553
+ : "${FH_DIR:?FH_DIR is unset — refusing to write a broken source line into $ZSHRC}"
554
+ : "${CC_HUB_DIR:?CC_HUB_DIR is unset — refusing to write an incomplete block into $ZSHRC}"
555
+ if [ ! -f "$FH_DIR/templates/fh_audit_check.zsh" ]; then
556
+ echo "ABORT: \$FH_DIR/templates/fh_audit_check.zsh does not exist — would wire a dead source line"
557
+ elif [ "${FH_WIZARD_ZSHRC_CONSENT:-N}" != "Y" ]; then
558
+ echo "zshrc hook: SKIPPED (no explicit Y) — nothing written"
559
+ elif grep -q "fh_audit_check.zsh" "$ZSHRC" 2>/dev/null; then
560
+ echo "zshrc hook: already present — no change"
561
+ else
562
+ cat >> "$ZSHRC" <<EOF
563
+ export FH_DIR="$FH_DIR"
564
+ export CC_HUB_DIR="$CC_HUB_DIR"
565
+ export CC_SENTINELS_DIR="\$HOME/.cc_sentinels"
566
+ source "\$FH_DIR/templates/fh_audit_check.zsh"
521
567
  EOF
568
+ echo "zshrc hook: appended"
522
569
  fi
523
570
  # On N: do NOT append; record the decline and state its consequence in one line —
524
571
  # echo "zshrc_hook" >> "$HOME/.cc_sentinels/{project}_wizard_declined"
@@ -56,15 +56,31 @@ re-verification is possible).
56
56
 
57
57
  ### Step 1 — Scan memory/*.md
58
58
 
59
+ Enumerate **every** root with a per-root state — an absent root and an empty root are different
60
+ facts, and the Step-4 snapshot scope is bound to this enumeration:
61
+
59
62
  ```bash
60
- ls ~/.claude/projects/*/memory/*.md 2>/dev/null
61
- # or hub-local:
62
- ls memory/*.md 2>/dev/null
63
+ for ROOT in "$HOME"/.claude/projects/*/memory "$PWD/memory"; do
64
+ if [ -d "$ROOT" ]; then
65
+ n=$(ls "$ROOT"/*.md 2>/dev/null | wc -l | tr -d ' ')
66
+ echo "EXISTS(n=$n) $ROOT"
67
+ else
68
+ echo "ABSENT $ROOT"
69
+ fi
70
+ done
63
71
  ```
64
72
 
73
+ **`ABSENT` is never counted as 0 entries.** Swallowing both into `2>/dev/null` makes "this root does
74
+ not exist here" indistinguishable from "this root is empty" — and the snapshot in Constraints below
75
+ must cover every root reported `EXISTS`, so a root mis-rendered as empty silently narrows the
76
+ snapshot scope.
77
+
65
78
  For each file, extract:
66
79
  - `metadata.type` from frontmatter
67
- - `date:` or any date-like field in frontmatter or body
80
+ - **Age source, in this order** (measured over the live corpus, n=267): `metadata.modified`
81
+ (ISO8601, present on 77/267) → else the file's filesystem mtime → else **`AGE-UNKNOWN`**.
82
+ There is **no top-level `date:` field in this corpus** (measured 0/267) — do not look for one,
83
+ and never treat `AGE-UNKNOWN` as fresh.
68
84
  - Key factual claims (GitHub URLs, status strings, version numbers, dates)
69
85
 
70
86
  ### Step 2 — Classify by Staleness
@@ -89,7 +105,9 @@ For each stale entry, run the appropriate re-verification:
89
105
  **Reference type** (URLs, DOIs, GitHub):
90
106
  - Use `gh api` for GitHub resources
91
107
  - Use `WebFetch` for DOIs and arXiv records
92
- - Mark `verified_at: YYYY-MM-DD` in frontmatter if still valid
108
+ - Mark `metadata.verified_at: YYYY-MM-DD` **nested under `metadata:`**, the layer this corpus
109
+ actually uses (`metadata:` present on 265/267; a top-level `verified_at` measured 0/267 and would
110
+ be invisible to the Step-1 extractor)
93
111
  - Flag `⚠ DRIFTED` if content has changed materially
94
112
 
95
113
  **Project type** (status, milestones):
@@ -121,19 +139,47 @@ Apply updates? [y / N per item]
121
139
 
122
140
  ### Step 5 — Record Run
123
141
 
142
+ Use `printf`, never `echo "…\n…"` — `echo` expands `\n` in zsh but writes it **literally** in bash
143
+ (measured: 6 lines vs 1), which silently produces a one-line file with a broken frontmatter.
144
+
124
145
  ```bash
125
- # Create hygiene log
126
- echo "---\ndate: $(date +%Y-%m-%d)\nentries_checked: N\nupdated: N\ndrifted: N\n---" \
127
- > tracks/_meta/memory_hygiene_$(date +%Y-%m-%d).md
146
+ mkdir -p tracks/_meta
147
+ printf '%s\n' \
148
+ '---' \
149
+ "date: $(date +%Y-%m-%d)" \
150
+ "entries_checked: ${ENTRIES_CHECKED}" \
151
+ "demoted_to_archive: ${DEMOTED}" \
152
+ "orphans_indexed: ${ORPHANS}" \
153
+ "merged: ${MERGED}" \
154
+ "drifted: ${DRIFTED}" \
155
+ "hard_deleted: ${HARD_DELETED}" \
156
+ '---' \
157
+ > "tracks/_meta/memory_hygiene_$(date +%Y-%m-%d).md"
128
158
  ```
129
159
 
160
+ **Every count is a measured value from Steps 1–4.** A log still containing a literal `N` (or an
161
+ unexpanded `${…}`) is **an invalid log** — treat that run as not-recorded, because the log's
162
+ existence is what arms the 7-day cadence guard, so a placeholder log suppresses the *next* real run.
163
+
130
164
  ## Constraints
131
165
 
132
166
  - **No auto-deletion**: Archive candidates are proposed, not deleted. Human confirmation required.
133
167
  - **Snapshot before archive (Destructive-Op Gate for memory)**: before applying any confirmed
134
168
  archive/removal in Step 4, snapshot **every memory root the entry could live in** first — tar **all**
135
169
  roots Step 1 enumerated (`~/.claude/projects/*/memory/` AND hub-local `memory/`), not just one:
136
- `tar czf tracks/_meta/memory_snapshot_$(date +%Y%m%d-%H%M%S).tgz <each Step-1 root that exists>`so
170
+ the snapshot **must be gated on its own exit status** an unwritable or missing `tracks/_meta`
171
+ makes `tar` exit 1 and produce no tarball (measured), and without a gate the irreversible archive
172
+ proceeds anyway:
173
+
174
+ ```bash
175
+ SNAP="tracks/_meta/memory_snapshot_$(date +%Y%m%d-%H%M%S).tgz"
176
+ mkdir -p tracks/_meta || { echo "SNAPSHOT FAILED (mkdir) — abort archive"; exit 1; }
177
+ tar czf "$SNAP" <each Step-1 root reported EXISTS> || { echo "SNAPSHOT FAILED (tar) — abort archive"; exit 1; }
178
+ [ -s "$SNAP" ] || { echo "SNAPSHOT FAILED (empty tarball) — abort archive"; exit 1; }
179
+ echo "SNAPSHOT OK: $SNAP"
180
+ ```
181
+
182
+ Only on `SNAPSHOT OK` may the archive proceed — so
137
183
  a wrong archive in any scanned root is one-command recoverable (a single-root snapshot can pass while
138
184
  the archived entry lived in the other root — bind the scope to the entry, not to "a tarball exists").
139
185
  Archive moves entries to a `.archive/` sibling, never hard-deletes (mirrors the Curator's
@@ -147,14 +193,15 @@ echo "---\ndate: $(date +%Y-%m-%d)\nentries_checked: N\nupdated: N\ndrifted: N\n
147
193
 
148
194
  ## Done When
149
195
 
150
- ```
151
- Step 1~5 complete
152
- + Staleness roster output (Step 2 = mechanical no-LLM pre-pass)
153
- + Re-verification run for all STALE entries
154
- + User gate presented and responded to (y/N per item)
155
- + If any archive confirmed: snapshot covering the archived entry's root written before the move (tracks/_meta/memory_snapshot_*.tgz, spanning all existing Step-1 roots) check class: mandatory-pass (snapshot covers the entry's dir, not merely "a tarball exists")
156
- + Hygiene log written to tracks/_meta/memory_hygiene_{date}.md
157
- ```
196
+ | Condition | Check class |
197
+ |---|---|
198
+ | Step 1~5 complete, every memory root reported `EXISTS(n=…)` or `ABSENT` | **mandatory-pass** |
199
+ | Staleness roster output (Step 2 = mechanical no-LLM pre-pass), with `AGE-UNKNOWN` entries listed separately and not as FRESH | **measured** (age from `metadata.modified` / mtime, not recall) |
200
+ | Re-verification run for all STALE entries | **mandatory-pass** |
201
+ | Each re-verified entry's verdict (VERIFIED / DRIFTED) is correct | **judged** adversarial pairing: `fh-meta:fact-checker` re-greps the hub for the same claim; any disagreement downgrades the entry to DRIFTED (no judge-only PASS) |
202
+ | User gate presented and responded to (y/N per item) | **mandatory-pass** |
203
+ | If any archive confirmed: snapshot written **and its exit status checked** before the move (`tracks/_meta/memory_snapshot_*.tgz`, spanning every root reported `EXISTS`) | **mandatory-pass** — snapshot must cover the archived entry's dir; `SNAPSHOT OK` printed, not merely "a tarball exists" |
204
+ | Hygiene log written to `tracks/_meta/memory_hygiene_{date}.md` with real counts (no literal `N`, no unexpanded `${…}`) | **measured** |
158
205
 
159
206
  ## References
160
207
 
@@ -153,11 +153,45 @@ Acceptable form examples:
153
153
 
154
154
  ## Done When
155
155
 
156
+ This skill's own Done When must satisfy the 3-part completeness check it imposes on others
157
+ (**measurement subject · measurement timing · version/baseline**). The previous version —
158
+ `All steps 0–3 completed` — met 0 of 3 and matched its own `Unconditioned completion` WARN pattern.
159
+
156
160
  ```
157
- All steps 0–3 completed
158
- + Per-agent prompt drafts output per Wave (Goal/Context/Constraints/Done When 4-field included)
159
- + Step 3 quality self-validation complete ([WARN] items delegated to user if present)
160
- + Generated prompts awaiting user review
161
+ Measurement subject: this skill's own output — the prompt draft set
162
+ produced in this invocation (not the dispatched
163
+ agents' results, which this skill never sees)
164
+ Measurement timing: absolute, at end of this run — no comparison to a
165
+ previous run is implied or required
166
+ Version / baseline: the Step 3 checklist and the WARN-trigger table in
167
+ THIS file; note the SKILL.md revision used if the
168
+ table has changed since the draft was written
169
+
170
+ ☐ Steps 0–3 each produced their named artifact — a step with no
171
+ artifact is incomplete, not "completed" (measured: 4 steps, 4 artifacts)
172
+ ☐ One prompt draft exists per agent named in the dispatch plan (measured: drafts == agents
173
+ in plan)
174
+ ☐ Every draft carries all four fields (Goal / Context /
175
+ Constraints / Done When), none empty (measured: 4 fields × N drafts,
176
+ 0 empty)
177
+ ☐ Each draft's own Done When was run through the 3-part
178
+ completeness check and the WARN-trigger table, with the
179
+ result recorded per draft (mandatory-pass)
180
+ ☐ Every Goal was checked against the target agent's declared
181
+ allowed-tools by READING that agent's SKILL.md — not from
182
+ recall (mandatory-pass)
183
+ ☐ [WARN] items are surfaced to the user with the reason, and
184
+ the decision is the user's (mandatory-pass)
185
+ ☐ Drafts are handed over for review, not dispatched (mandatory-pass)
186
+ ☐ The drafts are usable by the target agent as written (judged — adversarial pairing:
187
+ generator and validator are the
188
+ same model, so self-validation
189
+ repeats its own bias. Have the
190
+ target agent, or a different
191
+ model, read one draft cold and
192
+ state what it would do; a
193
+ divergence from the intended
194
+ task is a FAIL)
161
195
  ```
162
196
 
163
197
  ## Simplification Guard
@@ -2,7 +2,7 @@
2
2
  name: persona-roster-expander
3
3
  description: Expands a named persona seed into a tiered, judgment-mapped cast — tiering each persona by a domain safety rule, mapping each to a decision-lens in the user's vocabulary, then proposing additional voices with sourced anchors.
4
4
  user-invocable: true
5
- allowed-tools: ["Read", "Grep", "WebSearch", "WebFetch"]
5
+ allowed-tools: ["Read", "Grep", "WebSearch", "WebFetch", "Write", "Agent"]
6
6
  model: sonnet
7
7
  ---
8
8
 
@@ -15,7 +15,12 @@ caller-supplied safety rule so the expansion stays faithful to the domain's cons
15
15
 
16
16
  > Origin: harvested from the-bible (2026-06-20) — an operator persona seed (priest/nun/angel/devil/
17
17
  > God/Jesus/Holy-Spirit/apostles) tiered relay-vs-lens by a relay-safety rule and mapped to
18
- > engineering-judgment lenses, +4 sourced proposals. See
18
+ > engineering-judgment lenses, +4 sourced proposals. **The grounds, inline, because the harvest
19
+ > record does not ship**: an ungated persona cast drifts into inventing its own authority — a voice
20
+ > given a lens will speak past what the domain lets it assert — so tiering by a *caller-supplied*
21
+ > rule is what keeps the expansion faithful, and the lens mapping is what makes a named voice
22
+ > usable as a decision instrument instead of flavor. Full harvest record — **hub-local, not
23
+ > distributed in the npm package**:
19
24
  > `tracks/_contrib/field_harvest_2026-06-20_gate-locality-and-grounding-capabilities.md`.
20
25
 
21
26
  ## Triggers
@@ -41,14 +46,17 @@ caller-supplied safety rule so the expansion stays faithful to the domain's cons
41
46
  4. **Propose 2–4 additions** filling lenses the seed doesn't cover (delegate net-new *name*
42
47
  generation to the `persona-innovator` agent — this skill's distinct value is the tiering +
43
48
  lens-mapping, not naming). For the strongest 2–3, find a real anchor (a sourced
44
- example/reference), not a shallow stub.
45
- 5. **Emit the tiered, lens-mapped cast** (named + proposed) as a structured artifact the system can
46
- load (e.g. `personas.json`).
49
+ example/reference); **any remaining proposal ships with a literal `stub:` prefix** — unanchored
50
+ and unlabeled is not an allowed output (see Done When).
51
+ 5. **Emit the tiered, lens-mapped cast** (named + proposed) by **writing it to a structured file**
52
+ the system can load (e.g. `personas.json`), then confirm the written file parses. Leaving the
53
+ cast in the response text only does not complete this step.
47
54
 
48
55
  ## Done When
49
56
  - **Every persona has a tier + lens label + invoke-condition.** *Check class: mandatory-pass (binary — all three fields present per persona).*
50
- - **Each net-new proposal names a real anchor** (not a stub). *Check class: judged, pair: an anchor-existence check (the cited source/reference resolves).*
51
- - **The tiering respects the caller's safety rule** (no persona exceeds its tier's allowed emission). *Check class: judged, pair: an adversarial read find a persona whose mapping lets it act beyond its tier.*
57
+ - **Every proposal is either anchored or explicitly labeled a stub.** An anchored proposal names a source/reference that resolves; a proposal without one is emitted with a literal `stub:` prefix and is **not counted as a candidate**. *Check class: mandatory-pass (binary each proposal carries either a resolving anchor or a `stub:` label; an unanchored, unlabeled proposal is FAIL). Anchor resolution is checked mechanically (fetch/look up the cited reference), not by judgment.* (This is the Done-When floor; Step 4's "strongest 2–3" is the effort target that sits above it — a 4th proposal may ship as `stub:` without violating either.)
58
+ - **The cast is materialized as a loadable artifact** (e.g. `personas.json`) that a consumer can parse — the file exists on disk and a parse of it succeeds against the declared schema (per persona: tier / lens / invoke-condition; per proposal additionally `anchor` or `stub:`). *Check class: mandatory-pass (binary file exists and parses; a cast that exists only in the response text is UNMET).*
59
+ - **The tiering respects the caller's safety rule** (no persona exceeds its tier's allowed emission). *Check class: judged, pair: dispatch `fh-meta:challenger` at the tier-escalation angle — "find a persona whose lens mapping lets it emit beyond its tier". The pairing is a **different-agent** read; the author's own re-read does not satisfy it. If that agent is unreachable, record `pair: unavailable (<reason>)` — the condition stays UNMET and is **reported as a named residual**, never self-scored closed. It does **not** block delivery: a roster is a reversible artifact, so the degrade direction here is declare-and-ship, not fail-closed (that direction is reserved for irreversible surfaces — publish, delete, history-rewrite). Shipping with an UNMET pairing is honest; silently marking it met is the defect.*
52
60
 
53
61
  ## Guards
54
62
  - **Caller-supplied safety rule is mandatory** — the skill tiers by the domain's rule, it does not
@@ -2,7 +2,7 @@
2
2
  name: plugin-recommender
3
3
  description: Given a task description, searches internal and external open-source ecosystems (including Codex marketplace and Claude Code marketplace) to find and recommend suitable plugins with installation guidance. Recommendation is quality-validation based (marketplace-listed + performance-validated), not source-origin based. Activates on "recommend a plugin", "what tool should I use?", "is there a plugin for this?", "recommend a tool". Also checks for duplicate installations.
4
4
  user-invocable: true
5
- allowed-tools: ["Grep", "GoogleWebSearch", "Bash"]
5
+ allowed-tools: ["Read", "Grep", "Bash", "WebSearch", "WebFetch"]
6
6
  model: sonnet
7
7
  ---
8
8
 
@@ -59,7 +59,9 @@ Tier is **independent of platform origin** (Anthropic / OpenAI / community). A w
59
59
 
60
60
  2. **[Priority 1.5] Internal GHE Sister Assets (partially completed work — Tier 2)**: Check if user's task domain already exists in internal sister asset clusters. Prioritize direct use or adoption of sister assets if the user's task falls within these cluster domains.
61
61
 
62
- 3. **[Priority 2.5] Project Reference/Contribution Path**:
62
+ 3. **[Priority 2] Organization's Internal GHE (Tier 1–4)**: Search internal GHE with keywords like `claude-plugin`, `gemini-plugin` + user keywords / API search. Replace with your organization's internal GHE orgs.
63
+
64
+ 4. **[Priority 2.5] Project Reference/Contribution Path**:
63
65
 
64
66
  For cases where referencing or contributing to the project itself is more appropriate than installing a plugin. Provide guidance when there's intent for medium-term contribution rather than immediate use.
65
67
 
@@ -71,9 +73,7 @@ Tier is **independent of platform origin** (Anthropic / OpenAI / community). A w
71
73
  - `plugin-recommender` [Priority 2.5]: When no immediately usable plugin is available → guide to project-level reference/contribution path (alternative to installing)
72
74
  - `cross-ecosystem-synergy-detection`: Discover hidden synergies among already-installed skills (post-install utilization optimization)
73
75
 
74
- Activation condition: Automatically entered when Step 2 [Priority 1]~[Priority 2] search yields no suitable plugin.
75
-
76
- 4. **[Priority 2] Organization's Internal GHE (Tier 1–4)**: Search internal GHE with keywords like `claude-plugin`, `gemini-plugin` + user keywords / API search. Replace with your organization's internal GHE orgs.
76
+ Activation condition: Automatically entered when Step 2 [Priority 1]~[Priority 2] search yields no suitable plugin. **This block therefore runs only after [Priority 2] has actually been executed** — the list above is in execution order (1 → 1.5 → 2 → 2.5 → 3), and the priority labels are names, not the run sequence.
77
77
 
78
78
  5. **[Priority 3] External Open-Source Ecosystem**: WebSearch / WebFetch — "best github actions for X", "claude plugin for Y", etc. Simplification guard: defer external install if internal assets suffice.
79
79
 
@@ -91,16 +91,18 @@ When queried for a specific capability (e.g., "adversarial reviewer for bash cod
91
91
  0. **Platform built-ins (Tier 0)** — does a built-in skill/command already cover the capability? Check the live session's available-skills list before any plugin search. A built-in that covers ~80% beats installing a plugin for the rest
92
92
  1. **Installed locally** — `.claude/agents/`, `plugins/` in current cwd
93
93
  2. **FH native skills** — always-loaded knowledge in `plugins/fh-meta/` and `plugins/fh-commons/`
94
- 3. **Claude Code marketplace** — `claude mcp search [capability]` or known CC registry (see verified targets above)
95
- 4. **Codex marketplace** — `npx @openai/codex list-agents [capability]` or known Codex registry
94
+ 3. **Claude Code marketplace** — `claude plugin list --available --json` (marketplace plugins; `--available` **requires** `--json`) + `claude plugin marketplace list` to see which marketplaces are even configured. There is **no** CLI keyword-search subcommand — filter the JSON yourself, and fall back to the known CC registry (see verified targets above) for anything not in a configured marketplace
95
+ 4. **Codex marketplace** — `npx --yes @openai/codex plugin list --available --json` (optionally `-m <marketplace>`) + `npx --yes @openai/codex plugin marketplace list`. Same limitation: no keyword search, filter the JSON
96
96
  5. **npm ecosystem** — `@chrono-meta/`, `@anthropic/`, and other known-quality scoped packages
97
97
 
98
+ ⚠️ **A failed or empty discovery lane is NOT "no candidates".** Both CLIs above list only *configured* marketplaces, and a non-zero exit / empty array means the lane did not answer — not that nothing exists. Report each lane's state explicitly (`EXECUTED` / `EMPTY` / `FAILED: <stderr>`) and never render a `FAILED` lane as a zero result; a lane that could not run must be re-run or replaced by the web-search fallback (Priority 3) before you tell the user nothing was found.
99
+
98
100
  **Discovery priority**: built-in (Tier 0) > installed > FH native > Tier 1 (any platform) > Tier 2 > Tier 3 > Tier 4
99
101
  **Tier 0 guard**: FH native wins over a built-in only when the FH skill adds governance the built-in lacks (e.g. `/goal` → `goal-quench` adds budget+quality gates; code diff review stays with built-in `/code-review`, FH-asset coherence with `hub-cc-pr-reviewer`)
100
102
 
101
103
  **When sim-conductor chains here for persona discovery**: apply the same platform-aware search scoped to persona/simulation/review capability tags. Return discovered agents with their Tier rating so sim-conductor can decide whether to install or use a built-in brief.
102
104
 
103
- For discovery bash commands (`claude mcp search`, `npx @openai/codex list-agents`, npm scoped search), see `SKILL_detail.md §Discovery-Bash`.
105
+ For discovery bash commands (`claude plugin list --available --json`, `npx --yes @openai/codex plugin list --available --json`, npm scoped search), see `SKILL_detail.md §Discovery-Bash`.
104
106
 
105
107
  ### Step 2.6: Quality Validation Signals
106
108
 
@@ -149,7 +151,8 @@ When user selects desired plugin from recommendation list, help with installatio
149
151
  ## Constraints
150
152
 
151
153
  - **Recommendations, not guarantees**: Does not guarantee plugin performance, stability, or security.
152
- - **Inbound supply-chain risk (Tier 3/4)**: a Tier 3/4 candidate (§Tier Classification Table) has no marketplace vetting — public skill registries have shipped malicious skills at scale (pointers: [HN 47370624](https://news.ycombinator.com/item?id=47370624) — 824 malicious skills reported on ClawHub; [HN 48678603](https://news.ycombinator.com/item?id=48678603) — Snyk ToxicSkills study. Secondary sources — verify before quoting figures). Flag this explicitly to the user before recommending a Tier 3/4 candidate.
154
+ - **Inbound supply-chain risk EVERY tier, not just 3/4**: public skill registries have shipped malicious skills at scale (pointers: [HN 47370624](https://news.ycombinator.com/item?id=47370624) — 824 malicious skills reported on ClawHub; [HN 48678603](https://news.ycombinator.com/item?id=48678603) — Snyk ToxicSkills study. Secondary sources — verify before quoting figures). ⚠️ **The channel in both incidents was the marketplace listing itself — i.e. Tier 1/2.** An earlier version of this bullet scoped the warning to Tier 3/4, which inverted its own evidence: it warned loudest exactly where vetting exists and stayed silent where the cited attacks landed. Flag the risk before recommending **any** candidate; a high Tier means *listed and maintained*, never *audited for intent*.
155
+ - **Install string must be resolved, not relayed**: the `<name>` in an install command frequently comes from a web-search result. Before surfacing it, confirm the name resolves to the intended repository (owner/repo matches the source you are citing) — a plausible near-name is the cheapest form of this attack, and installation is irreversible on the machine that runs it.
153
156
  - **User consent required**: Does not auto-install without explicit consent.
154
157
  - **Search scope limitations**: Only searches within configured search space.
155
158
 
@@ -213,9 +216,34 @@ sim-conductor needs persona X (no installed/built-in match)
213
216
 
214
217
  ```
215
218
  All Steps 0~5 completed
216
- + Recommendation list table output (top 2~3 items, Tier + Platform + synergy grade included)
217
- + Install completed after user selection (or install skipped / 5-B migration path guided)
219
+ mandatory-pass: each step produced its stated output, or is
220
+ explicitly marked N/A with the reason
221
+
222
+ + Recommendation list table output (top 2~3 items, Tier + Platform + synergy
223
+ grade included)
224
+ — mandatory-pass: the table exists with all three columns populated
225
+
226
+ + Every discovery lane reported with an explicit state
227
+ (EXECUTED / EMPTY / FAILED: <stderr>)
228
+ — measured: count lanes attempted vs lanes reporting a state; the two
229
+ numbers must match. A FAILED lane rendered as "no candidates" is a FAIL
230
+ of this condition, not a pass — the CLI lanes have no keyword search and
231
+ see only configured marketplaces, so an empty result is routinely a
232
+ non-answer rather than an absence (`not found` != `0`)
233
+
234
+ + Install completed after user selection (or install skipped / 5-B migration
235
+ path guided)
236
+ — mandatory-pass: explicit user consent recorded before any install ran
237
+
218
238
  + Duplicate detection results reported
239
+ — mandatory-pass: `claude plugin list` output consulted in this run
240
+
241
+ + Every install string surfaced to the user resolves to the repository it is
242
+ cited from (§Constraints — resolved, not relayed)
243
+ — judged; adversarial pairing: in the same run, resolve one candidate name
244
+ you already know is correct AND check one near-name variant. If the
245
+ procedure cannot separate that pair, the resolution check is
246
+ UNCALIBRATED and no install string may be presented as verified
219
247
  ```
220
248
 
221
249
  ## Failure Response
@@ -57,16 +57,24 @@ gh auth status # default host (github.com) only
57
57
  # Unauthenticated → guide github.com PAT generation above
58
58
  ```
59
59
 
60
- **Claude Code marketplace search:**
60
+ **Claude Code marketplace discovery** (there is **no** `search` subcommand — `claude mcp search` does not
61
+ exist and exits 1 with `unknown command 'search'`; list, then filter yourself):
61
62
  ```bash
62
- claude mcp search [capability-keyword]
63
+ claude plugin marketplace list --json # which marketplaces are configured at all
64
+ claude plugin list --available --json # installed + available marketplace plugins (--available REQUIRES --json)
63
65
  ```
64
66
 
65
- **Codex marketplace search:**
67
+ **Codex marketplace discovery** (`list-agents` does not exist either — the real noun is `plugin`):
66
68
  ```bash
67
- npx @openai/codex list-agents [capability-keyword]
69
+ npx --yes @openai/codex plugin marketplace list
70
+ npx --yes @openai/codex plugin list --available --json # add -m <marketplace> to scope
68
71
  ```
69
72
 
73
+ > **Lane-state reporting (mandatory).** Neither CLI supports a keyword query, and both see only
74
+ > *configured* marketplaces. Record each lane as `EXECUTED` / `EMPTY` / `FAILED: <stderr>` and carry
75
+ > that state into the recommendation. **A `FAILED` lane must never be rendered as "no candidates"** —
76
+ > re-run it or fall back to Priority 3 web search before reporting an empty result.
77
+
70
78
  **npm ecosystem search (scoped packages):**
71
79
  ```bash
72
80
  npm search @chrono-meta [keyword]
@@ -113,11 +121,20 @@ If either duplicate condition met → skip install → report "Already active"
113
121
  #### 5-1 through 5-3. Install Steps
114
122
 
115
123
  1. Confirm intent: "Would you like to install the `[plugin-name]` plugin?"
116
- 2. On agreement:
124
+ 2. On agreement — **the two commands take different arguments**: `marketplace add` takes a *source*
125
+ (`<owner/repo>`, a URL, or a local path), never a plugin name; only `install` takes the plugin name.
117
126
  ```bash
118
- claude plugin marketplace add [plugin-name]
119
- claude plugin install [plugin-name]
127
+ # Usage: claude plugin marketplace add [options] SOURCE
128
+ # SOURCE = owner/repo, a URL, or a local path — NEVER a plugin name
129
+ SOURCE="owner/repo"
130
+ PLUGIN="plugin-name" # optionally "plugin-name@marketplace" to disambiguate
131
+
132
+ claude plugin marketplace add "$SOURCE"
133
+ # Usage: claude plugin install|i [options] PLUGIN
134
+ claude plugin install "$PLUGIN"
120
135
  ```
136
+ Skip the `marketplace add` line when the plugin already resolves from a configured marketplace
137
+ (check `claude plugin marketplace list`).
121
138
  3. Post-install initial configuration guidance:
122
139
  - **API token input**: Guide token generation path for external service APIs (Jira/Confluence/Slack — specify each service's token page URL + env var or plugin config storage location)
123
140
  - **MCP connection**: If plugin uses MCP server, guide auto-update of `.mcp.json` or `claude mcp add` command
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  name: prompt-regression
3
- description: Detects harness regressions by running standard prompt probes after rule/skill changes and comparing outputs against saved baselines. Triggers on "prompt regression", "did my changes break anything", "regression check", "test harness changes".
3
+ description: Statically checks harness assets against a known-answer probe set after rule/skill changes — inspects the changed source for the trigger phrases, chain links and gate conditions each probe expects, and reports PASS/FAIL/SKIP per probe. Source inspection only; it does not run live sessions, so it catches assets that no longer SAY the right thing, not models that stop DOING it. Triggers on "prompt regression", "did my changes break anything", "regression check", "test harness changes".
4
4
  user-invocable: true
5
- allowed-tools: ["Read", "Bash", "Glob", "Grep"]
5
+ allowed-tools: ["Read", "Write", "Bash", "Glob", "Grep"]
6
6
  model: sonnet
7
7
  complexity_routing:
8
8
  base: sonnet
@@ -58,8 +58,15 @@ ls .claude/regression/probes.md 2>/dev/null || echo "NO_CUSTOM_PROBES"
58
58
  ```
59
59
 
60
60
  **If custom probes exist**: load and use them. The hub repo ships its golden probe set
61
- (known-answer offline eval, 32 probes with check classes) at exactly this path when
62
- present it is canonical and supersedes the default matrix below.
61
+ (known-answer offline eval, **33 probes** with check classes 27 mandatory-pass · 1 measured ·
62
+ 5 judged, of which 2 rows are inert deletion anchors, so live coverage is 31) at exactly this path
63
+ — when present it is canonical and supersedes the default matrix below.
64
+
65
+ > Count from the file, not from this line: `grep -cE '^\| *`[A-Z][A-Z0-9-]*-[0-9]+` *\|'
66
+ > .claude/regression/probes.md`. Both this number and the tally inside `probes.md` read `32` until
67
+ > 2026-08-12, when the actual count was 33 — a probe had been added to section A without touching
68
+ > either summary. A count duplicated in two files rots independently; if the two disagree, the file
69
+ > wins and both get corrected.
63
70
 
64
71
  **If no custom probes** (e.g. Mode C install without the hub repo): use the default
65
72
  probe matrix below.
@@ -93,7 +100,23 @@ If change scope is `CLAUDE.md` core (10+ lines changed): run **full suite** (all
93
100
 
94
101
  ---
95
102
 
96
- ### Step 4. Run Affected Probes
103
+ ### Step 4. Evaluate Affected Probes (static source inspection)
104
+
105
+ **What this step is, stated plainly.** Every check below reads the changed *source* and asks whether
106
+ the text a probe expects is still there. No live session is started, no model is prompted, no output
107
+ is compared against a recorded transcript. The description said "running standard prompt probes …
108
+ comparing outputs against saved baselines", which reads as live execution; the honest label is a
109
+ **known-answer static check**.
110
+
111
+ **What it therefore cannot catch** — say this in the report, do not leave it implied:
112
+ - a rule that is still present but has stopped *firing* (salience loss, ordering, competition
113
+ from another rule)
114
+ - a trigger phrase present in the file but shadowed by a higher-priority route
115
+ - any behavior change that leaves the source text identical
116
+
117
+ Those need a dispatched blind sim at the target tier (`sim-conductor`, or the target-tier sim gate in
118
+ `.claude/rules/fh_4axis_gate.md`). A green report here means *the assets still say the right thing*.
119
+ Treating it as behavioral evidence is the misread this section exists to prevent.
97
120
 
98
121
  For each affected probe, evaluate:
99
122
 
@@ -150,24 +173,44 @@ If all probes pass:
150
173
 
151
174
  After a deliberate behavior change (not a regression — an intentional improvement), update the baseline:
152
175
 
176
+ Prompt user first: *"Probe `G-GATE-03` now expects the new gate format. Update baseline? (y/n)"*
177
+ Only update on explicit `y` — never auto-update.
178
+
179
+ On `y`, actually perform the write. The previous version of this step was three lines of which two
180
+ were comments — it created the directory and stopped, so "baseline updated" was reported by a step
181
+ that had written nothing.
182
+
153
183
  ```bash
154
- # Baseline stored as markdown in .claude/regression/
155
184
  mkdir -p .claude/regression
156
- # Write updated probe expectations
157
185
  ```
158
186
 
159
- Prompt user: *"Probe P-CHAIN-01 now expects the new gate format. Update baseline? (y/n)"*
187
+ Then **use the `Write` tool** on `.claude/regression/probes.md` (create it from the SKILL.md default
188
+ matrix if absent) to apply the approved edit. Each approved change edits the probe's row in place —
189
+ Expected Behavior, Scope, and Class — and:
160
190
 
161
- Only update on explicit `y` never auto-update.
191
+ - update the `**Count**:` tally line **in the same edit** if a row was added or removed, including
192
+ the per-section breakdown and the class counts (a stale tally is exactly how the `32`/`33`
193
+ mismatch was introduced)
194
+ - append a one-line note under `**Baseline**:` recording the date and the reason for the change
195
+ - never rewrite rows the user did not approve
196
+
197
+ Confirm afterwards by re-reading the file and reporting the row's new content plus the recount —
198
+ `grep -cE '^\| *`[A-Z][A-Z0-9-]*-[0-9]+` *\|' .claude/regression/probes.md`. Do not report a
199
+ baseline update whose result you have not read back.
162
200
 
163
201
  ---
164
202
 
165
203
  ## Done When
166
204
 
167
- - All affected probes are evaluated (PASS / FAIL / SKIP) class: mandatory-pass
205
+ - All affected probes are evaluated (PASS / FAIL / SKIP) and the three counts sum to the number of
206
+ probes selected in Step 3 — class: measured (`PASS + FAIL + SKIP == selected`)
168
207
  - Regression report is output with clear PASS/FAIL verdict — class: mandatory-pass
208
+ - The report states its own scope: **static source inspection, no live session run** — a green
209
+ verdict is never presented as behavioral evidence — class: mandatory-pass
169
210
  - If FAIL: specific file + line fix is recommended — class: judged, paired with verify-bidirectional (the fix recommendation is re-checked, not trusted as-is)
170
- - Baseline updated only on explicit user approval class: mandatory-pass (HITL)
211
+ - Baseline updated only on explicit user approval, and the written file is **read back** and its
212
+ probe count reported — class: mandatory-pass (HITL). Approval alone does not close this: a step
213
+ that prompts, gets `y`, and writes nothing satisfied the old wording.
171
214
 
172
215
  ---
173
216