Skip to content

feat(sdlc): daemon-independent escape grant shim (NEW-2) + deprecate HAPAX_*_OFF#3805

Merged
ryanklee merged 2 commits into
mainfrom
beta/reform-fix-grant-escape-shim-20260531
Jun 1, 2026
Merged

feat(sdlc): daemon-independent escape grant shim (NEW-2) + deprecate HAPAX_*_OFF#3805
ryanklee merged 2 commits into
mainfrom
beta/reform-fix-grant-escape-shim-20260531

Conversation

@ryanklee
Copy link
Copy Markdown
Collaborator

@ryanklee ryanklee commented May 31, 2026

Summary

Wires the daemon-independent escape grant shim (reform Phase 4, NEW-2/INV-4) and deprecates the HAPAX_*_OFF off-switch to incident-only. Closes the audit's central safety correction: the EscapeGrant substrate (shared/governance/coord_capabilities.py) shipped with zero non-test callers, so the only working escape from an irreversible-harm gate was the deprecated, silent, unconditional HAPAX_*_OFF — the exact inversion of "obligatory but never stuck via an authorized scoped grant."

  • Task: reform-fix-grant-escape-shim-20260531
  • AuthorityCase: CASE-FORMAL-GOVERNANCE-001
  • Parent spec: coordination-reform-master-design-2026-05-30.md §4.4 (NEW-2), INV-4

What changed

  • hooks/scripts/escape-grant.sh (new) — shared sourced helper. escape_grant_allows <gate> scans /var/lib/hapax/coord/grants for a signed EscapeGrant covering the gate and verifies it via python3 -m shared.governance.coord_capabilities verify-grant — a pure file read, never an RPC, so it works with the kernel down. Degrades closed (missing dir/key/python ⇒ no escape); ledgers each honored grant.
  • hooks/scripts/cc-task-gate.sh — every BLOCK now routes through _emit_block, which honors a covering grant before failing closed. HAPAX_CC_TASK_GATE_OFF (formerly silent) is ledgered (cc_task_gate_off_bypass) + deprecation-warned; HAPAX_METHODOLOGY_EMERGENCY records a pending retro-grant obligation (1h deadline) on both bypass paths.
  • hooks/scripts/no-stale-branches.sh — same helper wired into all 4 block points (destructive ×2, worktree-cap, stale-branches) — the exact gate that hard-walls a stale-worktree lane from creating a branch.
  • scripts/coord-grant-mint (new) — operator/lane CLI: writes a signed, scoped, time-boxed grant to /var/lib/hapax/coord/grants/<id>.grant; auto-creates the signing key (0600, O_EXCL) on first mint.
  • scripts/coord-retro-grant-watch (new) — closes the deprecation loop: fulfils a retro-grant obligation when a covering grant lands, ntfy-escalates when the 1h deadline passes with none.

Acceptance criteria

  • coord-grant-mint / CLI writes a signed grant file to /var/lib/hapax/coord/grants/<id>.grant with a real signing key.
  • The gate (and the no-stale-branches shim) read+verify the grant file on a block before failing closed — daemon-independent (no RPC).
  • HAPAX_CC_TASK_GATE_OFF and HAPAX_METHODOLOGY_EMERGENCY are ledgered; emergency records a retro-grant obligation.
  • Chaos test: no daemon present + a hand-written grant unblocks a lane (INV-4).
  • Ruff + shellcheck + tests pass.

Test evidence (TDD — red→green for every behavior)

  • tests/hooks/test_cc_task_gate.py129 passed (16 new: grant honor / scope / expiry / forged-key / ledger, chaos/INV-4, gate-off ledger + warning, emergency retro-grant + 1h deadline)
  • tests/hooks/test_no_stale_branches.py17 passed (4 new: grant override on branch-create + destructive reset, wrong-scope control)
  • tests/scripts/test_coord_grant_mint.py4 passed
  • tests/scripts/test_coord_retro_grant_watch.py6 passed
  • tests/test_coord_capabilities.py — green (substrate unmodified)
  • ruff check + ruff format clean; shellcheck -S warning clean on all three shims.

Notes

  • Threat model: single-user (axiom single_user). The HMAC key shares the operator's uid, so this enforces a deliberate, scoped, time-boxed, audited escape — not adversarial isolation between the operator's own lanes. It replaces the blunt, silent, unconditional off-switch.
  • Chaos test fidelity: the test harness has no daemon at all, so it proves a grant unblocks with no daemon present — strictly stronger than "daemon killed."
  • Follow-up (out of this task's scope): add a systemd timer for coord-retro-grant-watch (systemd/ is outside the task's mutation_scope_refs); the watcher is runnable now.
  • The Lisp coord.grant.mint verb lives in the constitution repo (outside council's mutation scope); AC feat(dev-story): add git bundle and archived conversation indexing #1 is satisfied via the /CLI path per the task note.
  • Unrelated test_session_context_* timeouts seen locally are pre-existing session-context.sh (>10s) load flakiness on this box — zero coupling to this change (that script references none of these surfaces).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Daemon-independent, signed "escape grant" overrides for protected gates; gates now honor scoped grants and record usage to a methodology ledger
    • CLI to mint scoped, time-limited grants and a watcher to track/fulfill retro-grant obligations (escalation/notification)
  • Bug Fixes / Behavior

    • Gate bypass and emergency paths now ledger deprecation and retro-obligation entries while preserving prior blocking behavior
  • Tests

    • Extensive tests added for grant minting, validation, gate overrides, emergency bypasses, and retro-grant processing

…cate HAPAX_*_OFF

Closes the audit's central safety correction (NEW-2/INV-4). The EscapeGrant
substrate shipped with ZERO non-test callers, so the only working escape from an
irreversible-harm gate was the deprecated, silent, unconditional HAPAX_*_OFF
off-switch — the exact inversion of "obligatory but never stuck via an authorized
scoped grant."

- hooks/scripts/escape-grant.sh (new): shared sourced helper. `escape_grant_allows
  <gate>` scans /var/lib/hapax/coord/grants for a signed EscapeGrant covering the
  gate and verifies it via `python3 -m shared.governance.coord_capabilities
  verify-grant` — a PURE FILE READ, never an RPC, so it works with the kernel
  down. Degrades closed (missing dir/key/python ⇒ no escape). Ledgers each grant.
- hooks/scripts/cc-task-gate.sh: every BLOCK now routes through _emit_block, which
  honors a covering grant before failing closed. HAPAX_CC_TASK_GATE_OFF (formerly
  silent) is ledgered (cc_task_gate_off_bypass) + deprecation-warned;
  HAPAX_METHODOLOGY_EMERGENCY records a pending retro-grant obligation (1h
  deadline) on both bypass paths.
- hooks/scripts/no-stale-branches.sh: same helper wired into all 4 block points
  (destructive x2, worktree-cap, stale-branches) — the exact gate that hard-walls
  a stale-worktree lane from creating a branch.
- scripts/coord-grant-mint (new): operator/lane CLI — writes a signed, scoped,
  time-boxed grant to /var/lib/hapax/coord/grants/<id>.grant; auto-creates the
  signing key (0600, O_EXCL) on first mint.
- scripts/coord-retro-grant-watch (new): closes the deprecation loop — fulfils a
  retro-grant obligation when a covering grant lands, ntfy-escalates when the 1h
  deadline passes with none. (Deploy via a timer — out of this task's scope.)

Threat model: single-user (axiom single_user). The HMAC key shares the operator's
uid, so this enforces a DELIBERATE, SCOPED, TIME-BOXED, AUDITED escape, not
adversarial isolation between the operator's own lanes.

TDD: chaos/INV-4 acceptance (a hand-written grant unblocks with NO daemon present
— strictly stronger than "daemon killed"), scope/expiry/forged-key negatives,
gate-off ledgering, retro-grant obligation + 1h deadline, watcher
fulfil/escalate/idempotent, minter key+scope. 26 new tests; full tests/hooks/ +
tests/scripts/ + test_coord_capabilities green; ruff + shellcheck clean.

AuthorityCase: CASE-FORMAL-GOVERNANCE-001
Task: reform-fix-grant-escape-shim-20260531
Parent spec: coordination-reform-master-design-2026-05-30.md (NEW-2 sec 4.4, INV-4)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 31, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

Adds daemon-independent, signed, scoped, time-limited "escape grants" and tooling: a Bash gate checker (escape-grant.sh), a mint CLI (scripts/coord-grant-mint), a retro-obligation watcher (scripts/coord-retro-grant-watch), hook integrations in cc-task-gate.sh and no-stale-branches.sh, and tests exercising minting, verification, obligation lifecycle, and hook overrides.

Changes

Escape grant system for HAPAX gates

Layer / File(s) Summary
Reusable escape-grant gate checker library
hooks/scripts/escape-grant.sh
Fail-closed gate checker that scans *.grant files, validates them via Python governance helpers, records best-effort JSONL ledger entries, and returns success (0) only on the first valid matching grant.
no-stale-branches: escape-grant override support
hooks/scripts/no-stale-branches.sh (dir resolution + injection points)
Sources escape-grant.sh, adds _nsb_escape_or_block() and invokes it before destructive git commands and branch-creation checks to permit override when a valid no-stale-branches grant exists.
Grant minting CLI
scripts/coord-grant-mint
CLI to mint signed, scoped, time-limited .grant files; atomically creates/reuses a 32-byte signing key with 0600 perms; supports path overrides and prints the written grant path.
Retro-grant obligation watcher
scripts/coord-retro-grant-watch
Watches JSONL obligation ledger, classifies pending obligations as fulfilled (covering grant found), escalated (deadline passed), or pending; optionally notifies operators and atomically persists updates; supports --dry-run/--now.
cc-task-gate: escape-grant helpers and bypass handling
hooks/scripts/cc-task-gate.sh (GATE_NAME, _emit_block, obligation recording)
Adds GATE_NAME, central _emit_block honoring escape_grant_allows, _record_retro_grant_obligation for time-bounded obligations, and ledgering/deprecation guidance for HAPAX_CC_TASK_GATE_OFF/HAPAX_METHODOLOGY_EMERGENCY.
cc-task-gate: centralize block message handling
hooks/scripts/cc-task-gate.sh (20+ branches)
Replaces direct stderr echo/cat blocks across many fail-closed branches with _emit_block to allow escape-grant-based overrides without changing denial content or exit codes.
no-stale-branches test coverage
tests/hooks/test_no_stale_branches.py
Adds TestEscapeGrant suite and import/setup changes to validate no-stale-branches grant overrides and negative cases.
coord-grant-mint tests
tests/scripts/test_coord_grant_mint.py
Tests minting behavior: single .grant creation, printed path, auto key creation with 0600, scope-specific verification, and key reuse.
coord-retro-grant-watch tests
tests/scripts/test_coord_retro_grant_watch.py
Tests obligation lifecycle and watcher behavior: escalation, fulfillment, predating grants, pending, idempotence, and missing ledger handling.
cc-task-gate tests
tests/hooks/test_cc_task_gate.py (Phase 4 additions)
Adds grant-key/test helpers and suites asserting that scoped grants unblock cc-task-gate.sh, ledger entries are recorded, HAPAX_CC_TASK_GATE_OFF is ledgered with deprecation, and HAPAX_METHODOLOGY_EMERGENCY records retro-grant obligations.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I mint a grant with tiny hop and key,

Scoped to gates that let the hurry be,
A watcher keeps the ledger neat and bright,
Helpers whisper: "emit_block" — then light,
Operators leap home safely through the night.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: daemon-independent escape grant shim (NEW-2) and deprecation of HAPAX_*_OFF.
Description check ✅ Passed The PR description addresses all key template sections: Summary, AuthorityCase (CASE-FORMAL-GOVERNANCE-001), detailed What Changed, Acceptance Criteria, Test Evidence, and Notes. CLAUDE.md hygiene checklist is included.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch beta/reform-fix-grant-escape-shim-20260531

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 323cf5a57c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +85 to +86
if _has_covering_grant(grant_dir, key, gate, ts_s, now):
out.append({**rec, "status": "fulfilled", "resolved_s": int(now)})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Enforce the retro-grant deadline before fulfilling

When the watcher runs after an obligation is already overdue, this check accepts any covering grant minted after the bypass, even if it was minted after deadline_s. That means an emergency HAPAX_*_OFF use can be marked fulfilled instead of escalated by creating a grant late, defeating the documented 1h retro-grant requirement; include the deadline in the covering-grant predicate (e.g. require issued_at <= deadline_s) before marking the record fulfilled.

Useful? React with 👍 / 👎.

grant = read_grant_file(path)
if grant is None or grant.issued_at < min_issued_at:
continue
if verify_escape_grant(grant, key=key, now=now, gate=gate):
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Check retro grants at the obligation time, not watcher time

If the watcher is delayed until after an otherwise timely retro grant has expired, passing the current watcher time into verify_escape_grant makes that historical grant look invalid and the obligation escalates anyway. With the default 1h grant TTL, a grant minted within the required hour can expire before this script next runs, so fulfillment depends on watcher cadence rather than whether the signed grant landed on time; verify the signature/scope and evaluate expiry against the obligation window instead of now.

Useful? React with 👍 / 👎.

Comment on lines +212 to 217
printf '{"ts":"%s","kind":"cc_task_gate_off_bypass","role":"%s","tool":"%s"}\n' \
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$_gate_off_role" "$tool_name" \
>>"$_gate_off_ledger" 2>/dev/null || true
echo "cc-task-gate: HAPAX_CC_TASK_GATE_OFF bypass used — LEDGERED. This switch is DEPRECATED" >&2
echo " (incident-only). Prefer a scoped signed escape: scripts/coord-grant-mint --scope $GATE_NAME." >&2
exit 0
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Record retro-grant obligations for the gate-off bypass

When HAPAX_CC_TASK_GATE_OFF=1 is used, this path only writes a ledger line and exits, so coord-retro-grant-watch never receives a pending obligation for this deprecated unconditional bypass. In the incident path that still relies on the old off-switch, no one is forced or reminded to mint the scoped signed grant within the 1h window, leaving the original HAPAX_*_OFF escape effectively outside the new retro-grant enforcement loop.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
tests/hooks/test_cc_task_gate.py (1)

1159-1167: 💤 Low value

Minor inefficiency: json.loads called twice per line.

Each line is parsed twice—once to check "kind" in json.loads(line) and again to extract the value. Consider parsing once:

♻️ Suggested refactor
 def _ledger_kinds(home: Path) -> list[str]:
     ledger = home / ".cache" / "hapax" / "methodology-emergency-ledger.jsonl"
     if not ledger.exists():
         return []
+    kinds = []
+    for line in ledger.read_text().splitlines():
+        if not line.strip():
+            continue
+        obj = json.loads(line)
+        if "kind" in obj:
+            kinds.append(obj["kind"])
+    return kinds
-    return [
-        json.loads(line)["kind"]
-        for line in ledger.read_text().splitlines()
-        if line.strip() and "kind" in json.loads(line)
-    ]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/hooks/test_cc_task_gate.py` around lines 1159 - 1167, The _ledger_kinds
function currently calls json.loads(line) twice per non-empty line; change it to
parse each non-blank line once into a local variable (e.g., obj =
json.loads(line)) then check "kind" in obj and append obj["kind"]; you can
implement this by replacing the current list comprehension with a simple for
loop or a single-expression comprehension that binds the parsed object once so
each line is only json.loads()ed one time.
scripts/coord-retro-grant-watch (2)

123-166: ⚡ Quick win

Consider resilient JSONL parsing for corrupted ledger lines.

Line 153 uses json.loads(line) in a list comprehension. If the obligations ledger contains a malformed JSON line, the watcher will crash. Adding error handling to skip invalid lines would improve resilience.

♻️ Proposed resilient parsing
-    records = [json.loads(line) for line in obligations.read_text().splitlines() if line.strip()]
+    records = []
+    for line in obligations.read_text().splitlines():
+        if not line.strip():
+            continue
+        try:
+            records.append(json.loads(line))
+        except json.JSONDecodeError:
+            pass  # skip malformed line
     key = _load_key(key_path)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/coord-retro-grant-watch` around lines 123 - 166, The list
comprehension in main that builds records via json.loads(line) will crash on any
malformed JSON; change it to iterate lines from
obligations.read_text().splitlines(), attempt json.loads(line) inside a
try/except catching json.JSONDecodeError (and ValueError for safety), skip
invalid lines and optionally record/log the failure (e.g., include the line
index or content) so the watcher continues; update the variable construction
around "records" in main and ensure any skipped-line accounting is reflected in
the summary passed to process.

71-96: ⚡ Quick win

Consider defensive handling for malformed obligation records.

Lines 83-84 use float() directly on record fields. If the obligations ledger contains malformed entries (non-numeric ts_s or deadline_s), the watcher will crash with ValueError. Since the ledger is operator-controlled and best-effort, this may be acceptable, but adding a try/except to skip malformed records would improve resilience.

🛡️ Proposed defensive skip for malformed records
     for rec in records:
         if rec.get("status") != "pending":
             out.append(rec)
             continue
-        gate = str(rec.get("gate", "*"))
-        ts_s = float(rec.get("ts_s", 0))
-        deadline_s = float(rec.get("deadline_s", 0))
+        try:
+            gate = str(rec.get("gate", "*"))
+            ts_s = float(rec.get("ts_s", 0))
+            deadline_s = float(rec.get("deadline_s", 0))
+        except (ValueError, TypeError):
+            out.append(rec)  # preserve malformed record as-is
+            continue
         if _has_covering_grant(grant_dir, key, gate, ts_s, now):
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/coord-retro-grant-watch` around lines 71 - 96, The process function
currently calls float() on rec["ts_s"] and rec["deadline_s"] unguarded, which
will raise ValueError/TypeError for malformed ledger entries; wrap the parsing
of ts_s and deadline_s in a try/except (catch ValueError and TypeError) inside
process (next to where ts_s = float(...) and deadline_s = float(...)) and on
parse failure skip or mark the record as malformed: append the original rec (or
a copy with status "invalid") to out, increment a new summary["invalid"] counter
(initialize it alongside total/pending/fulfilled/escalated), do not change
to_notify for that record, and continue the loop so the watcher doesn't crash;
keep all other logic (including _has_covering_grant calls) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@scripts/coord-retro-grant-watch`:
- Around line 123-166: The list comprehension in main that builds records via
json.loads(line) will crash on any malformed JSON; change it to iterate lines
from obligations.read_text().splitlines(), attempt json.loads(line) inside a
try/except catching json.JSONDecodeError (and ValueError for safety), skip
invalid lines and optionally record/log the failure (e.g., include the line
index or content) so the watcher continues; update the variable construction
around "records" in main and ensure any skipped-line accounting is reflected in
the summary passed to process.
- Around line 71-96: The process function currently calls float() on rec["ts_s"]
and rec["deadline_s"] unguarded, which will raise ValueError/TypeError for
malformed ledger entries; wrap the parsing of ts_s and deadline_s in a
try/except (catch ValueError and TypeError) inside process (next to where ts_s =
float(...) and deadline_s = float(...)) and on parse failure skip or mark the
record as malformed: append the original rec (or a copy with status "invalid")
to out, increment a new summary["invalid"] counter (initialize it alongside
total/pending/fulfilled/escalated), do not change to_notify for that record, and
continue the loop so the watcher doesn't crash; keep all other logic (including
_has_covering_grant calls) unchanged.

In `@tests/hooks/test_cc_task_gate.py`:
- Around line 1159-1167: The _ledger_kinds function currently calls
json.loads(line) twice per non-empty line; change it to parse each non-blank
line once into a local variable (e.g., obj = json.loads(line)) then check "kind"
in obj and append obj["kind"]; you can implement this by replacing the current
list comprehension with a simple for loop or a single-expression comprehension
that binds the parsed object once so each line is only json.loads()ed one time.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 78c537fa-033e-4702-a7e1-726520300c59

📥 Commits

Reviewing files that changed from the base of the PR and between 1dd37ed and 323cf5a.

📒 Files selected for processing (9)
  • hooks/scripts/cc-task-gate.sh
  • hooks/scripts/escape-grant.sh
  • hooks/scripts/no-stale-branches.sh
  • scripts/coord-grant-mint
  • scripts/coord-retro-grant-watch
  • tests/hooks/test_cc_task_gate.py
  • tests/hooks/test_no_stale_branches.py
  • tests/scripts/test_coord_grant_mint.py
  • tests/scripts/test_coord_retro_grant_watch.py

@ryanklee ryanklee added this pull request to the merge queue May 31, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch May 31, 2026
…into beta/reform-fix-grant-escape-shim-20260531

# Conflicts:
#	tests/hooks/test_cc_task_gate.py
@ryanklee ryanklee enabled auto-merge June 1, 2026 00:13
@ryanklee ryanklee added this pull request to the merge queue Jun 1, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a9df13ba34

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

_record_retro_grant_obligation() {
local role="${1:-unknown}" task="${2:-unknown}" case_id="${3:-unknown}"
local tool="${4:-unknown}" trigger="${5:-HAPAX_METHODOLOGY_EMERGENCY}"
local obligations="${HOME}/.cache/hapax/coord-retro-grant-obligations.jsonl"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor the configured retro-obligation ledger path

When coord-retro-grant-watch is run with --obligations/HAPAX_COORD_RETRO_OBLIGATIONS (the new watcher explicitly supports this for non-default deployments or timers), the gate still appends emergency obligations to the hard-coded $HOME/.cache/... file here. In that configuration the watcher reads a different ledger, reports zero pending records, and the mandatory retro-grant escalation loop is silently bypassed; use the same env/configured path when recording obligations.

Useful? React with 👍 / 👎.

"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$role" "$task_id" "$authority_case" "$tool_name" \
>> "$_emergency_ledger"
echo "cc-task-gate: EMERGENCY BYPASS — logged to $_emergency_ledger" >&2
_record_retro_grant_obligation "$role" "$task_id" "$authority_case" "$tool_name" "HAPAX_METHODOLOGY_EMERGENCY"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Record emergency obligations after resolving task context

For a normal claimed task with HAPAX_METHODOLOGY_EMERGENCY=1, control exits in the earlier emergency block before role/task/case parsing, so this contextual retro-grant obligation is unreachable and every recorded obligation/notification uses unknown for the task and AuthorityCase. That defeats the new audit trail exactly on the incident bypass path; defer the early exit until after context is available, falling back to the early record only when the infrastructure lookup really fails.

Useful? React with 👍 / 👎.

if to_notify and not args.dry_run:
_escalate(to_notify)

_atomic_write(obligations, updated)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve obligations appended during watcher runs

When the watcher runs concurrently with a gate invocation that appends a new emergency obligation, this read/process/replace cycle can drop the newly appended line: the watcher reads the old file, the gate appends, then _atomic_write replaces the whole ledger with only the old snapshot. In the timer/manual-run context this leaves a real HAPAX_METHODOLOGY_EMERGENCY use with no pending record to fulfill or escalate; take a lock shared with the appender or merge newly appended records before replacing.

Useful? React with 👍 / 👎.

print(json.dumps({"total": 0, "pending": 0, "fulfilled": 0, "escalated": 0}))
return 0

records = [json.loads(line) for line in obligations.read_text().splitlines() if line.strip()]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Skip bad obligation lines instead of aborting the watcher

A single malformed or partially written JSONL line makes json.loads raise before any pending records are processed, so one corrupt append/manual edit permanently prevents later overdue obligations from being fulfilled or escalated. Because the ledger is append-only and written by shell hooks, the watcher should tolerate bad lines the way the grant reader tolerates malformed grants, e.g. skip and report them rather than exiting.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
hooks/scripts/cc-task-gate.sh (1)

677-686: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing error suppression on section 10 emergency bypass.

Unlike the early emergency path (lines 223, 226), this path lacks 2>/dev/null || true. With set -euo pipefail, a mkdir or write failure would cause the script to exit before the bypass completes — defeating the purpose of an emergency override that should work when infrastructure is broken.

Proposed fix to match the early emergency path
 if [[ "${HAPAX_METHODOLOGY_EMERGENCY:-0}" == "1" ]]; then
   _emergency_ledger="$HOME/.cache/hapax/methodology-emergency-ledger.jsonl"
-  mkdir -p "$(dirname "$_emergency_ledger")"
+  mkdir -p "$(dirname "$_emergency_ledger")" 2>/dev/null || true
   printf '{"ts":"%s","role":"%s","task":"%s","case":"%s","tool":"%s"}\n' \
     "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$role" "$task_id" "$authority_case" "$tool_name" \
-    >> "$_emergency_ledger"
+    >> "$_emergency_ledger" 2>/dev/null || true
   echo "cc-task-gate: EMERGENCY BYPASS — logged to $_emergency_ledger" >&2
   _record_retro_grant_obligation "$role" "$task_id" "$authority_case" "$tool_name" "HAPAX_METHODOLOGY_EMERGENCY"
   exit 0
 fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hooks/scripts/cc-task-gate.sh` around lines 677 - 686, The emergency bypass
branch guarded by HAPAX_METHODOLOGY_EMERGENCY sets _emergency_ledger and
performs mkdir/printf/echo/_record_retro_grant_obligation but lacks error
suppression, so failures under set -euo pipefail can abort the bypass; update
the mkdir, file write/append and echo calls in that block (the mkdir -p
"$(dirname "$_emergency_ledger")", the printf >> "$_emergency_ledger" append,
and the echo to stderr) to ignore errors by appending redirection and a harmless
true fallback (e.g., add 2>/dev/null || true to the mkdir and the write/echo
commands) so the emergency path always continues and still calls
_record_retro_grant_obligation and exit 0 even if filesystem ops fail.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@hooks/scripts/cc-task-gate.sh`:
- Around line 677-686: The emergency bypass branch guarded by
HAPAX_METHODOLOGY_EMERGENCY sets _emergency_ledger and performs
mkdir/printf/echo/_record_retro_grant_obligation but lacks error suppression, so
failures under set -euo pipefail can abort the bypass; update the mkdir, file
write/append and echo calls in that block (the mkdir -p "$(dirname
"$_emergency_ledger")", the printf >> "$_emergency_ledger" append, and the echo
to stderr) to ignore errors by appending redirection and a harmless true
fallback (e.g., add 2>/dev/null || true to the mkdir and the write/echo
commands) so the emergency path always continues and still calls
_record_retro_grant_obligation and exit 0 even if filesystem ops fail.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c31f1ca9-6e28-4f10-aa86-1f9fa9ad4274

📥 Commits

Reviewing files that changed from the base of the PR and between 323cf5a and a9df13b.

📒 Files selected for processing (2)
  • hooks/scripts/cc-task-gate.sh
  • tests/hooks/test_cc_task_gate.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/hooks/test_cc_task_gate.py

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