fix(sdlc): collapse 6-way gate fanout to a stable abs-path shim + hooks-doctor (INV-5)#3832
Conversation
…ks-doctor (INV-5)
Reform FM-6: cc-task-gate.sh was physically copied into ~26 worktrees, so the
fleet ran SIX gate versions (427/651/779/786/832/905 lines) and the oldest lanes
silently violated INV-5 (cognition-always-writable) — their 427-line gate
predates is_cognition_path. The promised "hooks -> stable abs-path shims" + a
hooks-doctor drift detector were never built; this builds them.
- Move the gate logic to hooks/scripts/cc-task-gate.impl.sh. cc-task-gate.sh
becomes a thin stable-abs-path SHIM resolving ONE canonical impl at
$HAPAX_CANONICAL_HOOKS (default ~/.local/lib/hapax/hooks), canonical-first so
the LIVE hook escapes rebuild-worktree drift, with a co-located-impl fallback
for fresh checkouts/CI and a fail-open-with-ledger floor (never fail-stuck →
honors INV-5). A -ef self-guard prevents a misconfigured-canonical exec loop.
- Add hooks/scripts/hooks-doctor.sh: classify / --check (CI refusal) /
--deploy-canonical / --fanout. Detects a worktree running a stale gate and
refuses gate drift; deploy refuses any impl missing the INV-5 carve-out.
- escape-grant.sh: resolve the repo root (for shared.governance.coord_capabilities)
via an explicit override + a known-checkout fallback gated on shared/ presence,
so the canonical-deployed gate keeps honoring escape grants (INV-4) when it runs
from ~/.local/lib/hapax/hooks where ../.. has no shared/ tree. In-repo behavior
is unchanged.
- hapax-post-merge-deploy: redeploy the canonical gate closure when the impl or
its sourced siblings change (materialized from the merge SHA), via
hooks-doctor --deploy-canonical.
- hapax-hooks-doctor.{service,timer}: 6h advisory fleet drift sweep (catches a
rebuild clobbering canonical, NEW-CATCH-3).
- install-claude-code.sh: register hooks-doctor --session as a SessionStart hook.
- Repoint gate-logic tests at cc-task-gate.impl.sh; peer-glob lint follows the
vault globs to the impl. New tests pin shim resolution/passthrough/fail-open and
doctor classify/check/deploy/fanout.
Acceptance: every worktree's gate is a shim resolving to one canonical impl
(one-file update); hooks-doctor detects + refuses drift (SessionStart + CI +
timer); the INV-5 cognition carve-out is present fleet-wide via that resolution.
Task: reform-improve-gate-shim-collapse-20260601
AuthorityCase: CASE-CROSS-RUNTIME-COMMS-001
Parent-Spec: coordination-reform-master-design-2026-05-30 (FM-6)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
📝 WalkthroughWalkthroughThis PR refactors the CC task gate from a monolithic inline script into a canonical deployable implementation ( ChangesCC Task Gate Canonical Deployment & Drift Management
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3875b00499
ℹ️ 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".
| for s in agent-role.sh escape-grant.sh hooks-doctor.sh; do | ||
| [[ -r "$src/$s" ]] && install -m 0755 "$src/$s" "$CANONICAL_DIR/$s" | ||
| done |
There was a problem hiding this comment.
Include the bootstrap helper in the canonical gate closure
When the canonical gate is deployed, this closure only installs the impl and three shell siblings, but cc-task-gate.impl.sh now executes $SCRIPT_DIR/cc-task-gate-bootstrap.py on every protected mutation before claim validation. In the canonical layout (~/.local/lib/hapax/hooks) that Python helper is absent, so python3 returns an error, the *) exit 2 branch is taken, and all non-cognition mutating tool calls are blocked even for valid claimed sessions. The post-merge staging list also omits this file, so the helper needs to be staged and installed with the rest of the closure.
Useful? React with 👍 / 👎.
| fi | ||
| done | ||
| if [ -x "$GATE_STAGE/hooks/scripts/hooks-doctor.sh" ]; then | ||
| bash "$GATE_STAGE/hooks/scripts/hooks-doctor.sh" --deploy-canonical --from "$GATE_STAGE" \ |
There was a problem hiding this comment.
Fan out the shim when deploying the gate collapse
For the merge that converts cc-task-gate.sh into a shim, this deploy path only refreshes the canonical implementation; it never runs the new hooks-doctor --fanout path. Existing lane worktrees whose hooks/scripts/cc-task-gate.sh is still a full stale gate do not resolve the canonical copy at all, and the added SessionStart/timer doctor paths are advisory-only, so those lanes continue running the old gate until someone manually fanouts/rebases them. The post-merge deploy should rewrite lane gates to the shim after a successful canonical deploy so the stable-abs-path collapse actually reaches active worktrees.
Useful? React with 👍 / 👎.
| if [[ -n "$ROOT_OVERRIDE" ]]; then | ||
| REPO_ROOT="$ROOT_OVERRIDE" | ||
| else | ||
| REPO_ROOT="$(cd "$SCRIPT_DIR/../.." 2>/dev/null && pwd || echo "$SCRIPT_DIR")" |
There was a problem hiding this comment.
Resolve the doctor symlink before deriving repo root
After --deploy-canonical creates ~/.local/bin/hapax-hooks-doctor as a symlink to this script, invoking that documented command without --root makes SCRIPT_DIR resolve to the symlink directory (~/.local/bin), so this fallback root becomes ~/.local instead of the council checkout. In that installed path, hapax-hooks-doctor --check, --fanout, or the warning-suggested --deploy-canonical inspect ~/.local/hooks/scripts and fail/miss the worktree fleet unless the caller knows to pass overrides; resolve the script path or use a stable checkout default before computing ../...
Useful? React with 👍 / 👎.
| bash "$GATE_STAGE/hooks/scripts/hooks-doctor.sh" --deploy-canonical --from "$GATE_STAGE" \ | ||
| || echo "warning: canonical gate deploy failed" >&2 |
There was a problem hiding this comment.
Propagate canonical deploy failures
If hooks-doctor --deploy-canonical refuses the staged gate or cannot install into the canonical directory, this || echo converts the failure into a successful post-merge deploy and the script continues to mark work as done. For commits that changed the gate closure, that leaves the live canonical gate stale while the deploy trace reports completion, contrary to this script's contract that deploy-step failures exit nonzero; let the failure propagate after printing context.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 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.
Inline comments:
In `@hooks/scripts/cc-task-gate.impl.sh`:
- Around line 371-376: The lease heartbeat currently touches "$claim_file"
before the task note is located and before validating assigned_to and status,
allowing stale or reassigned claims to be kept alive; move the touch call so it
only runs after the code has found the task note and confirmed the claim's
assigned_to matches and status is non-terminal, and keep it best-effort
(2>/dev/null || true) so it doesn't block decision logic—i.e., perform the
validations (assigned_to/status) first, then call touch "$claim_file" to refresh
the lease only for a proven-valid claim.
- Around line 248-250: The canonical deploy currently omits the helper script
used by cc-task-gate.impl.sh, causing python3
"$SCRIPT_DIR/cc-task-gate-bootstrap.py" to fail; update the canonical deployment
closure used by hapax-post-merge-deploy / hooks-doctor.sh --deploy-canonical so
that cc-task-gate-bootstrap.py is installed alongside cc-task-gate.sh (and the
other gate files), or alter the deploy manifest that builds the closure to
explicitly include cc-task-gate-bootstrap.py; ensure the installed SCRIPT_DIR
contains cc-task-gate-bootstrap.py so the cc-task-gate.impl.sh bootstrap
invocation succeeds.
In `@hooks/scripts/hooks-doctor.sh`:
- Around line 177-179: The script currently performs mutating operations (e.g.,
mkdir -p "$CANONICAL_DIR" and install -m 0755 "$src/cc-task-gate.impl.sh"
"$CANONICAL_DIR/cc-task-gate.sh") and other state-changing writes at the other
noted locations (lines around 188-189 and 210-212) without checking their exit
status; update each mutating command so failures are detected and cause the
script to fail the deployment/fanout instead of printing success—for each
command (mkdir, install and the other write/replace operations referenced) check
the command's exit code and on non-zero log an error and exit non-zero (or use a
strict error handler like set -e preceded by explicit error messages) so writes
cannot be silently ignored.
- Around line 181-183: The loop that installs closure siblings (for s in
agent-role.sh escape-grant.sh hooks-doctor.sh) currently treats missing source
files as optional; change it to require all siblings exist and are readable in
$src before proceeding: iterate the same list, test [[ -r "$src/$s" ]] and if
any check fails, print a clear error mentioning the missing $s and exit 1
(refuse deploy); only when all checks pass run the install -m 0755 "$src/$s"
"$CANONICAL_DIR/$s" for each file.
In `@scripts/hapax-post-merge-deploy`:
- Around line 779-783: The post-merge deploy currently swallows failures by
appending "|| echo ..." when invoking hooks-doctor.sh; change that so any
non-zero exit from the canonical gate deploy propagates to the caller instead of
being downgraded to a warning. Specifically, in the block that checks executable
"$GATE_STAGE/hooks/scripts/hooks-doctor.sh" and in the other similar block near
the later occurrence, replace the "bash ... || echo 'warning: canonical gate
deploy failed' >&2" pattern with logic that runs bash, captures its exit status,
prints a meaningful error to stderr, and then exits with that same non-zero
status (or simply let the script exit non-zero), ensuring failures bubble up to
the caller.
In `@systemd/units/hapax-hooks-doctor.service`:
- Line 18: The ExecStart shell currently ends with `exit 0` when no
hooks-doctor.sh is found, which treats missing doctor script as success; update
the hapax-hooks-doctor.service ExecStart command so the final fallback exits
non‑zero (e.g., `exit 1`) to make the unit fail when hooks-doctor.sh cannot be
resolved, keeping the rest of the loop and search order for
"$HOME/.local/lib/hapax/hooks",
"$HOME/.cache/hapax/rebuild/worktree/hooks/scripts", and
"$HOME/projects/hapax-council/hooks/scripts" intact and ensuring systemd marks
the unit failed instead of green.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: add855a9-4028-4281-a77f-1570a2bac33d
📒 Files selected for processing (18)
hooks/scripts/cc-task-gate.impl.shhooks/scripts/cc-task-gate.shhooks/scripts/escape-grant.shhooks/scripts/hooks-doctor.shscripts/check-peer-glob-coherence.pyscripts/hapax-post-merge-deployscripts/install-claude-code.shsystemd/units/hapax-hooks-doctor.servicesystemd/units/hapax-hooks-doctor.timertests/hooks/test_antigrav_gate_enforcement.pytests/hooks/test_authority_case_gate.pytests/hooks/test_cc_task_gate.pytests/hooks/test_cc_task_gate_bootstrap.pytests/hooks/test_cc_task_gate_shim.pytests/hooks/test_hooks_doctor.pytests/hooks/test_sdlc_frictionless_loosening.pytests/scripts/test_codex_crashout_prevention.pytests/test_peer_glob_coherence.py
| set +e | ||
| _bootstrap_output="$( | ||
| printf '%s' "$input" | python3 "$SCRIPT_DIR/cc-task-gate-bootstrap.py" 2>&1 |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
fd -a '^(hapax-post-merge-deploy|hooks-doctor\.sh)$' scripts hooks 2>/dev/null || true
rg -n -C2 'cc-task-gate-bootstrap\.py|cc-task-gate\.impl\.sh|agent-role\.sh|escape-grant\.sh|deploy-canonical|canonical gate closure' scripts hooksRepository: hapax-systems/hapax-council
Length of output: 23252
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the exact bootstrap call + handling logic in cc-task-gate.impl.sh
sed -n '230,310p' hooks/scripts/cc-task-gate.impl.sh
# Show how the shim resolves/executed canonical gate
sed -n '1,120p' hooks/scripts/cc-task-gate.sh
# Show what hooks-doctor.sh deploys into the canonical dir
sed -n '140,220p' hooks/scripts/hooks-doctor.sh
# Confirm whether cc-task-gate-bootstrap.py is present in canonical-closure lists elsewhere
rg -n 'cc-task-gate-bootstrap\.py' hooks/scripts scripts | head -n 50
# Confirm whether hapax-post-merge-deploy’s canonical closure list includes the bootstrap helper
sed -n '730,810p' scripts/hapax-post-merge-deployRepository: hapax-systems/hapax-council
Length of output: 13661
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Locate the bootstrap helper in the repo
fd -a 'cc-task-gate-bootstrap\.py$' . || true
# 2) Check canonical deploy closure sources for bootstrap inclusion
rg -n --no-heading 'cc-task-gate-bootstrap\.py' scripts hooks || true
# 3) Inspect deploy_canonical copy list in hooks-doctor.sh
rg -n --no-heading 'deploy_canonical|for s in agent-role\.sh escape-grant\.sh' hooks/scripts/hooks-doctor.sh
sed -n '150,210p' hooks/scripts/hooks-doctor.sh
# 4) Inspect hapax-post-merge-deploy canonical closure copy list
rg -n --no-heading 'canonical gate closure|GATE_STAGE|cc-task-gate\.impl\.sh|agent-role\.sh|escape-grant\.sh|hooks-doctor\.sh' scripts/hapax-post-merge-deploy
sed -n '740,810p' scripts/hapax-post-merge-deploy
# 5) Confirm cc-task-gate.impl.sh resolves SCRIPT_DIR and runs the helper from there
sed -n '35,80p' hooks/scripts/cc-task-gate.impl.sh
sed -n '235,270p' hooks/scripts/cc-task-gate.impl.sh
# 6) Confirm python3 exit code when the target file is missing
python3 /path/does/not/exist/cc-task-gate-bootstrap.py >/dev/null 2>&1
echo "python_missing_exit_code=$?"Repository: hapax-systems/hapax-council
Length of output: 10678
🏁 Script executed:
#!/bin/bash
set -u
# Capture python3 exit code when bootstrap helper is missing
set +e
python3 "/tmp/cc-task-gate-bootstrap.py" >/dev/null 2>&1
rc=$?
set -e
echo "python3_missing_bootstrap_exit_code=$rc"
# Re-check canonical deploy closure: hapax-post-merge-deploy source list
rg -n --no-heading 'canonical gate closure|GATE_STAGE|cc-task-gate\.impl\.sh|agent-role\.sh|escape-grant\.sh|hooks-doctor\.sh' scripts/hapax-post-merge-deploy
# Re-check hooks-doctor deploy_canonical copy list (what makes it into $HAPAX_CANONICAL_HOOKS)
rg -n --no-heading 'deploy_canonical\(\)|install -m|for s in agent-role\.sh escape-grant\.sh hooks-doctor\.sh|CANONICAL_DIR' hooks/scripts/hooks-doctor.shRepository: hapax-systems/hapax-council
Length of output: 2519
Include cc-task-gate-bootstrap.py in the canonical gate deployment closure.
hooks/scripts/cc-task-gate.impl.sh runs the helper as python3 "$SCRIPT_DIR/cc-task-gate-bootstrap.py"; canonical deploy via hapax-post-merge-deploy + hooks/scripts/hooks-doctor.sh --deploy-canonical installs only the gate impl (as cc-task-gate.sh) plus agent-role.sh, escape-grant.sh, and hooks-doctor.sh—it does not copy cc-task-gate-bootstrap.py. When it’s missing, python3 exits 2, and the gate’s unclaimed bootstrap path exits 2 (fail every bootstrap-write attempt after leaving a repo checkout).
🤖 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.impl.sh` around lines 248 - 250, The canonical
deploy currently omits the helper script used by cc-task-gate.impl.sh, causing
python3 "$SCRIPT_DIR/cc-task-gate-bootstrap.py" to fail; update the canonical
deployment closure used by hapax-post-merge-deploy / hooks-doctor.sh
--deploy-canonical so that cc-task-gate-bootstrap.py is installed alongside
cc-task-gate.sh (and the other gate files), or alter the deploy manifest that
builds the closure to explicitly include cc-task-gate-bootstrap.py; ensure the
installed SCRIPT_DIR contains cc-task-gate-bootstrap.py so the
cc-task-gate.impl.sh bootstrap invocation succeeds.
| # Lease heartbeat (reform Phase 1, cluster 6): refresh the resolved claim file's | ||
| # mtime — this session is demonstrably alive (it is making a gated call against a | ||
| # valid claim). cc-claim treats a claim older than the lease TTL as free, so this | ||
| # keeps a live session's lease from aging out and being reaped while a dead | ||
| # session's lease still expires. Best-effort; never blocks the decision below. | ||
| touch "$claim_file" 2>/dev/null || true |
There was a problem hiding this comment.
Don't renew the lease until the claim is proven valid.
Lines 371-376 refresh the claim file before the task note is found and before assigned_to / status are checked. A stale, reassigned, or terminal claim can therefore stay alive indefinitely just by triggering blocked mutations, which defeats the lease-expiry cleanup this heartbeat is meant to support.
🤖 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.impl.sh` around lines 371 - 376, The lease
heartbeat currently touches "$claim_file" before the task note is located and
before validating assigned_to and status, allowing stale or reassigned claims to
be kept alive; move the touch call so it only runs after the code has found the
task note and confirmed the claim's assigned_to matches and status is
non-terminal, and keep it best-effort (2>/dev/null || true) so it doesn't block
decision logic—i.e., perform the validations (assigned_to/status) first, then
call touch "$claim_file" to refresh the lease only for a proven-valid claim.
| mkdir -p "$CANONICAL_DIR" | ||
| # impl deploys AS cc-task-gate.sh (the name shims + settings.json resolve to). | ||
| install -m 0755 "$src/cc-task-gate.impl.sh" "$CANONICAL_DIR/cc-task-gate.sh" |
There was a problem hiding this comment.
Fail deployment/fanout on write errors instead of reporting success.
Line 177, Line 179, Line 188, and Line 210 perform state-changing writes without checking success. Since the script is not running with set -e, failures can be silently ignored while printing successful deploy/shim messages.
Suggested hard-fail guards for mutating writes
- mkdir -p "$CANONICAL_DIR"
+ mkdir -p "$CANONICAL_DIR" || { echo "deploy: failed to create $CANONICAL_DIR" >&2; return 1; }
# impl deploys AS cc-task-gate.sh (the name shims + settings.json resolve to).
- install -m 0755 "$src/cc-task-gate.impl.sh" "$CANONICAL_DIR/cc-task-gate.sh"
+ install -m 0755 "$src/cc-task-gate.impl.sh" "$CANONICAL_DIR/cc-task-gate.sh" \
+ || { echo "deploy: failed to install canonical gate impl" >&2; return 1; }
@@
- ln -sf "$CANONICAL_DIR/hooks-doctor.sh" "$bindir/hapax-hooks-doctor"
+ ln -sf "$CANONICAL_DIR/hooks-doctor.sh" "$bindir/hapax-hooks-doctor" \
+ || { echo "deploy: failed to create hapax-hooks-doctor symlink" >&2; return 1; }
@@
- install -m 0755 "$shim_src" "$gate"
+ install -m 0755 "$shim_src" "$gate" || { echo "fanout: failed to write $gate" >&2; return 1; }
echo "shimmed $gate"
n=$((n + 1))Also applies to: 188-189, 210-212
🤖 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/hooks-doctor.sh` around lines 177 - 179, The script currently
performs mutating operations (e.g., mkdir -p "$CANONICAL_DIR" and install -m
0755 "$src/cc-task-gate.impl.sh" "$CANONICAL_DIR/cc-task-gate.sh") and other
state-changing writes at the other noted locations (lines around 188-189 and
210-212) without checking their exit status; update each mutating command so
failures are detected and cause the script to fail the deployment/fanout instead
of printing success—for each command (mkdir, install and the other write/replace
operations referenced) check the command's exit code and on non-zero log an
error and exit non-zero (or use a strict error handler like set -e preceded by
explicit error messages) so writes cannot be silently ignored.
| for s in agent-role.sh escape-grant.sh hooks-doctor.sh; do | ||
| [[ -r "$src/$s" ]] && install -m 0755 "$src/$s" "$CANONICAL_DIR/$s" | ||
| done |
There was a problem hiding this comment.
Refuse deploy if any gate-closure sibling is missing in source.
Line 181 currently treats closure siblings as optional. That can deploy a partial or mixed-version canonical closure when source inputs are incomplete.
Require full source closure before deploy
- local s
- for s in agent-role.sh escape-grant.sh hooks-doctor.sh; do
- [[ -r "$src/$s" ]] && install -m 0755 "$src/$s" "$CANONICAL_DIR/$s"
- done
+ local s
+ for s in agent-role.sh escape-grant.sh hooks-doctor.sh; do
+ [[ -r "$src/$s" ]] || { echo "deploy: source closure missing $src/$s" >&2; return 1; }
+ install -m 0755 "$src/$s" "$CANONICAL_DIR/$s" || { echo "deploy: failed to install $s" >&2; return 1; }
+ done📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| for s in agent-role.sh escape-grant.sh hooks-doctor.sh; do | |
| [[ -r "$src/$s" ]] && install -m 0755 "$src/$s" "$CANONICAL_DIR/$s" | |
| done | |
| for s in agent-role.sh escape-grant.sh hooks-doctor.sh; do | |
| [[ -r "$src/$s" ]] || { echo "deploy: source closure missing $src/$s" >&2; return 1; } | |
| install -m 0755 "$src/$s" "$CANONICAL_DIR/$s" || { echo "deploy: failed to install $s" >&2; return 1; } | |
| done |
🤖 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/hooks-doctor.sh` around lines 181 - 183, The loop that installs
closure siblings (for s in agent-role.sh escape-grant.sh hooks-doctor.sh)
currently treats missing source files as optional; change it to require all
siblings exist and are readable in $src before proceeding: iterate the same
list, test [[ -r "$src/$s" ]] and if any check fails, print a clear error
mentioning the missing $s and exit 1 (refuse deploy); only when all checks pass
run the install -m 0755 "$src/$s" "$CANONICAL_DIR/$s" for each file.
| if [ -x "$GATE_STAGE/hooks/scripts/hooks-doctor.sh" ]; then | ||
| bash "$GATE_STAGE/hooks/scripts/hooks-doctor.sh" --deploy-canonical --from "$GATE_STAGE" \ | ||
| || echo "warning: canonical gate deploy failed" >&2 | ||
| else | ||
| echo "warning: hooks-doctor.sh absent at $SHA; skipping canonical gate deploy" >&2 |
There was a problem hiding this comment.
Do not swallow canonical gate deploy failure in post-merge path.
Line 780-783 converts deploy errors into warnings and continues. That allows a “successful” post-merge deploy with stale canonical gate state.
Propagate deployment failure to caller
- if [ -x "$GATE_STAGE/hooks/scripts/hooks-doctor.sh" ]; then
- bash "$GATE_STAGE/hooks/scripts/hooks-doctor.sh" --deploy-canonical --from "$GATE_STAGE" \
- || echo "warning: canonical gate deploy failed" >&2
- else
- echo "warning: hooks-doctor.sh absent at $SHA; skipping canonical gate deploy" >&2
- fi
+ if [ -x "$GATE_STAGE/hooks/scripts/hooks-doctor.sh" ]; then
+ bash "$GATE_STAGE/hooks/scripts/hooks-doctor.sh" --deploy-canonical --from "$GATE_STAGE"
+ else
+ echo "canonical gate deploy unavailable: hooks-doctor.sh absent at $SHA" >&2
+ rm -rf "$GATE_STAGE" 2>/dev/null || true
+ exit 1
+ fi
@@
- DID_ANYTHING=1
+ DID_ANYTHING=1Also applies to: 786-786
🤖 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/hapax-post-merge-deploy` around lines 779 - 783, The post-merge
deploy currently swallows failures by appending "|| echo ..." when invoking
hooks-doctor.sh; change that so any non-zero exit from the canonical gate deploy
propagates to the caller instead of being downgraded to a warning. Specifically,
in the block that checks executable "$GATE_STAGE/hooks/scripts/hooks-doctor.sh"
and in the other similar block near the later occurrence, replace the "bash ...
|| echo 'warning: canonical gate deploy failed' >&2" pattern with logic that
runs bash, captures its exit status, prints a meaningful error to stderr, and
then exits with that same non-zero status (or simply let the script exit
non-zero), ensuring failures bubble up to the caller.
| # --session always exits 0 (never fail-stuck); --notify ntfy's when drift is | ||
| # present, catching a rebuild that clobbers the canonical gate (NEW-CATCH-3) or a | ||
| # lane that re-drifts between SessionStart/CI checks. | ||
| ExecStart=/bin/bash -c 'r="$HOME/projects/hapax-council"; for d in "$HOME/.local/lib/hapax/hooks" "$HOME/.cache/hapax/rebuild/worktree/hooks/scripts" "$r/hooks/scripts"; do if [ -x "$d/hooks-doctor.sh" ]; then exec bash "$d/hooks-doctor.sh" --session --notify --root "$r"; fi; done; echo "hooks-doctor.sh not found in any known location" >&2; exit 0' |
There was a problem hiding this comment.
Do not treat “hooks-doctor not found” as success.
Line 18 ends with exit 0 when no script is found, which silently disables drift sweeps while leaving the unit green.
Fail the unit when doctor script cannot be resolved
-ExecStart=/bin/bash -c 'r="$HOME/projects/hapax-council"; for d in "$HOME/.local/lib/hapax/hooks" "$HOME/.cache/hapax/rebuild/worktree/hooks/scripts" "$r/hooks/scripts"; do if [ -x "$d/hooks-doctor.sh" ]; then exec bash "$d/hooks-doctor.sh" --session --notify --root "$r"; fi; done; echo "hooks-doctor.sh not found in any known location" >&2; exit 0'
+ExecStart=/bin/bash -c 'r="$HOME/projects/hapax-council"; for d in "$HOME/.local/lib/hapax/hooks" "$HOME/.cache/hapax/rebuild/worktree/hooks/scripts" "$r/hooks/scripts"; do if [ -x "$d/hooks-doctor.sh" ]; then exec bash "$d/hooks-doctor.sh" --session --notify --root "$r"; fi; done; echo "hooks-doctor.sh not found in any known location" >&2; exit 1'📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ExecStart=/bin/bash -c 'r="$HOME/projects/hapax-council"; for d in "$HOME/.local/lib/hapax/hooks" "$HOME/.cache/hapax/rebuild/worktree/hooks/scripts" "$r/hooks/scripts"; do if [ -x "$d/hooks-doctor.sh" ]; then exec bash "$d/hooks-doctor.sh" --session --notify --root "$r"; fi; done; echo "hooks-doctor.sh not found in any known location" >&2; exit 0' | |
| ExecStart=/bin/bash -c 'r="$HOME/projects/hapax-council"; for d in "$HOME/.local/lib/hapax/hooks" "$HOME/.cache/hapax/rebuild/worktree/hooks/scripts" "$r/hooks/scripts"; do if [ -x "$d/hooks-doctor.sh" ]; then exec bash "$d/hooks-doctor.sh" --session --notify --root "$r"; fi; done; echo "hooks-doctor.sh not found in any known location" >&2; exit 1' |
🤖 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 `@systemd/units/hapax-hooks-doctor.service` at line 18, The ExecStart shell
currently ends with `exit 0` when no hooks-doctor.sh is found, which treats
missing doctor script as success; update the hapax-hooks-doctor.service
ExecStart command so the final fallback exits non‑zero (e.g., `exit 1`) to make
the unit fail when hooks-doctor.sh cannot be resolved, keeping the rest of the
loop and search order for "$HOME/.local/lib/hapax/hooks",
"$HOME/.cache/hapax/rebuild/worktree/hooks/scripts", and
"$HOME/projects/hapax-council/hooks/scripts" intact and ensuring systemd marks
the unit failed instead of green.
Reform FM-6 — the gate-fanout collapse
cc-task-gate.shwas physically copied into ~26 worktrees, so the fleet ran six gate versions (427/651/779/786/832/905 lines) and the AGENT-RUNNING lanes carried the oldest 427-line gate, which lacksis_cognition_path→ INV-5 (cognition-always-writable) was silently violated on 9 lanes. The master design promised "hooks → stable abs-path shims" + ahooks-doctordrift detector; neither existed. This builds them.What changed
cc-task-gate.sh→ thin stable-abs-path SHIM. The gate logic moves tocc-task-gate.impl.sh. The shim resolves ONE canonical impl at$HAPAX_CANONICAL_HOOKS(default~/.local/lib/hapax/hooks), canonical-first (so the LIVE hook escapes rebuild-worktree drift), with a co-located-impl fallback (hermetic for fresh checkouts/CI) and a fail-open-with-ledger floor — never fail-stuck, honoring INV-5. An-efself-guard prevents a misconfigured-canonical exec loop. "Update the gate" is now a one-file change, not 26.hooks-doctor.sh— the drift detector:--classify/--check(CI refusal) /--deploy-canonical(refuses any impl missing the INV-5 carve-out) /--fanout(re-shims stale lanes). Wired as SessionStart + CI (pytest) + a 6h timer sweep.escape-grant.sh— repo-root resolution gains an explicit override + known-checkout fallback (gated onshared/presence) so the canonical-deployed gate keeps honoring escape grants (INV-4) from~/.local/lib/hapax/hooks. In-repo behavior unchanged.hapax-post-merge-deploy— redeploys the canonical gate closure (materialized from the merge SHA) whenever the impl or its sourced siblings change.hapax-hooks-doctor.{service,timer}— 6h advisory fleet drift sweep (catches a rebuild clobbering canonical, NEW-CATCH-3).cc-task-gate.impl.sh; the peer-glob coherence lint follows the vault globs to the impl;install-claude-code.shregisters the SessionStart hook.Acceptance criteria (delivered by this PR)
hooks-doctordetects + refuses on gate drift (SessionStart + CI + timer)./dev/shmcognition writes, blocks out-of-scope writes).Post-merge runtime (this lane, immediately after merge):
hooks-doctor --deploy-canonical+--fanoutto re-shim the live fleet (fixes INV-5 on the 9 stale lanes now), wire the SessionStart drift hook, thenhooks-doctor --checkto witness a clean fleet, andcc-close.Test evidence
tests/hooks/test_cc_task_gate_shim.py(8) +tests/hooks/test_hooks_doctor.py(10) — shim resolution/precedence/stdin+exit passthrough/fail-open/self-loop; doctor classify/check-refusal/deploy/fanout. All pass.ruff check/ruff format --check: clean. systemd units verify clean.Note (pre-existing, unrelated)
tests/hooks/test_codex_hook_adapter.py::test_session_start_returns_codex_additional_contextcan time out on its hard 15s ceiling under load —session-context.sh(unchanged here) takes ~8s alone and the full adapter path completes in ~14s (rc=0, not a hang). Not in this changeset.Task:
reform-improve-gate-shim-collapse-20260601· AuthorityCase:CASE-CROSS-RUNTIME-COMMS-001🤖 Generated with Claude Code