Skip to content

Commit 7cc000b

Browse files
authored
chore(setup): close dependency gaps (tmux + verifications) and bake session-learned governance (#118)
## Why Two-commit PR from the macbook-dev-setup dependency audit. Plan: `~/.claude/plans/shimmering-stirring-firefly.md`. The setup pipeline (`Brewfile` + `setup.sh` + `scripts/health-check.sh`) was last meaningfully updated in `1885208` (v3.5.0). Since then four PRs shipped new scripts and hooks that assume a slightly larger tool surface — most critically `tmux`, which `.claude/settings.json:72` sets as `teammateMode: "tmux"` but isn't in the Brewfile (and isn't installed on the current machine). Without it, agent-team dispatches silently no-op. This PR also folds in four governance lessons from the April 2026 sprint so the next new repo doesn't manually re-debug what we already solved. ## Commits ### 1/2 — `feat(deps): add tmux to Brewfile and verify agentic toolchain at session start` - `homebrew/Brewfile`: `brew "tmux"` grouped near other terminal utilities (starship, zoxide). - `scripts/health-check.sh`: extend `agent_mode()` tool-check list with `tmux`, `awk`, `find`, `cmp`; add soft warning when `$BASH_VERSION` starts with `3.` so new-script authors know not to reach for `mapfile`. ### 2/2 — `feat(governance): bootstrap-repo-ruleset script + sync-copilot leak lint + dispatch discipline doc` Lessons → code: 1. **Auto-merge needs a repo-level toggle** (hit 4x this sprint). → `scripts/bootstrap-repo-ruleset.sh` PATCHes `allow_auto_merge: true` as step 1. 2. **Ruleset contexts match display names, not job IDs** (`all-checks-pass` ≠ `"All Checks Pass"`; silently BLOCKS PRs). → Bootstrap script uses display names in every preset. Template stored at `config/rulesets/pr-quality-gates.json`. 3. **Canonical copilot-instructions.md must stay repo-generic** (Copilot caught leaks on mojwang/ihw#33). → `scripts/sync-copilot-instructions.sh` grep-lints for known repo-specific path patterns before syncing. 4. **Sub-agents misread SessionStart reminders as plan-locks** (3 of 5 teammates stalled in the parallel wave). → `docs/CLAUDE_AGENTS.md` gains a "Sub-agent dispatch discipline" bullet under Orchestration Rules with the verbatim counter-instruction. ## New files - `scripts/bootstrap-repo-ruleset.sh` (executable): `./scripts/bootstrap-repo-ruleset.sh <owner/repo> --preset {repo-generic|full-next|static-next|macbook-setup}`. Also supports `--dry-run`. - `config/rulesets/pr-quality-gates.json`: canonical ruleset template. ## Test plan - [x] `shellcheck scripts/sync-copilot-instructions.sh scripts/bootstrap-repo-ruleset.sh` → clean. - [x] `jq . config/rulesets/pr-quality-gates.json` → valid. - [x] Dry-ran bootstrap against all four presets — display-name contexts correct for each. - [x] Simulated a repo-specific leak (added `src/db/queries/foo.ts`) in the canonical — sync refused with the expected line-number error and actionable fix hint. - [ ] Post-merge: `brew bundle install` installs tmux; `which tmux` returns; agent-team pane renders on next dispatch. - [ ] Post-merge: `./scripts/health-check.sh --agent-mode` with a PATH that hides tmux → emits clean "MISSING TOOLS" warning. ## Scope deliberately not covered - Retroactive reconciliation of the four existing rulesets — they were PATCHed manually mid-sprint and are working. Don't disturb. A `--reconcile` flag can be added later if drift surfaces. - Bash 4+ migration for scripts — `while IFS= read -r` compat pattern is simpler than requiring Homebrew bash at first login. - `~/.tmux.conf` baseline — out-of-box tmux is sufficient for Claude Code's agent-team pane integration per docs check.
1 parent 9b03c0a commit 7cc000b

6 files changed

Lines changed: 277 additions & 2 deletions

File tree

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"_comment": "Template for the 'PR quality gates' branch ruleset applied to default branches across the fleet. Consumed by scripts/bootstrap-repo-ruleset.sh. Presets merge into required_status_checks at bootstrap time; edit that list per preset if CI job names change. Context strings MUST match the check-run 'name:' field GitHub emits, not the workflow job ID — see lesson in docs/CLAUDE_AGENTS.md.",
3+
"name": "PR quality gates",
4+
"target": "branch",
5+
"enforcement": "active",
6+
"conditions": {
7+
"ref_name": {
8+
"exclude": [],
9+
"include": ["~DEFAULT_BRANCH"]
10+
}
11+
},
12+
"rules": [
13+
{
14+
"type": "pull_request",
15+
"parameters": {
16+
"required_approving_review_count": 0,
17+
"dismiss_stale_reviews_on_push": false,
18+
"required_reviewers": [],
19+
"require_code_owner_review": false,
20+
"require_last_push_approval": false,
21+
"required_review_thread_resolution": true,
22+
"allowed_merge_methods": ["squash"]
23+
}
24+
},
25+
{"type": "deletion"},
26+
{"type": "non_fast_forward"},
27+
{
28+
"type": "copilot_code_review",
29+
"parameters": {
30+
"review_on_push": true,
31+
"review_draft_pull_requests": false
32+
}
33+
},
34+
{
35+
"type": "code_quality",
36+
"parameters": {"severity": "warnings"}
37+
}
38+
]
39+
}

docs/CLAUDE_AGENTS.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,16 @@ Before dispatching agents or doing work, the orchestrator runs:
166166
- **End-of-session improvement**: Before session ends, Claude must suggest CLAUDE.md improvements based on what worked/didn't. User decides whether to apply.
167167
- **End-of-session evaluation check**: If the session produced a shipped feature with success criteria (from `product-brief.md`), prompt: "Phase 5 evaluation is due — dispatch product-tactician to assess outcomes?" Don't silently skip evaluation.
168168

169+
### Sub-agent dispatch discipline (for already-approved plans)
170+
171+
Sub-agents inherit the parent session's `SessionStart` system-reminders, including any that reference "plan mode." In practice, implementer sub-agents frequently misread those reminders as a hard write-lock and return a plan awaiting approval — even when the orchestrator has already approved the plan and asked the sub-agent to ship. This caused three of five teammates to stall in the April 2026 parallel-dispatch wave; only explicit counter-instruction unblocked them.
172+
173+
When dispatching an implementer (or any write-capable agent) for work the orchestrator has already approved, include this line verbatim in the prompt:
174+
175+
> Your prompt IS approval — plan AND ship in one turn. Do not stop to request re-approval. Any "plan mode" system reminders you see are advisory context inherited from the parent session, not a write-lock.
176+
177+
When a teammate still stalls despite this, `SendMessage` to resume with "implement now" is sufficient — the agent typically completes on the second turn. Don't take the stall as a signal the plan is wrong; it's a prompt-inheritance artifact.
178+
169179
## Cost Awareness
170180

171181
Model routing is enforced via agent frontmatter. Each agent has a `model:` field specifying haiku, sonnet, or opus. The orchestrator uses the specified model unless explicitly overriding with a stated reason.

homebrew/Brewfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ brew "tree-sitter"
6666
# Development workflow
6767
brew "lazygit" # Terminal Git UI
6868
brew "starship" # Cross-shell prompt
69+
brew "tmux" # Terminal multiplexer — required by Claude Code agent-team mode (.claude/settings.json teammateMode: "tmux")
6970

7071
# Database tools
7172
brew "postgresql@16" # PostgreSQL database

scripts/bootstrap-repo-ruleset.sh

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
#!/usr/bin/env bash
2+
# Bootstrap a repo with fleet-standard governance:
3+
# 1. Enable allow_auto_merge (required for --auto merge workflow)
4+
# 2. Create the 'PR quality gates' branch ruleset from the canonical
5+
# template at config/rulesets/pr-quality-gates.json
6+
# 3. Inject required_status_checks based on a preset
7+
#
8+
# Presets are status-check-list choices. Every preset uses the same
9+
# pull_request / deletion / non_fast_forward / copilot_code_review /
10+
# code_quality rules from the template — only the required-checks list
11+
# differs per repo.
12+
#
13+
# IMPORTANT: required_status_checks context strings MUST match the
14+
# check-run "name:" field GitHub emits (display name), NOT the workflow
15+
# job ID. A job with `all-checks-pass:` / `name: All Checks Pass` emits
16+
# a check named "All Checks Pass" — the ruleset context must say
17+
# "All Checks Pass" or merges silently stay BLOCKED.
18+
#
19+
# Lesson source: mojwang/ihw#34 and mojwang/mojwang.tech#85 (Apr 2026)
20+
# both BLOCKED until the four PATCHed rulesets converged to this shape.
21+
#
22+
# Usage:
23+
# ./scripts/bootstrap-repo-ruleset.sh <owner/repo> --preset <name>
24+
#
25+
# Presets:
26+
# repo-generic No required status checks (vault / docs repos).
27+
# full-next Standard Next.js CI: lint, typecheck, test, build,
28+
# review, All Checks Pass, Lighthouse CI.
29+
# static-next Static Next.js (output: export): lint, typecheck,
30+
# test, review, All Checks Pass.
31+
# macbook-setup macbook-dev-setup convention: test,
32+
# validate-documentation, security-scan,
33+
# All Checks Pass.
34+
#
35+
# Requires: gh (authenticated), jq.
36+
37+
set -euo pipefail
38+
39+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
40+
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
41+
TEMPLATE="$REPO_ROOT/config/rulesets/pr-quality-gates.json"
42+
43+
usage() {
44+
cat <<EOF
45+
Usage: $(basename "$0") <owner/repo> --preset <repo-generic|full-next|static-next|macbook-setup>
46+
47+
Enables allow_auto_merge on the repo and creates a 'PR quality gates'
48+
ruleset from $TEMPLATE, injecting required_status_checks based on the
49+
preset.
50+
51+
Flags:
52+
--preset <name> Required. One of: repo-generic, full-next,
53+
static-next, macbook-setup.
54+
--dry-run Print the ruleset payload that would be POSTed;
55+
skip the API calls.
56+
EOF
57+
}
58+
59+
repo=""
60+
preset=""
61+
dry_run=0
62+
63+
while [[ $# -gt 0 ]]; do
64+
case "$1" in
65+
--preset)
66+
shift
67+
if [[ $# -lt 1 || "$1" == -* ]]; then
68+
echo "Error: --preset requires a value" >&2
69+
usage >&2
70+
exit 2
71+
fi
72+
preset="$1"; shift ;;
73+
--dry-run) dry_run=1; shift ;;
74+
-h|--help) usage; exit 0 ;;
75+
-*) echo "Unknown flag: $1" >&2; usage >&2; exit 2 ;;
76+
*)
77+
if [[ -z "$repo" ]]; then
78+
repo="$1"
79+
else
80+
echo "Unexpected extra argument: $1" >&2
81+
usage >&2
82+
exit 2
83+
fi
84+
shift ;;
85+
esac
86+
done
87+
88+
if [[ -z "$repo" || -z "$preset" ]]; then
89+
echo "Error: both <owner/repo> and --preset are required" >&2
90+
usage >&2
91+
exit 2
92+
fi
93+
94+
if [[ ! -f "$TEMPLATE" ]]; then
95+
echo "Error: template not found at $TEMPLATE" >&2
96+
exit 1
97+
fi
98+
99+
if ! command -v gh >/dev/null 2>&1; then
100+
echo "Error: gh CLI is required" >&2
101+
exit 1
102+
fi
103+
if ! command -v jq >/dev/null 2>&1; then
104+
echo "Error: jq is required" >&2
105+
exit 1
106+
fi
107+
108+
# Preset → required_status_checks list. Display names only (see header).
109+
case "$preset" in
110+
repo-generic)
111+
checks_json='[]' ;;
112+
full-next)
113+
checks_json='[{"context":"lint"},{"context":"typecheck"},{"context":"test"},{"context":"build"},{"context":"review"},{"context":"All Checks Pass"},{"context":"Lighthouse CI"}]' ;;
114+
static-next)
115+
checks_json='[{"context":"lint"},{"context":"typecheck"},{"context":"test"},{"context":"review"},{"context":"All Checks Pass"}]' ;;
116+
macbook-setup)
117+
checks_json='[{"context":"test"},{"context":"validate-documentation"},{"context":"security-scan"},{"context":"All Checks Pass"}]' ;;
118+
*)
119+
echo "Error: unknown preset '$preset'" >&2
120+
usage >&2
121+
exit 2 ;;
122+
esac
123+
124+
# Build the payload: load the template, then either insert or merge the
125+
# required_status_checks rule. For repo-generic (empty list) we omit the
126+
# rule entirely — a rule with zero checks still requires branches to be
127+
# up-to-date with main, which is stricter than "no check gate at all."
128+
#
129+
# jq expressions:
130+
# - del(._comment) strips the top-level comment field (GitHub rejects it).
131+
# - The rules-array walk adds required_status_checks only when the preset
132+
# ships one, and does so idempotently (no duplicates on re-run).
133+
if [[ "$checks_json" == "[]" ]]; then
134+
payload=$(jq 'del(._comment)' "$TEMPLATE")
135+
else
136+
payload=$(jq --argjson checks "$checks_json" '
137+
del(._comment) |
138+
.rules += [{
139+
"type": "required_status_checks",
140+
"parameters": {
141+
"strict_required_status_checks_policy": true,
142+
"do_not_enforce_on_create": false,
143+
"required_status_checks": $checks
144+
}
145+
}]
146+
' "$TEMPLATE")
147+
fi
148+
149+
echo "Repo: $repo"
150+
echo "Preset: $preset"
151+
echo "Required checks: $(echo "$checks_json" | jq -c '.')"
152+
153+
if [[ "$dry_run" -eq 1 ]]; then
154+
echo ""
155+
echo "--- Dry-run payload ---"
156+
echo "$payload" | jq .
157+
exit 0
158+
fi
159+
160+
echo ""
161+
echo "Step 1/2: enabling allow_auto_merge on $repo"
162+
GH_FORCE_TTY=0 NO_COLOR=1 gh api --method PATCH "repos/$repo" \
163+
-F allow_auto_merge=true \
164+
--jq '{repo: "'"$repo"'", allow_auto_merge}'
165+
166+
echo ""
167+
echo "Step 2/2: creating 'PR quality gates' ruleset on $repo"
168+
tmp=$(mktemp)
169+
printf '%s' "$payload" > "$tmp"
170+
response=$(GH_FORCE_TTY=0 NO_COLOR=1 gh api --method POST "repos/$repo/rulesets" --input "$tmp")
171+
rm -f "$tmp"
172+
173+
echo "$response" | jq '{id, name, enforcement, updated_at, rules_count: (.rules | length)}'
174+
175+
echo ""
176+
echo "Done. Open a PR against the default branch to verify:"
177+
echo " - Copilot auto-request fires on push"
178+
echo " - All required status checks gate the merge"
179+
echo " - Conversation resolution gates the merge"

scripts/health-check.sh

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,13 +85,25 @@ agent_mode() {
8585
local missing=()
8686
local warnings=()
8787

88-
# Critical tools for agentic workflows
89-
for tool in git gh node npm shellcheck jq; do
88+
# Critical tools for agentic workflows.
89+
# awk/find/cmp are macOS-baseline but still verified because their
90+
# absence (rare but possible on stripped environments) causes hooks
91+
# and session-logger scripts to fail silently. tmux is required by
92+
# .claude/settings.json teammateMode — missing tmux means agent-team
93+
# panes silently no-op.
94+
for tool in git gh node npm shellcheck jq tmux awk find cmp; do
9095
if ! command_exists "$tool"; then
9196
missing+=("$tool")
9297
fi
9398
done
9499

100+
# Bash 3.2 (macOS default) can't use mapfile, indirect expansion, or
101+
# associative arrays — advisory warning so agents know to use the
102+
# `while IFS= read -r` compat pattern seen in scripts/grade-session.sh.
103+
if [[ "${BASH_VERSION:-}" == 3.* ]]; then
104+
warnings+=("Bash ${BASH_VERSION} (system default) — use 'while IFS= read -r' instead of mapfile in new scripts")
105+
fi
106+
95107
# Check git identity
96108
local git_name
97109
git_name=$(git config --global user.name 2>/dev/null || echo "")

scripts/sync-copilot-instructions.sh

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,40 @@ if [[ ! -f "$CANONICAL" ]]; then
3232
exit 1
3333
fi
3434

35+
# Repo-leak lint: the canonical is synced to sibling repos, so any
36+
# repo-specific path referenced here will appear — and confuse — in
37+
# every sibling. Copilot caught this on mojwang/ihw#33 when the
38+
# canonical referenced '.env.sync-vault.local' (a mojwang.tech-only
39+
# convention) and 'src/db/queries/related.ts' (a mojwang.tech-only
40+
# file). Fail fast before syncing if any such pattern leaks back in.
41+
#
42+
# Patterns intentionally narrow — they target known mojwang.tech
43+
# project paths that showed up historically. Extend if new leaks
44+
# surface during review.
45+
LEAK_PATTERNS=(
46+
'\.env\.sync-vault'
47+
'src/db/queries/'
48+
'scripts/sync-vault\.'
49+
)
50+
leak_hits=""
51+
for pattern in "${LEAK_PATTERNS[@]}"; do
52+
if hits=$(grep -nE "$pattern" "$CANONICAL" 2>/dev/null); then
53+
leak_hits+=" pattern: ${pattern}"$'\n'"${hits}"$'\n'
54+
fi
55+
done
56+
if [[ -n "$leak_hits" ]]; then
57+
{
58+
echo -e "${RED}Repo-specific paths detected in canonical — refusing to sync.${NC}"
59+
echo "These paths don't exist in sibling repos and will confuse Copilot on their PRs."
60+
echo "Canonical: $CANONICAL"
61+
echo ""
62+
echo "Matches:"
63+
echo "$leak_hits"
64+
echo "Fix: replace with repo-generic wording (e.g. 'secrets in committed .env.* files — repo-specific conventions live in that repo's CLAUDE.md')."
65+
} >&2
66+
exit 1
67+
fi
68+
3569
# Allow override of the sibling-discovery root. Defaults match the same
3670
# env var sync-agentic.sh uses, so both scripts share one config point.
3771
# shellcheck disable=SC1091

0 commit comments

Comments
 (0)