Commit Graph

401 Commits

  • fix(hooks): log fail-open breadcrumb on parse/read errors in metrics bridge
    coderabbitai flagged: the two `catch` blocks in `readSessionCost`
    silently swallowed every failure mode. A malformed `costs.jsonl`
    row, a permission error opening the file, or any other unexpected
    I/O failure would silently return zero cost — masking real
    problems and feeding stale or zero numbers into
    `ecc-context-monitor.js` (which then injects them as
    `additionalContext` into the live model turn).
    
    Fix two things, both fail-open-preserving:
    
    1. **Inner JSON.parse catch** — count malformed lines and write
       one aggregated breadcrumb per call:
    
         [ecc-metrics-bridge] skipped N malformed line(s) in <path>
    
       Aggregating (rather than per-line) keeps a log-flooded
       `costs.jsonl` diagnosable without overwhelming stderr.
    
    2. **Outer fs.readFileSync catch** — write a breadcrumb on real
       errors, but stay silent on `ENOENT`. The "no costs.jsonl yet"
       case is genuinely normal (no Stop event has fired this session)
       and producing noise on every PreToolUse before the first Stop
       would be reviewer-visible spam. All other error codes
       (`EACCES`, `EISDIR`, `EMFILE`, …) get:
    
         [ecc-metrics-bridge] failing open after <name> reading <path>: <msg>
    
    In both cases the function still returns the zero-cost fallback
    so the bridge never breaks tool execution — only the
    diagnosability changes.
    
    Two new regression tests in
    `tests/hooks/ecc-metrics-bridge.test.js`:
    
      ✓ readSessionCost writes a stderr breadcrumb when malformed
        lines are skipped — feeds 4 rows (2 valid, 2 malformed),
        asserts the last valid row still wins AND captured stderr
        contains "skipped 2 malformed line(s)".
    
      ✓ readSessionCost stays silent when costs.jsonl does not exist
        (ENOENT) — uses a fresh tmp HOME with no metrics dir, asserts
        zero return AND empty stderr.
    
    Test count: 16 → 18; `npm test` green; `yarn lint` clean.
  • fix(hooks): scan full costs.jsonl when locating session row
    `readSessionCost` read only the trailing 8 KiB of
    `~/.claude/metrics/costs.jsonl` to "avoid scanning entire file".
    That ceiling is the opposite-sign sibling of the double-count bug
    fixed in the previous commit: once a session's most recent
    cumulative row gets pushed past the 8 KiB window by newer rows
    from other sessions, the bridge silently reports `totalCost: 0`,
    `totalIn: 0`, `totalOut: 0` for that session — same false signal
    to `ecc-context-monitor.js`, same wrong number injected into the
    live model turn as `additionalContext`.
    
    `cost-tracker.js` has no rotation policy, so on any non-trivial
    workstation costs.jsonl grows past 8 KiB within minutes of normal
    use. For users who keep multiple concurrent sessions, this means
    the second-and-later sessions silently report zero almost
    immediately.
    
    Reproduced before this commit:
    
      $ HOME=/tmp/eccc node -e '
          const fs = require("fs");
          const m = require("./scripts/hooks/ecc-metrics-bridge.js");
          // S1 row at file start, then 200 rows of OTHER-session noise (~16 KiB).
          // S1 is the row we want, but it sits past the 8 KiB tail.
          const s1 = `{"session_id":"S1","estimated_cost_usd":0.5,"input_tokens":500,"output_tokens":250}`;
          const other = `{"session_id":"OTHER","estimated_cost_usd":1,"input_tokens":100,"output_tokens":50}`;
          fs.mkdirSync("/tmp/eccc/.claude/metrics", { recursive: true });
          fs.writeFileSync("/tmp/eccc/.claude/metrics/costs.jsonl",
            [s1, ...Array(200).fill(other)].join("\\n") + "\\n");
          console.log(JSON.stringify(m.readSessionCost("S1")));'
      {"totalCost":0,"totalIn":0,"totalOut":0}
    
    Expected: `{"totalCost":0.5, "totalIn":500, "totalOut":250}` (the
    S1 row that exists in the file).
    Actual: zero — the row is past the 8 KiB tail.
    
    Fix: drop the `fs.openSync` + bounded `fs.readSync` + position
    arithmetic in favour of `fs.readFileSync(costsPath, 'utf8')` and
    iterate every line. Each row is ~150 bytes; even 100k rows is
    ~15 MB and a single sync read on PreToolUse is in the low ms.
    If file rotation lands in `cost-tracker.js` later, this scan
    becomes proportionally cheaper.
    
    After this commit the reproduction above returns
    `{"totalCost":0.5, "totalIn":500, "totalOut":250}`.
    
    Regression test in `tests/hooks/ecc-metrics-bridge.test.js`:
    `readSessionCost finds session row beyond the old 8 KiB tail
    boundary`. The test asserts the costs.jsonl fixture is > 8 KiB
    before reading so any reintroduction of a bounded tail would
    re-fail the test (i.e. the assertion is the contract, not the
    specific number 8192).
    
    Together with the previous commit, both directions of the
    metrics-bridge cost-reporting bug are closed.
  • fix(hooks): use last cumulative row for session cost in metrics bridge
    `ecc-metrics-bridge.js#readSessionCost` summed the
    `estimated_cost_usd`, `input_tokens`, and `output_tokens` of
    every matching row in `~/.claude/metrics/costs.jsonl`. That breaks
    the documented contract of `scripts/hooks/cost-tracker.js`, which
    explicitly states (in its module docblock):
    
      Cumulative behavior: Stop fires per assistant response, not
      per session. Each row therefore represents the cumulative
      session total up to that point. To get per-session cost, take
      the last row per session_id.
    
    Summing N cumulative rows over-counts by roughly (N+1)/2 ×. For a
    session with 3 rows at 0.01, 0.02, 0.03 USD (true running total
    0.03), the bridge today reports 0.06 USD. The over-counted value
    feeds `ecc-context-monitor.js`, which then trips its
    COST_NOTICE_USD / COST_WARNING_USD / COST_CRITICAL_USD thresholds
    on phantom spend AND injects the inflated number as
    `additionalContext` into the live model turn — so the agent
    itself is told a wrong cost.
    
    Reproduced on `main` before this commit:
    
      $ cat > /tmp/eccc/.claude/metrics/costs.jsonl <<EOF
      {"session_id":"S1","estimated_cost_usd":0.01,"input_tokens":333,"output_tokens":166}
      {"session_id":"S1","estimated_cost_usd":0.02,"input_tokens":666,"output_tokens":333}
      {"session_id":"S1","estimated_cost_usd":0.03,"input_tokens":1000,"output_tokens":500}
      EOF
    
      $ HOME=/tmp/eccc node -e 'const m = require("./scripts/hooks/ecc-metrics-bridge.js"); \
          console.log(JSON.stringify(m.readSessionCost("S1")))'
      {"totalCost":0.06,"totalIn":1999,"totalOut":999}
    
    Expected: `{"totalCost":0.03,"totalIn":1000,"totalOut":500}` (the
    last cumulative row).
    Actual: 2× over-count.
    
    Fix: replace `+=` with `=` in the matching branch so the assigned
    values reflect the most recent row encountered. The iteration
    order is file order, which is also event time order, so the last
    assignment wins — exactly the contract cost-tracker writes
    against.
    
    After this commit the reproduction above returns
    `{"totalCost":0.03,"totalIn":1000,"totalOut":500}`.
    
    Regression test in `tests/hooks/ecc-metrics-bridge.test.js`:
    `readSessionCost returns the LAST cumulative row, not the sum
    (cost-tracker contract)`. The existing
    `readSessionCost does not include unrelated default-session rows`
    test happened to pass even with the bug because it only had one
    target-session row — single-row sessions are coincidentally
    correct under both formulas. The new test uses three rows so the
    two formulas diverge.
    
    A second issue in the same function — the 8 KiB tail-only read
    silently drops older rows once a session's recent cumulative
    totals scroll past that window — is fixed in the next commit.
  • chore: update statusline ANSI color palette
    - Replace blinking red (5;31m) with bold red (1;31m) for critical context bar
    - Replace cyan metrics (36m) with sky blue (38;5;117m)
    - Replace plain bold task (1m) with bold bright white (1;97m)
    - Update test assertion to match new bold red code
  • add platform audit export output
    Adds JSON/markdown export and write-to-file support for the platform audit operator artifact.
  • fix(hooks): close grouped command bypasses in gateguard (#1912)
    Inspect executable bodies inside plain subshells and brace groups before applying destructive command classifiers.\n\nCo-authored-by: Jamkris <82251632+Jamkris@users.noreply.github.com>
  • feat: add command registry and coverage checks (#1906)
    Salvages the useful parts of #1897 without generated .caliber state or stale counts.
    
    - adds a deterministic command registry generator and drift check
    - commits the current command registry for 75 commands
    - validates the rc.1 README catalog summary against live counts
    - adds a single Ubuntu Node 20 coverage job instead of running coverage in every matrix cell
    
    Co-authored-by: jodunk <jodunk@users.noreply.github.com>
  • fix: integrate recent hook and docs PRs (#1905)
    Integrates useful changes from #1882, #1884, #1889, #1893, #1898, #1899, and #1903:
    - fix rule install docs to preserve language directories
    - correct Ruby security command examples
    - harden dev-server hook command-substitution parsing
    - add Prisma patterns skill and catalog/package surfaces
    - allow first-time protected config creation while blocking existing configs
    - read cost metrics from Stop hook transcripts
    - emit suggest-compact additionalContext on stdout
    
    Co-authored-by: Jamkris <dltmdgus1412@gmail.com>
    Co-authored-by: Levi-Evan <levishantz@gmail.com>
    Co-authored-by: gaurav0107 <gauravdubey0107@gmail.com>
    Co-authored-by: richm-spp <richard.millar@salarypackagingplus.com.au>
    Co-authored-by: zomia <zomians@outlook.jp>
    Co-authored-by: donghyeun02 <donghyeun02@gmail.com>
  • ci: gate observability on release safety evidence
    Add release-safety evidence coverage to observability readiness and refresh rc.1 publication gate docs.
  • docs: gate ECC progress sync readiness
    Make the ECC 2.0 GitHub/Linear/handoff/roadmap progress-sync model part of the local observability readiness gate instead of leaving it as roadmap prose only.
    
    - add `docs/architecture/progress-sync-contract.md` for GitHub, Linear, handoff, roadmap, and work-items sync
    - add a `Tracker Sync` check to `scripts/observability-readiness.js`
    - update observability tests with passing and missing-contract coverage
    - update observability and GA roadmap docs so the local readiness gate is now 18/18 and records #1848 supply-chain hardening evidence
    
    Validation:
    - node tests/scripts/observability-readiness.test.js (9 passed, 0 failed)
    - npm run observability:ready -- --format json (18/18, ready true)
    - npx markdownlint-cli 'docs/architecture/progress-sync-contract.md' 'docs/architecture/observability-readiness.md' 'docs/ECC-2.0-GA-ROADMAP.md'
    - git diff --check
    - node tests/docs/ecc2-release-surface.test.js (18 passed)
    - node tests/run-all.js (2378 passed, 0 failed)
    - GitHub CI for #1849 green across Ubuntu, Windows, and macOS
    
    No release, tag, npm publish, plugin tag, marketplace submission, or announcement was performed.
  • docs: add supply-chain incident response playbook
    Add a repo-level supply-chain incident response playbook for npm/GitHub Actions package-registry incidents, anchored on the May 2026 TanStack compromise and prior Shai-Hulud-style npm incidents.
    
    - add `docs/security/supply-chain-incident-response.md` with exposure checks, immediate response steps, workflow rules, publication rules, and escalation triggers
    - link the playbook from `SECURITY.md`
    - reject `pull_request_target` workflows that restore or save shared dependency caches
    - add a regression test for the new `pull_request_target + actions/cache` guardrail
    
    Validation:
    - node tests/ci/validate-workflow-security.test.js (12 passed, 0 failed)
    - node scripts/ci/validate-workflow-security.js (validated 7 workflow files)
    - npx markdownlint-cli 'SECURITY.md' 'docs/security/supply-chain-incident-response.md'
    - npx markdownlint-cli '**/*.md' --ignore node_modules
    - git diff --check
    - node tests/run-all.js (2377 passed, 0 failed)
    - GitHub CI for #1848 green across Ubuntu, Windows, and macOS
    
    No release, tag, npm publish, plugin tag, marketplace submission, or announcement was performed.
  • ci: require npm audit signature checks
    Require npm registry signature verification wherever workflow npm audit checks run.
    
    - add npm audit signatures to CI Security Scan and maintenance security audit jobs
    - teach the workflow security validator to reject npm audit without signature verification
    - keep the repair and Copilot prompt tests portable across Windows path/case and CRLF frontmatter behavior
    
    Validation:
    - node tests/run-all.js (2376 passed, 0 failed)
    - CI current-head matrix green on #1846