Skip to content

feat(#351): agent-routing.yaml schema + portfolio_agent_routing resolver (Wave 1 PR 1)#353

Merged
atlas-apex merged 4 commits into
devfrom
feature/GH-351-agent-routing-schema-and-resolver
May 20, 2026
Merged

feat(#351): agent-routing.yaml schema + portfolio_agent_routing resolver (Wave 1 PR 1)#353
atlas-apex merged 4 commits into
devfrom
feature/GH-351-agent-routing-schema-and-resolver

Conversation

@atlas-apex

Copy link
Copy Markdown
Collaborator

Summary

  • Adds agent-routing.yaml.example at 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). Empty agents: {} block is identical to "no file" — adopters get framework defaults out-of-box.
  • Adds portfolio_agent_routing resolver to _lib-portfolio-paths.sh — mirrors the existing portfolio_registry / portfolio_onboarding_path 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", which is the documented out-of-box behaviour. Resolves the split-portfolio v2 path via .portfolio.agent_routing in .claude/project-config.json; default is ./agent-routing.yaml against the ops-fork root.
  • Gitignores /agent-routing.yaml at the fork root so single-fork adopters' routing choices never accidentally leak to the public fork — sibling pattern to .claude/project-config.json and workspace/*/ already in .gitignore. Split-portfolio adopters keep it in the private repo via the config block instead.
  • docs/multi-project.md gains 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, /setup integration 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).
  • No sync hook yet — ships in [Feature] Centralised agent-routing config — agent-routing.yaml in private repo, propagates to .claude/agents/*.md at SessionStart #351 PR 2. Until apply-agent-routing.sh lands the YAML file is documented but inert: portfolio_agent_routing resolves the path and the schema example parses cleanly, but no hook applies overrides to .claude/agents/*.md frontmatter. 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_cache resets cache; agent-routing.yaml.example parses + 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).
  • Manual: confirmed agent-routing.yaml.example is renamed-from-example shape (same as apexyard.projects.yaml.example) — .gitignore covers /agent-routing.yaml but not the .example so the schema spec ships with the framework.

Refs #351

Glossary

Term Definition
Agent-routing config The centralised agent-routing.yaml file — 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.
Framework default The per-agent model assignment shipped by the framework in .claude/agents/<name>.md frontmatter — 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 New resolver in _lib-portfolio-paths.sh that returns the absolute path to agent-routing.yaml — sibling to portfolio_registry, portfolio_onboarding_path, etc. Caller-tolerant of a non-existent file; that's the documented "no overrides" case.
Endpoint override The endpoint: field in a routing entry — sets ANTHROPIC_BASE_URL at 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).
Sync hook Forward-ref to apply-agent-routing.sh — the SessionStart hook shipping in #351 PR 2 that reads agent-routing.yaml and rewrites the affected .claude/agents/*.md frontmatter in-place. This PR ships the schema + resolver + docs only; the file is inert until PR 2 lands.
Drift prevention Forward-ref to the pre-commit + pre-push guards shipping in #351 PR 2 — block accidental commits of routing-config-driven 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).

me2resh added 4 commits May 20, 2026 20:07
…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 atlas-apex left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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/general don'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_CACHE matches the _PORTFOLIO_{NAME}_CACHE convention
  • 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_dir and portfolio_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_PROFILE env-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.yaml correctly anchors to root only (won't match nested <somewhere>/agent-routing.yaml)
  • The .example suffix means agent-routing.yaml.example is 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_routing slotted 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_SRC resolved via $(dirname "$0")
  • EXAMPLE_SRC added (specific to this test) — correctly uses $(dirname "$0")/../../.. to reach the fork root
  • red/green helpers ✓
  • make_fork uses mktemp -d + pwd -P canonicalisation (correct for macOS) ✓
  • Each case rm -rf "$SB" cleans up — no trap needed because cleanup is per-case explicit
  • PASS/FAIL counters, exit 0/1 ✓
  • Case 6 has a yq / grep fallback — graceful on hosts without yq, consistent with portfolio_validate's own yq-or-grep fallback
  • Case 5 (portfolio_clear_cache) correctly clears _CONFIG_CACHE="" in addition to calling portfolio_clear_cache — necessary because _lib-read-config.sh has its own cache that portfolio_clear_cache doesn'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 NOT Closes; #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_PROFILE env-var-ref pattern is a load-bearing semantic — the docstring above describes it as "Supports $VAR_NAME refs to the parent shell's env" but the actual expansion mechanism will be defined by apply-agent-routing.sh in 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.sh in three places (agent-routing.yaml.example header, 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

@atlas-apex atlas-apex merged commit 80ebb4e into dev May 20, 2026
5 checks passed
@atlas-apex atlas-apex deleted the feature/GH-351-agent-routing-schema-and-resolver branch May 20, 2026 19:22
atlas-apex added a commit that referenced this pull request May 21, 2026
…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>
atlas-apex added a commit that referenced this pull request May 21, 2026
…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>
me2resh added a commit that referenced this pull request Jun 5, 2026
…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>
me2resh added a commit that referenced this pull request Jun 5, 2026
…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>
me2resh added a commit that referenced this pull request Jun 5, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants