feat(hooks): version-stamp + heal stale Claude hook bodies (T2c / C3) - #77
Conversation
A deployed hook whose settings entry exists but whose on-disk BODY is frozen at an older install was invisible to the settings-based installer — `hooks claude --write` reported 'already wired' and never refreshed it. So a box wired before a body change (e.g. the session-id join key, the agent-mode stamp) kept running the stale script indefinitely. - Version stamp: each generated body carries `# roam-hook-version: N` inserted after the shebang (a bare comment — compile-code's mode-override surgery rewrites only the roam INVOCATION lines, so a healed body keeps its marker). Bump _HOOK_BODY_VERSION on any material body change. - Heal classification (_hook_heal_state): a deployed body is 'current' (identical / same version), 'heal' (carries our marker at an older version, OR its SHA is in _KNOWN_HOOK_BODY_SHAS — roam's own past output, safe to refresh), or 'foreign' (unstamped + unrecognized = user-customized or externally managed — NEVER auto-overwrite). - `hooks claude --write` now heals stale roam bodies in place; foreign bodies are left untouched and REPORTED (surfaced in the verdict + JSON facts). New `--force` overwrites foreign bodies deliberately (the escape hatch for a very old pre-stamp body that predates the SHA registry). This is the deployment mechanism for future hook-body changes (incl. the async Loop-B Stop hook): bump the version + register the prior SHA, and every wired install self-heals on the next `--write` / `compile claude`. 8 tests (2 existing verdict-wording assertions updated); bodies still parse + keep shebang line 1; prepush 7/7.
Roam Agent ReviewVerdict: SAFE (risk_level low) blast-radius 0/100 · ai-likelihood 21/100 · rule violations 0 · critique high-severity 0 Verdict: SAFE. All structural signals clean at the configured thresholds. Next steps
Powered by roam-code — Apache 2.0, 100% local. Customize thresholds in |
roam-code AnalysisMode: incremental ( Health Score: 79/100 health: Fair codebase (79/100) — 48 critical, 7 warnings, focus: god_components Health Metrics
PR Risk
Quality Gate: PASSEDGate expression: SARIF Upload
Full analysis outputhealth{
"_meta": {
"cache_ttl_s": 300,
"cacheable": true,
"index_age_s": 3,
"index_status": {
"dirty_files": 0,
"fresh": false,
"head_commit": "c8a51478cff0",
"hint": "index latest commit 3ba4b81582b3 != HEAD c8a51478cff0 — git-derived metrics (commits, churn, co-change, weather) may be stale. Run `roam index --force`.",
"indexed_commit": "3ba4b81582b3"
},
"latency_ms": null,
"response_tokens": 4083,
"roam_version": "13.9.0",
"timestamp": "2026-07-16T12:43:02Z"
},
"actionable_count": 15,
"actionable_cycles": 0,
"agent_contract": {
"confidence": null,
"facts": [
"Fair codebase (79/100) — 48 critical, 7 warnings, focus: god_components",
"health score 79",
"tangle ratio 0.0",
"0.0013 propagation cost findings",
"issue count 65"
],
"next_commands": [
"roam debt",
"roam trends --days 30"
],
"risks": []
},
"algebraic_connectivity": null,
"algebraic_connectivity_available": false,
"bottleneck_thresholds": {
"p70": 409.7,
"p90": 3678.7,
"population": 2293,
"utility_multiplier": 1.5
},
"category_severity": {
"bottlenecks": {
"critical": 15,
"info": 0,
"warning": 0
},
"cycles": {
"critical": 0,
"info": 0,
"warning": 0
},
"god_components": {
"critical": 33,
"info": 10,
"warning": 7
},
"layer_violations": {
"critical": 0,
"info": 0,
"warning": 0
}
},
"command": "health",
"cycles_actionable": 0,
"cycles_total": 17,
"framework_filtered": 0,
"health_score": 79,
"ignored_cycles": 17,
"imported_coverable_lines": 0,
"imported_coverage_files": 0,
"imported_coverage_pct": null,
"imported_covered_lines": 0,
"index_status": {
"dirty_files": 0,
"fresh": false,
"head_commit": "c8a51478cff0",
"hint": "index latest commit 3ba4b81582b3 != HEAD c8a51478cff0 — git-derived metrics (commits, churn, co-change, weather) may be stale. Run `roam index --force`.",
"indexed_commit": "3ba4b81582b3"
},
"issue_count": 65,
"list_counts": {
"bottlenecks": 15,
"cycle_break_suggestions": 0,
"cycles": 17,
"god_components": 50,
"layer_violations": 0,
"next_steps": 2,
"score_breakdown": 5
},
"project": "roam-code",
"propagation_cost": 0.0013,
"schema": "roam-envelope-v1",
"schema_version": "1.1.0",
"severity": {
"critical": 48,
"info": 27,
"warning": 7
},
"summary": {
"actionable_cycles": 0,
"algebraic_connectivity": null,
"algebraic_connectivity_available": false,
"category_severity": {
"bottlenecks": {
"critical": 15,
"info": 0,
"warning": 0
},
"cycles": {
"critical": 0,
"info": 0,
"warning": 0
},
"god_components": {
"critical": 33,
"info": 10,
"warning": 7
},
"layer_violations": {
"critical": 0,
"info": 0,
"warning": 0
}
},
"cycles_actionable": 0,
"cycles_definition": "Cycle counts derived from `roam.graph.cycles.find_cycles(G, min_size=2)` on the symbol graph. `cycles_total` = all SCCs of size >= 2; `cycles_actionable` = SCCs spanning >=2 files AND no test files (same-file and test-only cycles are informational). Run `roam health` for the per-cycle breakdown.",
"cycles_total": 17,
"detail_available": true,
"god_components": 50,
"god_components_definition": "God components: symbols where `(in_degree + out_degree) > 20` from the `graph_metrics` table, with utility-aware severity bands (standard >50=CRITICAL >30=WARNING; utility >150=CRITICAL >90=WARNING). Run `roam health` for the per-symbol breakdown. Legacy aliases: `god_objects` (fingerprint), `god_classes` (rules).",
"health_score": 79,
"health_score_definition": "weighted geometric mean (0-100) of 5 sigmoid health factors: tangle_ratio, god_components, bottlenecks, layer_violations, file_health (+coverage if available).",
"ignored_cycles": 17,
"imported_coverage_files": 0,
"imported_coverage_pct": null,
"issue_count": 65,
"partial_success": true,
"preserved_list_truncations": {},
"propagation_cost": 0.0013,
"severity": {
"critical": 48,
"info": 27,
"warning": 7
},
"tangle_ratio": 0,
"tangle_ratio_definition": "fraction of symbols inside non-trivial SCCs; higher = more cyclic coupling.",
"total_cycles": 17,
"truncated": true,
"verdict": "Fair codebase (79/100) — 48 critical, 7 warnings, focus: god_components",
"warnings_out": [
"health_algebraic_connectivity_warning:RuntimeWarning:algebraic_connectivity compute failed (ModuleNotFoundError): No module named 'numpy'; returning 0.0 sentinel — value is NOT a legitimate disconnected-graph reading"
]
},
"tangle_ratio": 0,
"total_cycles": 17,
"utility_count": 35,
"version": "13.9.0",
"warnings_out": [
"health_algebraic_connectivity_warning:RuntimeWarning:algebraic_connectivity compute failed (ModuleNotFoundError): No module named 'numpy'; returning 0.0 sentinel — value is NOT a legitimate disconnected-graph reading"
]
}pr-risk{
"_meta": {
"cache_ttl_s": 60,
"cacheable": true,
"index_age_s": 5,
"index_status": {
"dirty_files": 0,
"fresh": false,
"head_commit": "c8a51478cff0",
"hint": "index latest commit 3ba4b81582b3 != HEAD c8a51478cff0 — git-derived metrics (commits, churn, co-change, weather) may be stale. Run `roam index --force`.",
"indexed_commit": "3ba4b81582b3"
},
"latency_ms": null,
"response_tokens": 1012,
"roam_version": "13.9.0",
"timestamp": "2026-07-16T12:43:04Z"
},
"actor": null,
"agent_contract": {
"confidence": null,
"facts": [
"Moderate risk (43/100) — review recommended (risk_level medium) (driver: hotspot_score)",
"risk score 43",
"2 risk rank findings",
"2 changed files",
"211 lines added"
],
"next_commands": [],
"risks": [
"pr-risk: moderate (43/100) on c37556a75d3e72dcb8ac48c4b0c2c18265c9b84c..HEAD — driver: hotspot_score"
]
},
"author": null,
"blast_radius_pct": 0.2,
"bus_factor_risk": 0.5,
"change_shape": "mixed",
"changed_files": 2,
"closest_historical_pattern": [
"src/roam/commands/cmd_hooks.py",
"tests/test_hooks_claude_setup.py"
],
"closest_similarity": 1,
"cluster_spread": 0,
"clusters_touched": 23,
"command": "pr-risk",
"coupling_score": 0.5,
"dead_code": [
{
"file": "src/roam/commands/cmd_hooks.py",
"kind": "function",
"name": "install"
},
{
"file": "src/roam/commands/cmd_hooks.py",
"kind": "function",
"name": "uninstall"
},
{
"file": "src/roam/commands/cmd_hooks.py",
"kind": "function",
"name": "status"
},
{
"file": "src/roam/commands/cmd_hooks.py",
"kind": "function",
"name": "claude_setup"
}
],
"dead_exports": 4,
"familiarity": {
"avg_familiarity": 1,
"files": [],
"files_assessed": 0
},
"findings": [
{
"claim": "pr-risk: moderate (43/100) on c37556a75d3e72dcb8ac48c4b0c2c18265c9b84c..HEAD — driver: hotspot_score",
"confidence": "heuristic",
"evidence": {
"actor": null,
"author": null,
"blast_radius_pct": 0.2,
"bus_factor_risk": 0.5,
"changed_files_count": 2,
"commit_range": "c37556a75d3e72dcb8ac48c4b0c2c18265c9b84c..HEAD",
"coupling_score": 0.5,
"created_at_epoch": 1784205784,
"diff_id": "7b490c44645b",
"familiarity_risk": 0,
"file_list": [
"src/roam/commands/cmd_hooks.py",
"tests/test_hooks_claude_setup.py"
],
"hotspot_score": 1,
"label": "c37556a75d3e72dcb8ac48c4b0c2c18265c9b84c..HEAD",
"lines_added": 211,
"lines_removed": 13,
"minor_risk": 0,
"novelty_score": 0,
"reductive_change": false,
"risk_level": "moderate",
"risk_score": 43,
"staged": false,
"test_coverage_pct": 100,
"top_driver": "hotspot_score"
},
"finding_id_str": "pr-risk:composite-risk-score:7b490c44645b",
"kind": "pr-risk:composite-risk-score",
"severity": "medium",
"source_detector": "pr-risk",
"source_version": "1.0.0",
"subject_id": null,
"subject_kind": "commit"
}
],
"hotspot_score": 1,
"label": "c37556a75d3e72dcb8ac48c4b0c2c18265c9b84c..HEAD",
"layer_spread": 0.16,
"layers_touched": 5,
"lines_added": 211,
"lines_removed": 13,
"minor_risk": {
"files": [],
"files_assessed": 0,
"minor_files": 0
},
"novelty_score": 0,
"per_file": [
{
"blast": 96,
"churn": 1882,
"is_test": false,
"lines_added": 141,
"lines_removed": 11,
"path": "src/roam/commands/cmd_hooks.py",
"symbols": 48
},
{
"blast": 20,
"churn": 410,
"is_test": true,
"lines_added": 70,
"lines_removed": 2,
"path": "tests/test_hooks_claude_setup.py",
"symbols": 39
}
],
"project": "roam-code",
"reductive_change": false,
"reductive_discount_applied": false,
"risk_level": "moderate",
"risk_level_canonical": "medium",
"risk_rank": 2,
"risk_score": 43,
"schema": "roam-envelope-v1",
"schema_version": "1.1.0",
"suggested_reviewers": [
{
"actor": "Cranot",
"author": "Cranot",
"lines": 1623
},
{
"actor": "Dimitris",
"author": "Dimitris",
"lines": 13
}
],
"summary": {
"change_shape": "mixed",
"changed_files": 2,
"findings_count": 1,
"lines_added": 211,
"lines_removed": 13,
"partial_success": false,
"risk_level": "moderate",
"risk_level_canonical": "medium",
"risk_rank": 2,
"risk_score": 43,
"score_classification": "classified",
"verdict": "Moderate risk (43/100) — review recommended (risk_level medium) (driver: hotspot_score)"
},
"test_coverage_pct": 100,
"total_clusters": 13127,
"total_layers": 31,
"version": "13.9.0",
"warnings_out": []
}roam-code analysis | Commands: |
|
Post-merge adversarial review (all findings CONFIRMED by repro at 3ba4b81 / merged 1832681). Verdict: the stamping/classification skeleton is solid and idempotent for post-PR installs, and the stamp DOES survive compile-code's mode-override surgery — but the healing feature the PR is named for does not function, and there are confirmed overwrite and crash paths. Fix-forward PR in progress. BLOCKER
MAJOR
MINOR
Spec scorecard: stamp-new-bodies MET; registry-heal NOT MET (F1/F2); unknown-untouched+doctor-reported PARTIAL (F4/F7); stamp-survives-surgery MET (repro: surgered same-version body classifies "current"). |
…-verified states (#81) Post-merge adversarial review of #77 found the healing feature inert and several unsafe paths (findings F1-F10 on the PR thread). This closes them: - F1/F2: _KNOWN_HOOK_BODY_SHAS was EMPTY, so no pre-stamp install ever healed. Seeded with all 13 distinct deployed-form bodies across the file's git history — pristine AND compile-code-surgered variants (the transform rewrites the Stop body on every `compile claude` wire, so pristine-only hashing could never match real fleets). scripts/seed_hook_body_shas.py regenerates the block; a registry test guards against re-emptying. - F3/F6a: a version marker alone no longer authorizes a heal — content is SHA-verified against the shipped registry in every non-identical case. Stamped-but-edited (or truncated-behind-a-stamp) bodies classify "modified": reported, overwritten only with --force. - F4: a missing settings entry no longer licenses a body overwrite — the install path wires the entry but preserves unrecognized bodies unless --force. - F5: UnicodeDecodeError (UTF-16 bodies, PowerShell's default) degrades to an "unreadable" report instead of a traceback in `hooks claude` and the `compile claude` wire path. - F6b: entry-wired-but-deleted bodies are reinstalled on --write. - F8: healing over a surgered body appends a re-run `compile claude` notice. - F9: heal/--force keeps a .bak of the prior body; writes are atomic (temp + os.replace) so a crash can never leave a truncated stamped body. - F10: dry-run distinguishes "heal stale/missing" from "force-overwrite unrecognized". - F7: `roam doctor` gains an advisory "Claude hook bodies" check covering both install levels, including entry-wired-body-missing. 12 heal-class tests (was 5), all states exercised; 32/32 file-suite green. Co-authored-by: Test <t@t.com>
Tranche-2 (C3 redesigned). A hook whose settings entry exists but whose on-disk body is frozen at an older install was invisible to the settings-based installer —
hooks claude --writesaid 'already wired' and never refreshed it (so a box wired before the session-id/agent-mode body changes ran the stale script forever).# roam-hook-version: Nafter the shebang — a bare comment compile-code's invocation-line surgery never touches, so a healed body keeps its marker._KNOWN_HOOK_BODY_SHAS= roam's own past output) / 'foreign' (unstamped + unrecognized = user/external — never auto-overwrite).--writeheals stale roam bodies in place; foreign bodies are left alone and reported; new--forceoverwrites foreign deliberately.This is the deployment mechanism for future body changes (incl. the async Loop-B Stop hook): bump the version + register the prior SHA → every wired install self-heals. 8 tests; bodies still parse + keep shebang line 1; prepush 7/7.