-
Notifications
You must be signed in to change notification settings - Fork 0
fix(sdlc): collapse 6-way gate fanout to a stable abs-path shim + hooks-doctor (INV-5) #3832
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,274 @@ | ||||||||||||||||
| #!/usr/bin/env bash | ||||||||||||||||
| # hooks-doctor.sh — gate-drift detector + canonical deployer (reform FM-6). | ||||||||||||||||
| # | ||||||||||||||||
| # The cc-task-gate used to be physically copied into every worktree, so the fleet | ||||||||||||||||
| # carried SIX gate versions (427/651/779/786/832/905 lines) and the oldest lanes | ||||||||||||||||
| # silently violated INV-5 (cognition-always-writable) because their 427-line gate | ||||||||||||||||
| # predates is_cognition_path. The fix is one canonical impl + thin shims; this | ||||||||||||||||
| # tool is the drift detector the design promised, plus the deployer/fanout that | ||||||||||||||||
| # makes "update the gate" a one-file change. | ||||||||||||||||
| # | ||||||||||||||||
| # Modes: | ||||||||||||||||
| # --session (default) advisory fleet report; ALWAYS exit 0 (never | ||||||||||||||||
| # wedges a SessionStart); prints CRITICAL/WARN drift lines. | ||||||||||||||||
| # --check | --strict strict: exit 1 on CRITICAL drift (CI + manual). The | ||||||||||||||||
| # committed cc-task-gate.sh MUST be a shim and the impl | ||||||||||||||||
| # MUST carry is_cognition_path, else CI refuses. | ||||||||||||||||
| # --deploy-canonical copy the gate closure (impl + agent-role + escape-grant + | ||||||||||||||||
| # this doctor) to $HAPAX_CANONICAL_HOOKS and write a | ||||||||||||||||
| # sha256 MANIFEST; symlink ~/.local/bin/hapax-hooks-doctor. | ||||||||||||||||
| # --fanout rewrite every lane worktree's cc-task-gate.sh to the | ||||||||||||||||
| # canonical shim (the one-shot that fixes INV-5 on stale | ||||||||||||||||
| # lanes immediately, without waiting for them to rebase). | ||||||||||||||||
| # --classify FILE print a single gate file's classification; exit nonzero | ||||||||||||||||
| # if it is drifted (used by the test-suite + ad-hoc). | ||||||||||||||||
| # | ||||||||||||||||
| # Options: --from DIR (deploy source, default this repo), --root DIR (override the | ||||||||||||||||
| # repo/worktree root for --check/fleet, for tests), --dry-run, --verbose|-v, | ||||||||||||||||
| # --notify (best-effort ntfy on drift, for the timer/service). | ||||||||||||||||
| set -uo pipefail | ||||||||||||||||
|
|
||||||||||||||||
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||||||||||||||||
| CANONICAL_DIR="${HAPAX_CANONICAL_HOOKS:-$HOME/.local/lib/hapax/hooks}" | ||||||||||||||||
| SHIM_MARKER="HAPAX-GATE-SHIM" | ||||||||||||||||
| COGNITION_MARKER="is_cognition_path()" | ||||||||||||||||
|
|
||||||||||||||||
| MODE=session | ||||||||||||||||
| FROM="" | ||||||||||||||||
| ROOT_OVERRIDE="" | ||||||||||||||||
| DRY=0 | ||||||||||||||||
| VERBOSE=0 | ||||||||||||||||
| NOTIFY=0 | ||||||||||||||||
| CLASSIFY_FILE="" | ||||||||||||||||
|
|
||||||||||||||||
| while [[ $# -gt 0 ]]; do | ||||||||||||||||
| case "$1" in | ||||||||||||||||
| --session) MODE=session ;; | ||||||||||||||||
| --check|--strict) MODE=check ;; | ||||||||||||||||
| --deploy-canonical|--deploy) MODE=deploy ;; | ||||||||||||||||
| --fanout) MODE=fanout ;; | ||||||||||||||||
| --classify) MODE=classify; CLASSIFY_FILE="${2:?--classify needs a FILE}"; shift ;; | ||||||||||||||||
| --from) FROM="${2:?--from needs a DIR}"; shift ;; | ||||||||||||||||
| --root) ROOT_OVERRIDE="${2:?--root needs a DIR}"; shift ;; | ||||||||||||||||
| --dry-run) DRY=1 ;; | ||||||||||||||||
| --verbose|-v) VERBOSE=1 ;; | ||||||||||||||||
| --notify) NOTIFY=1 ;; | ||||||||||||||||
| -h|--help) grep -E '^#( |$)' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;; | ||||||||||||||||
| *) echo "hooks-doctor: unknown argument: $1" >&2; exit 64 ;; | ||||||||||||||||
| esac | ||||||||||||||||
| shift | ||||||||||||||||
| done | ||||||||||||||||
|
|
||||||||||||||||
| # Repo/worktree root: --root wins (tests), else this file's repo, else its dir. | ||||||||||||||||
| if [[ -n "$ROOT_OVERRIDE" ]]; then | ||||||||||||||||
| REPO_ROOT="$ROOT_OVERRIDE" | ||||||||||||||||
| else | ||||||||||||||||
| REPO_ROOT="$(cd "$SCRIPT_DIR/../.." 2>/dev/null && pwd || echo "$SCRIPT_DIR")" | ||||||||||||||||
| fi | ||||||||||||||||
|
|
||||||||||||||||
| # classify_gate <gate-file> — echo a label; return 0 shim, 2 warn-drift, | ||||||||||||||||
| # 3 critical-drift, 4 missing. A "shim" carries the HAPAX-GATE-SHIM marker and | ||||||||||||||||
| # resolves to canonical (so it inherits INV-5). A non-shim full gate copy is | ||||||||||||||||
| # drift; it is CRITICAL when it also lacks is_cognition_path (the stale-lane | ||||||||||||||||
| # INV-5 violation), else a WARN (drifted but carve-out present). | ||||||||||||||||
| classify_gate() { | ||||||||||||||||
| local f="$1" | ||||||||||||||||
| if [[ ! -e "$f" ]]; then echo "missing"; return 4; fi | ||||||||||||||||
| if grep -q "$SHIM_MARKER" "$f" 2>/dev/null; then echo "shim"; return 0; fi | ||||||||||||||||
| if grep -q "$COGNITION_MARKER" "$f" 2>/dev/null; then echo "drift-warn"; return 2; fi | ||||||||||||||||
| echo "drift-critical"; return 3 | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| # check_canonical — the deployed impl all shims resolve to must exist, be the impl | ||||||||||||||||
| # (not a shim), and carry INV-5. Echoes a status line; return 0 ok, 3 critical. | ||||||||||||||||
| check_canonical() { | ||||||||||||||||
| local c="$CANONICAL_DIR/cc-task-gate.sh" s | ||||||||||||||||
| if [[ ! -r "$c" ]]; then echo "CRITICAL canonical impl missing: $c"; return 3; fi | ||||||||||||||||
| if grep -q "$SHIM_MARKER" "$c" 2>/dev/null; then echo "CRITICAL canonical is a shim, not the impl: $c"; return 3; fi | ||||||||||||||||
| if ! grep -q "$COGNITION_MARKER" "$c" 2>/dev/null; then echo "CRITICAL canonical impl lacks INV-5 is_cognition_path: $c"; return 3; fi | ||||||||||||||||
| for s in agent-role.sh escape-grant.sh; do | ||||||||||||||||
| [[ -r "$CANONICAL_DIR/$s" ]] || { echo "CRITICAL canonical closure missing sibling: $CANONICAL_DIR/$s"; return 3; } | ||||||||||||||||
| done | ||||||||||||||||
| echo "ok canonical healthy: $c"; return 0 | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| # check_repo_self — the committed gate in REPO_ROOT must be a shim, and the impl | ||||||||||||||||
| # must exist with INV-5. This is the CI regression guard: nobody commits a full | ||||||||||||||||
| # gate copy back into cc-task-gate.sh. Echoes findings; return 0 ok, 3 critical. | ||||||||||||||||
| check_repo_self() { | ||||||||||||||||
| local rc=0 | ||||||||||||||||
| local shim="$REPO_ROOT/hooks/scripts/cc-task-gate.sh" | ||||||||||||||||
| local impl="$REPO_ROOT/hooks/scripts/cc-task-gate.impl.sh" | ||||||||||||||||
| if [[ ! -e "$shim" ]]; then | ||||||||||||||||
| echo "CRITICAL $shim missing"; rc=3 | ||||||||||||||||
| elif ! grep -q "$SHIM_MARKER" "$shim" 2>/dev/null; then | ||||||||||||||||
| echo "CRITICAL $shim is NOT a shim (regressed to a physical gate copy)"; rc=3 | ||||||||||||||||
| fi | ||||||||||||||||
| if [[ ! -r "$impl" ]]; then | ||||||||||||||||
| echo "CRITICAL $impl missing"; rc=3 | ||||||||||||||||
| elif ! grep -q "$COGNITION_MARKER" "$impl" 2>/dev/null; then | ||||||||||||||||
| echo "CRITICAL $impl lacks INV-5 is_cognition_path"; rc=3 | ||||||||||||||||
| fi | ||||||||||||||||
| return "$rc" | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| # list_lane_worktrees — lane worktrees only (basename hapax-council[--*]); the | ||||||||||||||||
| # SHA-named source-activation release snapshots and the rebuild/worktree (which | ||||||||||||||||
| # self-heals on rebuild) are intentionally excluded. | ||||||||||||||||
| list_lane_worktrees() { | ||||||||||||||||
| git -C "$REPO_ROOT" worktree list --porcelain 2>/dev/null \ | ||||||||||||||||
| | awk '/^worktree /{sub(/^worktree /,""); print}' \ | ||||||||||||||||
| | while IFS= read -r wt; do | ||||||||||||||||
| case "$(basename "$wt")" in | ||||||||||||||||
| hapax-council|hapax-council--*) printf '%s\n' "$wt" ;; | ||||||||||||||||
| esac | ||||||||||||||||
| done | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| # check_fleet — classify each lane worktree's gate. Echoes WARN/CRITICAL lines | ||||||||||||||||
| # (and ok lines when --verbose). Return 0 clean, 2 warn-only, 3 any-critical. | ||||||||||||||||
| check_fleet() { | ||||||||||||||||
| local rc=0 wt gate label crc | ||||||||||||||||
| while IFS= read -r wt; do | ||||||||||||||||
| [[ -n "$wt" ]] || continue | ||||||||||||||||
| gate="$wt/hooks/scripts/cc-task-gate.sh" | ||||||||||||||||
| [[ -e "$gate" ]] || continue | ||||||||||||||||
| label="$(classify_gate "$gate")"; crc=$? | ||||||||||||||||
| case "$crc" in | ||||||||||||||||
| 0) [[ "$VERBOSE" = 1 ]] && echo "ok $wt [$label]" ;; | ||||||||||||||||
| 2) echo "WARN $wt [$label] — drifted full gate (INV-5 carve-out present)"; (( rc < 2 )) && rc=2 ;; | ||||||||||||||||
| 3) echo "CRITICAL $wt [$label] — stale full gate WITHOUT INV-5 carve-out"; rc=3 ;; | ||||||||||||||||
| 4) [[ "$VERBOSE" = 1 ]] && echo "ok $wt [no gate]" ;; | ||||||||||||||||
| esac | ||||||||||||||||
| done < <(list_lane_worktrees) | ||||||||||||||||
| return "$rc" | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| _notify() { | ||||||||||||||||
| [[ "$NOTIFY" = 1 ]] || return 0 | ||||||||||||||||
| local msg="$1" | ||||||||||||||||
| if command -v hapax-notify >/dev/null 2>&1; then | ||||||||||||||||
| hapax-notify "hooks-doctor" "$msg" >/dev/null 2>&1 && return 0 | ||||||||||||||||
| fi | ||||||||||||||||
| ( cd "$REPO_ROOT" 2>/dev/null \ | ||||||||||||||||
| && python3 -c 'import sys | ||||||||||||||||
| try: | ||||||||||||||||
| from shared.notify import send_notification | ||||||||||||||||
| send_notification("hooks-doctor: gate drift", sys.argv[1], priority="high", tags=["warning"]) | ||||||||||||||||
| except Exception: | ||||||||||||||||
| pass' "$msg" ) >/dev/null 2>&1 || true | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| deploy_canonical() { | ||||||||||||||||
| local from="${FROM:-$REPO_ROOT}" src | ||||||||||||||||
| src="$from/hooks/scripts" | ||||||||||||||||
| if [[ ! -r "$src/cc-task-gate.impl.sh" ]]; then | ||||||||||||||||
| echo "deploy: source impl missing: $src/cc-task-gate.impl.sh" >&2 | ||||||||||||||||
| return 1 | ||||||||||||||||
| fi | ||||||||||||||||
| if ! grep -q "$COGNITION_MARKER" "$src/cc-task-gate.impl.sh" 2>/dev/null; then | ||||||||||||||||
| echo "deploy: REFUSING to deploy an impl that lacks INV-5 is_cognition_path: $src/cc-task-gate.impl.sh" >&2 | ||||||||||||||||
| return 1 | ||||||||||||||||
| fi | ||||||||||||||||
| if [[ "$DRY" = 1 ]]; then | ||||||||||||||||
| echo "[dry-run] would deploy gate closure: $src -> $CANONICAL_DIR" | ||||||||||||||||
| return 0 | ||||||||||||||||
| fi | ||||||||||||||||
| 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" | ||||||||||||||||
|
Comment on lines
+177
to
+179
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 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 |
||||||||||||||||
| 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 | ||||||||||||||||
|
Comment on lines
+181
to
+183
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the canonical gate is deployed, this closure only installs the impl and three shell siblings, but Useful? React with 👍 / 👎.
Comment on lines
+181
to
+183
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||
| ( cd "$CANONICAL_DIR" && sha256sum cc-task-gate.sh agent-role.sh escape-grant.sh hooks-doctor.sh 2>/dev/null ) \ | ||||||||||||||||
| > "$CANONICAL_DIR/MANIFEST.sha256" 2>/dev/null || true | ||||||||||||||||
| local bindir="${HAPAX_LOCAL_BIN:-$HOME/.local/bin}" | ||||||||||||||||
| mkdir -p "$bindir" | ||||||||||||||||
| ln -sf "$CANONICAL_DIR/hooks-doctor.sh" "$bindir/hapax-hooks-doctor" | ||||||||||||||||
| echo "deployed gate closure -> $CANONICAL_DIR (from $src)" | ||||||||||||||||
| check_canonical | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| fanout() { | ||||||||||||||||
| local shim_src="$REPO_ROOT/hooks/scripts/cc-task-gate.sh" wt gate n=0 | ||||||||||||||||
| if ! grep -q "$SHIM_MARKER" "$shim_src" 2>/dev/null; then | ||||||||||||||||
| echo "fanout: $shim_src is not the shim; aborting" >&2 | ||||||||||||||||
| return 1 | ||||||||||||||||
| fi | ||||||||||||||||
| while IFS= read -r wt; do | ||||||||||||||||
| [[ -n "$wt" ]] || continue | ||||||||||||||||
| gate="$wt/hooks/scripts/cc-task-gate.sh" | ||||||||||||||||
| [[ -d "$wt/hooks/scripts" ]] || continue | ||||||||||||||||
| if grep -q "$SHIM_MARKER" "$gate" 2>/dev/null; then | ||||||||||||||||
| [[ "$VERBOSE" = 1 ]] && echo "skip $gate (already shim)" | ||||||||||||||||
| continue | ||||||||||||||||
| fi | ||||||||||||||||
| if [[ "$DRY" = 1 ]]; then | ||||||||||||||||
| echo "[dry-run] would shim $gate"; n=$((n + 1)); continue | ||||||||||||||||
| fi | ||||||||||||||||
| install -m 0755 "$shim_src" "$gate" | ||||||||||||||||
| echo "shimmed $gate" | ||||||||||||||||
| n=$((n + 1)) | ||||||||||||||||
| done < <(list_lane_worktrees) | ||||||||||||||||
| echo "fanout: $n gate(s) updated$([[ "$DRY" = 1 ]] && echo ' (dry-run)')" | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| case "$MODE" in | ||||||||||||||||
| classify) | ||||||||||||||||
| classify_gate "$CLASSIFY_FILE" | ||||||||||||||||
| exit $? | ||||||||||||||||
| ;; | ||||||||||||||||
| deploy) | ||||||||||||||||
| deploy_canonical | ||||||||||||||||
| exit $? | ||||||||||||||||
| ;; | ||||||||||||||||
| fanout) | ||||||||||||||||
| fanout | ||||||||||||||||
| exit $? | ||||||||||||||||
| ;; | ||||||||||||||||
| check) | ||||||||||||||||
| rc=0 | ||||||||||||||||
| if out="$(check_repo_self)"; then :; else rc=3; fi | ||||||||||||||||
| [[ -n "${out:-}" ]] && echo "$out" | ||||||||||||||||
| if [[ -e "$CANONICAL_DIR/cc-task-gate.sh" ]]; then | ||||||||||||||||
| if cout="$(check_canonical)"; then :; else rc=3; fi | ||||||||||||||||
| echo "$cout" | ||||||||||||||||
| else | ||||||||||||||||
| echo "note: no deployed canonical at $CANONICAL_DIR (ok in CI / pre-deploy)" | ||||||||||||||||
| fi | ||||||||||||||||
| if git -C "$REPO_ROOT" rev-parse --git-dir >/dev/null 2>&1; then | ||||||||||||||||
| fout="$(check_fleet)"; fc=$? | ||||||||||||||||
| [[ -n "$fout" ]] && echo "$fout" | ||||||||||||||||
| (( fc >= 3 )) && rc=3 | ||||||||||||||||
| fi | ||||||||||||||||
| if (( rc >= 3 )); then | ||||||||||||||||
| echo "hooks-doctor: CRITICAL gate drift — REFUSE" | ||||||||||||||||
| _notify "CRITICAL gate drift detected" | ||||||||||||||||
| exit 1 | ||||||||||||||||
| fi | ||||||||||||||||
| echo "hooks-doctor: gate fleet clean (no critical drift)" | ||||||||||||||||
| exit 0 | ||||||||||||||||
| ;; | ||||||||||||||||
| session) | ||||||||||||||||
| # Advisory: never wedge a session. Print canonical + fleet drift, exit 0. | ||||||||||||||||
| crit=0 | ||||||||||||||||
| if [[ -e "$CANONICAL_DIR/cc-task-gate.sh" ]]; then | ||||||||||||||||
| cout="$(check_canonical)" || crit=1 | ||||||||||||||||
| [[ "$VERBOSE" = 1 || "$crit" = 1 ]] && echo "$cout" | ||||||||||||||||
| else | ||||||||||||||||
| echo "hooks-doctor: no deployed canonical gate at $CANONICAL_DIR — run 'hapax-hooks-doctor --deploy-canonical'" | ||||||||||||||||
| crit=1 | ||||||||||||||||
| fi | ||||||||||||||||
| if git -C "$REPO_ROOT" rev-parse --git-dir >/dev/null 2>&1; then | ||||||||||||||||
| fout="$(check_fleet)"; fc=$? | ||||||||||||||||
| [[ -n "$fout" ]] && echo "$fout" | ||||||||||||||||
| (( fc >= 3 )) && crit=1 | ||||||||||||||||
| fi | ||||||||||||||||
| if (( crit == 1 )); then | ||||||||||||||||
| echo "hooks-doctor: gate drift present (advisory) — run 'hapax-hooks-doctor --check' / '--fanout' / '--deploy-canonical'" | ||||||||||||||||
| _notify "gate drift present (advisory)" | ||||||||||||||||
| fi | ||||||||||||||||
| exit 0 | ||||||||||||||||
| ;; | ||||||||||||||||
| esac | ||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -40,6 +40,14 @@ | |
| # systemd/logrotate.d/* → logrotate configs; require sudo install | ||
| # systemd/system/** → system-scoped configs; require sudo install | ||
| # scripts/hapax-* → ensure ~/.local/bin/ symlink | ||
| # hooks/scripts/{cc-task-gate.impl.sh,agent-role.sh,escape-grant.sh, | ||
| # hooks-doctor.sh,cc-task-gate.sh} | ||
| # → redeploy the ONE canonical gate closure to | ||
| # ~/.local/lib/hapax/hooks via hooks-doctor | ||
| # --deploy-canonical (reform FM-6: every | ||
| # worktree's cc-task-gate.sh is a shim that | ||
| # resolves here, so "update the gate" is a | ||
| # one-file change, not a 26-worktree fan-out) | ||
| # | ||
| # Usage: | ||
| # hapax-post-merge-deploy <commit-sha> | ||
|
|
@@ -315,6 +323,7 @@ else | |
| fi | ||
|
|
||
| PW=(); HAPAX_CONF=(); WP=(); WP_LUA=(); SYSTEMD=(); SYSTEMD_SYSTEM=(); SYSTEMD_DROPINS=(); USER_PRESETS=(); WATCHDOGS=(); SCRIPTS=(); SCRIPT_PY=() | ||
| GATE_CLOSURE=() | ||
| SYSTEMD_UNCLASSIFIED=() | ||
| AUDIO_SAFE_RESTARTS=() | ||
| DID_ANYTHING=0 | ||
|
|
@@ -444,6 +453,8 @@ while IFS= read -r f; do | |
| systemd/udev/*.rules) ;; # udev rules; require sudo install + udevadm reload (operator action) | ||
| systemd/logrotate.d/*) ;; # logrotate configs; require sudo install | ||
| systemd/system/*) ;; # system-scoped configs; require sudo install | ||
| hooks/scripts/cc-task-gate.impl.sh|hooks/scripts/agent-role.sh|hooks/scripts/escape-grant.sh|hooks/scripts/hooks-doctor.sh|hooks/scripts/cc-task-gate.sh) | ||
| GATE_CLOSURE+=("$f") ;; | ||
| scripts/hapax-*) SCRIPTS+=("$f") ;; | ||
| scripts/*.py|scripts/*.sh) SCRIPT_PY+=("$f") ;; | ||
| systemd/expected-timers.yaml) ;; # frozen-baseline doc, not deployable | ||
|
|
@@ -746,6 +757,35 @@ if [ "${#SCRIPT_PY[@]}" -gt 0 ]; then | |
| done | ||
| fi | ||
|
|
||
| # --- canonical gate closure (reform FM-6 shim collapse) --- | ||
| # When the gate impl or its sourced siblings change, redeploy the ONE canonical | ||
| # copy that every worktree's cc-task-gate.sh shim resolves to. Materialize the | ||
| # closure from $SHA (commit-time content, decoupled from the working tree like | ||
| # every other deploy here), then hand it to hooks-doctor --deploy-canonical, which | ||
| # renames the impl to cc-task-gate.sh, writes a sha256 MANIFEST, symlinks | ||
| # ~/.local/bin/hapax-hooks-doctor, and REFUSES any impl missing the INV-5 | ||
| # is_cognition_path carve-out (so a bad gate never lands fleet-wide). | ||
| if [ "${#GATE_CLOSURE[@]}" -gt 0 ]; then | ||
| echo "gate closure changed (${#GATE_CLOSURE[@]}): redeploying canonical gate" | ||
| GATE_STAGE="$(mktemp -d)" | ||
| mkdir -p "$GATE_STAGE/hooks/scripts" | ||
| for f in hooks/scripts/cc-task-gate.impl.sh hooks/scripts/agent-role.sh \ | ||
| hooks/scripts/escape-grant.sh hooks/scripts/hooks-doctor.sh; do | ||
| if git cat-file -e "$SHA:$f" 2>/dev/null; then | ||
| git show "$SHA:$f" > "$GATE_STAGE/$f" | ||
| chmod +x "$GATE_STAGE/$f" 2>/dev/null || true | ||
| 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. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For the merge that converts Useful? React with 👍 / 👎. |
||
| || echo "warning: canonical gate deploy failed" >&2 | ||
|
Comment on lines
+780
to
+781
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If Useful? React with 👍 / 👎. |
||
| else | ||
| echo "warning: hooks-doctor.sh absent at $SHA; skipping canonical gate deploy" >&2 | ||
|
Comment on lines
+779
to
+783
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 |
||
| fi | ||
| rm -rf "$GATE_STAGE" 2>/dev/null || true | ||
| DID_ANYTHING=1 | ||
| fi | ||
|
|
||
| if [ "$DID_ANYTHING" -eq 0 ]; then | ||
| echo "merge $SHA: no manual deploys needed (only auto-rebuild paths changed)" | ||
| fi | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After
--deploy-canonicalcreates~/.local/bin/hapax-hooks-doctoras a symlink to this script, invoking that documented command without--rootmakesSCRIPT_DIRresolve to the symlink directory (~/.local/bin), so this fallback root becomes~/.localinstead of the council checkout. In that installed path,hapax-hooks-doctor --check,--fanout, or the warning-suggested--deploy-canonicalinspect~/.local/hooks/scriptsand 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 👍 / 👎.