feat(#351): agent-routing.yaml schema + portfolio_agent_routing resolver (Wave 1 PR 1)#353
Conversation
…0 Axis 3
- Documents the schema for the centralised agent-routing config that
ships in the private portfolio repo (split-portfolio v2) or
gitignored in the fork root (single-fork mode).
- Per-agent override fields: model (required), endpoint, env,
timeout_seconds, allowed_tools_override.
- Empty agents: {} block is identical to "no file" — adopters get
framework defaults out-of-box.
- Sync hook ships in PR 2; this file is documented but inert until
apply-agent-routing.sh lands.
Refs #351
…agent-routing.yaml - Adds `portfolio_agent_routing()` to `_lib-portfolio-paths.sh` — resolves the absolute path to the adopter's agent-routing.yaml (split-portfolio v2: sibling private repo via `.portfolio.agent_routing` in `.claude/project-config.json`; single-fork: `<fork>/agent-routing.yaml`, gitignored). - Resolver mirrors the existing `portfolio_*` shape (cache var, default fallback via `_portfolio_get`, absolute-path output via `_portfolio_resolve`). Caller-tolerant of a non-existent file — absence means "framework defaults apply". - Cache var added to `portfolio_clear_cache()` for test parity with other resolvers; header docstring updated to list the new resolver and default. - Adds `/agent-routing.yaml` (anchored) to `.gitignore` so single-fork adopters' routing choices never leak to the public fork. Refs #351
- Adds an adopter-facing section between "Private custom skills + handbooks" and "Migrating from split-portfolio v1 to v2" — slots in as a sibling private-repo customisation surface alongside custom-templates / custom-skills / custom-handbooks. - Documents the file location for both split-portfolio v2 (sibling private repo, config-block wired) and single-fork (gitignored at the fork root); seeding from `agent-routing.yaml.example`. - Tabulates the per-entry schema (model / endpoint / env / timeout_seconds / allowed_tools_override) with a pointer to the example file for full reference. - States the wave plan explicitly: this PR is schema + resolver + docs; sync hook + drift guards ship in #351 PR 2; /setup integration in PR 3; local-routing entries (gated on #348) in PR 4. - Cross-references AgDR-0050 axes 3-5, AgDR-0023 (closest prior-art), and AgDR-0041 (SessionStart-driven file rewrites — PR 2's pattern). Refs #351
…validity - Six cases following the existing test_portfolio_paths.sh shape (sandbox apexyard fork via mktemp; isolated subshell per case; red/green output; FAIL counter). - Covers the resolver contract: default path tolerates absence (unlike registry); single-fork mode; split-portfolio v2 override; relative override resolves against fork root; cache clear behaviour. - Validates the shipped agent-routing.yaml.example parses as YAML (yq when available, grep fallback otherwise) and has the documented top-level `version:` + `agents:` keys. - All six cases pass locally against the new resolver; existing test_portfolio_paths.sh (38 cases) continues to pass. Refs #351
atlas-apex
left a comment
There was a problem hiding this comment.
Code Review: PR #353
Commit: a4912ed37b4b54b12da039a429a6e45dddc7eefe
Summary
Wave 1 PR 1 of #351 — ships the customisation surface for centralised agent-routing per AgDR-0050 § Axis 3: schema example, portfolio_agent_routing resolver in _lib-portfolio-paths.sh, .gitignore entry for single-fork mode, adopter docs in docs/multi-project.md, and a 6-case sandbox smoke test. The YAML is documented but inert — the SessionStart sync hook that propagates routing choices into .claude/agents/*.md frontmatter, plus pre-commit + pre-push drift guards, ship in PR 2. No .claude/agents/*.md files are touched in this PR; scope discipline is clean.
Checklist Results
- Architecture & Design: Pass
- Code Quality: Pass
- Testing: Pass
- Security: Pass
- Performance: Pass
- PR Description & Glossary: Pass
- Technical Decisions (AgDR):Pass (AgDR-0050 already merged on
dev; PR references it correctly) - Adopter Handbooks: N/A (no handbooks loaded — diff doesn't trigger language buckets, and
handbooks/architecture+handbooks/generaldon't have rules matching this surface)
Verification of the 10 requested spot-checks
1. Resolver shape matches existing resolvers — Pass. The new portfolio_agent_routing is a verbatim shape-clone of portfolio_registry, portfolio_onboarding_path, portfolio_workspace_dir, portfolio_custom_skills_dir, portfolio_custom_handbooks_dir:
- Cache var
_PORTFOLIO_AGENT_ROUTING_CACHEmatches the_PORTFOLIO_{NAME}_CACHEconvention - Body is the same 4-statement idiom: cache-hit short-circuit →
_portfolio_get '.portfolio.<key>' './<default>'→_portfolio_resolve→ echo - Docstring follows the same
# Public:header shape with mode-description + Default block - Positioned correctly between
portfolio_custom_handbooks_dirandportfolio_validate(resolvers grouped together) - Added to
portfolio_clear_cache - Notable + correct divergence: docstring explicitly calls out that this resolver is allowed to return a non-existent path (the other 6 must exist). That's the right behaviour given absence = "no overrides", and correctly NOT validated by
portfolio_validate— adding it there would noise up SessionStart for fresh forks.
2. Schema example completeness — Pass. All AgDR-0050 § Axis 3 patterns are documented as worked examples in agent-routing.yaml.example:
- Example A: remote Claude override (qa-engineer → sonnet)
- Example B: multiple overrides (tech-lead/backend-engineer/data-analyst)
- Example C: local Ollama via LiteLLM (ticket-manager, data-analyst with
ollama/...model + endpoint) - Example D: Bedrock + env (
security-auditor+AWS_REGION+$APEXYARD_AWS_PROFILEenv-var-ref) - Example E: timeout (pen-tester at 180s)
- Example F:
allowed_tools_override(advanced; "use sparingly" caveat present) - Required field is
model:only; optional fields all clearly labelled - Empty
agents: {}documented as "identical to no file" - File header explicitly documents file location for both modes + the "rename from
.example" convention
3. .gitignore entry correctness — Pass.
- Leading slash
/agent-routing.yamlcorrectly anchors to root only (won't match nested<somewhere>/agent-routing.yaml) - The
.examplesuffix meansagent-routing.yaml.exampleis a different filename and unaffected — schema spec ships with the framework as intended - Comment block explains the single-fork-vs-split-portfolio distinction and cross-references the docs + AgDR
4. docs/multi-project.md section — Pass. Slots between "Private custom skills + handbooks" and "Migrating from split-portfolio v1 to v2", correctly framing it as the next sibling private-repo customisation surface alongside custom-templates / custom-skills / custom-handbooks. Covers:
- What the file is (24 sub-agents, framework defaults from AgDR-0050 § Axis 2)
- Per-mode location table (split-portfolio v2 = private repo; single-fork = gitignored fork root)
- Seed-from-example snippets for both modes
- Schema field summary table with one-line semantics per field
- Config-block wiring example with
agent_routingslotted alongside the other v2 keys - Explicit "no sync hook yet" status block naming it as Wave 1 PR 1 + forward-refs to PR 2, PR 3, PR 4
- "Out of scope (v1)" block (mixed remote+local, per-task overrides, web UI, auto-detect, cost dashboards)
- Cross-refs to AgDR-0050 (axes 3-5), AgDR-0023 (custom-templates same-pattern prior-art), AgDR-0041 (SessionStart-driven file rewrites — forward ref for PR 2)
5. Smoke test sandbox isolation — Pass. 6 cases, follows the existing test_portfolio_paths.sh convention faithfully:
set -u✓LIB_SRC/CONFIG_LIB_SRC/DEFAULTS_SRCresolved via$(dirname "$0")✓EXAMPLE_SRCadded (specific to this test) — correctly uses$(dirname "$0")/../../..to reach the fork root- red/green helpers ✓
make_forkusesmktemp -d+pwd -Pcanonicalisation (correct for macOS) ✓- Each case
rm -rf "$SB"cleans up — notrapneeded because cleanup is per-case explicit - PASS/FAIL counters, exit 0/1 ✓
- Case 6 has a
yq/grepfallback — graceful on hosts without yq, consistent withportfolio_validate's own yq-or-grep fallback - Case 5 (
portfolio_clear_cache) correctly clears_CONFIG_CACHE=""in addition to callingportfolio_clear_cache— necessary because_lib-read-config.shhas its own cache thatportfolio_clear_cachedoesn't touch. Matches the existing test's idiom.
6. No regression on test_portfolio_paths.sh — Confirmed by inspection. The lib changes are purely additive: one new function, one new cache var line in portfolio_clear_cache, two new comment lines in the file header. No existing function or signature touched. The PR body reports 38/38 PASS; the shape of the changes guarantees that.
7. PR body quality — Pass.
- 5 narrative bullets, each answering what changed AND why it matters (no label-only bullets)
- Glossary present with 6 terms (Agent-routing config, Framework default,
portfolio_agent_routing, Endpoint override, Sync hook, Drift prevention) Refs #351(correctly NOTCloses; #351 closes on PR 4 after wave completes)Per AgDR-0050-agent-runtime-overhaul.reference present- Explicit "no sync hook yet — ships in PR 2" callout in the last summary bullet AND in the Glossary's "Sync hook" + "Drift prevention" entries
8. No model: frontmatter touching agent files — Pass. Files list (5): _lib-portfolio-paths.sh (lib), new test file, .gitignore, new agent-routing.yaml.example, docs/multi-project.md. Zero .claude/agents/*.md files modified. No scope-creep into #347 PR 1's territory.
9. No drift guards yet — Pass. No .claude/hooks/pre-commit-* or pre-push-* files added or modified. The guards remain a forward-reference in the docs section and Glossary, deferred to PR 2 as planned.
10. Wave 1 invariants — PR body reports 4/4 PASS for test_token_efficiency_wave1.sh. The skill table in CLAUDE.md is not touched in this PR (no risk of row drifting past 25 words), and the SessionStart banner output isn't affected (no new SessionStart hook in this PR).
Issues Found
None.
Suggestions
- (Nit, non-blocking) In
agent-routing.yaml.example, Example D's$APEXYARD_AWS_PROFILEenv-var-ref pattern is a load-bearing semantic — the docstring above describes it as "Supports$VAR_NAMErefs to the parent shell's env" but the actual expansion mechanism will be defined byapply-agent-routing.shin PR 2. Worth a short note in PR 2's review checklist to verify the env-var-ref semantics match this example precisely so adopters who copy-paste from the example aren't surprised. - (Nit, non-blocking) The PR 1 → PR 2 forward-reference for the sync hook is consistently named
apply-agent-routing.shin three places (agent-routing.yaml.exampleheader,docs/multi-project.md, Glossary's "Sync hook" entry). Helpful — keeps the upcoming hook's filename discoverable by grep ahead of its actual landing.
Verdict
APPROVED
Clean, scope-disciplined Wave 1 PR 1. The resolver implementation is a verbatim shape-clone of the existing five resolvers in the same library; the example file fully covers the AgDR-0050 § Axis 3 anticipated patterns; the .gitignore placement is correct (root-anchored, doesn't clash with .example); the docs section slots correctly between "custom handbooks" and "v1 → v2 migration" with the right cross-references; the smoke test follows the existing sandbox convention and includes a yq/grep fallback. No agent files touched. No drift guards added (correctly deferred to PR 2). PR body is narrative-quality and explicitly flags the "inert until PR 2" status in three places.
CI 5/5 green. Approve.
Reviewed by Rex (Code Reviewer Agent)
Reviewed commit: a4912ed37b4b54b12da039a429a6e45dddc7eefe
…s (Wave 1 PR 2) (#357) * feat(#351): apply-agent-routing.sh SessionStart hook + framework-defaults snapshot per AgDR-0050 Axis 4 - SessionStart hook rewrites .claude/agents/*.md frontmatter model: lines from the adopter's agent-routing.yaml — makes the YAML LIVE on every session, closing the loop on Wave 1 PR 1 (#353) which only shipped the schema + portfolio_agent_routing resolver - Parser dispatches yq → python3+PyYAML → awk fallback; awk handles the v1 schema's 2/4/6-space indentation without a YAML dependency, so adopters without yq or PyYAML still get the routing applied - Snapshots the framework-default model: line per overridden agent into .claude/agents/.framework-defaults.json (gitignored). The drift guard reads this to know what model line to expect at commit time, so routing-induced rewrites can be told apart from intentional framework edits - Per-agent env files written to .claude/session/agent-env/<name>.env (gitignored) for endpoint / env / timeout_seconds entries; v1 is session-scoped per AgDR-0050 Axis 5 (mixed remote + local on one session deferred to v2) - Idempotent — second invocation produces same state, no compounded writes; env files are filter-then-emit, not blind-append - One-line banner only when applied > 0; silent on no-op so the ≤ 600-char SessionStart budget (Wave 1 invariant) is preserved - Sources libs from HOOK_DIR first (worktree-local) then walk-up ROOT to handle mid-upgrade forks where the parent fork's lib doesn't yet carry portfolio_agent_routing Refs #351 * feat(#351): block-agent-routing-drift.sh pre-commit + pre-push guards - Pre-commit + pre-push hook refuses to let routing-induced rewrites of .claude/agents/*.md escape to a public-class remote (the sibling enforcement to apply-agent-routing.sh — together they implement AgDR-0050 § Axis 4's "rewrite + drift guard" pattern) - Detects drift by comparing each staged/to-be-pushed agent file's model: line against the framework default. Resolution order: framework-defaults.json snapshot → dev:.claude/agents/<name>.md → upstream/main → HEAD; silent allow when no baseline resolves (avoids false positives on brand-new agent files) - Escape hatch: agent files carrying `# routing-config:override <reason>` in frontmatter or body are accepted — covers the rare framework-level intentional change (e.g. switching QA default haiku → sonnet organisation-wide). Comment is deliberately visible so the change ships with PR review attention - Detects both shapes: `git commit -m` AND `git push`. Push-time check diffs the upstream tracking ref → origin/<branch> → dev → HEAD~5 fallback ladder, so the guard works on brand-new branches without an established upstream - Blocking message names the specific file(s) + current model + expected default + the two unblock paths (revert OR escape-hatch comment) per the framework's standard self-correction shape Refs #351 * test(#351): smoke test for sync hook + drift guard (7 cases) - 7-case smoke test covers the apply-agent-routing.sh + block-agent- routing-drift.sh contracts: empty config no-op, single override rewrites + snapshot, orphan entry silently skipped, idempotency, drift guard fires on stage-with-drift, drift guard accepts with escape hatch, drift guard accepts on no-drift (committed file = framework default) - Pattern mirrors test_portfolio_paths.sh / test_split_portfolio_v2_ migration.sh — each case builds an isolated sandbox apexyard fork under $TMPDIR with real `git init` so drift baseline lookups via `git show dev:...` actually run; tests source the hooks from the sandbox so version-skew between fork and worktree can't false-pass - Wave 1 invariant 4 (SessionStart banner ≤ 600 chars) extended to include apply-agent-routing.sh in its hook iteration list — the new hook is silent on the no-routing-file happy path so the budget is preserved (153 chars total, well under the cap) Refs #351 * chore(#351): wire drift guards into settings.json + flip docs/multi-project.md to live - .claude/settings.json: add PreToolUse entries for block-agent-routing-drift.sh on Bash(git commit *) AND Bash(git push *) so the guard fires on every routing-frontmatter mutation BEFORE it can leave local. Pairs with the apply-agent-routing.sh SessionStart entry from commit 2aa8fbf. Two entries (commit + push) instead of one because the push shape can carry a forced commit that the commit-time gate already permitted. - docs/multi-project.md "Centralised agent routing" section: drop the "Wave 1 PR 1 (this PR) — inert until PR 2 lands" status block; replace with "How the overrides get applied" describing the SessionStart sync + the two drift guards in the live tense. The "What ships next" list narrows from three items to two (PR 3 + PR 4) since PR 2 is now in. Per AgDR-0050 Axis 4. Closes the v1 acceptance criteria for #351 PR 2: - SessionStart sync hook: 2aa8fbf - Pre-commit + pre-push drift guards: 52a8fc2 - Smoke test (7 cases, all PASS): d8e40a2 - Settings wire-up + docs flip: this commit Refs #351 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(#351): refresh site-counts copy — hooks 29 → 31 (apply-agent-routing + block-agent-routing-drift) PR #357 adds two new hooks: - .claude/hooks/apply-agent-routing.sh (SessionStart) - .claude/hooks/block-agent-routing-drift.sh (PreToolUse, x2 entries) Bumps the framework hook count in marketing copy (site/index.html, site/index.md.gen, site/architecture.html, site/architecture.md.gen, site/llms.txt, site/llms-full.txt) from 29 to 31. test_site_counts.sh now green. Refs #351 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(#351): close shell-hooks variant + stale layer-claim drift gaps (Rex follow-up) Rex caught two regression gaps Rex's broader audit found on the prior HEAD: 1. **`shell hooks` variant** — test_site_counts.sh's regex covered `hooks`, `shell scripts?`, `shell gates?`, `mechanical gates?` but not `shell hooks?`. `site/llms.txt:39` and `site/skill.md:35` were carrying `29 shell hooks` claims that survived the prior 29→31 sweep because they sat under that uncovered noun-pattern. Adds a `check_count "shell +hooks?"` line to the test so future bumps catch the variant too. 2. **Multi-line layer claims in `site/llms-full.txt:131-133`** — three stale layer-component counts (52 skills, 5 sub-agents, 24 mechanical enforcement scripts) survived multiple count refreshes because the regex assumes `<digits> <noun>` on the same line. The wrap pushed the digit to the previous line. Refreshed to actuals (53 / 12 / 31). Not a smoke-test fix — the regex would need cross-line lookback that isn't worth the complexity for three references; flagged for manual audit during count refreshes via the PR-author checklist. 3. **`site/llms-full.txt:92`** — long-stale `24 shell hooks` (predates this PR; only surfaced because of the new `shell hooks?` test pattern). Refreshed to 31. Refs #351 Refs #357 (review) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…y (Wave 2 PR 3) (#363) Split-portfolio adopters running `/setup --split-portfolio` no longer need to manually `cp ~/ops/apexyard/agent-routing.yaml.example ~/ops/ apexyard-portfolio/agent-routing.yaml` after the bootstrap finishes — the skill now copies it as part of Step 5 (private-repo init), and writes the matching `agent_routing` key into the public fork's .claude/project-config.json `portfolio:` block in Step 6. Single-fork adopters are deliberately NOT auto-seeded; the framework already gitignores `/agent-routing.yaml` (so no leak risk on a stray push), and adopters who never want to customise routing should not have an empty `agents: {}` file accumulating in the fork root. New Step 7a is purely advisory — it tells single-fork adopters where to start when they DO want to customise, with the explicit `cp` command. Files changed: - .claude/skills/setup/SKILL.md - Step 5 (private-repo init): new bullet for seeding `agent-routing.yaml` from `<fork>/agent-routing.yaml.example` - Step 6 (public-fork config-block JSON): new `agent_routing` key pointing at `../<sibling>/agent-routing.yaml`; updated trailing prose to note the key is optional (defaults to ./agent-routing.yaml against the ops-fork root, so single-fork adopters can skip it) - New Step 7a (single-fork agent-routing seeding, advisory): tells single-fork adopters about the cp + edit path; writes no files - .claude/skills/setup/tests/test_setup_split_portfolio_v2.sh - build_pre_setup_v2() now seeds an `agent-routing.yaml.example` in the public fork (the framework artefact #353 ships) - apply_setup_v2() extended: writes the 8th portfolio key `agent_routing`, and copies `agent-routing.yaml.example` → `<sibling>/agent-routing.yaml` (mirrors SKILL.md Step 5 cp) - EXPECTED_KEYS array bumped from 7 to 8 (adds `agent_routing`) - New Assertion 1b: agent-routing.yaml exists in the private repo post-setup AND carries the `agents:` key (framework example shape) - Updated success message from "all 7" to "all 8 v2 portfolio keys" - Test PASSES at 11/11. - docs/multi-project.md - "Seed from the framework example" prose now notes split-portfolio is automated by `/setup --split-portfolio` (#351 PR 3); single-fork stays manual + only-when-ready-to-customise - "What ships next" list drops the now-landed PR 3 row Refs #351 Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ver (Wave 1 PR 1) (#353) * feat(#351): add agent-routing.yaml.example — schema spec per AgDR-0050 Axis 3 - Documents the schema for the centralised agent-routing config that ships in the private portfolio repo (split-portfolio v2) or gitignored in the fork root (single-fork mode). - Per-agent override fields: model (required), endpoint, env, timeout_seconds, allowed_tools_override. - Empty agents: {} block is identical to "no file" — adopters get framework defaults out-of-box. - Sync hook ships in PR 2; this file is documented but inert until apply-agent-routing.sh lands. Refs #351 * feat(#351): portfolio_agent_routing resolver + gitignore single-fork agent-routing.yaml - Adds `portfolio_agent_routing()` to `_lib-portfolio-paths.sh` — resolves the absolute path to the adopter's agent-routing.yaml (split-portfolio v2: sibling private repo via `.portfolio.agent_routing` in `.claude/project-config.json`; single-fork: `<fork>/agent-routing.yaml`, gitignored). - Resolver mirrors the existing `portfolio_*` shape (cache var, default fallback via `_portfolio_get`, absolute-path output via `_portfolio_resolve`). Caller-tolerant of a non-existent file — absence means "framework defaults apply". - Cache var added to `portfolio_clear_cache()` for test parity with other resolvers; header docstring updated to list the new resolver and default. - Adds `/agent-routing.yaml` (anchored) to `.gitignore` so single-fork adopters' routing choices never leak to the public fork. Refs #351 * docs(#351): docs/multi-project.md section for centralised agent routing - Adds an adopter-facing section between "Private custom skills + handbooks" and "Migrating from split-portfolio v1 to v2" — slots in as a sibling private-repo customisation surface alongside custom-templates / custom-skills / custom-handbooks. - Documents the file location for both split-portfolio v2 (sibling private repo, config-block wired) and single-fork (gitignored at the fork root); seeding from `agent-routing.yaml.example`. - Tabulates the per-entry schema (model / endpoint / env / timeout_seconds / allowed_tools_override) with a pointer to the example file for full reference. - States the wave plan explicitly: this PR is schema + resolver + docs; sync hook + drift guards ship in #351 PR 2; /setup integration in PR 3; local-routing entries (gated on #348) in PR 4. - Cross-references AgDR-0050 axes 3-5, AgDR-0023 (closest prior-art), and AgDR-0041 (SessionStart-driven file rewrites — PR 2's pattern). Refs #351 * test(#351): smoke test for portfolio_agent_routing resolver + schema validity - Six cases following the existing test_portfolio_paths.sh shape (sandbox apexyard fork via mktemp; isolated subshell per case; red/green output; FAIL counter). - Covers the resolver contract: default path tolerates absence (unlike registry); single-fork mode; split-portfolio v2 override; relative override resolves against fork root; cache clear behaviour. - Validates the shipped agent-routing.yaml.example parses as YAML (yq when available, grep fallback otherwise) and has the documented top-level `version:` + `agents:` keys. - All six cases pass locally against the new resolver; existing test_portfolio_paths.sh (38 cases) continues to pass. Refs #351 --------- Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
…s (Wave 1 PR 2) (#357) * feat(#351): apply-agent-routing.sh SessionStart hook + framework-defaults snapshot per AgDR-0050 Axis 4 - SessionStart hook rewrites .claude/agents/*.md frontmatter model: lines from the adopter's agent-routing.yaml — makes the YAML LIVE on every session, closing the loop on Wave 1 PR 1 (#353) which only shipped the schema + portfolio_agent_routing resolver - Parser dispatches yq → python3+PyYAML → awk fallback; awk handles the v1 schema's 2/4/6-space indentation without a YAML dependency, so adopters without yq or PyYAML still get the routing applied - Snapshots the framework-default model: line per overridden agent into .claude/agents/.framework-defaults.json (gitignored). The drift guard reads this to know what model line to expect at commit time, so routing-induced rewrites can be told apart from intentional framework edits - Per-agent env files written to .claude/session/agent-env/<name>.env (gitignored) for endpoint / env / timeout_seconds entries; v1 is session-scoped per AgDR-0050 Axis 5 (mixed remote + local on one session deferred to v2) - Idempotent — second invocation produces same state, no compounded writes; env files are filter-then-emit, not blind-append - One-line banner only when applied > 0; silent on no-op so the ≤ 600-char SessionStart budget (Wave 1 invariant) is preserved - Sources libs from HOOK_DIR first (worktree-local) then walk-up ROOT to handle mid-upgrade forks where the parent fork's lib doesn't yet carry portfolio_agent_routing Refs #351 * feat(#351): block-agent-routing-drift.sh pre-commit + pre-push guards - Pre-commit + pre-push hook refuses to let routing-induced rewrites of .claude/agents/*.md escape to a public-class remote (the sibling enforcement to apply-agent-routing.sh — together they implement AgDR-0050 § Axis 4's "rewrite + drift guard" pattern) - Detects drift by comparing each staged/to-be-pushed agent file's model: line against the framework default. Resolution order: framework-defaults.json snapshot → dev:.claude/agents/<name>.md → upstream/main → HEAD; silent allow when no baseline resolves (avoids false positives on brand-new agent files) - Escape hatch: agent files carrying `# routing-config:override <reason>` in frontmatter or body are accepted — covers the rare framework-level intentional change (e.g. switching QA default haiku → sonnet organisation-wide). Comment is deliberately visible so the change ships with PR review attention - Detects both shapes: `git commit -m` AND `git push`. Push-time check diffs the upstream tracking ref → origin/<branch> → dev → HEAD~5 fallback ladder, so the guard works on brand-new branches without an established upstream - Blocking message names the specific file(s) + current model + expected default + the two unblock paths (revert OR escape-hatch comment) per the framework's standard self-correction shape Refs #351 * test(#351): smoke test for sync hook + drift guard (7 cases) - 7-case smoke test covers the apply-agent-routing.sh + block-agent- routing-drift.sh contracts: empty config no-op, single override rewrites + snapshot, orphan entry silently skipped, idempotency, drift guard fires on stage-with-drift, drift guard accepts with escape hatch, drift guard accepts on no-drift (committed file = framework default) - Pattern mirrors test_portfolio_paths.sh / test_split_portfolio_v2_ migration.sh — each case builds an isolated sandbox apexyard fork under $TMPDIR with real `git init` so drift baseline lookups via `git show dev:...` actually run; tests source the hooks from the sandbox so version-skew between fork and worktree can't false-pass - Wave 1 invariant 4 (SessionStart banner ≤ 600 chars) extended to include apply-agent-routing.sh in its hook iteration list — the new hook is silent on the no-routing-file happy path so the budget is preserved (153 chars total, well under the cap) Refs #351 * chore(#351): wire drift guards into settings.json + flip docs/multi-project.md to live - .claude/settings.json: add PreToolUse entries for block-agent-routing-drift.sh on Bash(git commit *) AND Bash(git push *) so the guard fires on every routing-frontmatter mutation BEFORE it can leave local. Pairs with the apply-agent-routing.sh SessionStart entry from commit f098d47. Two entries (commit + push) instead of one because the push shape can carry a forced commit that the commit-time gate already permitted. - docs/multi-project.md "Centralised agent routing" section: drop the "Wave 1 PR 1 (this PR) — inert until PR 2 lands" status block; replace with "How the overrides get applied" describing the SessionStart sync + the two drift guards in the live tense. The "What ships next" list narrows from three items to two (PR 3 + PR 4) since PR 2 is now in. Per AgDR-0050 Axis 4. Closes the v1 acceptance criteria for #351 PR 2: - SessionStart sync hook: f098d47 - Pre-commit + pre-push drift guards: 53f49d4 - Smoke test (7 cases, all PASS): 2f2a1f3 - Settings wire-up + docs flip: this commit Refs #351 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(#351): refresh site-counts copy — hooks 29 → 31 (apply-agent-routing + block-agent-routing-drift) PR #357 adds two new hooks: - .claude/hooks/apply-agent-routing.sh (SessionStart) - .claude/hooks/block-agent-routing-drift.sh (PreToolUse, x2 entries) Bumps the framework hook count in marketing copy (site/index.html, site/index.md.gen, site/architecture.html, site/architecture.md.gen, site/llms.txt, site/llms-full.txt) from 29 to 31. test_site_counts.sh now green. Refs #351 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(#351): close shell-hooks variant + stale layer-claim drift gaps (Rex follow-up) Rex caught two regression gaps Rex's broader audit found on the prior HEAD: 1. **`shell hooks` variant** — test_site_counts.sh's regex covered `hooks`, `shell scripts?`, `shell gates?`, `mechanical gates?` but not `shell hooks?`. `site/llms.txt:39` and `site/skill.md:35` were carrying `29 shell hooks` claims that survived the prior 29→31 sweep because they sat under that uncovered noun-pattern. Adds a `check_count "shell +hooks?"` line to the test so future bumps catch the variant too. 2. **Multi-line layer claims in `site/llms-full.txt:131-133`** — three stale layer-component counts (52 skills, 5 sub-agents, 24 mechanical enforcement scripts) survived multiple count refreshes because the regex assumes `<digits> <noun>` on the same line. The wrap pushed the digit to the previous line. Refreshed to actuals (53 / 12 / 31). Not a smoke-test fix — the regex would need cross-line lookback that isn't worth the complexity for three references; flagged for manual audit during count refreshes via the PR-author checklist. 3. **`site/llms-full.txt:92`** — long-stale `24 shell hooks` (predates this PR; only surfaced because of the new `shell hooks?` test pattern). Refreshed to 31. Refs #351 Refs #357 (review) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…y (Wave 2 PR 3) (#363) Split-portfolio adopters running `/setup --split-portfolio` no longer need to manually `cp ~/ops/apexyard/agent-routing.yaml.example ~/ops/ apexyard-portfolio/agent-routing.yaml` after the bootstrap finishes — the skill now copies it as part of Step 5 (private-repo init), and writes the matching `agent_routing` key into the public fork's .claude/project-config.json `portfolio:` block in Step 6. Single-fork adopters are deliberately NOT auto-seeded; the framework already gitignores `/agent-routing.yaml` (so no leak risk on a stray push), and adopters who never want to customise routing should not have an empty `agents: {}` file accumulating in the fork root. New Step 7a is purely advisory — it tells single-fork adopters where to start when they DO want to customise, with the explicit `cp` command. Files changed: - .claude/skills/setup/SKILL.md - Step 5 (private-repo init): new bullet for seeding `agent-routing.yaml` from `<fork>/agent-routing.yaml.example` - Step 6 (public-fork config-block JSON): new `agent_routing` key pointing at `../<sibling>/agent-routing.yaml`; updated trailing prose to note the key is optional (defaults to ./agent-routing.yaml against the ops-fork root, so single-fork adopters can skip it) - New Step 7a (single-fork agent-routing seeding, advisory): tells single-fork adopters about the cp + edit path; writes no files - .claude/skills/setup/tests/test_setup_split_portfolio_v2.sh - build_pre_setup_v2() now seeds an `agent-routing.yaml.example` in the public fork (the framework artefact #353 ships) - apply_setup_v2() extended: writes the 8th portfolio key `agent_routing`, and copies `agent-routing.yaml.example` → `<sibling>/agent-routing.yaml` (mirrors SKILL.md Step 5 cp) - EXPECTED_KEYS array bumped from 7 to 8 (adds `agent_routing`) - New Assertion 1b: agent-routing.yaml exists in the private repo post-setup AND carries the `agents:` key (framework example shape) - Updated success message from "all 7" to "all 8 v2 portfolio keys" - Test PASSES at 11/11. - docs/multi-project.md - "Seed from the framework example" prose now notes split-portfolio is automated by `/setup --split-portfolio` (#351 PR 3); single-fork stays manual + only-when-ready-to-customise - "What ships next" list drops the now-landed PR 3 row Refs #351 Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
agent-routing.yaml.exampleat the framework root — the schema spec for the centralised per-agent model + endpoint override surface (AgDR-0050 Axis 3). Documents required + optional fields (model,endpoint,env,timeout_seconds,allowed_tools_override) with worked examples (single-agent override, multiple overrides, local routing via Ollama + LiteLLM, Bedrock + AWS env, timeout bump). Emptyagents: {}block is identical to "no file" — adopters get framework defaults out-of-box.portfolio_agent_routingresolver to_lib-portfolio-paths.sh— mirrors the existingportfolio_registry/portfolio_onboarding_pathshape (cache var, default fallback via_portfolio_get, absolute-path output via_portfolio_resolve). Caller-tolerant of a non-existent file: absence means "framework defaults apply", which is the documented out-of-box behaviour. Resolves the split-portfolio v2 path via.portfolio.agent_routingin.claude/project-config.json; default is./agent-routing.yamlagainst the ops-fork root./agent-routing.yamlat the fork root so single-fork adopters' routing choices never accidentally leak to the public fork — sibling pattern to.claude/project-config.jsonandworkspace/*/already in.gitignore. Split-portfolio adopters keep it in the private repo via the config block instead.docs/multi-project.mdgains a "Centralised agent routing" section between "Private custom skills + handbooks" and "Migrating from split-portfolio v1 to v2" — slots in as a sibling private-repo customisation surface alongside custom-templates / custom-skills / custom-handbooks. Tabulates the file location for both modes, the per-entry schema, the config-block wiring, and an explicit wave plan (sync hook in PR 2,/setupintegration in PR 3, local-routing entries gated on [Spike] Local-model routing feasibility for ticket-manager, Data Analyst, QA Engineer via LiteLLM → Ollama #348 in PR 4).apply-agent-routing.shlands the YAML file is documented but inert:portfolio_agent_routingresolves the path and the schema example parses cleanly, but no hook applies overrides to.claude/agents/*.mdfrontmatter. PR 2 also adds the pre-commit + pre-push drift-prevention guards that block accidental commits of the rewritten frontmatter.Per AgDR-0050-agent-runtime-overhaul.
Testing
bash .claude/hooks/tests/test_portfolio_agent_routing.sh— 6/6 PASS locally (default path tolerates absence; single-fork file resolves + exists; split-portfolio v2 override → sibling repo path; relative override resolves against fork root;portfolio_clear_cacheresets cache;agent-routing.yaml.exampleparses + has documented top-level keys via grep fallback / yq when available).bash .claude/hooks/tests/test_portfolio_paths.sh— 38/38 PASS (no regression to the existing resolvers).bash .claude/hooks/tests/test_token_efficiency_wave1.sh— all 4 Wave 1 invariants PASS (CLAUDE.md skill-table compactness, SKILL.md description budget, no orphan skills, SessionStart banner ≤ 600 chars).agent-routing.yaml.exampleis renamed-from-example shape (same asapexyard.projects.yaml.example) —.gitignorecovers/agent-routing.yamlbut not the.exampleso the schema spec ships with the framework.Refs #351
Glossary
agent-routing.yamlfile — adopter's single-file surface for overriding the framework's per-agent model defaults across all 24 sub-agents (19 role-derived + 5 utility). Schema per AgDR-0050 Axis 3..claude/agents/<name>.mdfrontmatter — from the 24-entry matrix in AgDR-0050 Axis 2 (Opus 5 / Sonnet 17 / Haiku 2). Adopters override per agent; omitting an agent from the config inherits the default.portfolio_agent_routing_lib-portfolio-paths.shthat returns the absolute path toagent-routing.yaml— sibling toportfolio_registry,portfolio_onboarding_path, etc. Caller-tolerant of a non-existent file; that's the documented "no overrides" case.endpoint:field in a routing entry — setsANTHROPIC_BASE_URLat SessionStart (PR 2) so an agent routes through an alternative inference endpoint (e.g. LiteLLM proxy fronting Ollama). v1 is session-scoped: all agents on a session share the endpoint or none do (per AgDR-0050 Axis 5 + Risks).apply-agent-routing.sh— the SessionStart hook shipping in #351 PR 2 that readsagent-routing.yamland rewrites the affected.claude/agents/*.mdfrontmatter in-place. This PR ships the schema + resolver + docs only; the file is inert until PR 2 lands.model:rewrites so adopter routing choices never leak to the public fork. Operator escape hatch via a# routing-config:override <reason>comment (per AgDR-0050 § Axis 4).