Skip to content

feat(hooks): Loop B — opt-in detached whole-repo report refresh (T2b / C5, hardened)#82

Merged
Cranot merged 1 commit into
mainfrom
loop-b-fixes
Jul 16, 2026
Merged

feat(hooks): Loop B — opt-in detached whole-repo report refresh (T2b / C5, hardened)#82
Cranot merged 1 commit into
mainfrom
loop-b-fixes

Conversation

@Cranot

@Cranot Cranot commented Jul 16, 2026

Copy link
Copy Markdown
Owner

Supersedes #79 (rebased onto post-#81 main + all pre-merge review findings fixed; that branch would also merge-conflict with #81's cmd_hooks.py changes).

Carries #79's verified-correct core (detached spawn empirically confirmed on win32; never sync; never --diff-only into the report path; opt-in default-OFF) and closes the review's MAJORs:

  1. Single-flight claim marker (.roam/verify-refresh-claim, 30-min window) — the report-mtime throttle only moves AFTER the ~minutes-long verify persists, so first-enable/fleet bursts piled up concurrent whole-repo verifies; and any no-persist mode (empty targets short-circuits before persist, missing DB, crash) meant respawn-every-stop forever, each re-running ensure_index().
  2. Root anchoring — throttle, claim, and the spawned verify's cwd all resolve the same repo root (walk-up to .git); previously a hook in a repo subdir checked a cwd-relative path the root-persisting verify could never satisfy.
  3. Spawn AFTER the blocking gate — a whole-repo verify competing for CPU/DB locks can push the sync --diff-only gate past its 90s budget, and a timed-out gate silently allows; the refresh now fires only once the gate has completed.
  4. Hook-body compile test — during this work a body syntax error made every absence-asserting test pass vacuously (exactly the trap the review flagged); now guarded explicitly. Plus single-flight, stale-claim-respawn, and root-anchoring tests.
  5. Registry hygiene — v3 pristine + surgered SHAs seeded so surgered installs stay quiet and v2 fleets heal forward under fix(hooks): make the T2c heal actually heal — seed registry + content-verified states #81's content-verified heal.

40/40 file suite; prepush 7/7. repo-priors v1 (the other half of T2b) remains deferred — T2b is NOT closed by this PR.

…/ C5)

Closes the verify->compile feedforward. The compiler's known_findings probe
reads .roam/verify-report.json (whole-repo verify, disclosing report_age_hours)
to embed an edited file's OPEN findings — but nothing refreshed that report from
the Stop hook, so known_findings only populated if the user manually ran
verify --report --persist. Loop B was measured dead-on-arrival as a per-stop
SYNC job (whole-repo verify ~120s > the 90s hook budget), so this is the
detached fire-and-forget form: on an edit-stop, the hook spawns a background
whole-repo `verify --auto --report --persist` (never --diff-only into the
report path) and never reads the result. Opt-in via ROAM_HOOK_REPORT_REFRESH
(default OFF). Body version bumps to 3; v2 fleets heal forward.

Rebased onto post-#81 main and hardened per the pre-merge adversarial review
(PR #79 thread; supersedes that branch):
- single-flight claim marker (.roam/verify-refresh-claim, 30-min window)
  closes both the in-flight pile-up (report mtime only moves AFTER the
  ~minutes verify persists) and the unbounded-respawn mode when the persist
  never lands (empty targets / missing DB / crash re-ran ensure_index on
  every stop).
- throttle + claim + spawn cwd all anchored at the repo root (walk-up to
  .git) — a hook run from a repo subdir checked a cwd-relative report the
  root-persisting verify could never satisfy.
- refresh fires AFTER the blocking diff-only gate, not before: a whole-repo
  verify competing for CPU/DB locks can push the gate past its 90s budget,
  and a timed-out gate silently allows.
- hook-body compile test guards the vacuous-pass trap (absence-asserting
  tests all pass when the body has a syntax error); single-flight,
  stale-claim-respawn, and root-anchoring tests added.
- registry: v3 pristine + surgered body SHAs seeded so surgered installs
  classify current and v2 fleets heal forward under the #81 content-verified
  heal.
@Cranot
Cranot merged commit 046fece into main Jul 16, 2026
7 of 11 checks passed
@github-actions

Copy link
Copy Markdown

Roam Agent Review

Verdict: NOCHANGES (risk_level low)

blast-radius 0/100 · ai-likelihood 0/100 · rule violations 0 · critique high-severity 0

Verdict: NOCHANGES. All structural signals clean at the configured thresholds.


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 585d88f32411cfe61bbe392e0885fcb798782a22, 29 changed+dependent files

Health Score: 78/100 FAIR

health: Fair codebase (78/100) — 49 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 78/100
Tangle Ratio 0%
Propagation Cost 0.0014
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": "dd3eaba1ee76",
      "hint": "index latest commit 585d88f32411 != HEAD dd3eaba1ee76 — git-derived metrics (commits, churn, co-change, weather) may be stale. Run `roam index --force`.",
      "indexed_commit": "585d88f32411"
    },
    "latency_ms": null,
    "response_tokens": 4086,
    "roam_version": "13.9.0",
    "timestamp": "2026-07-16T15:36:06Z"
  },
  "actionable_count": 16,
  "actionable_cycles": 0,
  "agent_contract": {
    "confidence": null,
    "facts": [
      "Fair codebase (78/100) — 49 critical, 7 warnings, focus: god_components",
      "health score 78",
      "tangle ratio 0.0",
      "0.0014 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": 1024.4,
    "p90": 9506.6,
    "population": 1304,
    "utility_multiplier": 1.5
  },
  "category_severity": {
    "bottlenecks": {
      "critical": 15,
      "info": 0,
      "warning": 0
    },
    "cycles": {
      "critical": 0,
      "info": 0,
      "warning": 0
    },
    "god_components": {
      "critical": 34,
      "info": 9,
      "warning": 7
    },
    "layer_violations": {
      "critical": 0,
      "info": 0,
      "warning": 0
    }
  },
  "command": "health",
  "cycles_actionable": 0,
  "cycles_total": 17,
  "framework_filtered": 0,
  "health_score": 78,
  "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": "dd3eaba1ee76",
    "hint": "index latest commit 585d88f32411 != HEAD dd3eaba1ee76 — git-derived metrics (commits, churn, co-change, weather) may be stale. Run `roam index --force`.",
    "indexed_commit": "585d88f32411"
  },
  "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.0014,
  "schema": "roam-envelope-v1",
  "schema_version": "1.1.0",
  "severity": {
    "critical": 49,
    "info": 26,
    "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": 34,
        "info": 9,
        "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": 78,
    "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.0014,
    "severity": {
      "critical": 49,
      "info": 26,
      "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 (78/100) — 49 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": 34,
  "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": "dd3eaba1ee76",
      "hint": "index latest commit 585d88f32411 != HEAD dd3eaba1ee76 — git-derived metrics (commits, churn, co-change, weather) may be stale. Run `roam index --force`.",
      "indexed_commit": "585d88f32411"
    },
    "latency_ms": null,
    "response_tokens": 1012,
    "roam_version": "13.9.0",
    "timestamp": "2026-07-16T15:36:07Z"
  },
  "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",
      "264 lines added"
    ],
    "next_commands": [],
    "risks": [
      "pr-risk: moderate (43/100) on 585d88f32411cfe61bbe392e0885fcb798782a22..HEAD — driver: hotspot_score"
    ]
  },
  "author": null,
  "blast_radius_pct": 0.3,
  "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": 25,
  "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 585d88f32411cfe61bbe392e0885fcb798782a22..HEAD — driver: hotspot_score",
      "confidence": "heuristic",
      "evidence": {
        "actor": null,
        "author": null,
        "blast_radius_pct": 0.3,
        "bus_factor_risk": 0.5,
        "changed_files_count": 2,
        "commit_range": "585d88f32411cfe61bbe392e0885fcb798782a22..HEAD",
        "coupling_score": 0.5,
        "created_at_epoch": 1784216167,
        "diff_id": "eac50b1bfa83",
        "familiarity_risk": 0,
        "file_list": [
          "src/roam/commands/cmd_hooks.py",
          "tests/test_hooks_claude_setup.py"
        ],
        "hotspot_score": 1,
        "label": "585d88f32411cfe61bbe392e0885fcb798782a22..HEAD",
        "lines_added": 264,
        "lines_removed": 6,
        "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:eac50b1bfa83",
      "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": "585d88f32411cfe61bbe392e0885fcb798782a22..HEAD",
  "layer_spread": 0.16,
  "layers_touched": 5,
  "lines_added": 264,
  "lines_removed": 6,
  "minor_risk": {
    "files": [],
    "files_assessed": 0,
    "minor_files": 0
  },
  "novelty_score": 0,
  "per_file": [
    {
      "blast": 104,
      "churn": 2173,
      "is_test": false,
      "lines_added": 91,
      "lines_removed": 1,
      "path": "src/roam/commands/cmd_hooks.py",
      "symbols": 49
    },
    {
      "blast": 34,
      "churn": 772,
      "is_test": true,
      "lines_added": 173,
      "lines_removed": 5,
      "path": "tests/test_hooks_claude_setup.py",
      "symbols": 60
    }
  ],
  "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": 1573
    },
    {
      "actor": "Dimitris",
      "author": "Dimitris",
      "lines": 310
    }
  ],
  "summary": {
    "change_shape": "mixed",
    "changed_files": 2,
    "findings_count": 1,
    "lines_added": 264,
    "lines_removed": 6,
    "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": 13109,
  "total_layers": 31,
  "version": "13.9.0",
  "warnings_out": []
}

roam-code analysis | Commands: health pr-risk

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