๐Ÿ” cc-triage: stop the thin-changelog parse-failure churn

Closes #2757 (guard) + #2751, #2712 (the stragglers it kept spawning) ยท fix/cc-triage-thin-changelog-guard

The loop that wouldn't die

2.1.197 snapshot = ONE bullet: "Introducing Claude Sonnet 5 โ€ฆ 1M context โ€ฆ promotional pricing"
โ”œ isFeaturelessSnapshot() sees "Introducing" โ†’ hint word โ†’ NOT featureless โ†’ routes to LLM
โ”œ LLM correctly returns [] (a model launch is no plugin surface)
โ”œ M134 empty-array guard: [] on a "capability-bearing" snapshot = burp โ†’ sentinel parse_failed
โ”œ daily cron is token-free โ†’ LLM never re-runs โ†’ sentinel is permanent
โ”” Step-4 fallback files "manual triage needed for 2.1.197" โ€” every single run
= #2729 โ†’ #2742 โ†’ #2748 โ†’ #2751 โ†’ #2712 โ€ฆ the same issue, re-filed forever

The fix โ€” a token-free graduation, gated 3 ways

1Only fires on a thin changelog โ€” โ‰ค 2 bullets. A real multi-feature release still hits the LLM untouched.
2Requires a positive announcement signal โ€” ANNOUNCEMENT_RE (introducing / generally available / promotional pricing / per-Mtok / default model). An ordinary thin CLI release (claude project purge, --max-turns flag) carries a generic hint word but no announcement language โ†’ routes to the LLM.
3A plugin-surface term in any bullet (hook / SKILL.md / settings.json / MCP / โ€ฆ) disqualifies it โ†’ Added the SubagentStop hook still routes to the LLM.

Runs in reconcileDeterministic() โ€” before and independent of the LLM โ€” so it heals the token-free cron, which is exactly where the churn lived. On any miss it falls through to the existing path: never a regression.

Behavior table

changelog (thin)announcement?plugin surface?verdict
Introducing Claude Sonnet 5 โ€ฆ pricingyesnoGRADUATE thin_announcement
claude project purge; --max-turns flagnonoโ†’ LLM real CLI features
Added the SubagentStop hooknoyes (hook)โ†’ LLM adoptable

Verification

โœ“ cc-triage integration suite: 68 passed, 0 failed (2 new #2757 cases + 66 existing)
โœ“ Test 20 reproduces the real 2.1.197 โ†’ graduates thin_announcement=true, no parse_failed
โœ“ Test 21 (negative): SubagentStop hook is NOT graduated โ€” the guard can't swallow a real feature
โœ“ First draft graduated the 2-bullet test fixture (too loose) โ†’ caught by 8 red tests โ†’ tightened to a positive signal

Prompt โ†’ copy back to Claude

Implement the #2757 cc-triage guard on branch fix/cc-triage-thin-changelog-guard: in reconcileDeterministic(), graduate a thin (โ‰ค2-bullet) changelog as thin_announcement (token-free, clearing any parse_failed) when it positively matches an announcement signal (introducing / generally available / promotional pricing / per-Mtok / default model) AND names no plugin surface โ€” so the 2.1.197 Sonnet-5 GA churn stops without swallowing real thin CLI/hook features. Add integration tests (graduate + negative-guard), keep the existing suite green, and close #2751/#2712 (the stragglers it spawned) in the same PR.