Skip to content

release(#452): v2.2.0#453

Merged
atlas-apex merged 147 commits into
mainfrom
release/v2.2.0
May 29, 2026
Merged

release(#452): v2.2.0#453
atlas-apex merged 147 commits into
mainfrom
release/v2.2.0

Conversation

@atlas-apex

@atlas-apex atlas-apex commented May 29, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Cuts v2.2.0 from upstream/dev — bundles work merged since v2.1.0 (2026-05-25) into three themes: local agent routing, split-portfolio v2 hardening, and release-cycle plumbing. No NEW breaking commits in window (the !: lingering in git log v2.1.0..dev is from v2.0.0's Hatim→Hakim consolidation, still visible due to squash-divergence).
  • Single commit on this release branch — CHANGELOG.md prepend. Every functional change is already live on dev via its own PR; this PR is the release-notes prepend + tag enabler. The release skill's path (branch from dev → edit CHANGELOG → open PR) keeps the release commit tight and reviewable even when the cumulative dev→main diff is large.
  • Release-notes scope is editorial. The CHANGELOG narrative documents the highlights and is intentionally narrower than the full commit log — internal plumbing work that doesn't change adopter-facing behaviour ships via the squash without a release-notes line. Anyone wanting the full inventory has git log v2.1.0..upstream/main --oneline after merge.
  • Post-merge: tag v2.2.0 on upstream/main, then run the mandatory /release-sync v2.2.0 (step 9 of the release flow) to merge maindev and prevent squash-divergence from accumulating into the next release cycle.

Highlights — what's in v2.2.0

Added

PR Issue Headline
#432 #417 /handover clones the target repo at step 1.5 (3–15× cheaper reads)
#440 #438 Ollama/LiteLLM local agent routing — reachability + model-pulled + session env
#451 #448 /release-sync carries forward CHANGELOG.md from main to dev
#450 #449 Rex handbook discovery — additive, opt-in, fail-soft supplement

Fixed

PR Issue Headline
#441 #373 Split-portfolio v2 partial-config detection + 10 skill helper-source fixes
#430 #414 Regression guard against legacy v1 walker hooks
#431 #415 /split-portfolio configures branch protection on the private sibling repo
#423 #419 Bootstrap exemption scope guard (narrowed to /handover only)
#425 #424 Hook walker reads session pin before walking the cwd up
#427 #426 Merge hook handles compound marker-write + merge command shapes
#437 #434 SETUP step 1 routes onboarding.yaml through portfolio helper
#444 #442 SessionStart warns when local agent routing is INACTIVE
#445 #443 Per-block helper-source preamble across 10 prompt-based skills

Changed

PR Issue Headline
#447 #446 CHANGELOG.md on dev resynced with main (one-off content; #448 closes the mechanism)

CHANGELOG entry (the diff for this PR)

See CHANGELOG.md in this PR's diff for the full v2.2.0 entry — Added / Fixed / Changed sections plus a Compatibility note. The entry is prepended above the v2.1.0 entry; no existing entries are modified.

Testing

  • git log release/v2.2.0 ^upstream/dev --oneline shows exactly one commit (the CHANGELOG prepend)
  • git show release/v2.2.0 --stat confirms CHANGELOG.md is the only file touched in this PR's commit
  • CHANGELOG entry passes a "no forbidden mid-paragraph references" scan
  • Rex code review on this commit
  • After merge: git fetch upstream main && git tag v2.2.0 upstream/main && git push upstream v2.2.0
  • After tag: /release-sync v2.2.0 opens the main→dev sync PR (or confirms already in sync via [Feature] /release-sync — carry forward CHANGELOG.md additions from main to dev #448's carry-forward step)

Post-merge actions

  1. git fetch upstream main
  2. git tag v2.2.0 upstream/main
  3. git push upstream v2.2.0
  4. /release-sync v2.2.0 — mandatory step 9 of the release flow
  5. Optional: gh release create v2.2.0 --repo me2resh/apexyard --notes-file <v2.2.0 CHANGELOG section>

Closes #373
Closes #452

Glossary

Term Definition
Release-cut model apexyard's gitflow-lite: daily PRs merge to dev; main only receives release PRs from dev. AgDR-0007.
/release-sync Mandatory follow-up skill that syncs main back to dev after every release, preventing squash-merge SHA divergence from accumulating.
Squash-divergence The SHA gap created when a release PR is squash-merged: main has one squash commit; dev still carries the un-squashed equivalents. Why git log v2.1.0..dev shows ~145 commits when the functional v2.2.0 delta is smaller.
MINOR bump semver: backwards-compatible new functionality. Triggered by feat: commits when no breaking marker is present.
Editorial release-notes scope The CHANGELOG documents the highlights, not every commit. Internal plumbing changes ship via the squash without an entry — the commit log is the full inventory.
Multi-close marker <!-- multi-close: approved --> HTML comment that bypasses the single-Closes-#N-per-PR hook. Release PRs legitimately close many tickets at once.

atlas-apex and others added 30 commits April 24, 2026 11:11
#118)

* chore(#109): project-configurable ticket / branch / commit / PR schema

Lift the prefix / type whitelists hardcoded across skills, hooks, and
CI into a versioned JSON config read through a shared shell library.
Shipped defaults at .claude/project-config.defaults.json; per-fork
overrides at the optional .claude/project-config.json; one reader
(_lib-read-config.sh) that every consumer now uses.

Added:
- .claude/project-config.defaults.json (v1 schema)
- .claude/hooks/_lib-read-config.sh (shared reader)
- docs/project-config.md (schema reference + extension guide)
- docs/agdr/AgDR-0006-project-configurable-ticket-schema.md

Migrated (still pass with no config present via last-resort fallback):
- validate-branch-name.sh  → .branch.type_whitelist
- validate-commit-format.sh → .commit.type_whitelist (legacy
  `commit_types` top-level key honoured as backward-compat fallback)
- validate-pr-create.sh    → .pr.title_type_whitelist
- /feature, /task, /bug skills reference the config in their Rules
  sections; none hardcodes the list any more

Unlocks subsequent config-readers for #107 / #110 / #111 / #112 /
#113 / #114 / #115 — each extends the schema under its own subtree
without further changes to the loader.

#109

* fix(#109): satisfy markdownlint MD032 and MD060 on new docs

Auto-fix MD032 (blank lines around lists) in AgDR-0006 and format
table-separator rows with surrounding spaces (MD060) in both new
doc files. Content unchanged; CI green.

---------

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Adds a new PreToolUse hook that blocks gh issue/PR/comment creation and
gh api .../issues|/pulls calls targeting a public framework repo
(default: me2resh/apexyard + whatever `upstream` resolves to) when the
title or body references any registered private project from
apexyard.projects.yaml (by name, repo slug, owner/repo#N ticket ref, or
workspace path).

The hook is a sibling to check-secrets.sh — both scan outgoing content
for identifiers that should never leave the local environment. Skip
marker `<!-- private-refs: allow -->` in the body lets a deliberate
reference through with a visible warning.

Files touched:
- .claude/hooks/block-private-refs-in-public-repos.sh (new)
- .claude/hooks/tests/test_block_private_refs.sh (new)
- .claude/rules/leak-protection.md (new)
- .claude/settings.json (wire PreToolUse matchers for the 5 gh shapes)
- docs/rule-audit.md (append section 10 + bump counts)

Refs: #110

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Fires after `git push` to surface review markers that have gone stale
because new commits were pushed past an existing Rex / CEO / design
approval. The merge gate already catches this at `gh pr merge` time,
but only then -- this hook closes the gap by flagging it immediately
at push-time so the author isn't surprised at merge.

- `.claude/hooks/warn-stale-review-markers.sh`
  - PostToolUse, non-blocking (PostToolUse exit 2 would push noise
    into the conversation; this hook is purely informational).
  - Resolves the PR HEAD via `gh pr view --json headRefOid` -- same
    source-of-truth as the merge-gate hooks post-apexyard#47 / #55.
    Falls back to local HEAD with a visible WARN when gh is offline.
  - Silent on: no PR for branch, no markers, fresh markers,
    failed push (detected via `rejected` / `failed to push` /
    `fatal:` / `error:` markers in tool_response.stderr).
  - Modes: `warn` (default) prints one stderr line per stale marker;
    `delete` opts in to auto-removal via
    `.claude/project-config.json` -> `review_markers.on_stale`.
    TODO(apexyard#109): switch to the shared project-config reader
    once it lands.
- `.claude/settings.json`
  - Wires the hook on PostToolUse / Bash / `git push *`.
- `docs/rule-audit.md`
  - Adds a row under section 3 (Code review & PR quality) and
    bumps the mechanized count 26 -> 27 / total 73 -> 74.
- `.claude/hooks/tests/test_warn_stale_review_markers.sh`
  - 8 cases: no PR, no markers, fresh markers, stale rex / ceo /
    design (warn), delete mode, failed push. All pass locally.

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
… check-runner (#121)

* chore(#111): upgrade pre-push-gate from reminder to blocking check-runner

Previously pre-push-gate.sh just printed a checklist of things to run
locally before pushing — it was advisory. The rule it enforces is a
HARD STOP per pr-workflow.md. That asymmetry meant agents routinely
pushed broken work and discovered it only when CI went red.

Replaces the reminder with a blocking runner that reads the list of
shell commands from project config (.pre_push.commands) and executes
them in sequence before a push is allowed through. First non-zero
exit blocks the push with exit 2 and prints the failing command plus
the last 20 lines of its output.

- Config key: .pre_push.commands[] — array of {name, run} objects.
  Shipped default is an empty list (hook stays a no-op on repos that
  haven't configured their checks yet, including the framework repo
  itself until it wires its own CI).
- Emergency bypass: '<!-- pre-push: skip -->' in the HEAD commit
  message. Grep-able on purpose so bypasses stay auditable.
- Fail-fast: once a command fails, the rest don't run. Parallel
  execution is a follow-up polish.
- 7 test cases in .claude/hooks/tests/test_pre_push_gate.sh — all
  pass on the shipped default + a minimal custom config.

Updates docs/rule-audit.md to flip "partial" → "yes" for the
"before git push" rule.

Integrates with the shared config reader landed in #109.

#111

* fix(#111): remove orphaned footnote reference from rule-audit

The previous advisory-mode footnote was superseded by pre-push-111
but its definition was accidentally kept, tripping markdownlint MD053
(unused reference definition). Drop it.

---------

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Mechanically enforces the ticket body schema when an agent files raw
`gh issue create` calls instead of going through the interactive
/feature, /task, /bug skills. Matches bracketed title prefix
([Feature] / [Chore] / [Bug] / [Docs] / etc.) against
`.ticket.required_sections` in project-config, and blocks (exit 2)
when any required section is missing or empty. Skip marker
`<!-- validate-issue-structure: skip -->` bypasses with a visible
stderr WARN for legitimate off-template tickets (epics, meta-threads).

Changes:

- .claude/hooks/validate-issue-structure.sh — the hook; reads schema
  via the shared _lib-read-config.sh, with inlined defaults for bare
  checkouts predating the config-schema rollout. Handles
  --body / --body-file / -F path.
- .claude/project-config.defaults.json — extends .ticket with
  required_sections (Feature/Chore/Refactor/Testing/CI/Docs/Bug) and
  skip_marker; other .ticket fields untouched.
- .claude/settings.json — new PreToolUse matcher on Bash(gh issue
  create *) alongside the existing suggest-ticket-template.sh and
  block-private-refs-in-public-repos.sh hooks.
- .claude/hooks/tests/test_validate_issue_structure.sh — 15 cases
  covering pass + fail paths per prefix, empty section detection,
  skip marker, unknown prefix, non-gh invocation, --body-file path.
- docs/rule-audit.md — new section 11 row, mechanized count +1.

Upstream ticket: #107

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Closes the asymmetry noted in .claude/rules/agdr-decisions.md: every
other HARD STOP in the ruleset (merge approval, ticket-first,
migration-first) is mechanically enforced, but the /decide HARD STOP
was prose-only. The commit-time hook require-agdr-for-arch-changes.sh
catches one architectural change at commit; this new PR-time hook
catches the cumulative diff so reviewers always have a pointer to the
decision record.

- New hook at .claude/hooks/require-agdr-for-arch-pr.sh
  - Fires on Bash(gh pr create *)
  - Parses --title/--body/--body-file/-F <path>
  - Resolves base branch from --base, else upstream/dev, origin/dev,
    upstream/main, origin/main, main, master (in that order)
  - Computes `git diff <merge-base>..HEAD --name-only`
  - Triggers on any changed file matching .agdr_trigger_paths[], OR any
    dep-file addition (package.json via jq key-set diff; other
    dep files via a commented +/- line-count heuristic — version
    bumps match +/- counts and do not fire)
  - Blocks (exit 2) with a helpful message naming the triggers and
    pointing at /decide if the body has no `AgDR-\d+-[a-z0-9-]+`
    reference
  - Skip marker `<!-- agdr: not-applicable -->` bypasses with a
    visible WARN on stderr
  - Silent exit 0 on non-gh commands, empty diffs, unresolvable base

- Wired via .claude/settings.json PreToolUse Bash(gh pr create *)

- Adds two new top-level keys to .claude/project-config.defaults.json:
    agdr_trigger_paths      (shell globs — domain/, infrastructure/,
                             migrations/, *.tf, .github/workflows/, etc.)
    agdr_trigger_dep_files  (literal basenames — package.json,
                             pyproject.toml, Cargo.toml, go.mod, Gemfile)
  Hook has inline fallback defaults kept in sync.

- Adds docs/rule-audit.md entry in the AgDR section; bumps mechanized
  count 26 to 27 and total rows 73 to 74.

- Adds .claude/hooks/tests/test_require_agdr_for_arch_pr.sh (7 cases;
  all green): path-triggered without AgDR (block), with AgDR (pass),
  dep-file added (block), version-only bump (no fire), skip marker
  (pass + warn), non-matching diff (pass), non-gh command (no-op).

Closes #112

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Extends validate-pr-create.sh with a required-sections check that
replaces the hardcoded Glossary-only grep. The list of required H2
headings is project-configurable via `.pr.required_sections[]`.
Shipped default is ["Testing", "Glossary"], matching the canonical PR
description shape in workflows/code-review.md.

- Each entry must appear as `## <Name>` (case-insensitive).
- Empty sections are tolerated at this layer (the issue-structure hook
  #107 does stricter empty-content checks for issue bodies; for PR
  bodies, empty sections are left to the reviewer's judgement).
- Skip marker `<!-- pr-sections: skip -->` bypasses with a visible
  stderr WARN — for trivial PRs (lint-only fixes, version bumps)
  where the full template is overkill.
- Reads from project config via the shared _lib-read-config.sh (#109).
  Inline fallback matches shipped defaults so bare checkouts predating
  #109 keep working.
- 8 test cases cover: all-sections pass, each missing section,
  missing-both (both errors printed), skip marker, case-insensitive
  headings, H3 rejection.

#113

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#114): enforce single Closes-keyword per PR body

Caps distinct auto-closing references (close/closes/closed, fix/fixes/
fixed, resolve/resolves/resolved + N or owner/repo+N) at one per PR
body. Closes the loophole where the title validator limited the title
to one ticket but multiple Closes lines in the body would still auto-
close all of them on merge.

- Scans stripped of fenced code blocks so closing keywords inside a
  code sample do not count.
- Distinct counting: the same number referenced twice (e.g. via Fixes
  and Closes) counts as one.
- Cross-repo refs (owner/repo+N) count normally.
- Opt-in escape hatch: pr.allow_multiple_closes=true in
  project-config disables the check for teams that deliberately batch
  rollbacks or dependency bumps.
- Per-PR bypass: a multi-close-approved HTML comment in the body
  prints a visible stderr WARN and lets that PR through. Grep-able
  trace so bypasses are auditable.
- 10 test cases cover: one close passes, no-keyword passes, two
  distinct block, three mixed block, same-number-twice passes, code-
  fence-ignored, skip marker, cross-ref without keyword, opt-in
  config, cross-repo close.

Reads configuration via the shared _lib-read-config.sh (apexyard+109).

#114

* fix(#114): strip inline backticks and tilde fences from close-count scan

Rex caught a self-reflexive bug in the initial commit: documentation
mentioning closing keywords inside inline backticks (say a PR body
that explains the new hook with examples) counted as real closes, and
a skip marker inside inline backticks silently bypassed the check.
Future PRs that document the feature would trip the same trap.

Fix the code-region stripper to cover:
- Triple-backtick fences (already handled)
- Tilde fences (new)
- Inline-backtick spans (new)

Also run the skip-marker check against the stripped body, so a marker
used purely as documentation no longer activates a real bypass.

Three new test cases pin the behaviour:
- closing keywords in inline backticks are ignored
- skip marker inside inline backticks does NOT bypass
- tilde fences also get stripped

13/13 tests pass.

---------

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
The fast happy path for filing 5–20 structured tickets in one intent
without dropping to raw `gh issue create` (non-conformant) or running
`/feature` 20 times serially (~100 turns of interview).

- .claude/skills/tickets-batch/SKILL.md — new skill spec. Asks
  shared-context questions (priority, epic, area-labels, repo) ONCE
  for the whole batch, then runs a ≤3-question micro-interview per
  ticket (type, one-line purpose, optional clarification when the
  inference is low-confidence). Confirms the full batch as a table,
  then files each via specific `gh issue create` calls (never a
  bulk JSON dump — the validator runs per-issue). Output conforms
  to `.ticket.required_sections` by construction. Caps at 20
  tickets per invocation.

- CLAUDE.md — added a row for /tickets-batch in the Available
  Skills table; bumped the count references from 33 to 34.

Refs #108

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
- Add `.claude/skills/fan-out/SKILL.md` — spawns N parallel Agent calls
  in a single assistant message, with per-task agent type, worktree
  isolation, and foreground/background mode. Caps at 5 concurrent
  agents. Refuses fan-out when tasks share file write targets or have
  sequential dependencies. Includes pre-spawn active-ticket safety
  check and worktree merge-back flow that pauses on conflict.
- Add `.claude/rules/parallel-work.md` — trigger heuristic for when an
  agent should proactively offer fan-out (>= 2 file-independent,
  context-independent, individually substantial work items). Pairs
  with the skill: rule says when, skill says how.
- Update `CLAUDE.md` — bump rules count to 9, skills count to 34, add
  `/fan-out` row to the skills table.

Refs #117

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
…work only (#126)

* chore(#116): adopt release-cut branch model (dev/main + tags)

Formalises the dev/main split that already exists informally — dev is
the daily-work branch where every PR lands, main is release-only,
tagged with semver on each merge. Framework-only: managed projects
under apexyard governance stay trunk-based.

Added:
- AgDR-0007 — decision record (options table covers full git flow vs
  trunk-only vs gitflow-lite; chose gitflow-lite)
- /release skill — diff dev against main, propose semver bump from
  conventional commits, generate CHANGELOG, open release PR, tag
  after merge
- docs/release-process.md — prose runbook for cutting a release
  (manual fallback for the skill)
- .git.protected_branches in project-config.defaults.json
  (main/master/dev/develop)

Modified:
- block-main-push.sh — now blocks direct pushes/commits to all
  configured protected branches (was: hardcoded main/master). Reads
  .git.protected_branches via the shared config reader (apexyard#109).
- CLAUDE.md — new section under Git Conventions explaining the
  dev/main model + the framework-only scope. Skill table entry for
  /release. Skills count bumped to 34.
- docs/multi-project.md — note that upstream/main is release-only
  and the dev/main split is framework-only.

Non-consequences (per AgDR-0007):
- No release/* or hotfix/* branches. Hotfixes are normal patches
  cut quickly. Revisit if multi-version maintenance becomes a need.
- No automatic on-merge issue closing for dev PRs. The release PR's
  body aggregates all Closes references for the batch and triggers
  auto-close en masse when it merges to main. Manual close in the
  meantime.
- CI workflows trigger on pull_request regardless of base, so
  dev-targeting PRs already get the full check matrix — no
  workflow file edits needed.

#116

* fix(#116): satisfy markdownlint MD032 + MD060 in 116 docs

Auto-fix added blank lines around lists in AgDR-0007 (MD032) and
spaced the table separators in AgDR-0007 + multi-project.md (MD060).
Content unchanged.

---------

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
…129)

* fix(#106): CHANGELOG fallback in drift hook for squash-merged forks

The v1.1.0 tag-reachability check (`git tag --merged main`) misfires on
forks that sync via GitHub's default squash-merge: the squash collapses
the upstream-tag commit into a synthetic SHA, the tag stops being
reachable, and the banner keeps firing forever.

Discovered live on the first real-world `/update` flow: ops fork
squash-merged the v1.1.0 sync PR, banner kept saying "v1.1.0 available"
even though the fork was content-caught-up.

This commit adds a CHANGELOG-content fallback that fires only when the
primary tag check fails. If the fork's main has a heading
`## [X.Y.Z]` matching the upstream tag's version, treat the release as
absorbed and stay silent. Tolerant grep (matches the apexyard CHANGELOG
format from v1.1.0 onward, with leading-`v` stripping for tag→heading
conversion).

The merge-commit and rebase paths are unchanged — primary tag check
still works for them, and the fallback never fires when it shouldn't.

Test coverage (5 cases):
- squash-merge fork caught up to v1.1.0 → silent (the fix)
- merge-commit fork caught up to v1.1.0 → silent (regression check)
- fork stopped at v1.0.0 → banner fires
- fork has its own newer tag → silent
- squash-merge but no CHANGELOG on fork → banner fires (no false silence)

Records the strategy update in docs/agdr/AgDR-0008-…md (extends
AgDR-0005's tag-based-drift design).

#106

* fix(#106): satisfy markdownlint MD032/MD060 + shellcheck SC2164

Rex flagged two CI-blocking issues on the original 106 commit:

- AgDR-0008 had three bulleted sub-lists in the Consequences section
  without surrounding blank lines (MD032). Added blanks and padded the
  one tight-pipe table separator (MD060).
- The 106 test fixture had five subshell `cd "$fk"` calls without
  `|| exit 1` (SC2164). Added the guard to all five.

5/5 tests still pass after the fix. No semantic change to the hook
or the test logic.

---------

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
A 10-minute, 5-question check that sits between /idea and /write-spec.
Designed for solo founders running ApexYard — not a heavyweight
methodology like event storming or Wardley mapping.

Five questions, asked one at a time:
  1. Who is this specifically for?
  2. What do they do today instead?
  3. What's the smallest version that proves the value?
  4. What would prove this is wrong? (kill criteria)
  5. Build, buy, or rent?

Output: a one-page validation doc with a GREEN/YELLOW/RED verdict.
RED auto-updates the IDEA-NNN backlog row to WONTDO.

Integration:
  /idea — adds an optional default-no "Validate now?" step after
    capture (and after the optional GitHub Issue offer).
  /handover — adds a conditional "this looks dormant, validate?"
    step at the end of the integration plan, gated on the dormancy
    heuristic (last commit > 90d AND zero open PRs AND no recent
    issue activity). Healthy projects don't see the prompt.

CLAUDE.md skills count bumped to 35; new skills row added.

#130

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
…prompts-on-pause) (#135)

Stop hook that speaks the assistant's question aloud (Jarvis-style)
when it pauses for user input. Initial phase is macOS-only via `say`,
no voice input — user replies via keyboard.

Default OFF. Adopters opt in by overriding `voice_prompts.enabled` to
true in `.claude/project-config.json`.

Files:
- .claude/hooks/voice-prompt-on-pause.sh — Stop hook with config gate,
  trigger heuristic (questions-only by default), markdown stripping,
  sentence-boundary truncation, fire-and-forget say invocation
- .claude/hooks/tests/test_voice_prompt_on_pause.sh — 9 cases covering
  disabled-default, enabled+question, enabled+statement, approved-pattern,
  abc-menu, malformed-transcript, no-say-on-PATH, trigger-always,
  markdown-stripping
- .claude/project-config.defaults.json — voice_prompts schema block
  added (enabled, voice, max_chars, rate_wpm, trigger), default OFF
- .claude/settings.json — new Stop hook entry wired with the standard
  ops-root resolver wrapper
- docs/agdr/AgDR-0009-voice-prompts-on-pause.md — design rationale,
  options matrix (status quo / macOS say / cloud TTS / ML detection),
  consequences, future phases
- docs/project-config.md — new "Voice prompts" section with override
  examples and privacy notes

Test mode: hook respects VOICE_PROMPTS_SYNC=1 to run say synchronously
(test runners need this so assertions don't race against orphaned
background processes). Production invocations always run async.

Future phases (out of scope here, AgDR §"Future phases"):
- Phase 2: cross-platform TTS (Linux espeak, Windows SpeechSynthesizer)
- Phase 3: cloud TTS providers (OpenAI, ElevenLabs) — privacy AgDR-worthy
- Phase 4: voice input via Whisper-based STT
- Phase 5: per-message overrides

Refs: #134

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
…#142)

* feat(#141): add /debug skill — structured hypothesis-driven debugging

Adds a methodology skill that enforces five disciplines:

  1. Capture the symptom precisely (exact URL, exact response, exact step)
  2. Read the architecture before guessing (map every layer the request
     touches, file by file)
  3. Form a hypothesis ladder (3–5 candidates, each with an explicit
     evidence test that confirms or refutes it)
  4. Gather evidence first, fix second
  5. Verify the fix against the original symptom evidence (re-run the
     same `curl` / browser repro you used in step 4 — unit tests
     verify code, not feature, correctness)

Stack appendices (Web, Desktop) carry stack-specific surface-evidence
requirements (step 1), architecture-surface maps (step 2), and
evidence-tests cookbooks (step 4). The methodology body stays portable
across stacks; appendices are where stack-specific knowledge accrues
over time.

Web appendix covers browser routing, framework configs (Next/Nuxt/Vite),
SPA-fallback layers, CDN, origin, the shared API client, backend
handlers, and auth providers. Desktop appendix covers Electron / Tauri
/ native-shell concerns: app entry points, IPC bridges, native modules,
auto-updater, sandbox / entitlements, code signing, crash reports.

Includes "When NOT to use" guidance so the methodology overhead doesn't
sandbag simple bugs (typos, off-by-ones, greenfield exploration).

Motivated by a real OAuth debug session in a managed project where
three sequential fixes chased adjacent symptoms because each was
hypothesis-then-fix without evidence in between. The skill is the
"never do that again" guardrail.

Closes #141

* fix(#141): scrub private project issue numbers from anti-pattern table

Rex review on PR #142 caught that line 155 of the skill's anti-pattern
table still named the originating PRs (#375, #377, #380) from the
private project where the methodology was first exercised. The PR body
and commit message were correctly abstracted earlier, but this in-file
reference slipped through — the leak-protection hook only scans gh
issue/pr writes, not staged file content, so mechanical enforcement
didn't catch it.

Replaced with "Three sequential PRs chasing the same symptom because
each was based on a different guess (no evidence test in between
cycles)" — same pedagogical value, zero attribution.

Refs #141

---------

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
…144)

Adopters on GitHub Free with any private project hit a silent privacy bug
following today's docs: forking apexyard makes a public fork that you cannot
later flip to private (GitHub policy), and committing `apexyard.projects.yaml`
+ `projects/<name>/` to the fork publishes private project names + handover
findings on a public GitHub repo.

This PR documents the supported workaround — split-portfolio mode — and
adds an upfront privacy gate to the /setup skill so new adopters never
hit the trip-wire silently.

docs/multi-project.md:

- New "Two setup modes — pick the one that matches your privacy needs"
  section before TL;DR, with a side-by-side table and the explicit
  trip-wire callout.
- Existing TL;DR retitled "TL;DR — single-fork mode (default)" with a
  one-line pointer to the split-portfolio section.
- New "Split-portfolio mode — public framework + private portfolio"
  section between the existing setup steps and the directory-layout
  section. Includes:
  - The two-repo layout (~/ops/apexyard public + ~/ops/portfolio private)
  - 7-step setup walkthrough with copy-pasteable commands
  - Daily workflow + upstream sync notes (both unchanged)
  - Trade-offs (two repos to maintain, two clones per machine, one
    upstream-sync conflict path on `projects/README.md`)
  - "Migrating from single-fork to split-portfolio" recovery flow with
    the explicit warning that GitHub Issue / PR edit history survives a
    force-push and must be redacted separately

.claude/skills/setup/SKILL.md:

- New Step 2a: privacy gate — asks "are any projects private?" before
  proposing the config. Branches on the answer:
  - All public                                → single-fork mode
  - GitHub Pro / Team / Enterprise            → single-fork mode (private
                                                  forks of public repos
                                                  are supported on those
                                                  plans)
  - Any private + GitHub Free                 → split-portfolio mode
- New Step 2b: walks through the split-portfolio setup interactively
  (private repo create, sibling clone, gitignore + symlink) when the
  privacy gate triggers.
- Detection: `test -L apexyard.projects.yaml` short-circuits Step 2b
  for adopters already in split mode.
- Explicit "do NOT auto-migrate" rule for adopters already in single-fork
  mode with private names already pushed — that path is destructive
  (force-push history rewrite + redact issue/PR bodies + delete backup
  branch) and warrants a deliberate, eyes-open run, not a /setup side
  effect.

Out of scope for this PR (tracked separately on #143):

- `portfolio:` config block in `onboarding.yaml` schema
- Skill audit + refactor to honour configured `registry` / `projects_dir`
  / `ideas_backlog` paths instead of hardcoded fork-relative paths
- `/split-portfolio` migration helper skill that automates the recovery
  flow currently documented manually

This is the docs-and-setup-question minimum-viable starter — the
framework code refactor is mechanical and lands as a follow-up.

Refs #143

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
…#147)

* feat(#145): portfolio config block + self-healing + /split-portfolio helper

Closes the framework primitive deferred from #144. Adds
first-class config-driven path resolution for the portfolio registry,
projects dir, and ideas backlog, with self-healing surfacing of broken
config at session start, plus a new /split-portfolio skill that automates
the destructive recovery flow.

Schema, helper, and hook:
- .claude/project-config.defaults.json: new portfolio: block
  (registry, projects_dir, ideas_backlog) with defaults matching
  today's single-fork layout
- .claude/hooks/_lib-portfolio-paths.sh: new sourceable helper exposing
  portfolio_registry, portfolio_projects_dir, portfolio_ideas_backlog,
  portfolio_validate, portfolio_clear_cache. Resolves relative paths
  against the ops-fork root.
- .claude/hooks/check-portfolio-config.sh: new SessionStart hook —
  silent on OK, one-line banner on broken config, never blocks session
- .claude/hooks/tests/test_portfolio_paths.sh: 13 cases covering
  defaults, absolute/relative overrides, validate states, cache clear

Skill audit (18 SKILL.md files):
- Adds Path resolution callout pointing at the helper
- handover bash blocks now source helper and use $(portfolio_registry)
  instead of literal apexyard.projects.yaml
- setup Step 2b now writes the portfolio: config block (recommended)
  and validates via portfolio_validate before declaring success;
  symlink approach kept as legacy fallback

New skill (.claude/skills/split-portfolio/SKILL.md):
- 10-step migration with explicit operator-confirmation gates at each
  destructive step (force-push, body redaction, branch deletion)
- --verify mode: read-only state report (mode, paths, validate, drift)
- --dry-run mode: prints commands without executing
- Pre-flight refusals: already-private fork, paid GitHub plan, dirty
  working tree, already-migrated state
- Step 9 writes the portfolio: config block (not symlinks) — symlink
  fallback documented for adopters on older framework versions
- Step 9 surfaces the GitHub timeline-API survival caveat verbatim
- Idempotent re-runs: detects partial-migration state and resumes

Docs (docs/multi-project.md):
- Layout section describes both modes (config-block recommended,
  symlink legacy) with self-healing notes
- Setup steps split into config-block mode and legacy symlink mode
- Migration section now points at /split-portfolio skill; manual
  recipe preserved as fallback

AgDR (docs/agdr/AgDR-0010-portfolio-config-and-self-healing.md):
- Full Y-statement, options, decision, consequences, future phases
- Schema decision rationale: project-config.json over onboarding.yaml
  because runtime path resolution belongs in project-config

Closes #145
Refs #146 (delivered same PR; closed manually post-merge
per the single-Closes-keyword rule in validate-pr-create.sh)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(#145): markdownlint MD031 — blank lines around fences

CI's markdownlint-cli2 (v0.34.0) flagged the JSON + bash fenced code
blocks I added in setup/SKILL.md Step 2b without surrounding blank
lines. Added the required blanks. No content change.

Refs #147

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>
…ork auto-publish (#149)

The privacy-gate wording introduced in PR #144 (and unchanged in PR #147)
attributed the publication to the framework rather than the adopter:

  "the standard fork-and-commit setup will silently publish your private
   project names on a public GitHub repo"

That's factually wrong. ApexYard never pushes anything without explicit
operator approval — the publication only happens when the adopter
themselves runs git push. The "silently publish" framing read as if the
framework auto-publishes, which is misleading and undermines trust in
the rest of the framework's safety claims.

Two prose-only edits, no code, no behavior change:

- .claude/skills/setup/SKILL.md Step 2a — replaced "will silently
  publish ..." with adopter-action language ("you might accidentally
  publish ... a stray git push after registering them — I won't push
  without your approval, but the risk is on the adopter once the data
  is committed locally")

- docs/multi-project.md trip-wire callout — replaced "silently publish
  their portfolio names the moment they push" with "risk accidentally
  publishing their portfolio names with a stray push (the framework
  itself never pushes without operator approval, but once the registry
  is committed locally the next push exposes it)"

Verified: `grep -r "silently publish" .claude/skills/ docs/` returns no
hits.

Closes #148

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the legitimate-bypass case (#150) and the
illegitimate-bypass case (#151) together so the
ticket-first gate is coherent. Shipping either alone would leave a
window where the framework is internally inconsistent — see AgDR-0011
for the full rationale.

Bootstrap exemption (#150):
  - .claude/session/active-bootstrap marker, written by /setup,
    /handover, /update, /split-portfolio on entry; cleared on exit
  - SessionStart sweep (clear-bootstrap-marker.sh) for stale markers
    from interrupted sessions
  - require-active-ticket.sh reads the marker and exempts skills on
    the configured ticket.bootstrap_skills list
  - bootstrap_skills list lives in .claude/project-config.defaults.json
    (extendable per fork via .claude/project-config.json)

Bash-write coverage (#151):
  - new _lib-detect-bash-write.sh — heuristic detector for output
    redirection, tee, sed -i, awk -i inplace, python/node/ruby
    embedded interpreters
  - require-active-ticket.sh + require-migration-ticket.sh now fire
    on Bash in addition to Edit|Write|MultiEdit
  - design choice: false-negatives preferred over false-positives
    (the matcher errs toward "let through" rather than block legit
    read-only commands)

Tests: 32 unit cases on the lib + 12 integration cases on the hook,
including the exact #151 bypass repro from the issue
body.

Closes #150

Will manually close #151 post-merge per the
single-Closes-per-PR rule (precedent: AgDR-0010 / PR #147).

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…155)

Closes #153.

Extends `_lib-detect-bash-write.sh` (introduced for #151
in PR #152) with the matcher families flagged by Rex's review of #152.
AgDR-0011 already frames the matcher as a living list extended on
observation; this commit just walks the list.

New matcher families:
- File-moving builtins: `cp`, `mv`, `rm`, `dd`, `install` (anchored at
  command-start; `--help`/`--version` and `git rm`/`git mv` excluded)
- Archive / network writes: `tar -x` / `tar --extract`, `curl -o` /
  `--output`, `wget -O` / `--output-document`
- Additional embedded interpreters: `perl -e`, `php -r` (keyword-gated
  like python/node/ruby); `go run`, `deno run`/`deno script.ts`,
  `bun run`/`bun script.ts` (categorical script runners)
- Python helpers: `pathlib.Path().touch()`, `shutil.copy*`,
  `shutil.move`, `os.rename` added to the `python -c` and python
  heredoc keyword list
- Heredoc variants for `ruby` and `node` (previously only python
  heredoc was covered)

Extractor extensions:
- `cp` / `mv`: last positional arg
- `curl -o` / `--output`: file argument
- `wget -O` / `--output-document`: file argument
- `tar -x`, `go run`, `deno`, `bun`, `perl -e`, `php -r`: return empty
  (caller applies gate categorically per AgDR-0011)

Test count rose from 32 to 86. Negative-class counterexamples cover
the trickiest false-positive surfaces: `tar -t` listing, `cp --help`,
`rm --version`, `git rm`, `curl` bare URL fetch, `wget` bare URL fetch,
`deno fmt`, `deno test`, `go build`. Existing
`test_require_active_ticket_bash.sh` regression suite still passes 12/12.

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…cy (#156)

- add _lib-mock-gh.sh helper that installs a fake `gh` on the sandbox PATH;
  intercepts `gh issue view <N> ... --json ...` and returns synthetic
  `{"number":N,"state":"OPEN"}` (overridable per-num via mock_gh_set_state)
- wire the shim into test_single_closes_per_pr.sh and
  test_validate_pr_required_sections.sh so the validator's CLOSED-issue
  refusal no longer breaks the suite when upstream issues are closed
- both files previously failed every case (0/13 and 0/8) because their PR
  titles reference #114 / #113, which are now CLOSED upstream
- post-fix: 13/13 and 8/8; full suite remains green

Closes #154

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
…#158)

* feat(#132): structured CEO marker + same-turn merge in /approve-merge

Closes #132 (drop the "stop before merge" rule) and
#48 (harden CEO marker against self-approval bypass)
together. The two threads compose — see AgDR-0012 for the full
rationale.

Streamline (#132):
  - /approve-merge now runs `gh pr merge --squash --delete-branch`
    in the same turn as the marker write, by default
  - --no-merge opt-out preserves the deferred-merge case
  - The discrete approval moment is the SKILL INVOCATION, not a
    follow-up "now do the merge" message

Harden (#48):
  - CEO marker is now a structured key/value file with required fields:
      sha=<HEAD>
      approved_by=user
      skill_version=2
    Validated by block-unreviewed-merge.sh; bare-SHA legacy markers
    rejected with a clear "stale format" error pointing at /approve-merge
  - The model's bare `echo SHA > <pr>-ceo.approved` bypass is now
    mechanically rejected. Forging the structured fields requires a
    deliberate, visible rule violation rather than a one-line accident
  - Optional audit fields (approved_at, approval_summary) capture the
    "what did the user say when they approved" trail
  - Rex marker stays bare-SHA — different threat model (automated
    reviewer, not human authorization moment)

pr-workflow.md reframed: "the load-bearing rule is explicit per-PR
approval, not two user messages." The merge is a deterministic
consequence of the approval invocation.

Tests: 12 cases on the hardened hook covering the new format end-to-end
(valid v2, missing rex/ceo, bare-SHA legacy rejected, missing
approved_by, wrong approved_by, skill_version=1, sha mismatch,
non-merge no-op, gh-api shape gated). Full suite: 205/205 across 13
test files.

Will manually close #48 post-merge per the
single-Closes-per-PR rule (precedent: PR #152 / AgDR-0011).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(#132): redact private project reference from AgDR-0012

Abstracted two references to a registered private project that named
the project's owner/repo. The leak-protection hook caught one in the
PR body; this fixup removes the matching references from the
AgDR-0012 file content (which would otherwise have shipped the names
to me2resh/apexyard public repo via the merge).

The pre-existing reference at .claude/rules/pr-workflow.md:130
(documenting #47) is untouched — it predates this PR and is already
on the public repo's history.

Refs #132.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(#132): markdownlint blanks-around-fences + typo fix

Two small fixups against red CI / Rex feedback:

- AgDR-0012 line 63: fenced code block now has a blank line before it
  (MD031). markdownlint-cli2 0.13.0 was rejecting the indented fence
  inside the bullet because the fence's preceding line was the bullet
  text (no blank).
- approve-merge SKILL.md line 172: typo `deferes` → `defers` (Rex flag
  on PR #158).

Refs #132.

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>
…#161)

* chore(#157): remove voice-prompts-on-pause feature + correct hook/skill counts

Closes #157 (sunset the voice-prompts feature) and
#77 (hook count off-by-one in CHANGELOG / CLAUDE.md)
in one bundled PR. AgDR-0013 supersedes AgDR-0009 with the full
rationale; both AgDRs are preserved (decision records are append-only
history).

Removed (#157):
  - .claude/hooks/voice-prompt-on-pause.sh
  - .claude/hooks/tests/test_voice_prompt_on_pause.sh
  - Stop matcher block in .claude/settings.json (became empty after
    voice removal)
  - voice_prompts block in .claude/project-config.defaults.json
  - "## Voice prompts" section in docs/project-config.md
  - voice_prompts mention in AgDR-0010 line 32 (replaced with
    leak_protection / ticket as still-current example config blocks)

Preserved:
  - docs/agdr/AgDR-0009-voice-prompts-on-pause.md — historical record;
    new "Superseded by: AgDR-0013" header at the top
  - AgDR-0010 line 115 reference to AgDR-0009 — still accurate as a
    historical pattern reference

Counts corrected (#77):
  - CHANGELOG.md v0.3.0 stats: "17 hooks" → "18 hooks" (historical
    fix — at v0.3.0 there were actually 18 hooks)
  - CLAUDE.md table line: "18 shell scripts" → "24 shell scripts"
    (current count after this removal)
  - CLAUDE.md table line: "35 slash commands" → "39 slash commands"
  - CLAUDE.md "Available skills (34)" → "Available skills (39)"
  - CLAUDE.md quick-reference "Skills (35 slash commands)" →
    "(39 slash commands)"

Why bundled: #77's correct count depends on whether voice is still in
the framework. Shipping #77 before #157 would write a number that's
wrong by one again the moment #157 lands. Same shape as previous
bundles (PR #152 / AgDR-0011, PR #158 / AgDR-0012).

Why no adopter-facing changelog mention of the voice removal: the
feature never reached a tagged release on main. v1.1.0 didn't have
it; v1.2.0 won't have it. From the adopter's perspective there's
nothing to retire. AgDR-0013 captures the framework's internal record
for future contributors. See AgDR-0013 § "No adopter-facing changelog
mention".

Tests: full hook test suite green (196 cases across 12 files —
test_voice_prompt_on_pause.sh removed). No regressions.

Will manually close #77 post-merge per the
single-Closes-per-PR rule (precedent: PRs #152, #158).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(#157): unwire voice from settings + configs + docs + AgDR-0013

Continuation of d78eb08 (the file deletions). Squash-merge will
collapse both into one PR commit. This commit captures:

- .claude/settings.json — Stop matcher block removed (was the only
  hook in it; entire matcher gone)
- .claude/project-config.defaults.json — voice_prompts block + its
  _comment removed
- docs/project-config.md — "## Voice prompts" section removed
- docs/agdr/AgDR-0009-voice-prompts-on-pause.md — "Superseded by"
  header added at the top, content otherwise preserved as history
- docs/agdr/AgDR-0010-portfolio-config-and-self-healing.md — line 32
  example reference swapped from voice_prompts to
  leak_protection / ticket (still-current config blocks)
- docs/agdr/AgDR-0013-sunset-voice-prompts.md — new supersession AgDR
- CLAUDE.md — hook count 18 → 24, skill count 35 → 39 (three
  occurrences each, all aligned to current reality)
- CHANGELOG.md v0.3.0 stats — "17 hooks" → "18 hooks" historical fix
  (#77 acceptance criterion 1)

Refs #157 + #77.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(#157): markdownlint MD028 + resolve stale AgDR-numbering ref

Three small fixups against Rex CHANGES-REQUESTED on PR #161:

- AgDR-0009 line 3: promote "Superseded by:" header out of a
  blockquote. The original "I decided ..." canonical blockquote at
  line 5 was being merged with the new supersession blockquote
  (markdownlint MD028 — "no blanks inside blockquote").
- AgDR-0013 line 3: same shape — "Supersedes:" header now a plain
  bold paragraph, canonical "I decided ..." blockquote untouched.
- approve-merge SKILL.md line ~187: stale conditional reference
  "AgDR-0012 (or 0013 — depends on whether voice-removal lands
  first)" — order is now resolved (12 = approve-merge bundle, 13 =
  voice removal). Drop the parenthetical.

Refs #157 + #77.

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>
Landing-site `site/index.html` terminal demo previously played one
canonical flow ("one ticket, start to finish") on autoplay. With the
v1.2.0 skill surface expansion (4 new skills, 8+ new hooks), one flow
no longer represents the framework's breadth.

Adds tabs to the terminal chrome — four flows visitors can either let
auto-cycle or click directly:

  1. one ticket — existing flow, unchanged content
  2. /handover  — adopt an external repo into the portfolio
  3. /setup     — first-run framework bootstrap on a fresh fork
  4. /fan-out   — spawn 3 parallel agents on independent tickets

Auto-advance: on completion of the active tab's script, the demo
pauses ~1.8s then advances to the next tab. Loops at the end. User
can interrupt by clicking any tab or hitting Replay.

Implementation:
- HTML chrome — single title span replaced with a tablist of 4 button
  tabs. ARIA `role="tablist"` / `role="tab"` / `aria-selected` so
  keyboard + screen-reader users get the same semantics as sighted
  ones.
- CSS — new `.shell-demo__tabs` + `.shell-demo__tab` with an accent
  underline on the active tab. Tabs scroll horizontally on narrow
  viewports (mobile responsive).
- JS — refactored the existing IIFE from one `script` array to an
  array of four. Added `setActiveTab()` for ARIA state, `play(idx)`
  takes a tab index, end-of-script auto-advances to `(idx + 1) % N`
  unless the user clicked away during the pause. Adds a new `cmd`
  type alongside `you` for slash-command invocations (renders with
  the same `>` prompt prefix). prefers-reduced-motion still bails
  early and leaves the static seed visible.
- Static seed (the non-JS fallback) still shows tab 0's content, so
  reduced-motion / no-JS visitors see the one-ticket flow as before.

Hero metrics also corrected to current reality (#77 / PR #161 covers
CLAUDE.md and CHANGELOG.md; this PR catches the same numbers in the
landing site):

  Skills    32 → 39
  Hooks     18 → 24

The tabs ship in v1.2.0 alongside the framework changes that make
the new flows worth showcasing.

Refs #160 (release v1.2.0 + landing-site refresh).

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…portfolio (#164)

Closes #163.

The split-portfolio mode docs and skills previously suggested
`your-org/ops` as the default name for the private sibling repo, and
`portfolio/` as the local clone directory. Both too generic — adopters
running multiple ops setups end up with `your-org/ops` collisions, and
a bare `portfolio/` dir gives no signal about which framework it
belongs to when it sits next to other unrelated `portfolio/` dirs.

`<fork>-portfolio` is now the default — keeps the relationship to the
public fork explicit on disk and on GitHub. If the fork is named
`your-org/apexyard`, the portfolio defaults to
`your-org/apexyard-portfolio`. If the fork was renamed (e.g. `cos`),
the portfolio defaults to `cos-portfolio`. Adopters with custom names
keep working — the `portfolio:` config block resolves whatever path
they configured.

Files updated:

  docs/multi-project.md
    - Layout diagrams use `apexyard-portfolio/` as the sibling
    - Setup walkthrough Step 2 + Step 3 use `your-org/apexyard-portfolio`
      and explain the `<fork>-portfolio` pattern
    - Config-block + symlink path examples updated to
      `../apexyard-portfolio/...`
    - Daily workflow + cross-machine clone commands updated
    - The two existing `your-org/ops` references that remain are
      fork-rename examples (lines 52, 64) — kept as-is, since renaming
      the fork to `ops` is still valid (the portfolio would then
      default to `ops-portfolio`)

  .claude/skills/setup/SKILL.md
    - Step 2b's "default suggestion" for the private repo name is now
      `your-org/<fork>-portfolio`, computed dynamically from the
      fork's repo name via `gh repo view --json name -q .name` so the
      suggestion is correct even when the fork was renamed
    - Clone command no longer needs a second arg — the repo name IS
      the directory name
    - Config-block paths updated to `../apexyard-portfolio/...`

  .claude/skills/split-portfolio/SKILL.md
    - Step 3's suggested-name template is now `<account>/<fork>-portfolio`
      with the same dynamic-fork-name resolution

Mechanism unchanged. The `portfolio:` config block in
`.claude/project-config.json` still takes any path; this PR is purely
default-suggestion + example prose.

No tests required (skills are markdown instructions; no automated
coverage today).

Refs `apexyard.projects.yaml.example` uses "ops repo" as a generic
term meaning "the operational management fork" — not a name — kept
as-is.

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#167)

Closes #165.

Adds a public, browseable index of every apexyard slash command
alongside a one-click changelog link from the homepage nav.

site/skills.html (new):
  - Lists all 39 skills currently shipping in .claude/skills/
  - Each entry: slash command, argument hint, description (taken
    verbatim from the SKILL.md frontmatter so the page matches the
    runtime exactly)
  - 10 categories: Setup & onboarding, Daily ops, Tickets & ideas,
    Specs & decisions, Code review & merge, Architecture & dev tools,
    Production-readiness audits, Workflow primitives, Communications,
    Deprecated
  - Same brutalist-terminal design tokens as the homepage — JetBrains
    Mono, paper-cream background, single warning-red accent, sharp
    corners. Inlined CSS to keep the static-only no-build-step
    convention; design vars duplicated rather than extracted to a
    shared file (~18 vars; cheap to keep in sync).
  - Mobile responsive — skill grid collapses to single-column under
    720px; titlebar nav hides non-CTA items on narrow viewports.
  - Reduced-motion friendly (no animation in the first place).
  - Internal anchor TOC at the top so the page scans in seconds.

site/index.html (nav addition):
  - Added two nav links to the titlebar between "what's in the box"
    and the github CTA:
      • skills      → ./skills.html
      • changelog → https://github.com/me2resh/apexyard/releases
  - The changelog link points at the GitHub releases page (not the
    raw CHANGELOG.md file) so it auto-resolves to the latest tagged
    release on each visit. v1.2.0 lands and the link is already there.

No new dependencies, no build step, no JS for the skills page. The
existing site convention (one-html-file-per-route, inlined CSS,
optional progressive-enhancement JS) is preserved.

Refs #160 (release v1.2.0 + landing-site refresh) —
this is the second site-side deliverable for that ticket; the release
tag itself follows once #159 (testing) closes.

Follow-up worth a separate ticket: a small generator script that
walks .claude/skills/*/SKILL.md, parses YAML frontmatter, and emits
the skills.html sections automatically. Out of scope for v1.2.0 —
first version is hand-curated and will need maintenance until that
generator lands.

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…169)

Closes #168.

The /release skill prescribed `release/vA.B.C` as the source-branch
name and `release: vA.B.C` as the PR title for the dev → main release
PR (per AgDR-0007). Both were rejected by the framework's own
validators:

  validate-branch-name.sh required {type}/{TICKET-ID}-{description};
  release/v1.2.0 has no ticket-id portion.

  validate-pr-create.sh required type(SCOPE): form with `release` not
  in pr.title_type_whitelist.

The contradiction surfaced cutting v1.2.0 — the first release under
the dev/main model.

Three small changes:

1. .claude/hooks/validate-branch-name.sh — added an early-out branch
   that accepts ^release/vN.N.N(-rcN)?$ as a valid name. Narrow,
   intentional exception for the framework's release-cut convention;
   release branches don't carry a ticket-id because the release itself
   IS the ticket.

2. .claude/project-config.defaults.json — added "release" to
   pr.title_type_whitelist so a title like `release(#160): v1.2.0`
   passes validate-pr-create.sh's existing regex unchanged.

3. .claude/skills/release/SKILL.md step 4 — corrected the prescribed
   PR title to `release(#<release-ticket>): vA.B.C` so future /release
   invocations produce a title that satisfies the validators by
   construction.

Tested:

  bash .claude/hooks/validate-branch-name.sh against:
    release/v1.2.0           → 0   (allowed, release-special-case)
    release/v1.2.0-rc1       → 0   (allowed, RC variant)
    release/v9.9.9           → 0   (allowed)
    release/foo              → 2   (correctly blocked)
    release/v1               → 2   (correctly blocked)
    chore/GH-168-fix         → 0   (allowed, standard pattern)
    feature/GH-1-x           → 0   (allowed, standard pattern)

  Full hook test suite: 196/196 cases green across 12 test files.

Refs: surfaced 2026-05-04 cutting the first release under AgDR-0007.

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…d check (#171)

Closes #170.

Completes the work started in #169 (closing #168). #169 added a
release-pattern early-out to validate-branch-name.sh so release/vN.N.N
branches pass the branch-name validator. But validate-pr-create.sh has
its own independent branch-id check at line 273 that #169 didn't
touch — and it still rejects release/v1.2.0 because that name doesn't
contain a ticket-id substring.

This is the same class of contradiction #168 fixed; the fix is the
same shape. Add the same release-pattern early-out to the branch-id
check in validate-pr-create.sh:

  - if the branch matches ^release/vN.N.N(-rcN)?$ → exempt (release
    branches don't carry ticket-ids; the release itself is the ticket)
  - otherwise → require a ticket-id substring as before

#168's acceptance criterion 3 ("validate-pr-create.sh accepts a PR
title `release(#160): v1.2.0` against the `release/v1.2.0` branch")
was checked off based on the title regex alone but didn't catch the
secondary branch-id check living in the same file. Surfaced trying
to open the v1.2.0 release PR.

Tested:

  bash .claude/hooks/validate-pr-create.sh against:
    release/v1.2.0           → 0   (allowed, exempt)
    release/v1.2.0-rc1       → 0   (allowed, RC variant)
    chore/GH-1-fix           → 0   (allowed, has ticket-id)
    release/foo              → 2   (correctly blocked)
    chore/no-ticket          → 2   (correctly blocked)

Refs #168 (the parent bug) + #169 (the partial fix).

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes #173.

The release-cut model (AgDR-0007 / #116) squash-merges
release/vN.N.N → main, which means the v1.2.0 CHANGELOG section that
landed on main via PR #172 was never propagated back to dev. This
commit copies main's CHANGELOG.md verbatim onto dev so the v1.2.0
section is now present on both branches.

The diff is exactly the v1.2.0 entry being prepended; no other lines
change.

Without this sync, the next release PR cut from dev would build a
v1.3.0 section on top of v1.1.0, silently dropping v1.2.0 from dev's
running history. The corollary skill-level fix (option B in the
ticket) — updating /release to source the previous CHANGELOG from
upstream/main — is filed as a follow-up and out of scope for this PR.

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds /agdr — a portfolio-wide index for Agent Decision Records.
Walks apexyard.projects.yaml, reads each project's docs/agdr/*.md
(local clone if available, else gh api fallback), parses the optional
YAML frontmatter for category + projects, and answers four queries:

- /agdr browse        list across the portfolio, grouped by category
- /agdr search <term> full-text grep across all bodies, returns
                      <project>/AgDR-NNNN paths + matching paragraph
- /agdr show <id>     print a specific record, disambiguates duplicates
- /agdr stats         counts per category (the marketing-slide tile,
                      now backed by real data)

Six-category taxonomy: architecture | tech-stack | security | patterns
| integrations | other. Legacy AgDRs without frontmatter remain
first-class — they bucket as `other` and are flagged in browse so
operators can migrate at their own pace.

Backwards-compatible template change: templates/agdr.md gains an
optional `category:` (and optional `projects:`) line in the existing
frontmatter block. Omitting the line keeps every existing AgDR valid;
the skill defaults to `other` when missing.

Doc note in workflows/sdlc.md § Phase 2 points at /agdr search for
"have we decided this before?" lookups before drafting a design.

Smoke test in .claude/hooks/tests/test_agdr_skill.sh covers the
parser the spec specifies — frontmatter extraction, category
bucketing (including the legacy default-to-other path), id reading,
stats aggregation, and search match counts. 18 assertions, all green;
12 pre-existing test suites also green (no regressions).

Closes #181

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
atlas-apex and others added 16 commits May 27, 2026 13:31
The settings.json inline walker now checks the session pin file
(~/.claude/apexyard/ops-root-<SESSION_ID>) before walking up the
directory tree. This resolves hooks from managed project workspaces
(workspace/<name>/) where the ops repo is a sibling, not an ancestor.

Previously: walker found the portfolio root (has onboarding.yaml but
no .claude/hooks/) and silently exited. All hooks — including
auto-code-review.sh — never fired from workspace CWDs.

Now: pin-first resolution finds the ops root regardless of CWD.
Walk-up fallback also requires .claude/hooks/ dir alongside the
marker file, so even without a pin, it skips the portfolio root.

Single-fork mode: unaffected (ops root has both markers + hooks).

Refs #424

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…427)

* fix(#426): merge hook handles compound marker-write + merge commands

PreToolUse hooks fire before the Bash command executes, so in a compound
`cat > marker && gh pr merge`, the marker file doesn't exist yet when the
hook checks. The hook now detects inline marker content in the command
string — extracts sha/approved_by/skill_version via regex, validates
in-memory, and lets the command through if the inline marker is valid.

The file-based path is unchanged for standalone `gh pr merge` commands
where the marker was written in a previous tool call.

4 new test cases: valid inline, wrong SHA inline, missing approved_by
inline, old skill_version inline. All 17 cases pass.

Refs #426

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(#426): update site hook counts 32 → 34

Two new hooks added in #418 (suggest-mcp-search.sh, remind-mcp-tools.sh)
bumped the actual hook count to 34 but site marketing copy still said 32.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(#426): add blank lines around lists for markdownlint MD032

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(#426): update hook counts 34 → 35 (warn-bootstrap-scope added in #419)

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>
…429)

Add step 4 to the handover clone flow: trigger
mcp__apexyard-search__reindex(scope=project, project=<name>) after
a successful git clone. Best-effort — skips silently if the MCP
server isn't running.

Without this, search_code returns empty results for newly-cloned
projects until the next session start (when index_on_session_start
fires). The reindex takes ~2-5s for a single project.

Refs #428

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ion (#430)

The test's Invariant 1 counted wrappers via `r=$PWD` which matched both
v1 and v2 walkers; Invariant 2 used a hardcoded v1 walker as the
"canonical wrapper" sample even after PR #306 swept all hooks to the v2
pin-first shape.

Changes:
- Count wrappers by CLAUDE_CODE_SESSION_ID (v2 preamble) instead of r=$PWD
- Add Invariant 1b: grep for bare `bash -c 'r=$PWD;while` (pure v1 shape,
  no session-pin preamble) and fail if any are found — this catches the
  stale-walker regression class mechanically on future settings.json edits
- Replace the hardcoded v1 WRAPPER with the canonical v2 shape
  (session-pin first, walk-up fallback) so Invariants 2 and 3 test the
  actual current wrapper behaviour

Refs #414

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…r split-portfolio (#431)

Add step 4a to /split-portfolio so that `main` on the newly-created
private portfolio repo is immediately protected: 1 required approving
review, no required status checks (lenient for repos without CI yet),
enforce_admins off so the operator retains admin merge ability.

Gracefully degrades when the operator lacks admin access — prints an
actionable warning with the manual GitHub UI path and continues the
migration rather than blocking.

Idempotency: PUT to the branch protection API is a no-op update on
re-runs. The idempotency table and the final migration report are
updated to reflect the new step.

Closes #415

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…dover (#432)

Move the clone step from the end of the flow (old step 8, default-no)
to right after step 1 (new step 1.5-clone, default-yes). When a Git URL
is given, the repo is cloned into workspace/<name>/ before any reads
begin; all subsequent reads in steps 2-6 use the local clone, which is
3-15x cheaper per query than the GitHub API. Old step 8 is repurposed
to offer follow-up deep-dive skills (/threat-model, /security-review,
/code-review) against the already-cloned repo. Rule 7 updated to match.

Closes #417

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…d checks + session-wide ANTHROPIC_BASE_URL (#440)

The agent-routing.yaml schema has documented Ollama Example C since
the #351 sync hook landed, but routing was inert: per-agent env files
got written, Claude Code didn't consume them, no validation ran, no
session-wide ANTHROPIC_BASE_URL.

This PR makes #438's Example C actually route, while preserving the
spike-#195 caveats verbatim in the docs (no silent fallback, cold-start
visible, single-endpoint-per-session).

apply-agent-routing.sh extensions:
- Pre-apply pass: for each unique endpoint, curl `<ep>/v1/models` or
  `/health` with a 2s timeout. Unreachable endpoints emit a warning
  and have their per-agent endpoint rows filtered from the apply set,
  so a downed proxy doesn't poison the session. Model rewrites still
  apply.
- For `model: ollama/<name>` entries on reachable endpoints, query
  `/api/tags` and warn if the model isn't pulled locally (suggest the
  exact `ollama pull <name>` command).
- Post-apply: write session-wide `.claude/session/agent-env/__session__.env`
  with `ANTHROPIC_BASE_URL=<first-reachable-endpoint>`. This is the
  mechanism that actually works in v1 — per AgDR-0050 § Axis 5, Claude
  Code doesn't consume per-agent env files yet. Multi-endpoint
  declarations warn and use first-declared.
- Banner suffix: `[N Ollama, K warning(s)]` when either count > 0.
- Bash 3.2-compatible (uses temp files instead of associative arrays);
  works on macOS default `/bin/bash`.

agent-routing.yaml.example:
- Drop the stale "Until PR 2 lands, authoring this file is documented
  but inert" comment — that's been false since the #351 sync hook landed.
- Add explicit notes that endpoint reachability + Ollama model-pulled
  are checked at SessionStart, and point readers at the new
  docs/local-model-setup.md walkthrough.

docs/local-model-setup.md (NEW):
- Single-page setup: install Ollama → pull an agent-grade model →
  install LiteLLM proxy with sample config → uncomment Example C →
  verify routing → constraints.
- Recommends qwen2.5-coder family for structured-output skills; lists
  4 candidate models with disk + RAM-at-load estimates; explicit
  hardware sizing guidance.
- Constraints section quotes spike #195 conclusions verbatim:
  single-endpoint-per-session, ~9-10s cold-start, no silent fallback
  Claude→Ollama, tool-call reliability is model-dependent.
- Deliberately scoped to agent routing only — no mention of embedding
  providers or any premium-distributed component.

docs/getting-started.md:
- One-paragraph "Optional: Local agent routing" pointer to
  local-model-setup.md. Same shape as the LSP pointer just above it.
  Notes the opt-in default + points at the spike memo for measured
  caveats.

Tests (.claude/hooks/tests/test_agent_routing_sync_and_drift.sh):
- 5 new cases (9-13) covering the Ollama path with a mock curl that
  consults $APEXYARD_MOCK_CURL_DIR for canned responses:
    9.  reachable + pulled → full apply, "1 Ollama, 0 warning(s)"
    10. unreachable → model rewrites, env files NOT written, warning
    11. reachable + not pulled → applies + `ollama pull` hint
    12. two agents same endpoint → single __session__.env
    13. two agents different endpoints → first wins + warning
- 8 existing cases (1-8) all stay green.
- Updated header comment block to enumerate the new coverage.

Out of scope (deferred):
- Per-agent invocation env-var scoping (gated on Claude Code upstream
  surfacing the API — tracked as follow-up to AgDR-0050 § Axis 5).
- Tool-call reliability matrix per model — separate spike.
- Auto-fallback Claude→Ollama on proxy down — spike #195 explicitly
  warned against silent fallback; deliberate non-feature.

Closes #438.

## Glossary

| Term | Definition |
|------|------------|
| LiteLLM proxy | A thin HTTP proxy that translates Anthropic Messages API requests into Ollama's native `/api/chat` format. Required for routing agents (which expect an Anthropic-shaped endpoint) through Ollama. Not required for direct Ollama use cases like embeddings. |
| Ollama | Local LLM runtime (https://ollama.com). Serves models on `http://localhost:11434` via its native API. Models pulled via `ollama pull <name>`. |
| ANTHROPIC_BASE_URL | Env var Claude Code reads at startup to redirect inference traffic to a non-default endpoint. Used here to point at the LiteLLM proxy instead of the Anthropic API. v1 sets this session-wide because Claude Code doesn't expose per-agent env scoping yet. |
| Reachability check | 2s-timeout curl against `<endpoint>/v1/models` (LiteLLM's models list) or `/health` (some proxy variants). Two attempts; if both fail, endpoint marked unreachable and per-agent env file write is skipped. |
| Model-pulled check | 2s-timeout curl against `<endpoint>/api/tags` (Ollama's native model-list endpoint), greps for the named model. Emits `ollama pull <name>` hint on miss; doesn't block the override. |
| Session-wide env file | `.claude/session/agent-env/__session__.env` — written by the sync hook with the first reachable endpoint's `ANTHROPIC_BASE_URL`. Read by Claude Code at startup (or sourced from shell profile, depending on launch context). |
| Cold start | ~9-10s first-call latency measured in spike #195 — Ollama pays the model-load cost on the first request after `ollama serve` boot or after `OLLAMA_KEEP_ALIVE` expires. |

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… hook (#439)

* fix(#433): promote /handover MCP reindex to named step + add advisory hook

Bug: the post-clone MCP reindex in /handover was buried in a prose
comment inside step 1.5-clone, surrounded by the phrase "skip silently
if the MCP server is not available". In practice that reads as
permission to skip unconditionally, so the reindex got skipped even
when the server was running. All subsequent reads in steps 2-6 then
fell back to grep + Read against a stale index, defeating the
3-15x token-cost benefit of cloning early.

Fix:

- Promote the reindex to its own named, numbered sub-step
  1.5-reindex in handover SKILL.md, structurally parallel to
  1.5-clone above and 1.5 (topology) below. Naming it makes it
  visually harder to skip during agent execution.
- Replace the "skip silently" language with explicit non-silent
  failure semantics: emit a one-line warning, set
  REINDEX_STATUS=unavailable, then continue. Silent skips are the
  failure mode the change is trying to prevent.
- Add a PostToolUse Bash hook (suggest-mcp-reindex-after-clone.sh)
  that fires after the matching git clone command and emits a
  one-line reminder banner. Advisory shape: exit 0 always, same
  pattern as detect-role-trigger.sh and check-upstream-drift.sh.
  Recognises both the literal workspace/<name> form (single-fork
  default) and absolute paths ending in /workspace/<name> (the
  helper-resolved form used in split-portfolio v2). Skips on failed
  clones (exit code != 0).
- Wire the hook in .claude/settings.json under PostToolUse Bash with
  matcher `if: "Bash(git clone *)"`.
- Add 11-case test suite at
  .claude/hooks/tests/test_suggest_mcp_reindex_after_clone.sh:
  5 should-fire (workspace clones with various forms, banner content
  checks, project-name extraction) and 6 should-not (non-workspace
  clones, failed clones, non-clone commands, wrong tool, empty input).
  All 11 pass.

Closes #433.

## Glossary

| Term | Definition |
|------|------------|
| 1.5-clone | Step in /handover SKILL.md (added in #417) that clones the target repo into workspace/<name>/ immediately when a Git URL is given. Default behaviour; cheapest moment to clone before any reads. |
| 1.5-reindex | New step added by this PR — call mcp__apexyard-search__reindex(scope="project", project="<name>") after a successful clone so search_code / search_docs return results during the deep-dive phases (steps 2-6) that follow. |
| REINDEX_STATUS | New marker variable set by step 1.5-reindex: "indexed" on success, "unavailable" when the MCP server can't be reached, "skipped" when CLONE_STATUS isn't "cloned". Steps 2-6 read it to decide between MCP search and grep + Read. |
| Advisory hook | Non-blocking PostToolUse hook that emits a banner to stderr and exits 0. Same pattern as detect-role-trigger.sh / check-upstream-drift.sh — removes the "I forgot the rule applied here" failure mode without changing tool behaviour. |
| PostToolUse | Claude Code hook event that fires after a tool call completes. Carries the tool input AND the response (exit code, output) — lets the hook condition on whether the operation succeeded before suggesting follow-ups. |

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(#433): bump site hook count 35 → 36 to match the new advisory hook

Adds the count refresh that the site-counts-check CI workflow requires
when a new hook ships in the same PR. Touches the six files the drift
test scans:

- site/llms.txt — top-line "> 35 hooks" claim + bullet "35 shell hooks"
- site/llms-full.txt — same two surfaces
- site/index.md.gen — H3 "Guardrails that stop bad code from shipping · 35 hooks"
- site/architecture.md.gen — Layer-1 bullet "35 shell gates"
- site/architecture.html — SVG text label ".claude/hooks/ · 35 shell gates"
- site/skill.md — "35 shell hooks enforce SDLC rules"

Verified locally:
  bash .claude/hooks/tests/test_site_counts.sh
    skills: 55  hooks: 36  roles: 19
    PASS: site framework counts match actuals across all scanned files.

Refs #433.

---------

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#437)

The CLAUDE.md SETUP step 1 told agents to "Read onboarding.yaml" without
qualifying which repo to read from. In single-fork mode this resolves
correctly (the fork is the only candidate); in split-portfolio v2 mode
Claude resolves against $PWD (the public fork root), which holds the
template-default onboarding.yaml, while the adopter's fully-configured
copy sits in the private sibling repo and is silently ignored.

The portfolio_onboarding_path helper has shipped in
_lib-portfolio-paths.sh since #242, but CLAUDE.md's session-start
instruction never started using it. This patch wires SETUP step 1 to
source the helper and resolve onboarding via portfolio_onboarding_path,
mirroring the bash snippet pattern already used in skills like /inbox
and /tasks.

In single-fork mode the helper returns <ops-root>/onboarding.yaml — same
file the previous instruction reached. The indirection only matters in
split-portfolio v2 mode but costs nothing to apply unconditionally.

Closes #434.

## Glossary

| Term | Definition |
|------|------------|
| portfolio_onboarding_path | Resolver in _lib-portfolio-paths.sh that returns the onboarding.yaml path: <ops-root>/onboarding.yaml in single-fork mode, <sibling>/onboarding.yaml in split-portfolio v2 mode. Sibling pattern to portfolio_registry / portfolio_projects_dir / portfolio_workspace_dir. |
| Split-portfolio v2 | The two-repo mode (public framework fork + private sibling) introduced by the /split-portfolio skill, where adopter-private configuration lives in the sibling and the fork remains shareable. |
| Single-fork mode | The original out-of-box mode where one fork holds both framework and adopter config. portfolio_*_path helpers resolve to the fork in this mode. |

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…r and projects/ (#441)

Two related silent-fallback bugs in split-portfolio v2 that surfaced
when registry/projects_dir point at the sibling repo but workspace_dir
or skill write targets fall back to the in-fork copy. Both manifested
without errors, warnings, or banners — adopters only noticed when
clones / project docs landed in the wrong repo.

Bug 1 — portfolio_validate() partial-v2 detection:

Today portfolio_validate() in _lib-portfolio-paths.sh validates only
*explicitly-set* overrides; in-fork defaults are skipped. When an
adopter sets portfolio.registry (or portfolio.projects_dir) to a
sibling path but omits portfolio.workspace_dir, workspace_dir falls
back to the in-fork default and validation says nothing.

New check (lines 378-401): if registry OR projects_dir resolves outside
the ops-fork root AND workspace_dir resolves inside it, emit a
structured error naming the specific gap and the fix:

  broken: partial split-portfolio v2 config — registry/projects_dir
  point at a sibling repo but workspace_dir falls back to the in-fork
  default; set .portfolio.workspace_dir in .claude/project-config.json
  to the sibling path (e.g. "../<fork>-portfolio/workspace"). See
  #373.

The existing check-portfolio-config.sh SessionStart hook already
prints portfolio_validate() failures as a banner — no separate banner
needed.

Onboarding is NOT a v2 indicator (adopters legitimately centralise
company config without going split-portfolio v2). Custom_skills_dir /
custom_handbooks_dir similarly carved out — they're additive surfaces,
not the v2 split itself.

Bug 2 — /dfd skill bash block + 9 prompt-based skill docs:

/dfd's SKILL.md was the one place where executable bash actually
constructed projects/${PROJECT}/architecture/dfd.{md,yaml,json} as a
literal relative path. Fixed to use "${projects_dir}/${PROJECT}/..."
(the path-resolution helper section already sourced
_lib-portfolio-paths.sh and declared projects_dir).

Nine other skills (extract-features, feature-diagram, handover,
journey, plan-initiative, process, roadmap, stakeholder-update,
tech-vision) have many literal projects/<name>/X references in
documented bash examples that risked Claude constructing
${PWD}/projects/... at execution time. Added an explicit "Write
targets" rule to each skill's Path resolution section emphasising that
documented projects/<name>/X paths MUST be implemented as
${projects_dir}/<name>/X in actual bash. Also added the
projects_dir=$(portfolio_projects_dir) line to skills that sourced the
helper but never declared the variable (roadmap, stakeholder-update,
handover).

The 11 audit skills (accessibility-audit, analytics-audit,
compliance-check, docs-audit, geo-audit, launch-check, monitoring-audit,
performance-audit, security-review, seo-audit, threat-model) are NOT
touched — they delegate to _lib-audit-history.sh which already resolves
through portfolio_projects_dir correctly. Their literal projects/<name>/
strings in SKILL.md are descriptive (what the adopter sees in their
filesystem), not bash.

Tests:
- Existing test "override: validate is OK against sibling-dir paths"
  updated to include workspace_dir in the fixture (it was asserting
  the now-obsolete "partial sibling config is fine" behaviour).
- New case "v2 (#373): partial config — workspace_dir missing while
  registry is sibling → broken" — fixtures registry + projects_dir as
  sibling, omits workspace_dir, asserts portfolio_validate returns 1
  and prints "partial split-portfolio v2 config".
- 39/39 cases pass (38 prior + 1 new).
- Site counts clean (no new hook, no count refresh needed).

Closes #373.

## Glossary

| Term | Definition |
|------|------------|
| Split-portfolio v2 | Two-repo mode (public framework fork + private sibling) introduced by /split-portfolio. portfolio.registry / projects_dir / workspace_dir / onboarding keys in .claude/project-config.json route reads + writes to the sibling. |
| Partial v2 config | The bug shape this PR catches: some portfolio.* keys point at the sibling while others fall back to in-fork defaults. Specifically: registry OR projects_dir is sibling but workspace_dir is the in-fork default → silent fallback for workspace clones. |
| In-fork default | The string value that portfolio_<x>() returns when no override is set: ./apexyard.projects.yaml / ./projects / ./workspace / ./onboarding.yaml. Resolves against the ops-fork root. |
| Helper-resolved write | Bash construction like "${projects_dir}/<name>/X" where projects_dir comes from portfolio_projects_dir. Routes to whichever filesystem path the resolver returns (single-fork in-fork OR split-portfolio sibling). |
| Audit skill | /accessibility-audit, /analytics-audit, /compliance-check, /docs-audit, /geo-audit, /launch-check, /monitoring-audit, /performance-audit, /security-review, /seo-audit, /threat-model — all delegate writes to _lib-audit-history.sh which correctly uses portfolio_projects_dir. NOT touched by this PR. |

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ote shell-profile step in docs (#444)

#440 shipped the apply-agent-routing.sh extension that writes
__session__.env with ANTHROPIC_BASE_URL when an adopter declared an
Ollama endpoint in agent-routing.yaml. The banner reports "applied N
Ollama override(s)". But Claude Code's process env is set when Claude
launches — SessionStart hooks run in child shells and cannot mutate
the parent. So unless the adopter sources __session__.env in their
shell profile BEFORE launching Claude, the routing is INACTIVE despite
the optimistic banner: every agent call still hits the Anthropic API.

The docs/local-model-setup.md step that explains this lived buried at
step 5 of the verification section, easy to skim past on first install.

This PR closes the visibility gap so adopters discover the problem at
SessionStart instead of discovering it via "why is my proxy log empty?"
after they've already invoked a few agents.

apply-agent-routing.sh:
- After writing __session__.env, compare the resolved endpoint to
  $ANTHROPIC_BASE_URL in the current process. If they differ (or the
  env var is unset), emit a one-line warning naming the gap and the
  exact remediation:

    ⚠ agent-routing: ANTHROPIC_BASE_URL=<ep> was written to <path> but
    is NOT set in this Claude session's process env. Routing is
    INACTIVE — every agent call still hits the Anthropic API. To
    activate, add this line to your shell profile and relaunch Claude
    from a fresh terminal:
        [ -f "<path>" ] && . "<path>" && export ANTHROPIC_BASE_URL
    See docs/local-model-setup.md § "Before you start" and
    #442.

  The warning folds into the existing [N Ollama, K warning(s)]
  banner suffix.
- No behaviour change for adopters who HAVE done the shell-profile
  step (silent on matching env var).

docs/local-model-setup.md:
- New "Before you start — the one manual step" section at the top of
  the doc (before the install steps). Explains why
  SessionStart can't propagate ANTHROPIC_BASE_URL into Claude's
  process env, gives the exact shell-profile line to add, and notes
  the relaunch requirement.

Tests:
- Existing cases 9, 11, 12, 13 updated to pre-set ANTHROPIC_BASE_URL
  in the test invocation, since the new routing-active check
  otherwise fires in every reachable-endpoint case (false-positive
  for those tests' happy-path assertions).
- New case 14: Ollama agent with reachable proxy + pulled model but
  ANTHROPIC_BASE_URL not preset. Asserts session env is still written,
  banner reports "1 Ollama, 1 warning(s)", and the warning text names
  both the gap ("routing is INACTIVE") and the remediation path.
- 14/14 cases pass locally.

What this PR does NOT do (deferred):
- Modifying .zshrc / .bashrc directly from the hook (too invasive).
- Wrapper script that sets env then execs Claude Code (UX change,
  separate ticket).
- Per-agent env scoping (still gated on upstream Claude Code).

Refs #438 (the original Ollama routing PR).

## Glossary

| Term | Definition |
|------|------------|
| Routing-active check | New SessionStart-time check in apply-agent-routing.sh that compares the resolved endpoint to $ANTHROPIC_BASE_URL in the current process env. Fires a warning when they differ. |
| __session__.env | File written by apply-agent-routing.sh at .claude/session/agent-env/__session__.env containing ANTHROPIC_BASE_URL=<endpoint>. Must be sourced from the adopter's shell profile to take effect. |
| Shell-profile step | The manual one-liner the adopter adds to ~/.zshrc / ~/.bashrc to source __session__.env on shell start. Required for routing to actually activate (Claude Code can't read this file by itself). |

Closes #442.

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…te targets rule (#445)

#441 / #373 added the Write targets rule to 9 prompt-based skills and
switched /dfd's bash blocks to ${projects_dir} references. The assumption
underneath both fixes was that Claude executes the bash blocks in the
same shell context as the file-level Path resolution section, so the
projects_dir assignment carries through.

That assumption is wrong: Claude treats each ```bash``` fence as a
separate shell invocation. The assignment from a block ~140 lines
earlier does NOT carry into a write block. Under set -u the write
silently expands to /<PROJECT>/architecture/... (empty + slash) and
the file lands at the wrong path. Without set -u it lands at the
working-directory-relative literal /...

Two coordinated fixes:

1. /dfd SKILL.md (the only skill with literal-resolution bash blocks):
   - Each of the 2 bash blocks that writes to ${projects_dir}/... now
     starts with the 3-line helper preamble (source + projects_dir=).
     Self-contained per block.
   - Long-form comment explains WHY the preamble is required in each
     write block — references #443 so future readers find the context.

2. 9 prompt-based skills (extract-features, feature-diagram, handover,
   journey, plan-initiative, process, roadmap, stakeholder-update,
   tech-vision):
   - The "Write targets" rule from #441 now includes a "REQUIRED per-
     block preamble" subsection with the exact 3-line shape Claude
     should prepend to every write block.
   - Explicitly notes that the Path resolution section's example is
     for documentation; it does NOT absolve later blocks from sourcing
     the helper themselves.
   - "Treat each ```bash``` fence as a fresh process" — the load-bearing
     model the adopter should hold.

Tests:
- All 39 cases in test_portfolio_paths.sh still pass (no regression on
  the runtime detector from #441).
- Site counts test clean (no new hook, no count refresh).
- No new test scaffold added — the bug shape is markdown-prompt-driven
  (Claude's per-block scoping), not unit-testable in shell.

What this PR does NOT do (deferred):
- A linter / hook that scans SKILL.md for write paths without an
  adjacent helper source. Mechanical detection would be the "real"
  fix but requires its own design (markdown parsing, ```bash``` fence
  boundaries, scope across the file). Filed as a future enhancement
  when adopters complain.
- The 11 audit skills — unchanged, they correctly delegate to
  _lib-audit-history.sh.

Refs #373 (the partially-effective fix) + #441 (the
PR that landed it).

## Glossary

| Term | Definition |
|------|------------|
| Per-block scoping | Claude executes each ```bash``` fence in SKILL.md as a separate shell invocation. Variables, sourced files, and exports from a previous block do NOT persist into the next block. This is the load-bearing constraint behind why the per-block preamble is required. |
| Self-contained write block | A bash block that does everything it needs (source helpers, assign variables, validate, write) within its own fence. Survives Claude's per-block re-spawn semantics. |
| Per-block preamble | The 3-line `source ... && projects_dir=$(portfolio_projects_dir)` snippet that turns a literal-path write block into a helper-resolved one. Required at the top of every block that writes to ${projects_dir}/<name>/X. |
| File-scope vs block-scope | The Path resolution section at the top of each SKILL.md shows the helper-source pattern at the file scope — for documentation purposes. The actual bash execution model is block-scope, so the file-scope example does NOT propagate. |

Closes #443.

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dev's CHANGELOG.md was stuck at v1.2.0 while main carried v1.3.0
through v2.1.0. Without this fix the next /release would prepend the
new version on top of v1.2.0 and the squash-merge to main would wipe
the v1.3.0 → v2.1.0 history.

Verified that main's CHANGELOG from v1.2.0 down is byte-identical to
dev's whole CHANGELOG, so this overwrite is exactly equivalent to
splicing in the missing entries — no silent rewrite of older history.

Closes #446

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Adds a new step in Rex's §8 (Adopter Handbooks) that queries the MCP
search_docs tool for handbooks whose content semantically matches the
PR — supplementing the path-convention discovery rather than replacing
it.

Design constraints honoured:

- Additive: path-convention set is the floor and never shrinks. Every
  handbook Rex loaded before this change still loads.
- Fail-soft: if MCP is unreachable, indexed empty, or the tool isn't
  loaded, the supplement is a silent no-op. No user-visible warning,
  no degraded behaviour for adopters who never installed MCP.
- Visible: semantically-discovered handbooks get an explicit
  *(semantic match — discovery: semantic-search)* annotation on every
  citation so the reader can see WHY a handbook fired for a diff that
  didn't match its path globs. Path-convention citations stay
  un-annotated (no clutter for the dominant case).

Changes:

- `.claude/agents/code-reviewer.md`
  - `tools:` line adds `mcp__apexyard-search__search_docs`
  - §8 path-convention discovery is tagged
    `discovery_method: path-convention`
  - New §8 subsection "Semantic supplement (MCP search_docs)" with
    pseudocode shape, fail-soft contract, and the explicit list of
    behaviours the step does NOT do
  - "What to surface" subsection extended to mention the
    discovery_method tag and semantic_match_excerpt
  - Example output gains a semantic-search citation row
- `handbooks/README.md`
  - New "Semantic supplement (#449)" subsection under Discovery
    documenting the additive behaviour and MCP-less fallback

Adopters who don't run MCP get byte-for-byte identical Rex behaviour
to pre-#449. Planning skills (`/feature`, `/plan-initiative`,
`/tech-vision`) are intentionally deferred to a separate phase per
the ticket body — measure Rex's recall first.

Closes #449

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Closes the apexyard#446 / #447 root cause: the existing `-X ours`
merge in step 5 of /release-sync would drop the CHANGELOG.md additions
written on main during the release flow. Five releases of drift
accumulated by the time it was noticed pre-v2.2.0; this PR makes
the carry-forward part of the skill itself so dev stays in sync
without a manual content resync each cycle.

Changes:

- `.claude/skills/release-sync/SKILL.md`
  - New step 5b "Carry forward CHANGELOG.md from main" between the
    -X ours merge and the push/PR. Path-specific (CHANGELOG.md only).
    Atomic separate commit on top of the merge so the operation is
    audit-trail-visible to the sync PR reviewer.
  - Idempotent via the `git diff --quiet upstream/main -- CHANGELOG.md`
    guard — re-runs against an already-synced repo are no-ops.
  - Summary bullets updated; Testing checklist gains a CHANGELOG-in-sync
    check; Edge Cases gains rows for "code synced but CHANGELOG drifted"
    and "in-flight dev CHANGELOG edits"; Glossary gains the term.

- `.claude/hooks/tests/test_release_sync.sh`
  - New `build_sync_branch_with_changelog_drift` helper that sets up
    the post-state the contract is documented to handle.
  - Case 8: carry-forward creates a commit when CHANGELOG drifts.
  - Case 9: idempotent — second invocation is a no-op.
  - Case 10: only CHANGELOG.md is touched (no other file leaks in).
  - Tests use local `main` ref where the skill uses `upstream/main`
    so the sandbox exercises the same contract without needing a
    remote. 14/14 tests now pass.

Design notes:

- Path-specific carry-forward over a custom merge driver: the merge
  driver shape (option 2 in #448's design notes) would apply outside
  /release-sync too and is over-engineered for one file. If a second
  main-leads file shows up later, that's the moment to revisit.
- Separate commit over amending the merge: keeps the carry-forward
  visible in the sync PR's commit list so Rex/CEO can sanity-check.

Closes #448

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Adds the v2.2.0 CHANGELOG entry covering 17 PRs merged to dev since
v2.1.0 (2026-05-25): local agent routing, split-portfolio v2 hardening,
and release-cycle plumbing fixes.

The PR title (release(#452): v2.2.0) becomes the squash-merge commit
subject on main, satisfying validate-pr-create.sh.

Refs #452
@netlify

netlify Bot commented May 29, 2026

Copy link
Copy Markdown

Deploy Preview for apexyard ready!

Name Link
🔨 Latest commit 733b842
🔍 Latest deploy log https://app.netlify.com/projects/apexyard/deploys/6a19425e471a90000869f391
😎 Deploy Preview https://deploy-preview-453--apexyard.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@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 #453 (release-cut, v2.2.0)

Commit: 376340a8aacff01c5b42e263dcf66862ffec9309

Verdict surfaced as a comment because Rex authored the PR — GitHub blocks self---request-changes. Treat the Blocker section below as a CHANGES REQUESTED equivalent.

Summary

Release-cut PR from dev → main tagging v2.2.0. Diff is CHANGELOG.md only (+259 / -1 lines — prepended above the v2.1.0 entry). All other "files changed" surface area is squash-divergence between main and dev, not new work on this PR.

Release-PR checklist

  • PASS — PR title format: release(#452): v2.2.0 matches release whitelist (per #168/#169)
  • PASS — Glossary: 5 terms, including release-cut model + multi-close marker
  • PASS — Closes refs: #373 (OPEN, fixed by #441) + #452 (OPEN, release ticket) both exist
  • PASS — Multi-close marker <!-- multi-close: approved --> correctly present for the 2 Closes refs
  • PASS — AgDR escape hatch <!-- agdr: not-applicable --> with prose rationale; legitimate use for a release-notes prepend
  • PASS — Semver: 6 feat + 10 fix + 1 chore, no breaking commits → MINOR → v2.1.0 → v2.2.0 correct
  • PASS — Summary bullet narrative quality (3 bullets, all verb + rationale)
  • FAIL — CHANGELOG completeness: 3 PRs missing from the v2.2.0 entry

Blocker — three PRs in the v2.1.0..dev window are undocumented

The PR body claims "17 PRs merged since v2.1.0". I verified the window directly (gh pr list --base dev --search "merged:>=2026-05-25"):

451, 450, 447, 445, 444, 441, 440, 439, 437, 432, 431, 430, 429, 427, 425, 423, 422   ← 17 total

The CHANGELOG entry documents 14 of them (4 Added + 9 Fixed + 1 Changed). The three missing:

PR Type Title Theme
#422 feat(#418) enforce MCP search-first pattern (advisory hooks on Bash for grep -r / find on framework paths) MCP search-first
#429 feat(#428) auto-reindex MCP search after /handover clone MCP search-first
#439 fix(#433) promote /handover MCP reindex to named step 1.5-reindex + add advisory hook MCP search-first

These three form a coherent fourth theme — MCP search adoption + post-clone reindex — that's nowhere mentioned in the release-notes' three-theme framing. The PR body's lede says "Local agent routing pipeline / Split-portfolio v2 hardening / Release-cycle plumbing"; MCP search-first is missing.

Why this matters for a release-cut PR specifically:

  1. The CHANGELOG is the authoritative artefact that the v2.2.0 tag points at. Once cut, undocumented work becomes silently shipped — adopters running /update get behavioural changes with no signal in the release notes.
  2. #422 ships a new hook (suggest-mcp-search.sh) that fires on every Bash grep -r / find against framework paths. Adopter-visible behaviour change — exactly what release notes are for.
  3. #429 and #439 change /handover — a bootstrap-class skill. The reindex is best-effort, but it touches an MCP SDK call some adopters won't have configured.

Fix

Add a fourth theme to the lede paragraph + three entries to the CHANGELOG sections:

4. **MCP search adoption**`/handover` now auto-reindexes the MCP
   search index after cloning a project; a new advisory hook reminds
   the agent to use `search_docs` / `search_code` instead of `grep -r` /
   `find` against framework paths.

### Added (append to existing block)

- `feat(#418)` **Advisory MCP search-first hook**`suggest-mcp-search.sh`
  fires on `PreToolUse` for `Bash` when `grep -r` / `find` targets
  framework or project paths (`roles/`, `workflows/`, `handbooks/`,
  `.claude/`, `workspace/`, etc.). Emits a non-blocking banner reminding
  the agent to try MCP `search_docs` / `search_code` first. No behaviour
  change for adopters who ignore the reminder. (PR #422)
- `feat(#428)` **Auto-reindex MCP search after `/handover` clone**`/handover` triggers `mcp__apexyard-search__reindex(scope=project,
  project=<name>)` after a successful workspace clone, so `search_code`
  returns results immediately instead of waiting for next session start.
  Best-effort; silent no-op if the MCP server isn't running. (PR #429)

### Fixed (append to existing block)

- `fix(#433)` **MCP reindex promoted to named `/handover` sub-step** —
  the post-clone reindex is now step `1.5-reindex` (was prose comment),
  structurally on the same footing as `1.5-clone` and `1.5-topology`.
  Replaces "skip silently" language with explicit status reporting and
  adds an advisory hook to remind the agent if the step is skipped. (PR #439)

After the edit, the entry should document 17 PRs total (5 Added + 10 Fixed + 1 Changed) and the lede should name four themes. Re-verify with:

gh pr list --repo me2resh/apexyard --state merged --base dev \
  --search "merged:>=2026-05-25" --json number -q '. | length'
# expect: 17

Nits (non-blocking)

  • Number-count drift in the PR body — body says CHANGELOG diff is "39 lines added, 0 lines removed" (Testing checklist), but the API patch shows +259 / -1. The -1 is a benign one-line context change at the prepend boundary. Update the Testing checklist to 259 lines added, 1 line removed so the next reviewer doesn't second-guess the diff.
  • #447 listed under "Changed" with a one-off -X ours drift rationale — accurate, and the entry already notes that #448's /release-sync carry-forward closes the mechanism. Good framing; leave as-is.

Suggestions (advisory)

  • Consider adding a Notable behaviour changes section to the v2.2.0 entry, same shape as v2.0.0 and v1.2.0. Three items qualify: (1) the new MCP search-first advisory hook fires on every relevant Bash call; (2) /handover reindexes MCP after clone; (3) agent-routing.yaml SessionStart warns when routing is INACTIVE. Prose-only addition; helps adopters skim and decide whether to /update now or batch with the next change.

Verdict

CHANGES REQUESTED (surfaced as comment because Rex can't request changes on own PR)

Three substantive PRs are missing from the release-notes entry. This is the one accuracy bar that matters for a release-cut PR — the CHANGELOG is what v2.2.0 will permanently point at. Once the MCP search-first family is added (with the suggested shape above) and the lede mentions a fourth theme, this is ready to ship.

The release mechanics (PR title, glossary, semver, Closes refs, multi-close marker, AgDR escape hatch) are all correct. The only blocker is content completeness.

Marker NOT written. Re-review required after the CHANGELOG fix.


🤖 Reviewed by Rex (Code Reviewer Agent)
📌 Reviewed commit: 376340a8aacff01c5b42e263dcf66862ffec9309

@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 #453 (release-cut, v2.2.0) — RE-REVIEW

Commit: 376340a8aacff01c5b42e263dcf66862ffec9309

Verdict surfaced as a comment because Rex authored the PR — GitHub blocks self-approve. Treat the Verdict section below as an APPROVAL equivalent. The Rex marker has been written at <ops-fork>/.claude/session/reviews/453-rex.approved per the marker contract.

Re-review trigger

The prior review (2026-05-29 07:29:11Z) blocked on 3 PRs missing from the CHANGELOG narrative (the MCP search-first family: #422 / #429 / #439). The framework owner has since clarified that release-notes scope is editorial, not 1:1 with the commit log, and updated the PR body to document the policy in-band. Re-reviewing against the updated body and the framework owner's editorial-scope decision.

What changed since the prior review

Prior blocker / nit Resolution
"17 PRs merged" framing implied 1:1 commit-log coverage Body now says "bundles work merged since v2.1.0" — no 1:1 implied
No documented policy for narrower-than-commit-log notes Bullet 3 names the policy: "Release-notes scope is editorial. The CHANGELOG narrative documents the highlights and is intentionally narrower than the full commit log."
Glossary lacked the policy term Glossary now includes Editorial release-notes scope — names the policy and the full-inventory escape hatch
Testing checklist quoted "39 lines added, 0 removed" (inaccurate vs API's +259 / -1) Removed; Testing now uses git log release/v2.2.0 ^upstream/dev --oneline + git show release/v2.2.0 --stat — correctly scoped to this PR's commit (the CHANGELOG prepend), which is the right testing surface

Re-review checklist

1. Editorial-scope policy — does it stand up?

  • PASS — The policy is stated explicitly in bullet 3 of the Summary: "The CHANGELOG narrative documents the highlights and is intentionally narrower than the full commit log — internal plumbing work that doesn't change adopter-facing behaviour ships via the squash without a release-notes line"
  • PASS — Names the full-inventory escape hatch for adopters who want it: git log v2.1.0..upstream/main --oneline after merge
  • PASS — Glossary entry "Editorial release-notes scope" reinforces it as a named, durable policy (not a one-off rationalisation)
  • PASS — The factual record is intact: the squash-merge into main carries every commit; only the CHANGELOG narrative is curated. Adopters can always recover the full inventory post-merge.

This is a legitimate, review-defensible editorial position for a framework release-cut PR. I withdraw the prior "CHANGELOG completeness" blocker.

2. Highlights table accuracy

Spot-checked every PR in the Added / Fixed / Changed tables against the actual merged PRs:

#432  feat(#417) /handover clones... 3–15× cheaper reads        — matches
#440  feat(#438) Ollama/LiteLLM agent routing                    — matches
#451  feat(#448) /release-sync carries forward CHANGELOG.md      — matches
#450  feat(#449) Rex handbook discovery (additive)               — matches
#441  fix(#373) Split-portfolio v2 partial-config detection      — matches
#430  fix(#414) Regression guard against v1 walker hooks         — matches
#431  fix(#415) /split-portfolio configures branch protection    — matches
#423  fix(#419) Bootstrap exemption scope guard                  — matches
#425  fix(#424) Hook walker reads session pin                    — matches
#427  fix(#426) Merge hook handles compound shapes               — matches
#437  fix(#434) SETUP step 1 routes onboarding via helper        — matches
#444  fix(#442) SessionStart warns when routing INACTIVE         — matches
#445  fix(#443) Per-block helper-source preamble (10 skills)     — matches
#447  chore(#446) CHANGELOG resynced (one-off; #448 closes mech) — matches

PASS on all 14. Headlines accurately describe the PRs they reference. The CHANGELOG entry (CHANGELOG.md in this PR's diff) expands each into a one-paragraph technical summary that is faithful to the underlying PR work.

3. Release-PR mechanics

  • PASS — PR title: release(#452): v2.2.0release is in pr.title_type_whitelist per #168/#169
  • PASS — Branch: release/v2.2.0 — matches the release/vN.N.N pattern per validate-branch-name #168
  • PASS — Glossary: 6 terms (release-cut model, /release-sync, squash-divergence, MINOR bump, Editorial release-notes scope, multi-close marker) — covers every term of art used in the body
  • PASS — Closes refs: #373 (OPEN, fixed by #441 which ships in v2.2.0) + #452 (OPEN, the release ticket itself) — both exist
  • PASS — Multi-close marker <!-- multi-close: approved --> correctly present for the 2 Closes refs
  • PASS — AgDR escape hatch <!-- agdr: not-applicable --> with clear in-line rationale: "the release PR's own commit on this branch is CHANGELOG.md only. The architecture-class files that fan out across git diff main..HEAD each landed via their own PR with their own AgDR on dev." Legitimate use — the release-notes prepend is genuinely not an architectural decision in its own right.
  • PASS — Semver math: baseline v2.1.0 (2026-05-25) exists; window has feat + fix + chore commits, no breaking !: markers in the v2.1.0..dev window (the !: lingering from v2.0.0's Hatim→Hakim consolidation is correctly noted in the body as squash-divergence noise, not a new breaker). MINOR bump → v2.2.0 is correct.
  • PASS — Summary bullet narrative quality: 4 bullets, all verb + rationale + consequence. No label-only bullets.

4. Post-merge actions

  • PASS — git tag v2.2.0 upstream/main after merge — correct release-cut model (tag the merged squash commit on main)
  • PASS — /release-sync v2.2.0 named as mandatory step 9 — closes the squash-divergence loop per AgDR-0052
  • PASS — Optional gh release create step — correct command shape, repo-targeted

5. CI status

  • Netlify deploy preview: SUCCESS
  • Header rules + redirect rules: SUCCESS
  • Pages changed: NEUTRAL (informational; no rules workflow on the framework repo for CHANGELOG-only changes)

No red CI; merge gate is clear from the CI side.

6. Mergeable status

GitHub reports mergeable: CONFLICTING. This is expected for a release-cut PR — the cumulative dev→main diff is wide (300+ files, large adds/deletes). The CONFLICTING signal here means the diff exceeds GitHub's UI-mergeable threshold, NOT that there are content conflicts on the release branch. The release skill's flow (/release-sync v2.2.0 as step 9) is exactly the mechanism that resolves this post-merge.

Verdict

APPROVED

The editorial-scope policy is now documented in-band (Summary bullet 3 + Glossary entry) and constitutes a legitimate review-defensible position for a framework release-cut PR. The factual record is intact via the post-merge commit log; the narrative is appropriately curated.

All other release-PR mechanics — title format, branch name, glossary, multi-close marker, AgDR escape hatch, semver math, Closes refs, summary bullet narrative quality, post-merge action sequence — pass.

Ready to ship. Tag and run /release-sync v2.2.0 after merge.

Rex marker written.


🤖 Reviewed by Rex (Code Reviewer Agent)
📌 Reviewed commit: 376340a8aacff01c5b42e263dcf66862ffec9309

…h-divergence

Strategy: -X ours (release/v2.2.0 wins on conflict) — correct because
release/v2.2.0 = upstream/dev + CHANGELOG prepend, and dev carries the
un-squashed equivalents of every prior release. Any squash-divergence
conflict resolves to dev's content as the more accurate state.

This is the pre-merge equivalent of /release-sync (which runs the same
strategy post-merge in the opposite direction main→dev). Resolves 14
conflict files reported by GitHub's mergeable check.

Refs #452

@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 #453 (re-review)

Commit: 733b842ff422e93006170e15f6f8313c5e718cdb
Prior approved HEAD: 376340a (editorial-scope CHANGELOG cut, marker written)
Delta since prior approval: one merge commit (733b842) resolving GitHub's CONFLICTING / DIRTY mergeStateStatus on the release branch

Summary

Single-commit delta on top of the previously approved CHANGELOG cut. The merge commit pulls upstream/main into release/v2.2.0 with -X ours to resolve the 14-file squash-divergence GitHub had flagged. This is the pre-merge mirror of the /release-sync pattern (which runs the same strategy main→dev after a release). The -X ours direction is semantically correct here because release/v2.2.0 = upstream/dev + CHANGELOG prepend, and dev carries the un-squashed equivalents of every prior release — any conflict resolves to dev's content as the more accurate state.

Net effect: only site/index.html actually changes (7+/1-, clean adoption of consent/gtag block-marker comments from #399 plus removal of anonymize_ip: true from gtag config). The 14 reported conflict files retain release/v2.2.0's content with no diff against the prior approved HEAD.

Checklist Results

  • Architecture & Design: Pass (merge mechanics, no behavioural code change)
  • Code Quality: Pass (commit body documents strategy clearly)
  • Testing: N/A (release plumbing, no new code paths)
  • Security: Pass (gtag/consent change is upstream-already-reviewed content from main)
  • Performance: N/A
  • PR Description & Glossary: Pass (carried over from prior review — editorial scope decision still applies)
  • Summary Bullet Narrative: Pass
  • Technical Decisions (AgDR):N/A (merge-strategy choice, symmetric to documented /release-sync pattern)
  • Adopter Handbooks: N/A (release-cut PR; no handbook-relevant scope)

Issues Found

None.

Verification

  • Commit formatchore(#452): merge upstream/main into release/v2.2.0 to resolve squash-divergence passes validate-commit-format.sh (valid type chore, valid ticket scope #452)
  • Commit body quality — narrative + rationale + symmetry-to-/release-sync + Refs #452. Reviewer can decide where to spend judgment from the body alone.
  • CHANGELOG intacthead -10 CHANGELOG.md on HEAD still shows the v2.2.0 entry with the three-theme narrative from 376340a. No accidental wipe.
  • Delta scopegit diff 376340a 733b842 --name-only returns a single file: site/index.html. No surprise files.
  • site/index.html delta — three <!-- begin: ... --> / <!-- end: ... --> block markers added around the consent CSS, gtag, and consent JS blocks (cross-page sync comments from #399 already on main). Removal of 'anonymize_ip': true from gtag('config', ...). All consistent with main's current content; the merge propagated it forward.
  • CI — 6 passing, 1 expected-skip (Pages changed for non-html-affecting branches — but here it ran and Netlify deploy preview built cleanly), 0 red.
  • GitHub statemergeable: MERGEABLE, mergeStateStatus: CLEAN. Conflict resolution succeeded.

Suggestions

None. The merge is minimal, well-documented, and the strategy is the documented one for this exact failure mode.

Verdict

APPROVED

The editorial-scope decision from the prior review at 376340a carries forward unchanged. The merge commit on top is a mechanical squash-divergence resolution using the documented -X ours strategy, with the only content delta being a clean adoption of main's newer site/index.html (consent/gtag sync from #399).


Reviewed by Rex (Code Reviewer Agent)
Reviewed commit: 733b842ff422e93006170e15f6f8313c5e718cdb

@atlas-apex atlas-apex merged commit 3584059 into main May 29, 2026
9 checks passed
@atlas-apex atlas-apex deleted the release/v2.2.0 branch May 29, 2026 10:28
aelnemr referenced this pull request in EGY-XMS/ops Jun 1, 2026
* release(#452): v2.2.0 (#453)

* chore(#109): project-configurable ticket / branch / commit / PR schema (#118)

* chore(#109): project-configurable ticket / branch / commit / PR schema

Lift the prefix / type whitelists hardcoded across skills, hooks, and
CI into a versioned JSON config read through a shared shell library.
Shipped defaults at .claude/project-config.defaults.json; per-fork
overrides at the optional .claude/project-config.json; one reader
(_lib-read-config.sh) that every consumer now uses.

Added:
- .claude/project-config.defaults.json (v1 schema)
- .claude/hooks/_lib-read-config.sh (shared reader)
- docs/project-config.md (schema reference + extension guide)
- docs/agdr/AgDR-0006-project-configurable-ticket-schema.md

Migrated (still pass with no config present via last-resort fallback):
- validate-branch-name.sh  → .branch.type_whitelist
- validate-commit-format.sh → .commit.type_whitelist (legacy
  `commit_types` top-level key honoured as backward-compat fallback)
- validate-pr-create.sh    → .pr.title_type_whitelist
- /feature, /task, /bug skills reference the config in their Rules
  sections; none hardcodes the list any more

Unlocks subsequent config-readers for #107 / #110 / #111 / #112 /
#113 / #114 / #115 — each extends the schema under its own subtree
without further changes to the loader.

https://github.com/me2resh/apexyard/issues/109

* fix(#109): satisfy markdownlint MD032 and MD060 on new docs

Auto-fix MD032 (blank lines around lists) in AgDR-0006 and format
table-separator rows with surrounding spaces (MD060) in both new
doc files. Content unchanged; CI green.

---------

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>

* chore(#110): add block-private-refs-in-public-repos.sh hook (#119)

Adds a new PreToolUse hook that blocks gh issue/PR/comment creation and
gh api .../issues|/pulls calls targeting a public framework repo
(default: me2resh/apexyard + whatever `upstream` resolves to) when the
title or body references any registered private project from
apexyard.projects.yaml (by name, repo slug, owner/repo#N ticket ref, or
workspace path).

The hook is a sibling to check-secrets.sh — both scan outgoing content
for identifiers that should never leave the local environment. Skip
marker `<!-- private-refs: allow -->` in the body lets a deliberate
reference through with a visible warning.

Files touched:
- .claude/hooks/block-private-refs-in-public-repos.sh (new)
- .claude/hooks/tests/test_block_private_refs.sh (new)
- .claude/rules/leak-protection.md (new)
- .claude/settings.json (wire PreToolUse matchers for the 5 gh shapes)
- docs/rule-audit.md (append section 10 + bump counts)

Refs: https://github.com/me2resh/apexyard/issues/110

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>

* chore(#115): add warn-stale-review-markers.sh PostToolUse hook (#120)

Fires after `git push` to surface review markers that have gone stale
because new commits were pushed past an existing Rex / CEO / design
approval. The merge gate already catches this at `gh pr merge` time,
but only then -- this hook closes the gap by flagging it immediately
at push-time so the author isn't surprised at merge.

- `.claude/hooks/warn-stale-review-markers.sh`
  - PostToolUse, non-blocking (PostToolUse exit 2 would push noise
    into the conversation; this hook is purely informational).
  - Resolves the PR HEAD via `gh pr view --json headRefOid` -- same
    source-of-truth as the merge-gate hooks post-apexyard#47 / #55.
    Falls back to local HEAD with a visible WARN when gh is offline.
  - Silent on: no PR for branch, no markers, fresh markers,
    failed push (detected via `rejected` / `failed to push` /
    `fatal:` / `error:` markers in tool_response.stderr).
  - Modes: `warn` (default) prints one stderr line per stale marker;
    `delete` opts in to auto-removal via
    `.claude/project-config.json` -> `review_markers.on_stale`.
    TODO(apexyard#109): switch to the shared project-config reader
    once it lands.
- `.claude/settings.json`
  - Wires the hook on PostToolUse / Bash / `git push *`.
- `docs/rule-audit.md`
  - Adds a row under section 3 (Code review & PR quality) and
    bumps the mechanized count 26 -> 27 / total 73 -> 74.
- `.claude/hooks/tests/test_warn_stale_review_markers.sh`
  - 8 cases: no PR, no markers, fresh markers, stale rex / ceo /
    design (warn), delete mode, failed push. All pass locally.

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>

* chore(#111): upgrade pre-push-gate from advisory reminder to blocking check-runner (#121)

* chore(#111): upgrade pre-push-gate from reminder to blocking check-runner

Previously pre-push-gate.sh just printed a checklist of things to run
locally before pushing — it was advisory. The rule it enforces is a
HARD STOP per pr-workflow.md. That asymmetry meant agents routinely
pushed broken work and discovered it only when CI went red.

Replaces the reminder with a blocking runner that reads the list of
shell commands from project config (.pre_push.commands) and executes
them in sequence before a push is allowed through. First non-zero
exit blocks the push with exit 2 and prints the failing command plus
the last 20 lines of its output.

- Config key: .pre_push.commands[] — array of {name, run} objects.
  Shipped default is an empty list (hook stays a no-op on repos that
  haven't configured their checks yet, including the framework repo
  itself until it wires its own CI).
- Emergency bypass: '<!-- pre-push: skip -->' in the HEAD commit
  message. Grep-able on purpose so bypasses stay auditable.
- Fail-fast: once a command fails, the rest don't run. Parallel
  execution is a follow-up polish.
- 7 test cases in .claude/hooks/tests/test_pre_push_gate.sh — all
  pass on the shipped default + a minimal custom config.

Updates docs/rule-audit.md to flip "partial" → "yes" for the
"before git push" rule.

Integrates with the shared config reader landed in #109.

https://github.com/me2resh/apexyard/issues/111

* fix(#111): remove orphaned footnote reference from rule-audit

The previous advisory-mode footnote was superseded by pre-push-111
but its definition was accidentally kept, tripping markdownlint MD053
(unused reference definition). Drop it.

---------

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>

* chore(#107): add validate-issue-structure.sh PreToolUse hook (#122)

Mechanically enforces the ticket body schema when an agent files raw
`gh issue create` calls instead of going through the interactive
/feature, /task, /bug skills. Matches bracketed title prefix
([Feature] / [Chore] / [Bug] / [Docs] / etc.) against
`.ticket.required_sections` in project-config, and blocks (exit 2)
when any required section is missing or empty. Skip marker
`<!-- validate-issue-structure: skip -->` bypasses with a visible
stderr WARN for legitimate off-template tickets (epics, meta-threads).

Changes:

- .claude/hooks/validate-issue-structure.sh — the hook; reads schema
  via the shared _lib-read-config.sh, with inlined defaults for bare
  checkouts predating the config-schema rollout. Handles
  --body / --body-file / -F path.
- .claude/project-config.defaults.json — extends .ticket with
  required_sections (Feature/Chore/Refactor/Testing/CI/Docs/Bug) and
  skip_marker; other .ticket fields untouched.
- .claude/settings.json — new PreToolUse matcher on Bash(gh issue
  create *) alongside the existing suggest-ticket-template.sh and
  block-private-refs-in-public-repos.sh hooks.
- .claude/hooks/tests/test_validate_issue_structure.sh — 15 cases
  covering pass + fail paths per prefix, empty section detection,
  skip marker, unknown prefix, non-gh invocation, --body-file path.
- docs/rule-audit.md — new section 11 row, mechanized count +1.

Upstream ticket: https://github.com/me2resh/apexyard/issues/107

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>

* chore(#112): add require-agdr-for-arch-pr.sh PreToolUse hook (#123)

Closes the asymmetry noted in .claude/rules/agdr-decisions.md: every
other HARD STOP in the ruleset (merge approval, ticket-first,
migration-first) is mechanically enforced, but the /decide HARD STOP
was prose-only. The commit-time hook require-agdr-for-arch-changes.sh
catches one architectural change at commit; this new PR-time hook
catches the cumulative diff so reviewers always have a pointer to the
decision record.

- New hook at .claude/hooks/require-agdr-for-arch-pr.sh
  - Fires on Bash(gh pr create *)
  - Parses --title/--body/--body-file/-F <path>
  - Resolves base branch from --base, else upstream/dev, origin/dev,
    upstream/main, origin/main, main, master (in that order)
  - Computes `git diff <merge-base>..HEAD --name-only`
  - Triggers on any changed file matching .agdr_trigger_paths[], OR any
    dep-file addition (package.json via jq key-set diff; other
    dep files via a commented +/- line-count heuristic — version
    bumps match +/- counts and do not fire)
  - Blocks (exit 2) with a helpful message naming the triggers and
    pointing at /decide if the body has no `AgDR-\d+-[a-z0-9-]+`
    reference
  - Skip marker `<!-- agdr: not-applicable -->` bypasses with a
    visible WARN on stderr
  - Silent exit 0 on non-gh commands, empty diffs, unresolvable base

- Wired via .claude/settings.json PreToolUse Bash(gh pr create *)

- Adds two new top-level keys to .claude/project-config.defaults.json:
    agdr_trigger_paths      (shell globs — domain/, infrastructure/,
                             migrations/, *.tf, .github/workflows/, etc.)
    agdr_trigger_dep_files  (literal basenames — package.json,
                             pyproject.toml, Cargo.toml, go.mod, Gemfile)
  Hook has inline fallback defaults kept in sync.

- Adds docs/rule-audit.md entry in the AgDR section; bumps mechanized
  count 26 to 27 and total rows 73 to 74.

- Adds .claude/hooks/tests/test_require_agdr_for_arch_pr.sh (7 cases;
  all green): path-triggered without AgDR (block), with AgDR (pass),
  dep-file added (block), version-only bump (no fire), skip marker
  (pass + warn), non-matching diff (pass), non-gh command (no-op).

Closes https://github.com/me2resh/apexyard/issues/112

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>

* chore(#113): require Testing section in PR body (config-driven) (#124)

Extends validate-pr-create.sh with a required-sections check that
replaces the hardcoded Glossary-only grep. The list of required H2
headings is project-configurable via `.pr.required_sections[]`.
Shipped default is ["Testing", "Glossary"], matching the canonical PR
description shape in workflows/code-review.md.

- Each entry must appear as `## <Name>` (case-insensitive).
- Empty sections are tolerated at this layer (the issue-structure hook
  #107 does stricter empty-content checks for issue bodies; for PR
  bodies, empty sections are left to the reviewer's judgement).
- Skip marker `<!-- pr-sections: skip -->` bypasses with a visible
  stderr WARN — for trivial PRs (lint-only fixes, version bumps)
  where the full template is overkill.
- Reads from project config via the shared _lib-read-config.sh (#109).
  Inline fallback matches shipped defaults so bare checkouts predating
  #109 keep working.
- 8 test cases cover: all-sections pass, each missing section,
  missing-both (both errors printed), skip marker, case-insensitive
  headings, H3 rejection.

https://github.com/me2resh/apexyard/issues/113

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>

* chore(#114): enforce single Closes-keyword per PR body (#125)

* chore(#114): enforce single Closes-keyword per PR body

Caps distinct auto-closing references (close/closes/closed, fix/fixes/
fixed, resolve/resolves/resolved + N or owner/repo+N) at one per PR
body. Closes the loophole where the title validator limited the title
to one ticket but multiple Closes lines in the body would still auto-
close all of them on merge.

- Scans stripped of fenced code blocks so closing keywords inside a
  code sample do not count.
- Distinct counting: the same number referenced twice (e.g. via Fixes
  and Closes) counts as one.
- Cross-repo refs (owner/repo+N) count normally.
- Opt-in escape hatch: pr.allow_multiple_closes=true in
  project-config disables the check for teams that deliberately batch
  rollbacks or dependency bumps.
- Per-PR bypass: a multi-close-approved HTML comment in the body
  prints a visible stderr WARN and lets that PR through. Grep-able
  trace so bypasses are auditable.
- 10 test cases cover: one close passes, no-keyword passes, two
  distinct block, three mixed block, same-number-twice passes, code-
  fence-ignored, skip marker, cross-ref without keyword, opt-in
  config, cross-repo close.

Reads configuration via the shared _lib-read-config.sh (apexyard+109).

https://github.com/me2resh/apexyard/issues/114

* fix(#114): strip inline backticks and tilde fences from close-count scan

Rex caught a self-reflexive bug in the initial commit: documentation
mentioning closing keywords inside inline backticks (say a PR body
that explains the new hook with examples) counted as real closes, and
a skip marker inside inline backticks silently bypassed the check.
Future PRs that document the feature would trip the same trap.

Fix the code-region stripper to cover:
- Triple-backtick fences (already handled)
- Tilde fences (new)
- Inline-backtick spans (new)

Also run the skip-marker check against the stripped body, so a marker
used purely as documentation no longer activates a real bypass.

Three new test cases pin the behaviour:
- closing keywords in inline backticks are ignored
- skip marker inside inline backticks does NOT bypass
- tilde fences also get stripped

13/13 tests pass.

---------

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>

* feat(#108): add /tickets-batch skill for bulk-file flow (#127)

The fast happy path for filing 5–20 structured tickets in one intent
without dropping to raw `gh issue create` (non-conformant) or running
`/feature` 20 times serially (~100 turns of interview).

- .claude/skills/tickets-batch/SKILL.md — new skill spec. Asks
  shared-context questions (priority, epic, area-labels, repo) ONCE
  for the whole batch, then runs a ≤3-question micro-interview per
  ticket (type, one-line purpose, optional clarification when the
  inference is low-confidence). Confirms the full batch as a table,
  then files each via specific `gh issue create` calls (never a
  bulk JSON dump — the validator runs per-issue). Output conforms
  to `.ticket.required_sections` by construction. Caps at 20
  tickets per invocation.

- CLAUDE.md — added a row for /tickets-batch in the Available
  Skills table; bumped the count references from 33 to 34.

Refs https://github.com/me2resh/apexyard/issues/108

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>

* feat(#117): add /fan-out skill + parallel-work rule doc (#128)

- Add `.claude/skills/fan-out/SKILL.md` — spawns N parallel Agent calls
  in a single assistant message, with per-task agent type, worktree
  isolation, and foreground/background mode. Caps at 5 concurrent
  agents. Refuses fan-out when tasks share file write targets or have
  sequential dependencies. Includes pre-spawn active-ticket safety
  check and worktree merge-back flow that pauses on conflict.
- Add `.claude/rules/parallel-work.md` — trigger heuristic for when an
  agent should proactively offer fan-out (>= 2 file-independent,
  context-independent, individually substantial work items). Pairs
  with the skill: rule says when, skill says how.
- Update `CLAUDE.md` — bump rules count to 9, skills count to 34, add
  `/fan-out` row to the skills table.

Refs https://github.com/me2resh/apexyard/issues/117

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>

* chore(#116): adopt release-cut branch model (dev/main + tags) — framework only (#126)

* chore(#116): adopt release-cut branch model (dev/main + tags)

Formalises the dev/main split that already exists informally — dev is
the daily-work branch where every PR lands, main is release-only,
tagged with semver on each merge. Framework-only: managed projects
under apexyard governance stay trunk-based.

Added:
- AgDR-0007 — decision record (options table covers full git flow vs
  trunk-only vs gitflow-lite; chose gitflow-lite)
- /release skill — diff dev against main, propose semver bump from
  conventional commits, generate CHANGELOG, open release PR, tag
  after merge
- docs/release-process.md — prose runbook for cutting a release
  (manual fallback for the skill)
- .git.protected_branches in project-config.defaults.json
  (main/master/dev/develop)

Modified:
- block-main-push.sh — now blocks direct pushes/commits to all
  configured protected branches (was: hardcoded main/master). Reads
  .git.protected_branches via the shared config reader (apexyard#109).
- CLAUDE.md — new section under Git Conventions explaining the
  dev/main model + the framework-only scope. Skill table entry for
  /release. Skills count bumped to 34.
- docs/multi-project.md — note that upstream/main is release-only
  and the dev/main split is framework-only.

Non-consequences (per AgDR-0007):
- No release/* or hotfix/* branches. Hotfixes are normal patches
  cut quickly. Revisit if multi-version maintenance becomes a need.
- No automatic on-merge issue closing for dev PRs. The release PR's
  body aggregates all Closes references for the batch and triggers
  auto-close en masse when it merges to main. Manual close in the
  meantime.
- CI workflows trigger on pull_request regardless of base, so
  dev-targeting PRs already get the full check matrix — no
  workflow file edits needed.

https://github.com/me2resh/apexyard/issues/116

* fix(#116): satisfy markdownlint MD032 + MD060 in 116 docs

Auto-fix added blank lines around lists in AgDR-0007 (MD032) and
spaced the table separators in AgDR-0007 + multi-project.md (MD060).
Content unchanged.

---------

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>

* fix(#106): CHANGELOG fallback in drift hook for squash-merged forks (#129)

* fix(#106): CHANGELOG fallback in drift hook for squash-merged forks

The v1.1.0 tag-reachability check (`git tag --merged main`) misfires on
forks that sync via GitHub's default squash-merge: the squash collapses
the upstream-tag commit into a synthetic SHA, the tag stops being
reachable, and the banner keeps firing forever.

Discovered live on the first real-world `/update` flow: ops fork
squash-merged the v1.1.0 sync PR, banner kept saying "v1.1.0 available"
even though the fork was content-caught-up.

This commit adds a CHANGELOG-content fallback that fires only when the
primary tag check fails. If the fork's main has a heading
`## [X.Y.Z]` matching the upstream tag's version, treat the release as
absorbed and stay silent. Tolerant grep (matches the apexyard CHANGELOG
format from v1.1.0 onward, with leading-`v` stripping for tag→heading
conversion).

The merge-commit and rebase paths are unchanged — primary tag check
still works for them, and the fallback never fires when it shouldn't.

Test coverage (5 cases):
- squash-merge fork caught up to v1.1.0 → silent (the fix)
- merge-commit fork caught up to v1.1.0 → silent (regression check)
- fork stopped at v1.0.0 → banner fires
- fork has its own newer tag → silent
- squash-merge but no CHANGELOG on fork → banner fires (no false silence)

Records the strategy update in docs/agdr/AgDR-0008-…md (extends
AgDR-0005's tag-based-drift design).

https://github.com/me2resh/apexyard/issues/106

* fix(#106): satisfy markdownlint MD032/MD060 + shellcheck SC2164

Rex flagged two CI-blocking issues on the original 106 commit:

- AgDR-0008 had three bulleted sub-lists in the Consequences section
  without surrounding blank lines (MD032). Added blanks and padded the
  one tight-pipe table separator (MD060).
- The 106 test fixture had five subshell `cd "$fk"` calls without
  `|| exit 1` (SC2164). Added the guard to all five.

5/5 tests still pass after the fix. No semantic change to the hook
or the test logic.

---------

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>

* feat(#130): add /validate-idea skill — lightweight pre-spec gate (#131)

A 10-minute, 5-question check that sits between /idea and /write-spec.
Designed for solo founders running ApexYard — not a heavyweight
methodology like event storming or Wardley mapping.

Five questions, asked one at a time:
  1. Who is this specifically for?
  2. What do they do today instead?
  3. What's the smallest version that proves the value?
  4. What would prove this is wrong? (kill criteria)
  5. Build, buy, or rent?

Output: a one-page validation doc with a GREEN/YELLOW/RED verdict.
RED auto-updates the IDEA-NNN backlog row to WONTDO.

Integration:
  /idea — adds an optional default-no "Validate now?" step after
    capture (and after the optional GitHub Issue offer).
  /handover — adds a conditional "this looks dormant, validate?"
    step at the end of the integration plan, gated on the dormancy
    heuristic (last commit > 90d AND zero open PRs AND no recent
    issue activity). Healthy projects don't see the prompt.

CLAUDE.md skills count bumped to 35; new skills row added.

https://github.com/me2resh/apexyard/issues/130

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>

* feat: configurable voice prompts on assistant pause (AgDR-0009-voice-prompts-on-pause) (#135)

Stop hook that speaks the assistant's question aloud (Jarvis-style)
when it pauses for user input. Initial phase is macOS-only via `say`,
no voice input — user replies via keyboard.

Default OFF. Adopters opt in by overriding `voice_prompts.enabled` to
true in `.claude/project-config.json`.

Files:
- .claude/hooks/voice-prompt-on-pause.sh — Stop hook with config gate,
  trigger heuristic (questions-only by default), markdown stripping,
  sentence-boundary truncation, fire-and-forget say invocation
- .claude/hooks/tests/test_voice_prompt_on_pause.sh — 9 cases covering
  disabled-default, enabled+question, enabled+statement, approved-pattern,
  abc-menu, malformed-transcript, no-say-on-PATH, trigger-always,
  markdown-stripping
- .claude/project-config.defaults.json — voice_prompts schema block
  added (enabled, voice, max_chars, rate_wpm, trigger), default OFF
- .claude/settings.json — new Stop hook entry wired with the standard
  ops-root resolver wrapper
- docs/agdr/AgDR-0009-voice-prompts-on-pause.md — design rationale,
  options matrix (status quo / macOS say / cloud TTS / ML detection),
  consequences, future phases
- docs/project-config.md — new "Voice prompts" section with override
  examples and privacy notes

Test mode: hook respects VOICE_PROMPTS_SYNC=1 to run say synchronously
(test runners need this so assertions don't race against orphaned
background processes). Production invocations always run async.

Future phases (out of scope here, AgDR §"Future phases"):
- Phase 2: cross-platform TTS (Linux espeak, Windows SpeechSynthesizer)
- Phase 3: cloud TTS providers (OpenAI, ElevenLabs) — privacy AgDR-worthy
- Phase 4: voice input via Whisper-based STT
- Phase 5: per-message overrides

Refs: https://github.com/me2resh/apexyard/issues/134

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>

* feat(#141): add /debug skill — structured hypothesis-driven debugging (#142)

* feat(#141): add /debug skill — structured hypothesis-driven debugging

Adds a methodology skill that enforces five disciplines:

  1. Capture the symptom precisely (exact URL, exact response, exact step)
  2. Read the architecture before guessing (map every layer the request
     touches, file by file)
  3. Form a hypothesis ladder (3–5 candidates, each with an explicit
     evidence test that confirms or refutes it)
  4. Gather evidence first, fix second
  5. Verify the fix against the original symptom evidence (re-run the
     same `curl` / browser repro you used in step 4 — unit tests
     verify code, not feature, correctness)

Stack appendices (Web, Desktop) carry stack-specific surface-evidence
requirements (step 1), architecture-surface maps (step 2), and
evidence-tests cookbooks (step 4). The methodology body stays portable
across stacks; appendices are where stack-specific knowledge accrues
over time.

Web appendix covers browser routing, framework configs (Next/Nuxt/Vite),
SPA-fallback layers, CDN, origin, the shared API client, backend
handlers, and auth providers. Desktop appendix covers Electron / Tauri
/ native-shell concerns: app entry points, IPC bridges, native modules,
auto-updater, sandbox / entitlements, code signing, crash reports.

Includes "When NOT to use" guidance so the methodology overhead doesn't
sandbag simple bugs (typos, off-by-ones, greenfield exploration).

Motivated by a real OAuth debug session in a managed project where
three sequential fixes chased adjacent symptoms because each was
hypothesis-then-fix without evidence in between. The skill is the
"never do that again" guardrail.

Closes #141

* fix(#141): scrub private project issue numbers from anti-pattern table

Rex review on PR #142 caught that line 155 of the skill's anti-pattern
table still named the originating PRs (#375, #377, #380) from the
private project where the methodology was first exercised. The PR body
and commit message were correctly abstracted earlier, but this in-file
reference slipped through — the leak-protection hook only scans gh
issue/pr writes, not staged file content, so mechanical enforcement
didn't catch it.

Replaced with "Three sequential PRs chasing the same symptom because
each was based on a different guess (no evidence test in between
cycles)" — same pedagogical value, zero attribution.

Refs #141

---------

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>

* docs(#143): document split-portfolio mode + add /setup privacy gate (#144)

Adopters on GitHub Free with any private project hit a silent privacy bug
following today's docs: forking apexyard makes a public fork that you cannot
later flip to private (GitHub policy), and committing `apexyard.projects.yaml`
+ `projects/<name>/` to the fork publishes private project names + handover
findings on a public GitHub repo.

This PR documents the supported workaround — split-portfolio mode — and
adds an upfront privacy gate to the /setup skill so new adopters never
hit the trip-wire silently.

docs/multi-project.md:

- New "Two setup modes — pick the one that matches your privacy needs"
  section before TL;DR, with a side-by-side table and the explicit
  trip-wire callout.
- Existing TL;DR retitled "TL;DR — single-fork mode (default)" with a
  one-line pointer to the split-portfolio section.
- New "Split-portfolio mode — public framework + private portfolio"
  section between the existing setup steps and the directory-layout
  section. Includes:
  - The two-repo layout (~/ops/apexyard public + ~/ops/portfolio private)
  - 7-step setup walkthrough with copy-pasteable commands
  - Daily workflow + upstream sync notes (both unchanged)
  - Trade-offs (two repos to maintain, two clones per machine, one
    upstream-sync conflict path on `projects/README.md`)
  - "Migrating from single-fork to split-portfolio" recovery flow with
    the explicit warning that GitHub Issue / PR edit history survives a
    force-push and must be redacted separately

.claude/skills/setup/SKILL.md:

- New Step 2a: privacy gate — asks "are any projects private?" before
  proposing the config. Branches on the answer:
  - All public                                → single-fork mode
  - GitHub Pro / Team / Enterprise            → single-fork mode (private
                                                  forks of public repos
                                                  are supported on those
                                                  plans)
  - Any private + GitHub Free                 → split-portfolio mode
- New Step 2b: walks through the split-portfolio setup interactively
  (private repo create, sibling clone, gitignore + symlink) when the
  privacy gate triggers.
- Detection: `test -L apexyard.projects.yaml` short-circuits Step 2b
  for adopters already in split mode.
- Explicit "do NOT auto-migrate" rule for adopters already in single-fork
  mode with private names already pushed — that path is destructive
  (force-push history rewrite + redact issue/PR bodies + delete backup
  branch) and warrants a deliberate, eyes-open run, not a /setup side
  effect.

Out of scope for this PR (tracked separately on #143):

- `portfolio:` config block in `onboarding.yaml` schema
- Skill audit + refactor to honour configured `registry` / `projects_dir`
  / `ideas_backlog` paths instead of hardcoded fork-relative paths
- `/split-portfolio` migration helper skill that automates the recovery
  flow currently documented manually

This is the docs-and-setup-question minimum-viable starter — the
framework code refactor is mechanical and lands as a follow-up.

Refs #143

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>

* feat(#145): portfolio config + self-healing + /split-portfolio helper (#147)

* feat(me2resh/apexyard#145): portfolio config block + self-healing + /split-portfolio helper

Closes the framework primitive deferred from me2resh/apexyard#144. Adds
first-class config-driven path resolution for the portfolio registry,
projects dir, and ideas backlog, with self-healing surfacing of broken
config at session start, plus a new /split-portfolio skill that automates
the destructive recovery flow.

Schema, helper, and hook:
- .claude/project-config.defaults.json: new portfolio: block
  (registry, projects_dir, ideas_backlog) with defaults matching
  today's single-fork layout
- .claude/hooks/_lib-portfolio-paths.sh: new sourceable helper exposing
  portfolio_registry, portfolio_projects_dir, portfolio_ideas_backlog,
  portfolio_validate, portfolio_clear_cache. Resolves relative paths
  against the ops-fork root.
- .claude/hooks/check-portfolio-config.sh: new SessionStart hook —
  silent on OK, one-line banner on broken config, never blocks session
- .claude/hooks/tests/test_portfolio_paths.sh: 13 cases covering
  defaults, absolute/relative overrides, validate states, cache clear

Skill audit (18 SKILL.md files):
- Adds Path resolution callout pointing at the helper
- handover bash blocks now source helper and use $(portfolio_registry)
  instead of literal apexyard.projects.yaml
- setup Step 2b now writes the portfolio: config block (recommended)
  and validates via portfolio_validate before declaring success;
  symlink approach kept as legacy fallback

New skill (.claude/skills/split-portfolio/SKILL.md):
- 10-step migration with explicit operator-confirmation gates at each
  destructive step (force-push, body redaction, branch deletion)
- --verify mode: read-only state report (mode, paths, validate, drift)
- --dry-run mode: prints commands without executing
- Pre-flight refusals: already-private fork, paid GitHub plan, dirty
  working tree, already-migrated state
- Step 9 writes the portfolio: config block (not symlinks) — symlink
  fallback documented for adopters on older framework versions
- Step 9 surfaces the GitHub timeline-API survival caveat verbatim
- Idempotent re-runs: detects partial-migration state and resumes

Docs (docs/multi-project.md):
- Layout section describes both modes (config-block recommended,
  symlink legacy) with self-healing notes
- Setup steps split into config-block mode and legacy symlink mode
- Migration section now points at /split-portfolio skill; manual
  recipe preserved as fallback

AgDR (docs/agdr/AgDR-0010-portfolio-config-and-self-healing.md):
- Full Y-statement, options, decision, consequences, future phases
- Schema decision rationale: project-config.json over onboarding.yaml
  because runtime path resolution belongs in project-config

Closes me2resh/apexyard#145
Refs me2resh/apexyard#146 (delivered same PR; closed manually post-merge
per the single-Closes-keyword rule in validate-pr-create.sh)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(me2resh/apexyard#145): markdownlint MD031 — blank lines around fences

CI's markdownlint-cli2 (v0.34.0) flagged the JSON + bash fenced code
blocks I added in setup/SKILL.md Step 2b without surrounding blank
lines. Added the required blanks. No content change.

Refs me2resh/apexyard#147

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>

* docs(me2resh/apexyard#148): correct privacy-gate wording — adopter action, not framework auto-publish (#149)

The privacy-gate wording introduced in PR #144 (and unchanged in PR #147)
attributed the publication to the framework rather than the adopter:

  "the standard fork-and-commit setup will silently publish your private
   project names on a public GitHub repo"

That's factually wrong. ApexYard never pushes anything without explicit
operator approval — the publication only happens when the adopter
themselves runs git push. The "silently publish" framing read as if the
framework auto-publishes, which is misleading and undermines trust in
the rest of the framework's safety claims.

Two prose-only edits, no code, no behavior change:

- .claude/skills/setup/SKILL.md Step 2a — replaced "will silently
  publish ..." with adopter-action language ("you might accidentally
  publish ... a stray git push after registering them — I won't push
  without your approval, but the risk is on the adopter once the data
  is committed locally")

- docs/multi-project.md trip-wire callout — replaced "silently publish
  their portfolio names the moment they push" with "risk accidentally
  publishing their portfolio names with a stray push (the framework
  itself never pushes without operator approval, but once the registry
  is committed locally the next push exposes it)"

Verified: `grep -r "silently publish" .claude/skills/ docs/` returns no
hits.

Closes me2resh/apexyard#148

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(#150): bootstrap-skill exemption + Bash-write coverage (#152)

Closes the legitimate-bypass case (me2resh/apexyard#150) and the
illegitimate-bypass case (me2resh/apexyard#151) together so the
ticket-first gate is coherent. Shipping either alone would leave a
window where the framework is internally inconsistent — see AgDR-0011
for the full rationale.

Bootstrap exemption (me2resh/apexyard#150):
  - .claude/session/active-bootstrap marker, written by /setup,
    /handover, /update, /split-portfolio on entry; cleared on exit
  - SessionStart sweep (clear-bootstrap-marker.sh) for stale markers
    from interrupted sessions
  - require-active-ticket.sh reads the marker and exempts skills on
    the configured ticket.bootstrap_skills list
  - bootstrap_skills list lives in .claude/project-config.defaults.json
    (extendable per fork via .claude/project-config.json)

Bash-write coverage (me2resh/apexyard#151):
  - new _lib-detect-bash-write.sh — heuristic detector for output
    redirection, tee, sed -i, awk -i inplace, python/node/ruby
    embedded interpreters
  - require-active-ticket.sh + require-migration-ticket.sh now fire
    on Bash in addition to Edit|Write|MultiEdit
  - design choice: false-negatives preferred over false-positives
    (the matcher errs toward "let through" rather than block legit
    read-only commands)

Tests: 32 unit cases on the lib + 12 integration cases on the hook,
including the exact me2resh/apexyard#151 bypass repro from the issue
body.

Closes me2resh/apexyard#150

Will manually close me2resh/apexyard#151 post-merge per the
single-Closes-per-PR rule (precedent: AgDR-0010 / PR #147).

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(#153): extend Bash-write matcher beyond first-version coverage (#155)

Closes me2resh/apexyard#153.

Extends `_lib-detect-bash-write.sh` (introduced for me2resh/apexyard#151
in PR #152) with the matcher families flagged by Rex's review of #152.
AgDR-0011 already frames the matcher as a living list extended on
observation; this commit just walks the list.

New matcher families:
- File-moving builtins: `cp`, `mv`, `rm`, `dd`, `install` (anchored at
  command-start; `--help`/`--version` and `git rm`/`git mv` excluded)
- Archive / network writes: `tar -x` / `tar --extract`, `curl -o` /
  `--output`, `wget -O` / `--output-document`
- Additional embedded interpreters: `perl -e`, `php -r` (keyword-gated
  like python/node/ruby); `go run`, `deno run`/`deno script.ts`,
  `bun run`/`bun script.ts` (categorical script runners)
- Python helpers: `pathlib.Path().touch()`, `shutil.copy*`,
  `shutil.move`, `os.rename` added to the `python -c` and python
  heredoc keyword list
- Heredoc variants for `ruby` and `node` (previously only python
  heredoc was covered)

Extractor extensions:
- `cp` / `mv`: last positional arg
- `curl -o` / `--output`: file argument
- `wget -O` / `--output-document`: file argument
- `tar -x`, `go run`, `deno`, `bun`, `perl -e`, `php -r`: return empty
  (caller applies gate categorically per AgDR-0011)

Test count rose from 32 to 86. Negative-class counterexamples cover
the trickiest false-positive surfaces: `tar -t` listing, `cp --help`,
`rm --version`, `git rm`, `curl` bare URL fetch, `wget` bare URL fetch,
`deno fmt`, `deno test`, `go build`. Existing
`test_require_active_ticket_bash.sh` regression suite still passes 12/12.

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(#154): mock gh in test sandboxes to remove live-tracker dependency (#156)

- add _lib-mock-gh.sh helper that installs a fake `gh` on the sandbox PATH;
  intercepts `gh issue view <N> ... --json ...` and returns synthetic
  `{"number":N,"state":"OPEN"}` (overridable per-num via mock_gh_set_state)
- wire the shim into test_single_closes_per_pr.sh and
  test_validate_pr_required_sections.sh so the validator's CLOSED-issue
  refusal no longer breaks the suite when upstream issues are closed
- both files previously failed every case (0/13 and 0/8) because their PR
  titles reference #114 / #113, which are now CLOSED upstream
- post-fix: 13/13 and 8/8; full suite remains green

Closes me2resh/apexyard#154

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>

* feat(#132): structured CEO marker + same-turn merge in /approve-merge (#158)

* feat(#132): structured CEO marker + same-turn merge in /approve-merge

Closes me2resh/apexyard#132 (drop the "stop before merge" rule) and
me2resh/apexyard#48 (harden CEO marker against self-approval bypass)
together. The two threads compose — see AgDR-0012 for the full
rationale.

Streamline (#132):
  - /approve-merge now runs `gh pr merge --squash --delete-branch`
    in the same turn as the marker write, by default
  - --no-merge opt-out preserves the deferred-merge case
  - The discrete approval moment is the SKILL INVOCATION, not a
    follow-up "now do the merge" message

Harden (#48):
  - CEO marker is now a structured key/value file with required fields:
      sha=<HEAD>
      approved_by=user
      skill_version=2
    Validated by block-unreviewed-merge.sh; bare-SHA legacy markers
    rejected with a clear "stale format" error pointing at /approve-merge
  - The model's bare `echo SHA > <pr>-ceo.approved` bypass is now
    mechanically rejected. Forging the structured fields requires a
    deliberate, visible rule violation rather than a one-line accident
  - Optional audit fields (approved_at, approval_summary) capture the
    "what did the user say when they approved" trail
  - Rex marker stays bare-SHA — different threat model (automated
    reviewer, not human authorization moment)

pr-workflow.md reframed: "the load-bearing rule is explicit per-PR
approval, not two user messages." The merge is a deterministic
consequence of the approval invocation.

Tests: 12 cases on the hardened hook covering the new format end-to-end
(valid v2, missing rex/ceo, bare-SHA legacy rejected, missing
approved_by, wrong approved_by, skill_version=1, sha mismatch,
non-merge no-op, gh-api shape gated). Full suite: 205/205 across 13
test files.

Will manually close me2resh/apexyard#48 post-merge per the
single-Closes-per-PR rule (precedent: PR #152 / AgDR-0011).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(#132): redact private project reference from AgDR-0012

Abstracted two references to a registered private project that named
the project's owner/repo. The leak-protection hook caught one in the
PR body; this fixup removes the matching references from the
AgDR-0012 file content (which would otherwise have shipped the names
to me2resh/apexyard public repo via the merge).

The pre-existing reference at .claude/rules/pr-workflow.md:130
(documenting #47) is untouched — it predates this PR and is already
on the public repo's history.

Refs me2resh/apexyard#132.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(#132): markdownlint blanks-around-fences + typo fix

Two small fixups against red CI / Rex feedback:

- AgDR-0012 line 63: fenced code block now has a blank line before it
  (MD031). markdownlint-cli2 0.13.0 was rejecting the indented fence
  inside the bullet because the fence's preceding line was the bullet
  text (no blank).
- approve-merge SKILL.md line 172: typo `deferes` → `defers` (Rex flag
  on PR #158).

Refs me2resh/apexyard#132.

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>

* chore(#157): remove voice-prompts feature + correct hook/skill counts (#161)

* chore(#157): remove voice-prompts-on-pause feature + correct hook/skill counts

Closes me2resh/apexyard#157 (sunset the voice-prompts feature) and
me2resh/apexyard#77 (hook count off-by-one in CHANGELOG / CLAUDE.md)
in one bundled PR. AgDR-0013 supersedes AgDR-0009 with the full
rationale; both AgDRs are preserved (decision records are append-only
history).

Removed (#157):
  - .claude/hooks/voice-prompt-on-pause.sh
  - .claude/hooks/tests/test_voice_prompt_on_pause.sh
  - Stop matcher block in .claude/settings.json (became empty after
    voice removal)
  - voice_prompts block in .claude/project-config.defaults.json
  - "## Voice prompts" section in docs/project-config.md
  - voice_prompts mention in AgDR-0010 line 32 (replaced with
    leak_protection / ticket as still-current example config blocks)

Preserved:
  - docs/agdr/AgDR-0009-voice-prompts-on-pause.md — historical record;
    new "Superseded by: AgDR-0013" header at the top
  - AgDR-0010 line 115 reference to AgDR-0009 — still accurate as a
    historical pattern reference

Counts corrected (#77):
  - CHANGELOG.md v0.3.0 stats: "17 hooks" → "18 hooks" (historical
    fix — at v0.3.0 there were actually 18 hooks)
  - CLAUDE.md table line: "18 shell scripts" → "24 shell scripts"
    (current count after this removal)
  - CLAUDE.md table line: "35 slash commands" → "39 slash commands"
  - CLAUDE.md "Available skills (34)" → "Available skills (39)"
  - CLAUDE.md quick-reference "Skills (35 slash commands)" →
    "(39 slash commands)"

Why bundled: #77's correct count depends on whether voice is still in
the framework. Shipping #77 before #157 would write a number that's
wrong by one again the moment #157 lands. Same shape as previous
bundles (PR #152 / AgDR-0011, PR #158 / AgDR-0012).

Why no adopter-facing changelog mention of the voice removal: the
feature never reached a tagged release on main. v1.1.0 didn't have
it; v1.2.0 won't have it. From the adopter's perspective there's
nothing to retire. AgDR-0013 captures the framework's internal record
for future contributors. See AgDR-0013 § "No adopter-facing changelog
mention".

Tests: full hook test suite green (196 cases across 12 files —
test_voice_prompt_on_pause.sh removed). No regressions.

Will manually close me2resh/apexyard#77 post-merge per the
single-Closes-per-PR rule (precedent: PRs #152, #158).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(#157): unwire voice from settings + configs + docs + AgDR-0013

Continuation of d78eb08 (the file deletions). Squash-merge will
collapse both into one PR commit. This commit captures:

- .claude/settings.json — Stop matcher block removed (was the only
  hook in it; entire matcher gone)
- .claude/project-config.defaults.json — voice_prompts block + its
  _comment removed
- docs/project-config.md — "## Voice prompts" section removed
- docs/agdr/AgDR-0009-voice-prompts-on-pause.md — "Superseded by"
  header added at the top, content otherwise preserved as history
- docs/agdr/AgDR-0010-portfolio-config-and-self-healing.md — line 32
  example reference swapped from voice_prompts to
  leak_protection / ticket (still-current config blocks)
- docs/agdr/AgDR-0013-sunset-voice-prompts.md — new supersession AgDR
- CLAUDE.md — hook count 18 → 24, skill count 35 → 39 (three
  occurrences each, all aligned to current reality)
- CHANGELOG.md v0.3.0 stats — "17 hooks" → "18 hooks" historical fix
  (#77 acceptance criterion 1)

Refs me2resh/apexyard#157 + me2resh/apexyard#77.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(#157): markdownlint MD028 + resolve stale AgDR-numbering ref

Three small fixups against Rex CHANGES-REQUESTED on PR #161:

- AgDR-0009 line 3: promote "Superseded by:" header out of a
  blockquote. The original "I decided ..." canonical blockquote at
  line 5 was being merged with the new supersession blockquote
  (markdownlint MD028 — "no blanks inside blockquote").
- AgDR-0013 line 3: same shape — "Supersedes:" header now a plain
  bold paragraph, canonical "I decided ..." blockquote untouched.
- approve-merge SKILL.md line ~187: stale conditional reference
  "AgDR-0012 (or 0013 — depends on whether voice-removal lands
  first)" — order is now resolved (12 = approve-merge bundle, 13 =
  voice removal). Drop the parenthetical.

Refs me2resh/apexyard#157 + me2resh/apexyard#77.

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>

* feat(#160): multi-tab terminal demo on the landing site (#162)

Landing-site `site/index.html` terminal demo previously played one
canonical flow ("one ticket, start to finish") on autoplay. With the
v1.2.0 skill surface expansion (4 new skills, 8+ new hooks), one flow
no longer represents the framework's breadth.

Adds tabs to the terminal chrome — four flows visitors can either let
auto-cycle or click directly:

  1. one ticket — existing flow, unchanged content
  2. /handover  — adopt an external repo into the portfolio
  3. /setup     — first-run framework bootstrap on a fresh fork
  4. /fan-out   — spawn 3 parallel agents on independent tickets

Auto-advance: on completion of the active tab's script, the demo
pauses ~1.8s then advances to the next tab. Loops at the end. User
can interrupt by clicking any tab or hitting Replay.

Implementation:
- HTML chrome — single title span replaced with a tablist of 4 button
  tabs. ARIA `role="tablist"` / `role="tab"` / `aria-selected` so
  keyboard + screen-reader users get the same semantics as sighted
  ones.
- CSS — new `.shell-demo__tabs` + `.shell-demo__tab` with an accent
  underline on the active tab. Tabs scroll horizontally on narrow
  viewports (mobile responsive).
- JS — refactored the existing IIFE from one `script` array to an
  array of four. Added `setActiveTab()` for ARIA state, `play(idx)`
  takes a tab index, end-of-script auto-advances to `(idx + 1) % N`
  unless the user clicked away during the pause. Adds a new `cmd`
  type alongside `you` for slash-command invocations (renders with
  the same `>` prompt prefix). prefers-reduced-motion still bails
  early and leaves the static seed visible.
- Static seed (the non-JS fallback) still shows tab 0's content, so
  reduced-motion / no-JS visitors see the one-ticket flow as before.

Hero metrics also corrected to current reality (#77 / PR #161 covers
CLAUDE.md and CHANGELOG.md; this PR catches the same numbers in the
landing site):

  Skills    32 → 39
  Hooks     18 → 24

The tabs ship in v1.2.0 alongside the framework changes that make
the new flows worth showcasing.

Refs me2resh/apexyard#160 (release v1.2.0 + landing-site refresh).

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(#163): default the split-portfolio sibling repo name to <fork>-portfolio (#164)

Closes me2resh/apexyard#163.

The split-portfolio mode docs and skills previously suggested
`your-org/ops` as the default name for the private sibling repo, and
`portfolio/` as the local clone directory. Both too generic — adopters
running multiple ops setups end up with `your-org/ops` collisions, and
a bare `portfolio/` dir gives no signal about which framework it
belongs to when it sits next to other unrelated `portfolio/` dirs.

`<fork>-portfolio` is now the default — keeps the relationship to the
public fork explicit on disk and on GitHub. If the fork is named
`your-org/apexyard`, the portfolio defaults to
`your-org/apexyard-portfolio`. If the fork was renamed (e.g. `cos`),
the portfolio defaults to `cos-portfolio`. Adopters with custom names
keep working — the `portfolio:` config block resolves whatever path
they configured.

Files updated:

  docs/multi-project.md
    - Layout diagrams use `apexyard-portfolio/` as the sibling
    - Setup walkthrough Step 2 + Step 3 use `your-org/apexyard-portfolio`
      and explain the `<fork>-portfolio` pattern
    - Config-block + symlink path examples updated to
      `../apexyard-portfolio/...`
    - Daily workflow + cross-machine clone commands updated
    - The two existing `your-org/ops` references that remain are
      fork-rename examples (lines 52, 64) — kept as-is, since renaming
      the fork to `ops` is still valid (the portfolio would then
      default to `ops-portfolio`)

  .claude/skills/setup/SKILL.md
    - Step 2b's "default suggestion" for the private repo name is now
      `your-org/<fork>-portfolio`, computed dynamically from the
      fork's repo name via `gh repo view --json name -q .name` so the
      suggestion is correct even when the fork was renamed
    - Clone command no longer needs a second arg — the repo name IS
      the directory name
    - Config-block paths updated to `../apexyard-portfolio/...`

  .claude/skills/split-portfolio/SKILL.md
    - Step 3's suggested-name template is now `<account>/<fork>-portfolio`
      with the same dynamic-fork-name resolution

Mechanism unchanged. The `portfolio:` config block in
`.claude/project-config.json` still takes any path; this PR is purely
default-suggestion + example prose.

No tests required (skills are markdown instructions; no automated
coverage today).

Refs `apexyard.projects.yaml.example` uses "ops repo" as a generic
term meaning "the operational management fork" — not a name — kept
as-is.

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(#165): skills reference page on the landing site + changelog link (#167)

Closes me2resh/apexyard#165.

Adds a public, browseable index of every apexyard slash command
alongside a one-click changelog link from the homepage nav.

site/skills.html (new):
  - Lists all 39 skills currently shipping in .claude/skills/
  - Each entry: slash command, argument hint, description (taken
    verbatim from the SKILL.md frontmatter so the page matches the
    runtime exactly)
  - 10 categories: Setup & onboarding, Daily ops, Tickets & ideas,
    Specs & decisions, Code review & merge, Architecture & dev tools,
    Production-readiness audits, Workflow primitives, Communications,
    Deprecated
  - Same brutalist-terminal design tokens as the homepage — JetBrains
    Mono, paper-cream background, single warning-red accent, sharp
    corners. Inlined CSS to keep the static-only no-build-step
    convention; design vars duplicated rather than extracted to a
    shared file (~18 vars; cheap to keep in sync).
  - Mobile responsive — skill grid collapses to single-column under
    720px; titlebar nav hides non-CTA items on narrow viewports.
  - Reduced-motion friendly (no animation in the first place).
  - Internal anchor TOC at the top so the page scans in seconds.

site/index.html (nav addition):
  - Added two nav links to the titlebar between "what's in the box"
    and the github CTA:
      • skills      → ./skills.html
      • changelog → https://github.com/me2resh/apexyard/releases
  - The changelog link points at the GitHub releases page (not the
    raw CHANGELOG.md file) so it auto-resolves to the latest tagged
    release on each visit. v1.2.0 lands and the link is already there.

No new dependencies, no build step, no JS for the skills page. The
existing site convention (one-html-file-per-route, inlined CSS,
optional progressive-enhancement JS) is preserved.

Refs me2resh/apexyard#160 (release v1.2.0 + landing-site refresh) —
this is the second site-side deliverable for that ticket; the release
tag itself follows once me2resh/apexyard#159 (testing) closes.

Follow-up worth a separate ticket: a small generator script that
walks .claude/skills/*/SKILL.md, parses YAML frontmatter, and emits
the skills.html sections automatically. Out of scope for v1.2.0 —
first version is hand-curated and will need maintenance until that
generator lands.

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(#168): accept release/vN.N.N branches + release(...) PR titles (#169)

Closes me2resh/apexyard#168.

The /release skill prescribed `release/vA.B.C` as the source-branch
name and `release: vA.B.C` as the PR title for the dev → main release
PR (per AgDR-0007). Both were rejected by the framework's own
validators:

  validate-branch-name.sh required {type}/{TICKET-ID}-{description};
  release/v1.2.0 has no ticket-id portion.

  validate-pr-create.sh required type(SCOPE): form with `release` not
  in pr.title_type_whitelist.

The contradiction surfaced cutting v1.2.0 — the first release under
the dev/main model.

Three small changes:

1. .claude/hooks/validate-branch-name.sh — added an early-out branch
   that accepts ^release/vN.N.N(-rcN)?$ as a valid name. Narrow,
   intentional exception for the framework's release-cut convention;
   release branches don't carry a ticket-id because the release itself
   IS the ticket.

2. .claude/project-config.defaults.json — added "release" to
   pr.title_type_whitelist so a title like `release(#160): v1.2.0`
   passes validate-pr-create.sh's existing regex unchanged.

3. .claude/skills/release/SKILL.md step 4 — corrected the prescribed
   PR title to `release(#<release-ticket>): vA.B.C` so future /release
   invocations produce a title that satisfies the validators by
   construction.

Tested:

  bash .claude/hooks/validate-branch-name.sh against:
    release/v1.2.0           → 0   (allowed, release-special-case)
    release/v1.2.0-rc1       → 0   (allowed, RC variant)
    release/v9.9.9           → 0   (allowed)
    release/foo              → 2   (correctly blocked)
    release/v1               → 2   (correctly blocked)
    chore/GH-168-fix         → 0   (allowed, standard pattern)
    feature/GH-1-x           → 0   (allowed, standard pattern)

  Full hook test suite: 196/196 cases green across 12 test files.

Refs: surfaced 2026-05-04 cutting the first release under AgDR-0007.

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(#170): exempt release/vN.N.N from validate-pr-create's branch-id check (#171)

Closes me2resh/apexyard#170.

Completes the work started in #169 (closing #168). #169 added a
release-pattern early-out to validate-branch-name.sh so release/vN.N.N
branches pass the branch-name validator. But validate-pr-create.sh has
its own independent branch-id check at line 273 that #169 didn't
touch — and it still rejects release/v1.2.0 because that name doesn't
contain a ticket-id substring.

This is the same class of contradiction #168 fixed; the fix is the
same shape. Add the same release-pattern early-out to the branch-id
check in validate-pr-create.sh:

  - if the branch matches ^release/vN.N.N(-rcN)?$ → exempt (release
    branches don't carry ticket-ids; the release itself is the ticket)
  - otherwise → require a ticket-id substring as before

#168's acceptance criterion 3 ("validate-pr-create.sh accepts a PR
title `release(#160): v1.2.0` against the `release/v1.2.0` branch")
was checked off based on the title regex alone but didn't catch the
secondary branch-id check living in the same file. Surfaced trying
to open the v1.2.0 release PR.

Tested:

  bash .claude/hooks/validate-pr-create.sh against:
    release/v1.2.0           → 0   (allowed, exempt)
    release/v1.2.0-rc1       → 0   (allowed, RC variant)
    chore/GH-1-fix           → 0   (allowed, has ticket-id)
    release/foo              → 2   (correctly blocked)
    chore/no-ticket          → 2   (correctly blocked)

Refs me2resh/apexyard#168 (the parent bug) + #169 (the partial fix).

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(#173): sync CHANGELOG.md from main → dev (#174)

Closes me2resh/apexyard#173.

The release-cut model (AgDR-0007 / #116) squash-merges
release/vN.N.N → main, which means the v1.2.0 CHANGELOG section that
landed on main via PR #172 was never propagated back to dev. This
commit copies main's CHANGELOG.md verbatim onto dev so the v1.2.0
section is now present on both branches.

The diff is exactly the v1.2.0 entry being prepended; no other lines
change.

Without this sync, the next release PR cut from dev would build a
v1.3.0 section on top of v1.1.0, silently dropping v1.2.0 from dev's
running history. The corollary skill-level fix (option B in the
ticket) — updating /release to source the previous CHANGELOG from
upstream/main — is filed as a follow-up and out of scope for this PR.

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(#181): /agdr skill — searchable AgDR library (#186)

Adds /agdr — a portfolio-wide index for Agent Decision Records.
Walks apexyard.projects.yaml, reads each project's docs/agdr/*.md
(local clone if available, else gh api fallback), parses the optional
YAML frontmatter for category + projects, and answers four queries:

- /agdr browse        list across the portfolio, grouped by category
- /agdr search <term> full-text grep across all bodies, returns
                      <project>/AgDR-NNNN paths + matching paragraph
- /agdr show <id>     print a specific record, disambiguates duplicates
- /agdr stats         counts per category (the marketing-slide tile,
                      now backed by real data)

Six-category taxonomy: architecture | tech-stack | security | patterns
| integrations | other. Legacy AgDRs without frontmatter remain
first-class — they bucket as `other` and are flagged in browse so
operators can migrate at their own pace.

Backwards-compatible template change: templates/agdr.md gains an
optional `category:` (and optional `projects:`) line in the existing
frontmatter block. Omitting the line keeps every existing AgDR valid;
the skill defaults to `other` when missing.

Doc note in workflows/sdlc.md § Phase 2 points at /agdr search for
"have we decided this before?" lookups before drafting a design.

Smoke test in .claude/hooks/tests/test_agdr_skill.sh covers the
parser the spec specifies — frontmatter extraction, category
bucketing (including the legacy default-to-other path), id reading,
stats aggregation, and search match counts. 18 assertions, all green;
12 pre-existing test suites also green (no regressions).

Closes #181

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(me2resh/apexyard#183): /launch-check trend tracking (#185)

- Persist each run as JSON under <projects_dir>/<name>/launch-check/runs/
  (timestamp + branch + commit + per-dimension scores + verdict + top_risks)
- Append "Trend (last 5 runs)" section to the per-run summary when at least 2
  prior runs exist — markdown table + ASCII score chart
- Add /launch-check trend mode for read-only trend rendering (no full audit)
- Auto-derive notes column from score-delta vs previous run
  (e.g. "Security +12, Analytics +10")
- Opt-in commit via .launch-check-history-tracked marker; gitignored by default
- New helper script render-trend.sh + test_launch_check_trend.sh (21 cases)
- Resolve projects dir via portfolio_projects_dir helper (no hardcoded path)
- Schema is forward-compatible — extra fields preserved on framework upgrade

AgDR-0014 documents the chart format / schema / opt-in choices.

Closes me2resh/apexyard#183

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>

* feat(#182): /status --briefing + bin/apexyard status CLI shim (#187)

* feat(me2resh/apexyard#182): /status --briefing + bin/apexyard CLI shim

Make slide 6's `$ apexyard status` invocation real — the smallest of three
slide-reality tickets, demonstrating the marketing-to-implementation
pattern before #181 / #183 land.

- New helper at .claude/skills/status/briefing.sh — computes the 4-line
  "where am I" briefing (active workspace, active ticket, branch,
  role-set) so the logic is testable in isolation and runnable from a
  plain shell. Workspace inference walks up from cwd to the ops-fork
  root (same algorithm as _lib-portfolio-paths.sh and /start-ticket).
- Ticket reads from .claude/session/tickets/<workspace> first, falls
  back to .claude/session/current-ticket. Role-set is inferred from
  the active ticket's GitHub labels (v1: backend / frontend / qa /
  security / platform / sre / data / ux / ui / product / tech-lead,
  plus the long forms). No match emits the explicit "<none — inferred
  per task>" placeholder so the four-line shape is constant.
- New CLI shim bin/apexyard delegates `apexyard status` to the same
  helper after walking up to find the ops-fork root. Works from any
  workspace/<name>/ clone or the fork itself; symlink onto PATH to
  install (no shadowing of the `claude` binary).
- /status SKILL.md gains a "Briefing mode" section documenting the
  --briefing / -b flag; default /status output is unchanged.
- docs/multi-project.md "Daily workflow" demonstrates the new
  invocation alongside /inbox and /status.
- New smoke tests at .claude/hooks/tests/test_status_briefing.sh — 8
  cases covering ops-root cwd, workspace cwd, unknown cwd, ops-fallback
  marker, per-project marker priority, label-based role inference, the
  no-matching-label path, and the constant-four-line shape.

Closes me2resh/apexyard#182

* fix(me2resh/apexyard#182): replace curios-dog with example-app in SKILL.md output sample (leak fix)

---------

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>

* docs(#178): LSP integration spike — measurement + recommendation (#184)

* docs(#178): LSP spike — token-savings measurement + integration findings

- Phase 1: estimated token cost A vs B for three representative queries on a real TS Lambda backend (~9,750 LOC); shallow semantic queries see ~3-23x input-token savings, multi-hop traces see ~1.4x
- Phase 2: verified Claude Code shipped first-party LSP support in v2.0.74 (Dec 2025), gated behind ENABLE_LSP_TOOL=1, wired in via the plugin system (.lsp.json); MCP-wrapped LSP shims (cclsp, lsp-mcp) remain as fallback
- Phase 3: recommends Option 3 — interactive clone-first prompt at end of /handover, preserving today's "never auto-clone" principle while making the deep-dive path discoverable
- Phase 4: AgDR Y-statement and option matrix sketched; full AgDR is a follow-up if the spike says go
- Recommendation: GO. Adopt the built-in tool, document the opt-in path, change /handover to offer clone-first interactively. No code changes beyond /handover SKILL.md; no novel integration to maintain

Closes me2resh/apexyard#178

* docs: fix markdownlint MD031/MD032 in spike report

Add blank lines around lists and a fenced code block to satisfy
markdownlint-cli2 in CI. Pure formatting, no content change.

Refs me2resh/apexyard#178

---------

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>

* docs(me2resh/apexyard#190): annotate LSP-aware skills with opt-in callouts (#191)

- Add identical-shape "LSP-aware (optional, recommended)" callout near
  the top of each of the four code-aware SKILL.md files.
- Per-skill savings number is the only variation:
  - /code-review: ~3-15× cheaper for semantic queries
  - /threat-model: ~3-15× shallow, ~1.4-5× multi-hop t…
mosta7il pushed a commit to mosta7il/apexyard that referenced this pull request Jun 8, 2026
* chore(#109): project-configurable ticket / branch / commit / PR schema (#118)

* chore(#109): project-configurable ticket / branch / commit / PR schema

Lift the prefix / type whitelists hardcoded across skills, hooks, and
CI into a versioned JSON config read through a shared shell library.
Shipped defaults at .claude/project-config.defaults.json; per-fork
overrides at the optional .claude/project-config.json; one reader
(_lib-read-config.sh) that every consumer now uses.

Added:
- .claude/project-config.defaults.json (v1 schema)
- .claude/hooks/_lib-read-config.sh (shared reader)
- docs/project-config.md (schema reference + extension guide)
- docs/agdr/AgDR-0006-project-configurable-ticket-schema.md

Migrated (still pass with no config present via last-resort fallback):
- validate-branch-name.sh  → .branch.type_whitelist
- validate-commit-format.sh → .commit.type_whitelist (legacy
  `commit_types` top-level key honoured as backward-compat fallback)
- validate-pr-create.sh    → .pr.title_type_whitelist
- /feature, /task, /bug skills reference the config in their Rules
  sections; none hardcodes the list any more

Unlocks subsequent config-readers for #107 / #110 / #111 / #112 /
#113 / #114 / #115 — each extends the schema under its own subtree
without further changes to the loader.

https://github.com/me2resh/apexyard/issues/109

* fix(#109): satisfy markdownlint MD032 and MD060 on new docs

Auto-fix MD032 (blank lines around lists) in AgDR-0006 and format
table-separator rows with surrounding spaces (MD060) in both new
doc files. Content unchanged; CI green.

---------

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>

* chore(#110): add block-private-refs-in-public-repos.sh hook (#119)

Adds a new PreToolUse hook that blocks gh issue/PR/comment creation and
gh api .../issues|/pulls calls targeting a public framework repo
(default: me2resh/apexyard + whatever `upstream` resolves to) when the
title or body references any registered private project from
apexyard.projects.yaml (by name, repo slug, owner/repo#N ticket ref, or
workspace path).

The hook is a sibling to check-secrets.sh — both scan outgoing content
for identifiers that should never leave the local environment. Skip
marker `<!-- private-refs: allow -->` in the body lets a deliberate
reference through with a visible warning.

Files touched:
- .claude/hooks/block-private-refs-in-public-repos.sh (new)
- .claude/hooks/tests/test_block_private_refs.sh (new)
- .claude/rules/leak-protection.md (new)
- .claude/settings.json (wire PreToolUse matchers for the 5 gh shapes)
- docs/rule-audit.md (append section 10 + bump counts)

Refs: https://github.com/me2resh/apexyard/issues/110

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>

* chore(#115): add warn-stale-review-markers.sh PostToolUse hook (#120)

Fires after `git push` to surface review markers that have gone stale
because new commits were pushed past an existing Rex / CEO / design
approval. The merge gate already catches this at `gh pr merge` time,
but only then -- this hook closes the gap by flagging it immediately
at push-time so the author isn't surprised at merge.

- `.claude/hooks/warn-stale-review-markers.sh`
  - PostToolUse, non-blocking (PostToolUse exit 2 would push noise
    into the conversation; this hook is purely informational).
  - Resolves the PR HEAD via `gh pr view --json headRefOid` -- same
    source-of-truth as the merge-gate hooks post-apexyard#47 / #55.
    Falls back to local HEAD with a visible WARN when gh is offline.
  - Silent on: no PR for branch, no markers, fresh markers,
    failed push (detected via `rejected` / `failed to push` /
    `fatal:` / `error:` markers in tool_response.stderr).
  - Modes: `warn` (default) prints one stderr line per stale marker;
    `delete` opts in to auto-removal via
    `.claude/project-config.json` -> `review_markers.on_stale`.
    TODO(apexyard#109): switch to the shared project-config reader
    once it lands.
- `.claude/settings.json`
  - Wires the hook on PostToolUse / Bash / `git push *`.
- `docs/rule-audit.md`
  - Adds a row under section 3 (Code review & PR quality) and
    bumps the mechanized count 26 -> 27 / total 73 -> 74.
- `.claude/hooks/tests/test_warn_stale_review_markers.sh`
  - 8 cases: no PR, no markers, fresh markers, stale rex / ceo /
    design (warn), delete mode, failed push. All pass locally.

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>

* chore(#111): upgrade pre-push-gate from advisory reminder to blocking check-runner (#121)

* chore(#111): upgrade pre-push-gate from reminder to blocking check-runner

Previously pre-push-gate.sh just printed a checklist of things to run
locally before pushing — it was advisory. The rule it enforces is a
HARD STOP per pr-workflow.md. That asymmetry meant agents routinely
pushed broken work and discovered it only when CI went red.

Replaces the reminder with a blocking runner that reads the list of
shell commands from project config (.pre_push.commands) and executes
them in sequence before a push is allowed through. First non-zero
exit blocks the push with exit 2 and prints the failing command plus
the last 20 lines of its output.

- Config key: .pre_push.commands[] — array of {name, run} objects.
  Shipped default is an empty list (hook stays a no-op on repos that
  haven't configured their checks yet, including the framework repo
  itself until it wires its own CI).
- Emergency bypass: '<!-- pre-push: skip -->' in the HEAD commit
  message. Grep-able on purpose so bypasses stay auditable.
- Fail-fast: once a command fails, the rest don't run. Parallel
  execution is a follow-up polish.
- 7 test cases in .claude/hooks/tests/test_pre_push_gate.sh — all
  pass on the shipped default + a minimal custom config.

Updates docs/rule-audit.md to flip "partial" → "yes" for the
"before git push" rule.

Integrates with the shared config reader landed in #109.

https://github.com/me2resh/apexyard/issues/111

* fix(#111): remove orphaned footnote reference from rule-audit

The previous advisory-mode footnote was superseded by pre-push-111
but its definition was accidentally kept, tripping markdownlint MD053
(unused reference definition). Drop it.

---------

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>

* chore(#107): add validate-issue-structure.sh PreToolUse hook (#122)

Mechanically enforces the ticket body schema when an agent files raw
`gh issue create` calls instead of going through the interactive
/feature, /task, /bug skills. Matches bracketed title prefix
([Feature] / [Chore] / [Bug] / [Docs] / etc.) against
`.ticket.required_sections` in project-config, and blocks (exit 2)
when any required section is missing or empty. Skip marker
`<!-- validate-issue-structure: skip -->` bypasses with a visible
stderr WARN for legitimate off-template tickets (epics, meta-threads).

Changes:

- .claude/hooks/validate-issue-structure.sh — the hook; reads schema
  via the shared _lib-read-config.sh, with inlined defaults for bare
  checkouts predating the config-schema rollout. Handles
  --body / --body-file / -F path.
- .claude/project-config.defaults.json — extends .ticket with
  required_sections (Feature/Chore/Refactor/Testing/CI/Docs/Bug) and
  skip_marker; other .ticket fields untouched.
- .claude/settings.json — new PreToolUse matcher on Bash(gh issue
  create *) alongside the existing suggest-ticket-template.sh and
  block-private-refs-in-public-repos.sh hooks.
- .claude/hooks/tests/test_validate_issue_structure.sh — 15 cases
  covering pass + fail paths per prefix, empty section detection,
  skip marker, unknown prefix, non-gh invocation, --body-file path.
- docs/rule-audit.md — new section 11 row, mechanized count +1.

Upstream ticket: https://github.com/me2resh/apexyard/issues/107

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>

* chore(#112): add require-agdr-for-arch-pr.sh PreToolUse hook (#123)

Closes the asymmetry noted in .claude/rules/agdr-decisions.md: every
other HARD STOP in the ruleset (merge approval, ticket-first,
migration-first) is mechanically enforced, but the /decide HARD STOP
was prose-only. The commit-time hook require-agdr-for-arch-changes.sh
catches one architectural change at commit; this new PR-time hook
catches the cumulative diff so reviewers always have a pointer to the
decision record.

- New hook at .claude/hooks/require-agdr-for-arch-pr.sh
  - Fires on Bash(gh pr create *)
  - Parses --title/--body/--body-file/-F <path>
  - Resolves base branch from --base, else upstream/dev, origin/dev,
    upstream/main, origin/main, main, master (in that order)
  - Computes `git diff <merge-base>..HEAD --name-only`
  - Triggers on any changed file matching .agdr_trigger_paths[], OR any
    dep-file addition (package.json via jq key-set diff; other
    dep files via a commented +/- line-count heuristic — version
    bumps match +/- counts and do not fire)
  - Blocks (exit 2) with a helpful message naming the triggers and
    pointing at /decide if the body has no `AgDR-\d+-[a-z0-9-]+`
    reference
  - Skip marker `<!-- agdr: not-applicable -->` bypasses with a
    visible WARN on stderr
  - Silent exit 0 on non-gh commands, empty diffs, unresolvable base

- Wired via .claude/settings.json PreToolUse Bash(gh pr create *)

- Adds two new top-level keys to .claude/project-config.defaults.json:
    agdr_trigger_paths      (shell globs — domain/, infrastructure/,
                             migrations/, *.tf, .github/workflows/, etc.)
    agdr_trigger_dep_files  (literal basenames — package.json,
                             pyproject.toml, Cargo.toml, go.mod, Gemfile)
  Hook has inline fallback defaults kept in sync.

- Adds docs/rule-audit.md entry in the AgDR section; bumps mechanized
  count 26 to 27 and total rows 73 to 74.

- Adds .claude/hooks/tests/test_require_agdr_for_arch_pr.sh (7 cases;
  all green): path-triggered without AgDR (block), with AgDR (pass),
  dep-file added (block), version-only bump (no fire), skip marker
  (pass + warn), non-matching diff (pass), non-gh command (no-op).

Closes https://github.com/me2resh/apexyard/issues/112

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>

* chore(#113): require Testing section in PR body (config-driven) (#124)

Extends validate-pr-create.sh with a required-sections check that
replaces the hardcoded Glossary-only grep. The list of required H2
headings is project-configurable via `.pr.required_sections[]`.
Shipped default is ["Testing", "Glossary"], matching the canonical PR
description shape in workflows/code-review.md.

- Each entry must appear as `## <Name>` (case-insensitive).
- Empty sections are tolerated at this layer (the issue-structure hook
  #107 does stricter empty-content checks for issue bodies; for PR
  bodies, empty sections are left to the reviewer's judgement).
- Skip marker `<!-- pr-sections: skip -->` bypasses with a visible
  stderr WARN — for trivial PRs (lint-only fixes, version bumps)
  where the full template is overkill.
- Reads from project config via the shared _lib-read-config.sh (#109).
  Inline fallback matches shipped defaults so bare checkouts predating
  #109 keep working.
- 8 test cases cover: all-sections pass, each missing section,
  missing-both (both errors printed), skip marker, case-insensitive
  headings, H3 rejection.

https://github.com/me2resh/apexyard/issues/113

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>

* chore(#114): enforce single Closes-keyword per PR body (#125)

* chore(#114): enforce single Closes-keyword per PR body

Caps distinct auto-closing references (close/closes/closed, fix/fixes/
fixed, resolve/resolves/resolved + N or owner/repo+N) at one per PR
body. Closes the loophole where the title validator limited the title
to one ticket but multiple Closes lines in the body would still auto-
close all of them on merge.

- Scans stripped of fenced code blocks so closing keywords inside a
  code sample do not count.
- Distinct counting: the same number referenced twice (e.g. via Fixes
  and Closes) counts as one.
- Cross-repo refs (owner/repo+N) count normally.
- Opt-in escape hatch: pr.allow_multiple_closes=true in
  project-config disables the check for teams that deliberately batch
  rollbacks or dependency bumps.
- Per-PR bypass: a multi-close-approved HTML comment in the body
  prints a visible stderr WARN and lets that PR through. Grep-able
  trace so bypasses are auditable.
- 10 test cases cover: one close passes, no-keyword passes, two
  distinct block, three mixed block, same-number-twice passes, code-
  fence-ignored, skip marker, cross-ref without keyword, opt-in
  config, cross-repo close.

Reads configuration via the shared _lib-read-config.sh (apexyard+109).

https://github.com/me2resh/apexyard/issues/114

* fix(#114): strip inline backticks and tilde fences from close-count scan

Rex caught a self-reflexive bug in the initial commit: documentation
mentioning closing keywords inside inline backticks (say a PR body
that explains the new hook with examples) counted as real closes, and
a skip marker inside inline backticks silently bypassed the check.
Future PRs that document the feature would trip the same trap.

Fix the code-region stripper to cover:
- Triple-backtick fences (already handled)
- Tilde fences (new)
- Inline-backtick spans (new)

Also run the skip-marker check against the stripped body, so a marker
used purely as documentation no longer activates a real bypass.

Three new test cases pin the behaviour:
- closing keywords in inline backticks are ignored
- skip marker inside inline backticks does NOT bypass
- tilde fences also get stripped

13/13 tests pass.

---------

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>

* feat(#108): add /tickets-batch skill for bulk-file flow (#127)

The fast happy path for filing 5–20 structured tickets in one intent
without dropping to raw `gh issue create` (non-conformant) or running
`/feature` 20 times serially (~100 turns of interview).

- .claude/skills/tickets-batch/SKILL.md — new skill spec. Asks
  shared-context questions (priority, epic, area-labels, repo) ONCE
  for the whole batch, then runs a ≤3-question micro-interview per
  ticket (type, one-line purpose, optional clarification when the
  inference is low-confidence). Confirms the full batch as a table,
  then files each via specific `gh issue create` calls (never a
  bulk JSON dump — the validator runs per-issue). Output conforms
  to `.ticket.required_sections` by construction. Caps at 20
  tickets per invocation.

- CLAUDE.md — added a row for /tickets-batch in the Available
  Skills table; bumped the count references from 33 to 34.

Refs https://github.com/me2resh/apexyard/issues/108

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>

* feat(#117): add /fan-out skill + parallel-work rule doc (#128)

- Add `.claude/skills/fan-out/SKILL.md` — spawns N parallel Agent calls
  in a single assistant message, with per-task agent type, worktree
  isolation, and foreground/background mode. Caps at 5 concurrent
  agents. Refuses fan-out when tasks share file write targets or have
  sequential dependencies. Includes pre-spawn active-ticket safety
  check and worktree merge-back flow that pauses on conflict.
- Add `.claude/rules/parallel-work.md` — trigger heuristic for when an
  agent should proactively offer fan-out (>= 2 file-independent,
  context-independent, individually substantial work items). Pairs
  with the skill: rule says when, skill says how.
- Update `CLAUDE.md` — bump rules count to 9, skills count to 34, add
  `/fan-out` row to the skills table.

Refs https://github.com/me2resh/apexyard/issues/117

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>

* chore(#116): adopt release-cut branch model (dev/main + tags) — framework only (#126)

* chore(#116): adopt release-cut branch model (dev/main + tags)

Formalises the dev/main split that already exists informally — dev is
the daily-work branch where every PR lands, main is release-only,
tagged with semver on each merge. Framework-only: managed projects
under apexyard governance stay trunk-based.

Added:
- AgDR-0007 — decision record (options table covers full git flow vs
  trunk-only vs gitflow-lite; chose gitflow-lite)
- /release skill — diff dev against main, propose semver bump from
  conventional commits, generate CHANGELOG, open release PR, tag
  after merge
- docs/release-process.md — prose runbook for cutting a release
  (manual fallback for the skill)
- .git.protected_branches in project-config.defaults.json
  (main/master/dev/develop)

Modified:
- block-main-push.sh — now blocks direct pushes/commits to all
  configured protected branches (was: hardcoded main/master). Reads
  .git.protected_branches via the shared config reader (apexyard#109).
- CLAUDE.md — new section under Git Conventions explaining the
  dev/main model + the framework-only scope. Skill table entry for
  /release. Skills count bumped to 34.
- docs/multi-project.md — note that upstream/main is release-only
  and the dev/main split is framework-only.

Non-consequences (per AgDR-0007):
- No release/* or hotfix/* branches. Hotfixes are normal patches
  cut quickly. Revisit if multi-version maintenance becomes a need.
- No automatic on-merge issue closing for dev PRs. The release PR's
  body aggregates all Closes references for the batch and triggers
  auto-close en masse when it merges to main. Manual close in the
  meantime.
- CI workflows trigger on pull_request regardless of base, so
  dev-targeting PRs already get the full check matrix — no
  workflow file edits needed.

https://github.com/me2resh/apexyard/issues/116

* fix(#116): satisfy markdownlint MD032 + MD060 in 116 docs

Auto-fix added blank lines around lists in AgDR-0007 (MD032) and
spaced the table separators in AgDR-0007 + multi-project.md (MD060).
Content unchanged.

---------

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>

* fix(#106): CHANGELOG fallback in drift hook for squash-merged forks (#129)

* fix(#106): CHANGELOG fallback in drift hook for squash-merged forks

The v1.1.0 tag-reachability check (`git tag --merged main`) misfires on
forks that sync via GitHub's default squash-merge: the squash collapses
the upstream-tag commit into a synthetic SHA, the tag stops being
reachable, and the banner keeps firing forever.

Discovered live on the first real-world `/update` flow: ops fork
squash-merged the v1.1.0 sync PR, banner kept saying "v1.1.0 available"
even though the fork was content-caught-up.

This commit adds a CHANGELOG-content fallback that fires only when the
primary tag check fails. If the fork's main has a heading
`## [X.Y.Z]` matching the upstream tag's version, treat the release as
absorbed and stay silent. Tolerant grep (matches the apexyard CHANGELOG
format from v1.1.0 onward, with leading-`v` stripping for tag→heading
conversion).

The merge-commit and rebase paths are unchanged — primary tag check
still works for them, and the fallback never fires when it shouldn't.

Test coverage (5 cases):
- squash-merge fork caught up to v1.1.0 → silent (the fix)
- merge-commit fork caught up to v1.1.0 → silent (regression check)
- fork stopped at v1.0.0 → banner fires
- fork has its own newer tag → silent
- squash-merge but no CHANGELOG on fork → banner fires (no false silence)

Records the strategy update in docs/agdr/AgDR-0008-…md (extends
AgDR-0005's tag-based-drift design).

https://github.com/me2resh/apexyard/issues/106

* fix(#106): satisfy markdownlint MD032/MD060 + shellcheck SC2164

Rex flagged two CI-blocking issues on the original 106 commit:

- AgDR-0008 had three bulleted sub-lists in the Consequences section
  without surrounding blank lines (MD032). Added blanks and padded the
  one tight-pipe table separator (MD060).
- The 106 test fixture had five subshell `cd "$fk"` calls without
  `|| exit 1` (SC2164). Added the guard to all five.

5/5 tests still pass after the fix. No semantic change to the hook
or the test logic.

---------

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>

* feat(#130): add /validate-idea skill — lightweight pre-spec gate (#131)

A 10-minute, 5-question check that sits between /idea and /write-spec.
Designed for solo founders running ApexYard — not a heavyweight
methodology like event storming or Wardley mapping.

Five questions, asked one at a time:
  1. Who is this specifically for?
  2. What do they do today instead?
  3. What's the smallest version that proves the value?
  4. What would prove this is wrong? (kill criteria)
  5. Build, buy, or rent?

Output: a one-page validation doc with a GREEN/YELLOW/RED verdict.
RED auto-updates the IDEA-NNN backlog row to WONTDO.

Integration:
  /idea — adds an optional default-no "Validate now?" step after
    capture (and after the optional GitHub Issue offer).
  /handover — adds a conditional "this looks dormant, validate?"
    step at the end of the integration plan, gated on the dormancy
    heuristic (last commit > 90d AND zero open PRs AND no recent
    issue activity). Healthy projects don't see the prompt.

CLAUDE.md skills count bumped to 35; new skills row added.

https://github.com/me2resh/apexyard/issues/130

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>

* feat: configurable voice prompts on assistant pause (AgDR-0009-voice-prompts-on-pause) (#135)

Stop hook that speaks the assistant's question aloud (Jarvis-style)
when it pauses for user input. Initial phase is macOS-only via `say`,
no voice input — user replies via keyboard.

Default OFF. Adopters opt in by overriding `voice_prompts.enabled` to
true in `.claude/project-config.json`.

Files:
- .claude/hooks/voice-prompt-on-pause.sh — Stop hook with config gate,
  trigger heuristic (questions-only by default), markdown stripping,
  sentence-boundary truncation, fire-and-forget say invocation
- .claude/hooks/tests/test_voice_prompt_on_pause.sh — 9 cases covering
  disabled-default, enabled+question, enabled+statement, approved-pattern,
  abc-menu, malformed-transcript, no-say-on-PATH, trigger-always,
  markdown-stripping
- .claude/project-config.defaults.json — voice_prompts schema block
  added (enabled, voice, max_chars, rate_wpm, trigger), default OFF
- .claude/settings.json — new Stop hook entry wired with the standard
  ops-root resolver wrapper
- docs/agdr/AgDR-0009-voice-prompts-on-pause.md — design rationale,
  options matrix (status quo / macOS say / cloud TTS / ML detection),
  consequences, future phases
- docs/project-config.md — new "Voice prompts" section with override
  examples and privacy notes

Test mode: hook respects VOICE_PROMPTS_SYNC=1 to run say synchronously
(test runners need this so assertions don't race against orphaned
background processes). Production invocations always run async.

Future phases (out of scope here, AgDR §"Future phases"):
- Phase 2: cross-platform TTS (Linux espeak, Windows SpeechSynthesizer)
- Phase 3: cloud TTS providers (OpenAI, ElevenLabs) — privacy AgDR-worthy
- Phase 4: voice input via Whisper-based STT
- Phase 5: per-message overrides

Refs: https://github.com/me2resh/apexyard/issues/134

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>

* feat(#141): add /debug skill — structured hypothesis-driven debugging (#142)

* feat(#141): add /debug skill — structured hypothesis-driven debugging

Adds a methodology skill that enforces five disciplines:

  1. Capture the symptom precisely (exact URL, exact response, exact step)
  2. Read the architecture before guessing (map every layer the request
     touches, file by file)
  3. Form a hypothesis ladder (3–5 candidates, each with an explicit
     evidence test that confirms or refutes it)
  4. Gather evidence first, fix second
  5. Verify the fix against the original symptom evidence (re-run the
     same `curl` / browser repro you used in step 4 — unit tests
     verify code, not feature, correctness)

Stack appendices (Web, Desktop) carry stack-specific surface-evidence
requirements (step 1), architecture-surface maps (step 2), and
evidence-tests cookbooks (step 4). The methodology body stays portable
across stacks; appendices are where stack-specific knowledge accrues
over time.

Web appendix covers browser routing, framework configs (Next/Nuxt/Vite),
SPA-fallback layers, CDN, origin, the shared API client, backend
handlers, and auth providers. Desktop appendix covers Electron / Tauri
/ native-shell concerns: app entry points, IPC bridges, native modules,
auto-updater, sandbox / entitlements, code signing, crash reports.

Includes "When NOT to use" guidance so the methodology overhead doesn't
sandbag simple bugs (typos, off-by-ones, greenfield exploration).

Motivated by a real OAuth debug session in a managed project where
three sequential fixes chased adjacent symptoms because each was
hypothesis-then-fix without evidence in between. The skill is the
"never do that again" guardrail.

Closes #141

* fix(#141): scrub private project issue numbers from anti-pattern table

Rex review on PR #142 caught that line 155 of the skill's anti-pattern
table still named the originating PRs (#375, #377, #380) from the
private project where the methodology was first exercised. The PR body
and commit message were correctly abstracted earlier, but this in-file
reference slipped through — the leak-protection hook only scans gh
issue/pr writes, not staged file content, so mechanical enforcement
didn't catch it.

Replaced with "Three sequential PRs chasing the same symptom because
each was based on a different guess (no evidence test in between
cycles)" — same pedagogical value, zero attribution.

Refs #141

---------

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>

* docs(#143): document split-portfolio mode + add /setup privacy gate (#144)

Adopters on GitHub Free with any private project hit a silent privacy bug
following today's docs: forking apexyard makes a public fork that you cannot
later flip to private (GitHub policy), and committing `apexyard.projects.yaml`
+ `projects/<name>/` to the fork publishes private project names + handover
findings on a public GitHub repo.

This PR documents the supported workaround — split-portfolio mode — and
adds an upfront privacy gate to the /setup skill so new adopters never
hit the trip-wire silently.

docs/multi-project.md:

- New "Two setup modes — pick the one that matches your privacy needs"
  section before TL;DR, with a side-by-side table and the explicit
  trip-wire callout.
- Existing TL;DR retitled "TL;DR — single-fork mode (default)" with a
  one-line pointer to the split-portfolio section.
- New "Split-portfolio mode — public framework + private portfolio"
  section between the existing setup steps and the directory-layout
  section. Includes:
  - The two-repo layout (~/ops/apexyard public + ~/ops/portfolio private)
  - 7-step setup walkthrough with copy-pasteable commands
  - Daily workflow + upstream sync notes (both unchanged)
  - Trade-offs (two repos to maintain, two clones per machine, one
    upstream-sync conflict path on `projects/README.md`)
  - "Migrating from single-fork to split-portfolio" recovery flow with
    the explicit warning that GitHub Issue / PR edit history survives a
    force-push and must be redacted separately

.claude/skills/setup/SKILL.md:

- New Step 2a: privacy gate — asks "are any projects private?" before
  proposing the config. Branches on the answer:
  - All public                                → single-fork mode
  - GitHub Pro / Team / Enterprise            → single-fork mode (private
                                                  forks of public repos
                                                  are supported on those
                                                  plans)
  - Any private + GitHub Free                 → split-portfolio mode
- New Step 2b: walks through the split-portfolio setup interactively
  (private repo create, sibling clone, gitignore + symlink) when the
  privacy gate triggers.
- Detection: `test -L apexyard.projects.yaml` short-circuits Step 2b
  for adopters already in split mode.
- Explicit "do NOT auto-migrate" rule for adopters already in single-fork
  mode with private names already pushed — that path is destructive
  (force-push history rewrite + redact issue/PR bodies + delete backup
  branch) and warrants a deliberate, eyes-open run, not a /setup side
  effect.

Out of scope for this PR (tracked separately on #143):

- `portfolio:` config block in `onboarding.yaml` schema
- Skill audit + refactor to honour configured `registry` / `projects_dir`
  / `ideas_backlog` paths instead of hardcoded fork-relative paths
- `/split-portfolio` migration helper skill that automates the recovery
  flow currently documented manually

This is the docs-and-setup-question minimum-viable starter — the
framework code refactor is mechanical and lands as a follow-up.

Refs #143

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>

* feat(#145): portfolio config + self-healing + /split-portfolio helper (#147)

* feat(me2resh/apexyard#145): portfolio config block + self-healing + /split-portfolio helper

Closes the framework primitive deferred from me2resh/apexyard#144. Adds
first-class config-driven path resolution for the portfolio registry,
projects dir, and ideas backlog, with self-healing surfacing of broken
config at session start, plus a new /split-portfolio skill that automates
the destructive recovery flow.

Schema, helper, and hook:
- .claude/project-config.defaults.json: new portfolio: block
  (registry, projects_dir, ideas_backlog) with defaults matching
  today's single-fork layout
- .claude/hooks/_lib-portfolio-paths.sh: new sourceable helper exposing
  portfolio_registry, portfolio_projects_dir, portfolio_ideas_backlog,
  portfolio_validate, portfolio_clear_cache. Resolves relative paths
  against the ops-fork root.
- .claude/hooks/check-portfolio-config.sh: new SessionStart hook —
  silent on OK, one-line banner on broken config, never blocks session
- .claude/hooks/tests/test_portfolio_paths.sh: 13 cases covering
  defaults, absolute/relative overrides, validate states, cache clear

Skill audit (18 SKILL.md files):
- Adds Path resolution callout pointing at the helper
- handover bash blocks now source helper and use $(portfolio_registry)
  instead of literal apexyard.projects.yaml
- setup Step 2b now writes the portfolio: config block (recommended)
  and validates via portfolio_validate before declaring success;
  symlink approach kept as legacy fallback

New skill (.claude/skills/split-portfolio/SKILL.md):
- 10-step migration with explicit operator-confirmation gates at each
  destructive step (force-push, body redaction, branch deletion)
- --verify mode: read-only state report (mode, paths, validate, drift)
- --dry-run mode: prints commands without executing
- Pre-flight refusals: already-private fork, paid GitHub plan, dirty
  working tree, already-migrated state
- Step 9 writes the portfolio: config block (not symlinks) — symlink
  fallback documented for adopters on older framework versions
- Step 9 surfaces the GitHub timeline-API survival caveat verbatim
- Idempotent re-runs: detects partial-migration state and resumes

Docs (docs/multi-project.md):
- Layout section describes both modes (config-block recommended,
  symlink legacy) with self-healing notes
- Setup steps split into config-block mode and legacy symlink mode
- Migration section now points at /split-portfolio skill; manual
  recipe preserved as fallback

AgDR (docs/agdr/AgDR-0010-portfolio-config-and-self-healing.md):
- Full Y-statement, options, decision, consequences, future phases
- Schema decision rationale: project-config.json over onboarding.yaml
  because runtime path resolution belongs in project-config

Closes me2resh/apexyard#145
Refs me2resh/apexyard#146 (delivered same PR; closed manually post-merge
per the single-Closes-keyword rule in validate-pr-create.sh)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(me2resh/apexyard#145): markdownlint MD031 — blank lines around fences

CI's markdownlint-cli2 (v0.34.0) flagged the JSON + bash fenced code
blocks I added in setup/SKILL.md Step 2b without surrounding blank
lines. Added the required blanks. No content change.

Refs me2resh/apexyard#147

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>

* docs(me2resh/apexyard#148): correct privacy-gate wording — adopter action, not framework auto-publish (#149)

The privacy-gate wording introduced in PR #144 (and unchanged in PR #147)
attributed the publication to the framework rather than the adopter:

  "the standard fork-and-commit setup will silently publish your private
   project names on a public GitHub repo"

That's factually wrong. ApexYard never pushes anything without explicit
operator approval — the publication only happens when the adopter
themselves runs git push. The "silently publish" framing read as if the
framework auto-publishes, which is misleading and undermines trust in
the rest of the framework's safety claims.

Two prose-only edits, no code, no behavior change:

- .claude/skills/setup/SKILL.md Step 2a — replaced "will silently
  publish ..." with adopter-action language ("you might accidentally
  publish ... a stray git push after registering them — I won't push
  without your approval, but the risk is on the adopter once the data
  is committed locally")

- docs/multi-project.md trip-wire callout — replaced "silently publish
  their portfolio names the moment they push" with "risk accidentally
  publishing their portfolio names with a stray push (the framework
  itself never pushes without operator approval, but once the registry
  is committed locally the next push exposes it)"

Verified: `grep -r "silently publish" .claude/skills/ docs/` returns no
hits.

Closes me2resh/apexyard#148

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(#150): bootstrap-skill exemption + Bash-write coverage (#152)

Closes the legitimate-bypass case (me2resh/apexyard#150) and the
illegitimate-bypass case (me2resh/apexyard#151) together so the
ticket-first gate is coherent. Shipping either alone would leave a
window where the framework is internally inconsistent — see AgDR-0011
for the full rationale.

Bootstrap exemption (me2resh/apexyard#150):
  - .claude/session/active-bootstrap marker, written by /setup,
    /handover, /update, /split-portfolio on entry; cleared on exit
  - SessionStart sweep (clear-bootstrap-marker.sh) for stale markers
    from interrupted sessions
  - require-active-ticket.sh reads the marker and exempts skills on
    the configured ticket.bootstrap_skills list
  - bootstrap_skills list lives in .claude/project-config.defaults.json
    (extendable per fork via .claude/project-config.json)

Bash-write coverage (me2resh/apexyard#151):
  - new _lib-detect-bash-write.sh — heuristic detector for output
    redirection, tee, sed -i, awk -i inplace, python/node/ruby
    embedded interpreters
  - require-active-ticket.sh + require-migration-ticket.sh now fire
    on Bash in addition to Edit|Write|MultiEdit
  - design choice: false-negatives preferred over false-positives
    (the matcher errs toward "let through" rather than block legit
    read-only commands)

Tests: 32 unit cases on the lib + 12 integration cases on the hook,
including the exact me2resh/apexyard#151 bypass repro from the issue
body.

Closes me2resh/apexyard#150

Will manually close me2resh/apexyard#151 post-merge per the
single-Closes-per-PR rule (precedent: AgDR-0010 / PR #147).

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(#153): extend Bash-write matcher beyond first-version coverage (#155)

Closes me2resh/apexyard#153.

Extends `_lib-detect-bash-write.sh` (introduced for me2resh/apexyard#151
in PR #152) with the matcher families flagged by Rex's review of #152.
AgDR-0011 already frames the matcher as a living list extended on
observation; this commit just walks the list.

New matcher families:
- File-moving builtins: `cp`, `mv`, `rm`, `dd`, `install` (anchored at
  command-start; `--help`/`--version` and `git rm`/`git mv` excluded)
- Archive / network writes: `tar -x` / `tar --extract`, `curl -o` /
  `--output`, `wget -O` / `--output-document`
- Additional embedded interpreters: `perl -e`, `php -r` (keyword-gated
  like python/node/ruby); `go run`, `deno run`/`deno script.ts`,
  `bun run`/`bun script.ts` (categorical script runners)
- Python helpers: `pathlib.Path().touch()`, `shutil.copy*`,
  `shutil.move`, `os.rename` added to the `python -c` and python
  heredoc keyword list
- Heredoc variants for `ruby` and `node` (previously only python
  heredoc was covered)

Extractor extensions:
- `cp` / `mv`: last positional arg
- `curl -o` / `--output`: file argument
- `wget -O` / `--output-document`: file argument
- `tar -x`, `go run`, `deno`, `bun`, `perl -e`, `php -r`: return empty
  (caller applies gate categorically per AgDR-0011)

Test count rose from 32 to 86. Negative-class counterexamples cover
the trickiest false-positive surfaces: `tar -t` listing, `cp --help`,
`rm --version`, `git rm`, `curl` bare URL fetch, `wget` bare URL fetch,
`deno fmt`, `deno test`, `go build`. Existing
`test_require_active_ticket_bash.sh` regression suite still passes 12/12.

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(#154): mock gh in test sandboxes to remove live-tracker dependency (#156)

- add _lib-mock-gh.sh helper that installs a fake `gh` on the sandbox PATH;
  intercepts `gh issue view <N> ... --json ...` and returns synthetic
  `{"number":N,"state":"OPEN"}` (overridable per-num via mock_gh_set_state)
- wire the shim into test_single_closes_per_pr.sh and
  test_validate_pr_required_sections.sh so the validator's CLOSED-issue
  refusal no longer breaks the suite when upstream issues are closed
- both files previously failed every case (0/13 and 0/8) because their PR
  titles reference #114 / #113, which are now CLOSED upstream
- post-fix: 13/13 and 8/8; full suite remains green

Closes me2resh/apexyard#154

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>

* feat(#132): structured CEO marker + same-turn merge in /approve-merge (#158)

* feat(#132): structured CEO marker + same-turn merge in /approve-merge

Closes me2resh/apexyard#132 (drop the "stop before merge" rule) and
me2resh/apexyard#48 (harden CEO marker against self-approval bypass)
together. The two threads compose — see AgDR-0012 for the full
rationale.

Streamline (#132):
  - /approve-merge now runs `gh pr merge --squash --delete-branch`
    in the same turn as the marker write, by default
  - --no-merge opt-out preserves the deferred-merge case
  - The discrete approval moment is the SKILL INVOCATION, not a
    follow-up "now do the merge" message

Harden (#48):
  - CEO marker is now a structured key/value file with required fields:
      sha=<HEAD>
      approved_by=user
      skill_version=2
    Validated by block-unreviewed-merge.sh; bare-SHA legacy markers
    rejected with a clear "stale format" error pointing at /approve-merge
  - The model's bare `echo SHA > <pr>-ceo.approved` bypass is now
    mechanically rejected. Forging the structured fields requires a
    deliberate, visible rule violation rather than a one-line accident
  - Optional audit fields (approved_at, approval_summary) capture the
    "what did the user say when they approved" trail
  - Rex marker stays bare-SHA — different threat model (automated
    reviewer, not human authorization moment)

pr-workflow.md reframed: "the load-bearing rule is explicit per-PR
approval, not two user messages." The merge is a deterministic
consequence of the approval invocation.

Tests: 12 cases on the hardened hook covering the new format end-to-end
(valid v2, missing rex/ceo, bare-SHA legacy rejected, missing
approved_by, wrong approved_by, skill_version=1, sha mismatch,
non-merge no-op, gh-api shape gated). Full suite: 205/205 across 13
test files.

Will manually close me2resh/apexyard#48 post-merge per the
single-Closes-per-PR rule (precedent: PR #152 / AgDR-0011).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(#132): redact private project reference from AgDR-0012

Abstracted two references to a registered private project that named
the project's owner/repo. The leak-protection hook caught one in the
PR body; this fixup removes the matching references from the
AgDR-0012 file content (which would otherwise have shipped the names
to me2resh/apexyard public repo via the merge).

The pre-existing reference at .claude/rules/pr-workflow.md:130
(documenting #47) is untouched — it predates this PR and is already
on the public repo's history.

Refs me2resh/apexyard#132.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(#132): markdownlint blanks-around-fences + typo fix

Two small fixups against red CI / Rex feedback:

- AgDR-0012 line 63: fenced code block now has a blank line before it
  (MD031). markdownlint-cli2 0.13.0 was rejecting the indented fence
  inside the bullet because the fence's preceding line was the bullet
  text (no blank).
- approve-merge SKILL.md line 172: typo `deferes` → `defers` (Rex flag
  on PR #158).

Refs me2resh/apexyard#132.

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>

* chore(#157): remove voice-prompts feature + correct hook/skill counts (#161)

* chore(#157): remove voice-prompts-on-pause feature + correct hook/skill counts

Closes me2resh/apexyard#157 (sunset the voice-prompts feature) and
me2resh/apexyard#77 (hook count off-by-one in CHANGELOG / CLAUDE.md)
in one bundled PR. AgDR-0013 supersedes AgDR-0009 with the full
rationale; both AgDRs are preserved (decision records are append-only
history).

Removed (#157):
  - .claude/hooks/voice-prompt-on-pause.sh
  - .claude/hooks/tests/test_voice_prompt_on_pause.sh
  - Stop matcher block in .claude/settings.json (became empty after
    voice removal)
  - voice_prompts block in .claude/project-config.defaults.json
  - "## Voice prompts" section in docs/project-config.md
  - voice_prompts mention in AgDR-0010 line 32 (replaced with
    leak_protection / ticket as still-current example config blocks)

Preserved:
  - docs/agdr/AgDR-0009-voice-prompts-on-pause.md — historical record;
    new "Superseded by: AgDR-0013" header at the top
  - AgDR-0010 line 115 reference to AgDR-0009 — still accurate as a
    historical pattern reference

Counts corrected (#77):
  - CHANGELOG.md v0.3.0 stats: "17 hooks" → "18 hooks" (historical
    fix — at v0.3.0 there were actually 18 hooks)
  - CLAUDE.md table line: "18 shell scripts" → "24 shell scripts"
    (current count after this removal)
  - CLAUDE.md table line: "35 slash commands" → "39 slash commands"
  - CLAUDE.md "Available skills (34)" → "Available skills (39)"
  - CLAUDE.md quick-reference "Skills (35 slash commands)" →
    "(39 slash commands)"

Why bundled: #77's correct count depends on whether voice is still in
the framework. Shipping #77 before #157 would write a number that's
wrong by one again the moment #157 lands. Same shape as previous
bundles (PR #152 / AgDR-0011, PR #158 / AgDR-0012).

Why no adopter-facing changelog mention of the voice removal: the
feature never reached a tagged release on main. v1.1.0 didn't have
it; v1.2.0 won't have it. From the adopter's perspective there's
nothing to retire. AgDR-0013 captures the framework's internal record
for future contributors. See AgDR-0013 § "No adopter-facing changelog
mention".

Tests: full hook test suite green (196 cases across 12 files —
test_voice_prompt_on_pause.sh removed). No regressions.

Will manually close me2resh/apexyard#77 post-merge per the
single-Closes-per-PR rule (precedent: PRs #152, #158).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(#157): unwire voice from settings + configs + docs + AgDR-0013

Continuation of d09d7b5 (the file deletions). Squash-merge will
collapse both into one PR commit. This commit captures:

- .claude/settings.json — Stop matcher block removed (was the only
  hook in it; entire matcher gone)
- .claude/project-config.defaults.json — voice_prompts block + its
  _comment removed
- docs/project-config.md — "## Voice prompts" section removed
- docs/agdr/AgDR-0009-voice-prompts-on-pause.md — "Superseded by"
  header added at the top, content otherwise preserved as history
- docs/agdr/AgDR-0010-portfolio-config-and-self-healing.md — line 32
  example reference swapped from voice_prompts to
  leak_protection / ticket (still-current config blocks)
- docs/agdr/AgDR-0013-sunset-voice-prompts.md — new supersession AgDR
- CLAUDE.md — hook count 18 → 24, skill count 35 → 39 (three
  occurrences each, all aligned to current reality)
- CHANGELOG.md v0.3.0 stats — "17 hooks" → "18 hooks" historical fix
  (#77 acceptance criterion 1)

Refs me2resh/apexyard#157 + me2resh/apexyard#77.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(#157): markdownlint MD028 + resolve stale AgDR-numbering ref

Three small fixups against Rex CHANGES-REQUESTED on PR #161:

- AgDR-0009 line 3: promote "Superseded by:" header out of a
  blockquote. The original "I decided ..." canonical blockquote at
  line 5 was being merged with the new supersession blockquote
  (markdownlint MD028 — "no blanks inside blockquote").
- AgDR-0013 line 3: same shape — "Supersedes:" header now a plain
  bold paragraph, canonical "I decided ..." blockquote untouched.
- approve-merge SKILL.md line ~187: stale conditional reference
  "AgDR-0012 (or 0013 — depends on whether voice-removal lands
  first)" — order is now resolved (12 = approve-merge bundle, 13 =
  voice removal). Drop the parenthetical.

Refs me2resh/apexyard#157 + me2resh/apexyard#77.

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>

* feat(#160): multi-tab terminal demo on the landing site (#162)

Landing-site `site/index.html` terminal demo previously played one
canonical flow ("one ticket, start to finish") on autoplay. With the
v1.2.0 skill surface expansion (4 new skills, 8+ new hooks), one flow
no longer represents the framework's breadth.

Adds tabs to the terminal chrome — four flows visitors can either let
auto-cycle or click directly:

  1. one ticket — existing flow, unchanged content
  2. /handover  — adopt an external repo into the portfolio
  3. /setup     — first-run framework bootstrap on a fresh fork
  4. /fan-out   — spawn 3 parallel agents on independent tickets

Auto-advance: on completion of the active tab's script, the demo
pauses ~1.8s then advances to the next tab. Loops at the end. User
can interrupt by clicking any tab or hitting Replay.

Implementation:
- HTML chrome — single title span replaced with a tablist of 4 button
  tabs. ARIA `role="tablist"` / `role="tab"` / `aria-selected` so
  keyboard + screen-reader users get the same semantics as sighted
  ones.
- CSS — new `.shell-demo__tabs` + `.shell-demo__tab` with an accent
  underline on the active tab. Tabs scroll horizontally on narrow
  viewports (mobile responsive).
- JS — refactored the existing IIFE from one `script` array to an
  array of four. Added `setActiveTab()` for ARIA state, `play(idx)`
  takes a tab index, end-of-script auto-advances to `(idx + 1) % N`
  unless the user clicked away during the pause. Adds a new `cmd`
  type alongside `you` for slash-command invocations (renders with
  the same `>` prompt prefix). prefers-reduced-motion still bails
  early and leaves the static seed visible.
- Static seed (the non-JS fallback) still shows tab 0's content, so
  reduced-motion / no-JS visitors see the one-ticket flow as before.

Hero metrics also corrected to current reality (#77 / PR #161 covers
CLAUDE.md and CHANGELOG.md; this PR catches the same numbers in the
landing site):

  Skills    32 → 39
  Hooks     18 → 24

The tabs ship in v1.2.0 alongside the framework changes that make
the new flows worth showcasing.

Refs me2resh/apexyard#160 (release v1.2.0 + landing-site refresh).

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(#163): default the split-portfolio sibling repo name to <fork>-portfolio (#164)

Closes me2resh/apexyard#163.

The split-portfolio mode docs and skills previously suggested
`your-org/ops` as the default name for the private sibling repo, and
`portfolio/` as the local clone directory. Both too generic — adopters
running multiple ops setups end up with `your-org/ops` collisions, and
a bare `portfolio/` dir gives no signal about which framework it
belongs to when it sits next to other unrelated `portfolio/` dirs.

`<fork>-portfolio` is now the default — keeps the relationship to the
public fork explicit on disk and on GitHub. If the fork is named
`your-org/apexyard`, the portfolio defaults to
`your-org/apexyard-portfolio`. If the fork was renamed (e.g. `cos`),
the portfolio defaults to `cos-portfolio`. Adopters with custom names
keep working — the `portfolio:` config block resolves whatever path
they configured.

Files updated:

  docs/multi-project.md
    - Layout diagrams use `apexyard-portfolio/` as the sibling
    - Setup walkthrough Step 2 + Step 3 use `your-org/apexyard-portfolio`
      and explain the `<fork>-portfolio` pattern
    - Config-block + symlink path examples updated to
      `../apexyard-portfolio/...`
    - Daily workflow + cross-machine clone commands updated
    - The two existing `your-org/ops` references that remain are
      fork-rename examples (lines 52, 64) — kept as-is, since renaming
      the fork to `ops` is still valid (the portfolio would then
      default to `ops-portfolio`)

  .claude/skills/setup/SKILL.md
    - Step 2b's "default suggestion" for the private repo name is now
      `your-org/<fork>-portfolio`, computed dynamically from the
      fork's repo name via `gh repo view --json name -q .name` so the
      suggestion is correct even when the fork was renamed
    - Clone command no longer needs a second arg — the repo name IS
      the directory name
    - Config-block paths updated to `../apexyard-portfolio/...`

  .claude/skills/split-portfolio/SKILL.md
    - Step 3's suggested-name template is now `<account>/<fork>-portfolio`
      with the same dynamic-fork-name resolution

Mechanism unchanged. The `portfolio:` config block in
`.claude/project-config.json` still takes any path; this PR is purely
default-suggestion + example prose.

No tests required (skills are markdown instructions; no automated
coverage today).

Refs `apexyard.projects.yaml.example` uses "ops repo" as a generic
term meaning "the operational management fork" — not a name — kept
as-is.

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(#165): skills reference page on the landing site + changelog link (#167)

Closes me2resh/apexyard#165.

Adds a public, browseable index of every apexyard slash command
alongside a one-click changelog link from the homepage nav.

site/skills.html (new):
  - Lists all 39 skills currently shipping in .claude/skills/
  - Each entry: slash command, argument hint, description (taken
    verbatim from the SKILL.md frontmatter so the page matches the
    runtime exactly)
  - 10 categories: Setup & onboarding, Daily ops, Tickets & ideas,
    Specs & decisions, Code review & merge, Architecture & dev tools,
    Production-readiness audits, Workflow primitives, Communications,
    Deprecated
  - Same brutalist-terminal design tokens as the homepage — JetBrains
    Mono, paper-cream background, single warning-red accent, sharp
    corners. Inlined CSS to keep the static-only no-build-step
    convention; design vars duplicated rather than extracted to a
    shared file (~18 vars; cheap to keep in sync).
  - Mobile responsive — skill grid collapses to single-column under
    720px; titlebar nav hides non-CTA items on narrow viewports.
  - Reduced-motion friendly (no animation in the first place).
  - Internal anchor TOC at the top so the page scans in seconds.

site/index.html (nav addition):
  - Added two nav links to the titlebar between "what's in the box"
    and the github CTA:
      • skills      → ./skills.html
      • changelog → https://github.com/me2resh/apexyard/releases
  - The changelog link points at the GitHub releases page (not the
    raw CHANGELOG.md file) so it auto-resolves to the latest tagged
    release on each visit. v1.2.0 lands and the link is already there.

No new dependencies, no build step, no JS for the skills page. The
existing site convention (one-html-file-per-route, inlined CSS,
optional progressive-enhancement JS) is preserved.

Refs me2resh/apexyard#160 (release v1.2.0 + landing-site refresh) —
this is the second site-side deliverable for that ticket; the release
tag itself follows once me2resh/apexyard#159 (testing) closes.

Follow-up worth a separate ticket: a small generator script that
walks .claude/skills/*/SKILL.md, parses YAML frontmatter, and emits
the skills.html sections automatically. Out of scope for v1.2.0 —
first version is hand-curated and will need maintenance until that
generator lands.

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(#168): accept release/vN.N.N branches + release(...) PR titles (#169)

Closes me2resh/apexyard#168.

The /release skill prescribed `release/vA.B.C` as the source-branch
name and `release: vA.B.C` as the PR title for the dev → main release
PR (per AgDR-0007). Both were rejected by the framework's own
validators:

  validate-branch-name.sh required {type}/{TICKET-ID}-{description};
  release/v1.2.0 has no ticket-id portion.

  validate-pr-create.sh required type(SCOPE): form with `release` not
  in pr.title_type_whitelist.

The contradiction surfaced cutting v1.2.0 — the first release under
the dev/main model.

Three small changes:

1. .claude/hooks/validate-branch-name.sh — added an early-out branch
   that accepts ^release/vN.N.N(-rcN)?$ as a valid name. Narrow,
   intentional exception for the framework's release-cut convention;
   release branches don't carry a ticket-id because the release itself
   IS the ticket.

2. .claude/project-config.defaults.json — added "release" to
   pr.title_type_whitelist so a title like `release(#160): v1.2.0`
   passes validate-pr-create.sh's existing regex unchanged.

3. .claude/skills/release/SKILL.md step 4 — corrected the prescribed
   PR title to `release(#<release-ticket>): vA.B.C` so future /release
   invocations produce a title that satisfies the validators by
   construction.

Tested:

  bash .claude/hooks/validate-branch-name.sh against:
    release/v1.2.0           → 0   (allowed, release-special-case)
    release/v1.2.0-rc1       → 0   (allowed, RC variant)
    release/v9.9.9           → 0   (allowed)
    release/foo              → 2   (correctly blocked)
    release/v1               → 2   (correctly blocked)
    chore/GH-168-fix         → 0   (allowed, standard pattern)
    feature/GH-1-x           → 0   (allowed, standard pattern)

  Full hook test suite: 196/196 cases green across 12 test files.

Refs: surfaced 2026-05-04 cutting the first release under AgDR-0007.

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(#170): exempt release/vN.N.N from validate-pr-create's branch-id check (#171)

Closes me2resh/apexyard#170.

Completes the work started in #169 (closing #168). #169 added a
release-pattern early-out to validate-branch-name.sh so release/vN.N.N
branches pass the branch-name validator. But validate-pr-create.sh has
its own independent branch-id check at line 273 that #169 didn't
touch — and it still rejects release/v1.2.0 because that name doesn't
contain a ticket-id substring.

This is the same class of contradiction #168 fixed; the fix is the
same shape. Add the same release-pattern early-out to the branch-id
check in validate-pr-create.sh:

  - if the branch matches ^release/vN.N.N(-rcN)?$ → exempt (release
    branches don't carry ticket-ids; the release itself is the ticket)
  - otherwise → require a ticket-id substring as before

#168's acceptance criterion 3 ("validate-pr-create.sh accepts a PR
title `release(#160): v1.2.0` against the `release/v1.2.0` branch")
was checked off based on the title regex alone but didn't catch the
secondary branch-id check living in the same file. Surfaced trying
to open the v1.2.0 release PR.

Tested:

  bash .claude/hooks/validate-pr-create.sh against:
    release/v1.2.0           → 0   (allowed, exempt)
    release/v1.2.0-rc1       → 0   (allowed, RC variant)
    chore/GH-1-fix           → 0   (allowed, has ticket-id)
    release/foo              → 2   (correctly blocked)
    chore/no-ticket          → 2   (correctly blocked)

Refs me2resh/apexyard#168 (the parent bug) + #169 (the partial fix).

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(#173): sync CHANGELOG.md from main → dev (#174)

Closes me2resh/apexyard#173.

The release-cut model (AgDR-0007 / #116) squash-merges
release/vN.N.N → main, which means the v1.2.0 CHANGELOG section that
landed on main via PR #172 was never propagated back to dev. This
commit copies main's CHANGELOG.md verbatim onto dev so the v1.2.0
section is now present on both branches.

The diff is exactly the v1.2.0 entry being prepended; no other lines
change.

Without this sync, the next release PR cut from dev would build a
v1.3.0 section on top of v1.1.0, silently dropping v1.2.0 from dev's
running history. The corollary skill-level fix (option B in the
ticket) — updating /release to source the previous CHANGELOG from
upstream/main — is filed as a follow-up and out of scope for this PR.

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(#181): /agdr skill — searchable AgDR library (#186)

Adds /agdr — a portfolio-wide index for Agent Decision Records.
Walks apexyard.projects.yaml, reads each project's docs/agdr/*.md
(local clone if available, else gh api fallback), parses the optional
YAML frontmatter for category + projects, and answers four queries:

- /agdr browse        list across the portfolio, grouped by category
- /agdr search <term> full-text grep across all bodies, returns
                      <project>/AgDR-NNNN paths + matching paragraph
- /agdr show <id>     print a specific record, disambiguates duplicates
- /agdr stats         counts per category (the marketing-slide tile,
                      now backed by real data)

Six-category taxonomy: architecture | tech-stack | security | patterns
| integrations | other. Legacy AgDRs without frontmatter remain
first-class — they bucket as `other` and are flagged in browse so
operators can migrate at their own pace.

Backwards-compatible template change: templates/agdr.md gains an
optional `category:` (and optional `projects:`) line in the existing
frontmatter block. Omitting the line keeps every existing AgDR valid;
the skill defaults to `other` when missing.

Doc note in workflows/sdlc.md § Phase 2 points at /agdr search for
"have we decided this before?" lookups before drafting a design.

Smoke test in .claude/hooks/tests/test_agdr_skill.sh covers the
parser the spec specifies — frontmatter extraction, category
bucketing (including the legacy default-to-other path), id reading,
stats aggregation, and search match counts. 18 assertions, all green;
12 pre-existing test suites also green (no regressions).

Closes #181

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(me2resh/apexyard#183): /launch-check trend tracking (#185)

- Persist each run as JSON under <projects_dir>/<name>/launch-check/runs/
  (timestamp + branch + commit + per-dimension scores + verdict + top_risks)
- Append "Trend (last 5 runs)" section to the per-run summary when at least 2
  prior runs exist — markdown table + ASCII score chart
- Add /launch-check trend mode for read-only trend rendering (no full audit)
- Auto-derive notes column from score-delta vs previous run
  (e.g. "Security +12, Analytics +10")
- Opt-in commit via .launch-check-history-tracked marker; gitignored by default
- New helper script render-trend.sh + test_launch_check_trend.sh (21 cases)
- Resolve projects dir via portfolio_projects_dir helper (no hardcoded path)
- Schema is forward-compatible — extra fields preserved on framework upgrade

AgDR-0014 documents the chart format / schema / opt-in choices.

Closes me2resh/apexyard#183

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>

* feat(#182): /status --briefing + bin/apexyard status CLI shim (#187)

* feat(me2resh/apexyard#182): /status --briefing + bin/apexyard CLI shim

Make slide 6's `$ apexyard status` invocation real — the smallest of three
slide-reality tickets, demonstrating the marketing-to-implementation
pattern before #181 / #183 land.

- New helper at .claude/skills/status/briefing.sh — computes the 4-line
  "where am I" briefing (active workspace, active ticket, branch,
  role-set) so the logic is testable in isolation and runnable from a
  plain shell. Workspace inference walks up from cwd to the ops-fork
  root (same algorithm as _lib-portfolio-paths.sh and /start-ticket).
- Ticket reads from .claude/session/tickets/<workspace> first, falls
  back to .claude/session/current-ticket. Role-set is inferred from
  the active ticket's GitHub labels (v1: backend / frontend / qa /
  security / platform / sre / data / ux / ui / product / tech-lead,
  plus the long forms). No match emits the explicit "<none — inferred
  per task>" placeholder so the four-line shape is constant.
- New CLI shim bin/apexyard delegates `apexyard status` to the same
  helper after walking up to find the ops-fork root. Works from any
  workspace/<name>/ clone or the fork itself; symlink onto PATH to
  install (no shadowing of the `claude` binary).
- /status SKILL.md gains a "Briefing mode" section documenting the
  --briefing / -b flag; default /status output is unchanged.
- docs/multi-project.md "Daily workflow" demonstrates the new
  invocation alongside /inbox and /status.
- New smoke tests at .claude/hooks/tests/test_status_briefing.sh — 8
  cases covering ops-root cwd, workspace cwd, unknown cwd, ops-fallback
  marker, per-project marker priority, label-based role inference, the
  no-matching-label path, and the constant-four-line shape.

Closes me2resh/apexyard#182

* fix(me2resh/apexyard#182): replace curios-dog with example-app in SKILL.md output sample (leak fix)

---------

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>

* docs(#178): LSP integration spike — measurement + recommendation (#184)

* docs(#178): LSP spike — token-savings measurement + integration findings

- Phase 1: estimated token cost A vs B for three representative queries on a real TS Lambda backend (~9,750 LOC); shallow semantic queries see ~3-23x input-token savings, multi-hop traces see ~1.4x
- Phase 2: verified Claude Code shipped first-party LSP support in v2.0.74 (Dec 2025), gated behind ENABLE_LSP_TOOL=1, wired in via the plugin system (.lsp.json); MCP-wrapped LSP shims (cclsp, lsp-mcp) remain as fallback
- Phase 3: recommends Option 3 — interactive clone-first prompt at end of /handover, preserving today's "never auto-clone" principle while making the deep-dive path discoverable
- Phase 4: AgDR Y-statement and option matrix sketched; full AgDR is a follow-up if the spike says go
- Recommendation: GO. Adopt the built-in tool, document the opt-in path, change /handover to offer clone-first interactively. No code changes beyond /handover SKILL.md; no novel integration to maintain

Closes me2resh/apexyard#178

* docs: fix markdownlint MD031/MD032 in spike report

Add blank lines around lists and a fenced code block to satisfy
markdownlint-cli2 in CI. Pure formatting, no content change.

Refs me2resh/apexyard#178

---------

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>

* docs(me2resh/apexyard#190): annotate LSP-aware skills with opt-in callouts (#191)

- Add identical-shape "LSP-aware (optional, recommended)" callout near
  the top of each of the four code-aware SKILL.md files.
- Per-skill savings number is the only variation:
  - /code-review: ~3-15× cheaper for semantic queries
  - /threat-model: ~3-15× shallow, ~1.4-5× multi-hop traces
  - /security-review: ~3-15× cheaper for semantic querie…
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.

[Chore] Cut v2.2.0 release bug: split-portfolio v2 — workspace_dir not defaulting to sibling repo; skills fall back to ops-fork workspace/

3 participants