Hook metrics (1) a runner records every hook run - #503
Open
EdbertChan wants to merge 9 commits into
Open
Conversation
…ooks/_runner/run.py runs one hook script, passes its stdout, stderr and exit code through unchanged, and appends one metrics row with a classified outcome. Review lane: behavior Safety invariant: A hook run through the runner produces byte-identical stdout and the same exit code as running it directly; a failed metrics write changes neither and adds one stderr line. Effectiveness measurement: Fixture hooks run directly and through the runner give identical stdout bytes and exit codes, and each row carries the expected outcome. Slice rationale: The runner and its outcome rules are one claim, reviewable before any install wiring. Architectural effect: Adds a shared, harness-agnostic hook runner under engine/hooks/_runner/ next to engine/hooks/_markers/. Dormant until installed. Goal: Create the runner, the pure outcome classifier, and tests. Motivation: No record exists today of which hooks fire, stay silent, or crash. Alternative considerations: Editing all 82 entrypoints to import a logging decorator was rejected: it cannot record import errors, syntax errors, or timeouts, and touches every hook. An in-process runpy runner was rejected because a harness-killed or hanging hook would take the recorder down with it. Implementation details: A subprocess wrapper plus a pure classifier in outcome.py. Non-goals: No install.sh, settings, or hook fragment change; no report CLI; no change to any existing hook. Layer: domain Feature state: dormant Files: - engine/hooks/_runner/run.py - engine/hooks/_runner/outcome.py - engine/hooks/_runner/tests/test_outcome.py - engine/hooks/_runner/tests/test_run.py - engine/hooks/_runner/tests/fixtures/ Change types: - engine/hooks/_runner/run.py: create - engine/hooks/_runner/outcome.py: create - engine/hooks/_runner/tests/test_outcome.py: create - engine/hooks/_runner/tests/test_run.py: create - engine/hooks/_runner/tests/fixtures/: create Acceptance criteria: - `python3 -m unittest discover -s engine/hooks/_runner/tests -v` exits 0. - `bash scripts/run_all_tests.sh` exits 0. - `python3 scripts/check_hook_test_coverage.py` exits 0. Exit code: 0 Invoker-Finalize-Id: e5ba51e8-de9e-430f-8672-e0eef3e3bafc
…ine/hooks/_runner/README.md states what the runner records, where the rows go, and the outcome precedence. Review lane: docs Safety invariant: Only engine/hooks/_runner/README.md changes; no code, test, or config file is edited. Effectiveness measurement: Every row field and outcome named in the README appears in run.py and outcome.py, checked by reading both. Slice rationale: Prose in its own commit so the code commit stays one claim. Architectural effect: None; prose only. Goal: Create engine/hooks/_runner/README.md. Motivation: Readers of the hook directory need the row format without reading code. Alternative considerations: Code comments were rejected; the repo forbids new comments. Implementation details: One new Markdown file. Non-goals: No code, test, or config edits. Layer: docs Feature state: dormant Files: - engine/hooks/_runner/README.md Change types: - engine/hooks/_runner/README.md: create Acceptance criteria: - `test -f engine/hooks/_runner/README.md` exits 0. Exit code: 0 Invoker-Finalize-Id: 917f28d4-7b67-447f-bfd1-7a26a20061da
…unner tests, the repo test suite, and the hook coverage gate pass. Review lane: proof Safety invariant: Verification is read-only and does not alter any repository file. Effectiveness measurement: The three commands are the direct measurement. Slice rationale: One focused proof before review. Architectural effect: None; verification only. Goal: Prove pass-through and outcome classification. Motivation: Running the tests is the proof. Alternative considerations: A live-harness run is deferred to step 2, where the runner is installed. Implementation details: Run the three commands. Non-goals: No mutations. Layer: app_regression Feature state: active Acceptance criteria: - Exits 0 only when all pass. Exit code: 0 Invoker-Finalize-Id: e73029a9-e2ea-4d00-8c66-f727763900a1
….t0.a-ae5fdfe43-637dd362
…No ephemeral inter-task handoff files remain in the worktree before the merge gate. Review lane: cleanup Safety invariant: The scrub script only checks for known handoff artifact names and never touches source, tests, or other repository files. Effectiveness measurement: The script exits non-zero if any handoff artifact remains. Slice rationale: Required terminal scrub for every implementation workflow. Architectural effect: None; hygiene only. Goal: Leave the branch free of handoff artifacts. Motivation: Handoff files must not reach the PR. Alternative considerations: Manual cleanup was rejected as non-deterministic. Implementation details: Run scripts/scrub-handoff-artifacts.sh. Layer exception: allowed -- the terminal scrub must run after every task in the workflow, including the docs task. Non-goals: No product edits. Layer: app_regression Feature state: active Acceptance criteria: - `bash scripts/scrub-handoff-artifacts.sh` exits 0. Exit code: 0 Invoker-Finalize-Id: cb09f6f2-66d1-4564-adb3-38e63423dbfa
…af591618b-0e93f775 — Review claim: No ephemeral inter-task handoff files remain in the worktree before the merge gate. Review lane: cleanup Safety invariant: The scrub script only checks for known handoff artifact names and never touches source, tests, or other repository files. Effectiveness measurement: The script exits non-zero if any handoff artifact remains. Slice rationale: Required terminal scrub for every implementation workflow. Architectural effect: None; hygiene only. Goal: Leave the branch free of handoff artifacts. Motivation: Handoff files must not reach the PR. Alternative considerations: Manual cleanup was rejected as non-deterministic. Implementation details: Run scripts/scrub-handoff-artifacts.sh. Layer exception: allowed -- the terminal scrub must run after every task in the workflow, including the docs task. Non-goals: No product edits. Layer: app_regression Feature state: active Acceptance criteria: - `bash scripts/scrub-handoff-artifacts.sh` exits 0.
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_0722b5cf-6ea5-4d3a-a405-f2859b23a7a4) |
…n recording fails datetime.UTC exists only on 3.11+, so on CI's Python 3.9 the runner raised after the hook ran and dropped the hook's stdout and exit code. Use datetime.timezone.utc, and catch any failure while classifying or writing the metrics row so the hook's output is still forwarded, with one stderr line naming the error. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G95BG4NxDsW4NA6fcipHrv Change-Id: Ie823ca268e85e83dd57d3b6f1b026946daeb4c99
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_82413a95-721c-4568-9c8f-05d5c7157784) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This change adds a dormant wrapper that records every automated check while preserving its output and exit status.
Today, direct starts provide no record of which automated checks speak, stay silent, block, or fail.
The wrapper starts each check separately, forwards its standard streams, classifies the result, and appends one JSONL metrics row.
Installation wiring is intentionally deferred to the next step.
Review Claim
Approve a wrapper that records each check result without changing its visible output or exit status.
Review Lane
behavior
Review Unit
engine-runtime
Safety Invariant
A wrapped hook produces byte-identical stdout and the same exit code as a direct run; a failed metrics write leaves both unchanged and adds one stderr line.
Slice Rationale
The runner and its outcome rules form one behavior claim. Documentation describes the same contract, while installation wiring remains a later slice.
Non-goals
Architecture
Before
graph TD A["Harness"] --> B["Hook script"] B --> C["stdout, stderr, exit code"]After
graph TD A["Harness"] --> B["Hook runner"] B --> C["Hook subprocess"] C --> D["stdout, stderr, exit code"] B --> E["Outcome classifier"] B --> F["JSONL metrics row"]Test Plan
Test Plan
python3 -m unittest discover -s engine/hooks/_runner/tests -vbash scripts/run_all_tests.shpython3 scripts/check_hook_test_coverage.pypython3 engine/skills/make-pr/scripts/preflight.py --base mainpython3 scripts/check_no_new_comments.pybash scripts/scrub-handoff-artifacts.shRevert Plan
Revert Plan
git revert <merge-commit-sha>Note
Low Risk
New isolated runtime code with no install wiring; behavior is test-guarded to preserve hook stdout and exit codes when eventually enabled.
Overview
Adds a dormant hook runner under
engine/hooks/_runner/that wraps hook scripts in a subprocess, forwards stdout/stderr and the exit code unchanged, and appends one JSONL metrics row per run (default~/.cache/catstack-hook-metrics/runs.jsonl, overridable viaCATSTACK_HOOK_METRICS_DIR).Each row records harness (inferred from path), hook/script, stdin-derived
eventandsession_id(withconversation_idfallback), timing, stdout size, stderr tail, and anoutcomefromoutcome.classify(timeout, exit 2 / JSON deny signals → blocked, crashes,catstack-hook-errorlines, spoke vs silent). Optional--timeoutkills slow hooks and recordstimed_out. Metrics or classification failures add a singlecatstack-hook-metrics:stderr line without altering hook stdout/exit code.README documents the CLI and outcome precedence; unit tests lock the passthrough invariant and classification rules. Install wiring is explicitly deferred—nothing in the repo invokes the runner yet.
Reviewed by Cursor Bugbot for commit 6a83264. Bugbot is set up for automated code reviews on this repo. Configure here.