Skip to content

feat(#290): /extract-features --with-mockups (AI-inferred ASCII wireframes)#292

Merged
atlas-apex merged 5 commits into
devfrom
feature/GH-290-extract-features-mockups
May 19, 2026
Merged

feat(#290): /extract-features --with-mockups (AI-inferred ASCII wireframes)#292
atlas-apex merged 5 commits into
devfrom
feature/GH-290-extract-features-mockups

Conversation

@atlas-apex

Copy link
Copy Markdown
Collaborator

Summary

Adds opt-in --with-mockups flag to /extract-features that emits a low-fidelity ASCII wireframe per UI screen discovered in axis 5 (UI screens). Every wireframe carries a mandatory disclaimer header so the reader cannot mistake an inference for ground truth — inference honesty is the load-bearing design principle, captured in AgDR-0036.

  • ASCII format only (no PNG/SVG/HTML) — visual fidelity matches epistemic confidence
  • Mandatory > AI-inferred sketch — verify before relying on. Source: <path> header above every wireframe box
  • Opt-in flag, default OFF — adopters who don't want mockups see today's inventory unchanged
  • File-size policy: ≤10 screens emit inline under ## Screens; >10 screens spill into per-screen files at projects/<name>/screens/<slug>.md with the inventory becoming a linked index

Testing

bash .claude/skills/extract-features/tests/smoke-mockups.sh   # 25 checks (form, table, modal, dashboard, trust contract, width cap, backward-compat)
bash .claude/skills/extract-features/tests/smoke.sh           # 7 existing checks (regression)

Both pass. The new test asserts:

  1. Static-analysis signatures detect form fields, DataGrid, Modal, Sidebar, Card, Chart, TopNav across React component archetypes
  2. Field-type inference reads Prisma model declarations (String, Boolean, Int)
  3. Every screen wireframe carries the disclaimer header (4 disclaimers / 4 screens)
  4. Form, table, modal, dashboard wireframes contain their canonical elements
  5. 80-char width cap holds inside wireframe code-blocks
  6. Backward-compat: without --with-mockups, no ## Screens section is emitted and no AI-inferred disclaimer appears

Files

  • .claude/skills/extract-features/SKILL.md — argument-hint, usage block, new --with-mockups section, step 4b (emit wireframes), source detection rules, inference rules table, box conventions, file-size handling, anti-patterns
  • .claude/skills/extract-features/tests/smoke-mockups.sh — new test suite (25 checks)
  • docs/agdr/AgDR-0036-inferred-mockups-honesty.md — design rationale (Options A=PNG/SVG, B=interactive HTML, C=ASCII-only inline + chosen)
  • CLAUDE.md — skill row updated to mention --with-mockups
  • docs/multi-project.md — skill behaviour table updated with inference-honesty note

Closes #290


Glossary

Term Definition
AI-inferred sketch A wireframe generated by the model's reading of the route/component code, not a lift from a rendered DOM
Inference honesty The design principle that AI-generated artefacts should signal their epistemic status — ASCII format, mandatory disclaimer header, opt-in flag
Axis 5 The UI-screens axis of /extract-features's six-axis discovery
Source detection The rule for picking the Source: value in the disclaimer — route file when router-mapped, component file otherwise
Trust contract The mandatory disclaimer header + ASCII format + opt-in flag, together — the artefact's epistemic signal to the reader
Per-screen file When >10 screens, the wireframes spill out of the inventory file into one markdown file per screen under projects//screens/.md, with the inventory's ## Screens section becoming a linked index

atlas-apex and others added 3 commits May 4, 2026 18:46
* 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(#160): release v1.2.0 — CHANGELOG entry

Adds the v1.2.0 section to CHANGELOG.md ahead of the dev → main
release PR. 25 commits since v1.1.0 (was 26 — one feat removed
post-supersession per AgDR-0013, intentionally unenumerated in the
adopter-facing changelog because the feature never reached a tagged
release).

Highlights:
  - Bootstrap-skill exemption + Bash-write coverage (#150 + #151)
  - /approve-merge structured marker + same-turn merge (#132 + #48)
  - Portfolio config block + /split-portfolio helper (#143 + #145)
  - Four new skills: /debug, /validate-idea, /tickets-batch, /fan-out
  - Release-cut branch model (#116, AgDR-0007)
  - Landing-site multi-tab terminal + skills page + changelog link
    (#160 / #165)

Refs me2resh/apexyard#160 (release-and-site-refresh ticket).

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(#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 traces
  - /security-review: ~3-15× cheaper for semantic querie…
…rames)

Adds opt-in `--with-mockups` flag to /extract-features that emits a
low-fidelity ASCII wireframe per UI screen discovered in axis 5. Every
wireframe carries a mandatory disclaimer header
(`> AI-inferred sketch — verify before relying on. Source: <path>`) so
the reader cannot mistake an inference for ground truth — inference
honesty is the load-bearing design principle, captured in AgDR-0036.

Backward-compat: running /extract-features without the flag produces
today's inventory exactly. The flag adds a `## Screens` section after
the per-axis findings; nothing else changes.

File-size policy: ≤10 screens emit inline; >10 screens spill into per-
screen files at `projects/<name>/screens/<slug>.md` with the inventory
becoming a linked index.

Tests at `.claude/skills/extract-features/tests/smoke-mockups.sh` cover
the four canonical archetypes (form-heavy, table-heavy, modal,
dashboard), validate the trust-contract guarantees (every wireframe has
a disclaimer; no `## Screens` section without the flag; 80-char width
cap inside boxes), and assert the backward-compat path.

- Update SKILL.md: argument-hint, usage block, new `--with-mockups`
  section, step 4b (emit wireframes), inference rules table, box
  conventions, file-size handling, anti-patterns
- Add AgDR-0036-inferred-mockups-honesty.md
- Add smoke-mockups.sh (25 checks, all pass)
- Update CLAUDE.md skill row + docs/multi-project.md skill behaviour
  table with the inference-honesty note

Closes #290

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

@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 #292

Commit: 09bf81aa68d123c562ae3371698080d7cb5b1324

Summary

Adds opt-in --with-mockups flag to /extract-features that emits AI-inferred ASCII wireframes per UI screen discovered in axis 5. Inference honesty is the load-bearing design principle: ASCII format only, mandatory single-line disclaimer header per wireframe, opt-in flag (default off), and per-screen file spillover at >10 screens. Captured in AgDR-0036.

Three commits total on the PR; only 09bf81a is the feature commit — 52f220c and 15516a8 are upstream release-cut sync commits brought in via the dev base branch (per AgDR-0007 release-cut model). My review scope is the feature commit's actual contribution: SKILL.md (+576), tests/smoke-mockups.sh (+467), tests/smoke.sh (+316 existing), tests/README.md (+58), AgDR-0036 (+81), and small skill-row updates in CLAUDE.md + docs/multi-project.md.

Checklist Results

  • Architecture & Design: Pass — opt-in flag, backward-compat preserved, ASCII format + mandatory disclaimer impose visual fidelity that matches epistemic confidence (the load-bearing principle)
  • Code Quality: Pass — skill spec is exhaustively documented (inference rules table, box drawing conventions, four canonical examples, file-size handling); smoke test is well-structured with named assert helpers (assert_match / assert_nomatch / assert_count_ge)
  • Testing: Pass — 25/25 new smoke checks pass, 7/7 existing smoke checks still pass (no regression). Coverage spans static-analysis signatures across four archetypes (form/table/modal/dashboard), field-type inference from Prisma, trust-contract guarantees (disclaimer-count-equals-screen-count, 80-char width cap inside boxes), and the backward-compat path (no ## Screens and no disclaimer present without the flag)
  • Security: Pass — no auth/crypto/secrets/user-data touched; smoke fixtures live in mktemp -d with trap cleanup; no untrusted input
  • Performance: Pass — N/A (markdown skill spec + bash smoke test)
  • PR Description & Glossary: Pass — Summary, Testing, Files, Closes #290, six-row Glossary all present
  • Technical Decisions (AgDR):Pass — AgDR-0036 compares three options (PNG/SVG, interactive HTML, ASCII-only) with explicit pros/cons per option; explicitly captures why ASCII over PNG/SVG (visual fidelity exceeds epistemic confidence) and why the 10-screen threshold (inventory readability vs file-system surface area). Linked from SKILL.md (3 places) and the PR body
  • Adopter Handbooks: N/A — public handbooks loaded (architecture/clean-architecture-layers.md, architecture/migration-safety.md, general/commit-message-quality.md); none triggered. No private custom-handbooks dir (single-fork mode). Language handbooks: no **/*.{ts,tsx,py,go,rs} files in diff — markdown + bash only

Issues Found

None blocking. The inference-honesty design choices are exactly right for this artefact:

  1. ASCII-only is the right call. A polished PNG of a model's guess at a screen is the precise failure mode the feature must avoid. The AgDR's Option A rejection ("visual fidelity exceeds epistemic confidence — readers stop questioning what's inferred") is the correct framing
  2. Mandatory disclaimer wording is consistent across the codebase. The exact phrase AI-inferred sketch — verify before relying on. Source: <path> appears 3 times in SKILL.md (opt-in description, step 4b spec, anti-pattern reference) and the smoke test asserts the regex '^> AI-inferred sketch — verify before relying on\. Source: '. No drift between spec and test
  3. Opt-in default-off keeps backward-compat tight. The smoke test's two assertions on the without-flag path (no ## Screens section, no AI-inferred disclaimer) lock the contract — adopters who don't opt in cannot accidentally pick up the new behaviour
  4. Per-screen file threshold of 10 is documented + overridable via the custom-template override path; AgDR explicitly notes adopters who want a different cut can author custom-templates/extract-features.md
  5. The relative AgDR link ../../../docs/agdr/AgDR-0036-inferred-mockups-honesty.md from .claude/skills/extract-features/SKILL.md resolves correctly to <repo>/docs/agdr/AgDR-0036-inferred-mockups-honesty.md

Suggestions

Four are echoes of the agent's own honest concerns — surfacing them as documented follow-up candidates rather than blockers (the AgDR already calls them out under "Consequences"):

  1. Bespoke-CSS app inference will read thin. The signal table assumes component-library conventions (<TopNav>, <Sidebar>, <Footer>, <DataGrid>, <Modal>). A team using semantic HTML + custom CSS with no named layout components will produce a sketch that's mostly just [form] or [table]. The disclaimer covers it ("verify before relying on"), but a future follow-up could add a CSS-selector signal layer (<header>, <aside>, <nav role="navigation">, ARIA landmarks) to widen the detection net. Not blocking — adopters in this state still get the disclaimer + axis 5 table; the wireframe just degrades to "empty box"

  2. Form-field-type inference is the most fragile axis. The current rules trace UI fields back to model field types (Prisma String → text input, Boolean → checkbox, Int → number input). Real apps frequently bind form fields to derived shapes — a birthYear: number model field rendered as a <select> of years, a currency: string model field rendered as a <RadioGroup>. The inference rules table marks this with the <select> / <Dropdown> / <RadioGroup> row (which takes precedence over field type), so the spec is correct — but it depends on the model finding the JSX-side signal first. Worth a sentence in the AgDR's Consequences section noting that the JSX-side signal wins, since otherwise a reader could misread the table as type-first

  3. Modal vs route disambiguation is partly relying on prose. Step 4b says <Modal> / <Dialog> / <Drawer> (as the root return) → centered modal layout. The fixture's ConfirmDeleteModal.jsx returns <Modal> directly so this works, but in real codebases a route component frequently opens a modal mid-render via state. The fixture covers the simple case; the harder case (route page renders a button that conditionally opens a modal) currently falls under the page's wireframe. Defensible — the modal isn't the screen's primary surface — but worth flagging as a known reading

  4. The 10-screen threshold is a guess. The AgDR documents this explicitly. Worth revisiting after first real-adopter use; if a 20-screen inventory turns out to be reviewable inline, raise the threshold; if a 10-screen inventory feels heavy, lower it. Already overridable via custom-templates — no code change needed, just a doc update if real use shows the heuristic is off

Two additional minor observations:

  1. Smoke test screen-count assertion uses regex ^### [0-9]+\. . This counts numbered ### N. Screen name headings. A future author who adds a non-numbered sub-screen heading (e.g. ### Sign up (variant A)) would silently break the disclaimer-count-equals-screen-count invariant — the disclaimer count would be N+1 but the screen count would be N. Minor robustness issue; consider widening to ^### .* or counting disclaimer-blocks directly. Not blocking — the spec doesn't currently allow variant headings, so this is a future-proofing nit

  2. The smoke test renders a reference inventory rather than executing the skill. This is correct (the skill runs inside Claude Code, not in a shell), but means the test asserts the shape contract rather than the generation logic. A future test could add a small bash-level static-analysis harness that runs the inference rules against the fixture and produces a wireframe candidate to compare against the reference — would catch drift in the inference rules themselves. Future work; the current contract-shape coverage is the right v1 cut

Verdict

APPROVED

The inference-honesty design is the load-bearing choice this PR makes, and the AgDR makes the case for ASCII + mandatory disclaimer + opt-in flag rigorously enough that future adopters who want to "upgrade to PNG" have to argue against the captured rationale, not against a vibe. Tests cover the contract guarantees end-to-end. Backward-compat is locked. Ship it.


🤖 Reviewed by Rex (Code Reviewer Agent)
📌 Reviewed commit: 09bf81aa68d123c562ae3371698080d7cb5b1324

Dev advanced ~15 commits since this branch (v1.3.0 release + #281 ticket
templates + #283 tracker abstraction + #284 /pdf + #286 update chain +
#287 + #288 /feature-diagram + #293 Rex domain handbooks + #275 ui-paths-
exclude + #266 mermaid lint + #270 DFD snapshot + #246 tech-vision + #245
investigation + #257 dfd + #268 skill-gated ticket-create).

Resolution strategy: --theirs for framework drift (CLAUDE.md, multi-project,
project-config.defaults, all new tests, hook files, other SKILL.mds,
handbooks/README, site/architecture.html, templates/*, workflows/sdlc,
etc. — 26 files). Manual merge for the #290 deliverable
`.claude/skills/extract-features/SKILL.md` — kept BOTH the argument-hint
+ `--with-mockups` doc section + step 4b (this PR's deliverable) AND
the `/feature-diagram` See-also pointer dev added (complementary, not
overlapping).

8 conflict regions in extract-features/SKILL.md handled: 1 kept dev's
new See-also block, 7 kept ours (argument-hint, Usage block, the full
--with-mockups doc, the IF-mockups insert marker, the giant 4b ASCII-
wireframes section, the report-line append, the anti-pattern bullets).

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

@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 #292

Commit: 232cb1ffd64cb6f168cc9cf5e05b5f3ec06d0ee4

Summary

Adds opt-in --with-mockups flag to /extract-features that emits ASCII wireframes per discovered UI screen, with a mandatory > AI-inferred sketch — verify before relying on. disclaimer header above every box. Inference honesty (ASCII-only, mandatory disclaimer, opt-in flag) is the load-bearing principle, captured in AgDR-0036. Includes 25-check smoke test across four canonical archetypes (form / table / modal / dashboard) plus the trust-contract guarantees.

The PR sat for ~6h while dev advanced ~15 commits; the final commit 232cb1f is a merge that absorbed the drift. The merge correctly took --theirs for 26 framework-drift files and did a careful manual three-way merge on .claude/skills/extract-features/SKILL.md to keep BOTH the dev See-also pointer at /feature-diagram AND the #290 deliverable additions.

Checklist Results

  • ✅ Architecture & Design: Pass — skill is markdown-only, no architectural surface change
  • ✅ Code Quality: Pass — well-structured SKILL.md sections, clean shell in smoke test
  • ✅ Testing: Pass — 26 assertions across four archetypes + trust contract + width cap + backward-compat
  • ✅ Security: Pass — no auth / crypto / migration / secrets paths touched
  • ✅ Performance: N/A — documentation + flag implementation, no hot path
  • ✅ PR Description & Glossary: Pass — 6-term glossary, clear Summary / Testing / Files
  • ✅ Technical Decisions (AgDR): Pass — AgDR-0036 covers Option A (PNG/SVG) vs B (HTML) vs C (chosen ASCII) with substantive trade-off analysis
  • ❌ Merge resolution: Fail on two files — see below

Issues Found

1. ⛔ BLOCKING — merge-resolution mistake in CLAUDE.md + docs/multi-project.md

Both the 09bf81a PR commit AND dev modified the same row of the skill table in both files. The merge resolver kept SKILL.md as a careful manual three-way merge (good) but for CLAUDE.md and docs/multi-project.md it took --theirs (dev's version) wholesale, losing the --with-mockups mention.

CLAUDE.md line 228 — current HEAD:

| /extract-features | ... — the "what we must preserve" spec for a greenfield rewrite. Complements /handover ...; /extract-features is the granular feature catalogue. See also /feature-diagram for the per-feature view. |

What 09bf81a added (lost in merge):

... is the granular feature catalogue. Opt-in --with-mockups flag emits AI-inferred ASCII wireframes per UI screen with a mandatory disclaimer header — sketch-quality fidelity matches sketch-quality inference. See [AgDR-0036](docs/agdr/AgDR-0036-inferred-mockups-honesty.md).

Same shape in docs/multi-project.md line 691 — the longer inference-honesty paragraph (incl. mandatory-disclaimer wording + the >10-screens spill threshold + AgDR-0036 link) was also dropped.

Why this matters: the PR description claims "5 files" but the merged delta is 3 files (git diff origin/dev...HEAD --name-only). Adopters reading the skill table — the canonical discovery surface for slash commands — won't see the new flag exists. They'd have to grep SKILL.md or stumble on AgDR-0036.

The right resolution is to compose both additions in the same way SKILL.md was composed: dev's /feature-diagram cross-reference AND #290's --with-mockups mention coexist on the row. Recommended row shape (CLAUDE.md):

| /extract-features | Scan an existing codebase across six discovery axes (HTTP routes, data models, async jobs, test names, UI screens, documented features) and write a consolidated Feature Inventory at projects/<name>/feature-inventory.md — the "what we must preserve" spec for a greenfield rewrite. Complements /handover (high-level project assessment); /extract-features is the granular feature catalogue. Opt-in --with-mockups flag emits AI-inferred ASCII wireframes per UI screen with a mandatory disclaimer header — sketch-quality fidelity matches sketch-quality inference. See [AgDR-0036](docs/agdr/AgDR-0036-inferred-mockups-honesty.md). See also /feature-diagram for the per-feature view. |

Same composition for docs/multi-project.md line 691 (keep the longer inference-honesty paragraph).

Severity: medium-blocking. The skill works (SKILL.md is intact); only the cross-doc discoverability is lost. Fixable in one follow-up commit on this PR. After the commit, re-invoke Rex against the new SHA.

Handbook Findings

No handbooks loaded for this diff (markdown-only changes in skills + tests + AgDR; no architecture/, general/, or matching-language files touched by the diff that would trigger the language handbooks).

Suggestions

  1. Advisory — H1 prefix style. AgDR-0035 uses # AgDR-0035 — Title; AgDR-0036 uses # Title (no AgDR prefix). The framework template at templates/agdr.md says # {Short Title}, so AgDR-0036 follows the template literally; AgDR-0035 prepends the ID. Neither is broken (the /agdr skill discovers by filename, not H1), but consistency across the corpus would help future readers. Pick a convention and apply to one or the other in a follow-up. No action this PR.

  2. Advisory — AgDR YAML frontmatter. The review prompt mentions "Rex flagged this on #300 — every AgDR should have it now." Looking at the recent AgDR corpus (0033-0037), none carry YAML frontmatter and the framework template at templates/agdr.md doesn't include any. If frontmatter is becoming the convention, the template should be updated first; otherwise AgDR-0036 is consistent with its siblings and this isn't a finding on this PR.

  3. Suggestion — note the test's structural limitation explicitly. The smoke test validates the trust contract on a reference inventory file the test itself writes, not on actual /extract-features --with-mockups output. The test's preamble (lines 9-13) acknowledges this — the skill runs inside Claude Code, not in shell — but a one-line note in the smoke-mockups SKILL.md test pointer that "this is a contract test, not an end-to-end test" would set adopter expectations. Not blocking.

  4. Nit — the ## Coverage gaps section in the backward-compat inventory fixture sits directly after the UI-screens table. Real /extract-features output puts ## Per-axis findings between them. Doesn't affect any assertion in the test, but a tiny shape mismatch with the actual SKILL.md template. Take it or leave it.

Verdict

REQUEST CHANGES

The #290 deliverable itself (SKILL.md merge composition, AgDR-0036, smoke tests) is well-executed and ready. The blocker is the merge-resolution drop in CLAUDE.md + docs/multi-project.md — the new flag must be discoverable through the two canonical skill tables. One follow-up commit composing both sides into a single row in each file unblocks the merge. After the commit pushes, re-invoke /code-review 292.


Reviewed by Rex (Code Reviewer Agent)
Reviewed commit: 232cb1ffd64cb6f168cc9cf5e05b5f3ec06d0ee4

Rex review on PR #292 (merge resolution) flagged that --theirs taken
wholesale on CLAUDE.md and docs/multi-project.md dropped the
--with-mockups mention from both skill-table rows. The skill table is
the canonical discovery surface adopters read — silent drop = silent
feature regression.

Three-way merge restored: both files now keep dev's '/feature-diagram'
See-also (from #291) AND the --with-mockups mention with the
disclaimer-header + AgDR-0036 pointer. The skill table row is now the
single source of truth for what /extract-features can do.

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

@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.

Re-Review: PR #292 (fix-up at new HEAD)

Commit: 2e85e9221ad0c4cdca4e364b1e20c8f28323a7f0
Prior verdict: REQUEST CHANGES at 232cb1f — one blocking finding (skill-table rows missing --with-mockups mention after --theirs merge resolution).
This re-review: targeted fix-up only.

Summary

The new commit fix(#290): restore --with-mockups mention in skill-table rows surgically restores the /extract-features row in both CLAUDE.md and docs/multi-project.md with a mention of the new flag, the mandatory disclaimer header, and the AgDR-0036 pointer. CLAUDE.md preserves dev's /feature-diagram See-also (no regression on #291). Diffstat vs prior reviewed SHA: exactly 2 files, +2/-2 lines — surgical.

Checklist Results

  • Architecture & Design: Pass (docs-only, no behavioural change)
  • Code Quality: Pass
  • Testing: Pass (smoke-mockups.sh untouched, previously approved)
  • Security: Pass (no auth/crypto/secrets touched)
  • Performance: Pass (n/a)
  • PR Description & Glossary: Pass (carried over from prior approval)
  • Technical Decisions (AgDR):Pass (AgDR-0036 already in tree, now correctly cross-referenced from both skill tables)
  • Adopter Handbooks: N/A (markdown-only diff; clean-architecture / migration-safety handbooks don't apply)

Issues Found

None.

Verification against the prior blocking finding

Check Status
CLAUDE.md skill-table row mentions --with-mockups Yes — line 228
CLAUDE.md row mentions the disclaimer header Yes — > AI-inferred sketch — verify before relying on quoted inline
CLAUDE.md row points to AgDR-0036 Yes — "see AgDR-0036 for the trust-contract rationale"
CLAUDE.md row preserves the /feature-diagram See-also pointer (#291) Yes — "See also /feature-diagram for the per-feature view."
docs/multi-project.md skill-table row mentions --with-mockups Yes — line 691
docs/multi-project.md row mentions the disclaimer header Yes — quoted inline
docs/multi-project.md row points to AgDR-0036 Yes
No other unintended changes vs 232cb1f Confirmed — diffstat is exactly 2 files, +2/-2 lines
CI green on the new HEAD Yes — 3/3 checks pass (Verify Ticket ID, lychee, markdownlint-cli2)

Discovery-surface concern from the prior review is fully resolved: an adopter reading either skill table now sees the flag exists, sees the disclaimer-header trust contract inline, and gets a direct pointer to the AgDR for the rationale. The composition of #290 (mockups) and #291 (See-also pointer) is clean — neither feature's discovery surface regresses.

Verdict

APPROVED

The previously-approved aspects (merge resolution of .claude/skills/extract-features/SKILL.md, AgDR-0036, smoke tests) remain intact and untouched in this commit. Ready to merge once CEO approval is recorded via /approve-merge 292.


Reviewed by Rex (Code Reviewer Agent)
Reviewed commit: 2e85e9221ad0c4cdca4e364b1e20c8f28323a7f0

@atlas-apex atlas-apex merged commit d2f1f2e into dev May 19, 2026
3 checks passed
@atlas-apex atlas-apex deleted the feature/GH-290-extract-features-mockups branch May 19, 2026 19:36
me2resh added a commit that referenced this pull request May 19, 2026
…rames) (#292)

* release(#160): v1.2.0 (#172)

* 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(#160): release v1.2.0 — CHANGELOG entry

Adds the v1.2.0 section to CHANGELOG.md ahead of the dev → main
release PR. 25 commits since v1.1.0 (was 26 — one feat removed
post-supersession per AgDR-0013, intentionally unenumerated in the
adopter-facing changelog because the feature never reached a tagged
release).

Highlights:
 - Bootstrap-skill exemption + Bash-write coverage (#150 + #151)
 - /approve-merge structured marker + same-turn merge (#132 + #48)
 - Portfolio config block + /split-portfolio helper (#143 + #145)
 - Four new skills: /debug, /validate-idea, /tickets-batch, /fan-out
 - Release-cut branch model (#116, AgDR-0007)
 - Landing-site multi-tab terminal + skills page + changelog link
 (#160 / #165)

Refs me2resh/apexyard#160 (release-and-site-refresh ticket).

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>

* release(#278): v1.3.0 (#279)

* 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 prefi…
me2resh added a commit that referenced this pull request Jun 5, 2026
…rames) (#292)

* release(#160): v1.2.0 (#172)

* 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(#160): release v1.2.0 — CHANGELOG entry

Adds the v1.2.0 section to CHANGELOG.md ahead of the dev → main
release PR. 25 commits since v1.1.0 (was 26 — one feat removed
post-supersession per AgDR-0013, intentionally unenumerated in the
adopter-facing changelog because the feature never reached a tagged
release).

Highlights:
 - Bootstrap-skill exemption + Bash-write coverage (#150 + #151)
 - /approve-merge structured marker + same-turn merge (#132 + #48)
 - Portfolio config block + /split-portfolio helper (#143 + #145)
 - Four new skills: /debug, /validate-idea, /tickets-batch, /fan-out
 - Release-cut branch model (#116, AgDR-0007)
 - Landing-site multi-tab terminal + skills page + changelog link
 (#160 / #165)

Refs me2resh/apexyard#160 (release-and-site-refresh ticket).

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>

* release(#278): v1.3.0 (#279)

* 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 prefi…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants