Skip to content

feat(hooks): version-stamp + heal stale Claude hook bodies (T2c / C3) - #77

Merged
Cranot merged 1 commit into
mainfrom
feat/hook-heal-registry
Jul 16, 2026
Merged

feat(hooks): version-stamp + heal stale Claude hook bodies (T2c / C3)#77
Cranot merged 1 commit into
mainfrom
feat/hook-heal-registry

Conversation

@Cranot

@Cranot Cranot commented Jul 16, 2026

Copy link
Copy Markdown
Owner

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 --write said 'already wired' and never refreshed it (so a box wired before the session-id/agent-mode body changes ran the stale script forever).

  • Version stamp # roam-hook-version: N after the shebang — a bare comment compile-code's invocation-line surgery never touches, so a healed body keeps its marker.
  • Heal classification: 'current' / 'heal' (our marker at an older version, or a SHA in _KNOWN_HOOK_BODY_SHAS = roam's own past output) / 'foreign' (unstamped + unrecognized = user/external — never auto-overwrite).
  • --write heals stale roam bodies in place; foreign bodies are left alone and reported; new --force overwrites 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.

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.
@github-actions

Copy link
Copy Markdown

Roam Agent Review

Verdict: 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

  • No structural concerns at the configured thresholds. Standard review still recommended.

Powered by roam-code — Apache 2.0, 100% local. Customize thresholds in .roam/rules.yml. Docs.

@github-actions

Copy link
Copy Markdown

roam-code Analysis

Mode: incremental (changed-only) — base c37556a75d3e72dcb8ac48c4b0c2c18265c9b84c, 28 changed+dependent files

Health Score: 79/100 FAIR

health: Fair codebase (79/100) — 48 critical, 7 warnings, focus: god_components
pr-risk: Moderate risk (43/100) — review recommended (risk_level medium) (driver: hotspot_score)

Health Metrics

Metric Value
Health Score 79/100
Tangle Ratio 0%
Propagation Cost 0.0013
Total Issues 65

PR Risk

Metric Value
Risk Score 43/100

Quality Gate: PASSED

Gate expression: health_score>=50

SARIF Upload

Metric Value
Category roam-code-self-analysis/self-analysis/py3.12
Results Uploaded 82
Full analysis output

health

{
  "_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: health pr-risk

@Cranot
Cranot merged commit 1832681 into main Jul 16, 2026
11 checks passed
@Cranot
Cranot deleted the feat/hook-heal-registry branch July 16, 2026 14:44
@Cranot

Cranot commented Jul 16, 2026

Copy link
Copy Markdown
Owner Author

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

  • F1 cmd_hooks.py:1122 — the historical-hash registry is frozenset() (empty). Repro: exact pre-PR canonical bodies from c37556a classify "foreign" (SHA 527471d9… not registered) for both UPS and Stop. No deployed pre-stamp box ever heals on --write; the commit message's "every wired install self-heals" is false for all existing installs.

MAJOR

  • F2 :1116-1122 — registry hashes the whole body, but compile-code's surgery (cli.py:266-283, applied on every successful compile claude wire) rewrites part of the Stop body → pristine-SHA ≠ surgered-SHA. Most deployed pre-stamp Stop bodies are surgered variants the registry can never match. Registry needs (version → {pristine_sha, surgered_sha}) or region-masked hashing.
  • F3 :1137-1159 — any body with an older stamp line heals regardless of content: v1-stamped + user-customized body is silently overwritten (no backup) on the next --write after a version bump. Heal must verify the body matches its stamped version's known SHA first.
  • F4 :1331 + :1264-1276 — foreign body + missing settings entry (wiped settings.json, new machine) → file lands in to_install and is overwritten WITHOUT --force, while the same run prints the "not healed, use --force" NOTE about it.
  • F5 :1171 — scan catches only OSError; a UTF-16 hook body (PowerShell default; the exact hazard compile-code's _wired_in documents) raises UnicodeDecodeError → traceback in roam hooks claude AND the compile claude wire path. Regression: pre-PR never read bodies.
  • F6 :1137-1141, 1339-1346, 1355-1356 — truncated-body-with-intact-stamp (what a crash mid-heal produces; write_text is non-atomic) classifies "current"; body file deleted but settings entry present → "wired + current", not reinstalled, --force no-op. Verdict asserts body currency never established.
  • F7 — spec says unknown/stale bodies are "doctor-reported": cmd_doctor.py untouched, zero integration. Reporting exists only in hooks claude text/JSON.

MINOR

  • F8 :1354-1357 — a future heal writes pristine canon, silently reverting compile-code's surgery until the next compile claude. F9 :1355-1358 — heal/force overwrites bodies with no .bak (settings.json gets one). F10 :1360-1366 — dry-run --force labels foreign bodies "Would heal stale body".

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").

Cranot added a commit that referenced this pull request Jul 16, 2026
…-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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant