feat(#1): initial ApexStack project#1
Conversation
Complete open-source stack with: - 20 software development roles across 5 departments (Engineering, Product, Design, Security, Data) - 3 workflow processes (SDLC, code review, deployment) - 4 document templates (PRD, technical design, ADR, AgDR) - Onboarding config (onboarding.yaml) for team customization - CLAUDE.md entry point for Claude Code integration - Static landing page (site/index.html) - Getting started guide Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds the runnable layer that makes ApexStack actually do things in
Claude Code. Until now ApexStack shipped roles, workflows and templates
(passive surface) but had no commands users could invoke and no hooks
to enforce its own standards. This commit closes that gap.
New top-level structure:
.claude/
settings.json # PreToolUse hook wiring
hooks/ # 6 enforcement scripts
block-git-add-all.sh
block-main-push.sh
check-secrets.sh
pre-push-gate.sh
validate-branch-name.sh
validate-pr-create.sh
rules/ # 6 modular rule files (CLAUDE.md @imports)
agdr-decisions.md
code-standards.md
git-conventions.md
pr-quality.md
pr-workflow.md
workflow-gates.md
agents/ # 5 sub-agents
code-reviewer.md # Rex
security-reviewer.md # Shield
dependency-auditor.md # Guardian
pr-manager.md
ticket-manager.md
skills/ # 5 slash commands
decide/SKILL.md
code-review/SKILL.md
security-review/SKILL.md
audit-deps/SKILL.md
write-spec/SKILL.md
golden-paths/
pipelines/ # 7 drop-in GHA workflows
ci.yml
code-quality.yml
security.yml
dependency-audit.yml
pr-title-check.yml
review-check.yml
seo-check.yml
README.md
CLAUDE.md and README.md updated to document the new layers and the
two-step install model (markdown content under .apexstack/ + .claude/
copied or symlinked to project root for the executable layer).
All content generalised — no company-specific names, repo references,
ticket prefixes, or product names. Ticket validators accept any
[A-Z]+-[0-9]+ pattern so teams can use Linear, Jira, or GitHub Issues.
The agent identities (Rex / Shield / Guardian / Sentinel / Scout) are
preserved as defaults that adopters can rename.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two related changes bundled:
1. GitHub Issues as the default tracker
- Ticket Manager agent rewritten to lead with `gh issue create`
in the project's own repo. Linear / Jira mentioned only as
opt-in deviations.
- dependency-auditor, write-spec, git-conventions, and the
pr-title-check pipeline updated to lead with GitHub Issues
and treat other trackers as alternatives.
- Per-project tracking: each repo owns its own issue list,
no central / cross-project tracker. Co-located with the code.
2. Landing site revamped — terminal-native brutalism
- Aesthetic: single typeface (JetBrains Mono variable, weights
100-800), warm cream paper, single warning-red accent. Sharp
corners, hairline borders, no gradients, no shadows, no
rotating decor. Light + dark via prefers-color-scheme.
- Content priorities flipped to developer-first:
* One-line tagline + one copy-able install command in the
hero, not a decorative orbit.
* Full ASCII file tree showing exactly what you get on a
clone, with [NEW] markers on the .claude/ and golden-paths/
layers.
* Four-card "two passive layers, two runnable" explainer.
* Four real terminal-output demos: /decide, the git add -A
block, /code-review (Rex output), and gh issue create.
* Pipelines listed as a CLI-style table with descriptions
and the agent that runs them.
* Six-step install with copy-able code blocks for each step.
* Opinionated DO / DON'T table (the line apexstack draws).
- Accessibility: skip link, semantic landmarks, focus-visible,
reduced-motion respected, sufficient contrast in both themes.
- Zero JavaScript dependencies (one tiny copy-to-clipboard
handler), no build step, no external assets except the one
Google Fonts request for JetBrains Mono.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
atlas-apex
left a comment
There was a problem hiding this comment.
Code Review: PR #1 — Initial ApexStack Release
Commit: 5847ceef77e6a9319c8a9a479681f0a3707e2086
I reviewed the full diff from the PR base through 5847cee (covering commits 4625202, 8fa3c92, and 5847cee), since the prior review on 8fa3c92 was interrupted.
Note: GitHub blocks
--request-changeson own PRs, so this is posted as a--comment. The verdict is CHANGES REQUESTED — see Blocking Issues below.
Summary
This is a large, carefully structured initial release: 19 role definitions, 3 workflows, 4 templates, a complete .claude/ runnable layer (6 hooks, 6 rules, 5 agents, 5 skills), 7 golden-path CI pipelines, and a new terminal-native-brutalism landing page. The architectural move from passive markdown → runnable primitives is the right call and the file layout is clean. The GitHub-Issues-as-default migration is thoughtfully executed in the agents, skills, rules, and hook validators.
However, the landing site has three company-specific identifier leaks and several incorrect hero/tree metrics, and there is an agent naming drift (Sentinel/Scout) between the pipelines/site and the agents that actually ship in .claude/agents/. These must be fixed before merge.
Checklist Results
- Architecture & DDD: Pass — N/A for a content-first repo; layout is logical (
roles/,workflows/,templates/,.claude/,golden-paths/,docs/,site/). - Code Quality: Pass with nits — hook scripts use portable grep/sed/POSIX classes, no
grep -P, no bashisms that would break on Ubuntu runners. One minor JS smell in the site. - Testing: N/A — no code under test at this stage. Eating-your-own-dog-food (question 11) discussed below.
- Security: Pass — no secrets, no hardcoded credentials, no ARNs/account IDs;
check-secrets.shis correctly scoped. - Performance: Pass — static site, no perf issues.
- PR Description & Glossary: Pass — 12 glossary entries present and substantive.
- Technical Decisions (AgDR): Acceptable for v0.1 — no AgDR for "hooks vs MCP" / "which 5 agents" / "site aesthetic". See (10) below. Not a blocker for the initial release, but I'd open a follow-up ticket.
Blocking Issues (must fix before merge)
B1. LICENSE still credits ApexScript, not the project.
LICENSE:3 — Copyright (c) 2026 ApexScript. For an open-source repo that is explicitly not ApexScript-branded, this should be generalised. Either Copyright (c) 2026 ApexStack contributors or Copyright (c) 2026 Ahmed Elmorshedy (or whoever the legal copyright holder is). An MIT notice attributing the wrong legal entity is a real problem, not cosmetic.
B2. Landing site leaks company-specific identifiers in the demo examples.
The Grep for ApexScript|FlatMate|Curios|yumyum turned up two demo blocks in site/index.html:
- Line 1132 — the
/decidedemo says"and the team already knows it from FlatMate."Replace with a generic rationale (e.g."and we've used it before on the prior project."). - Lines 1188–1197 — the
ticket-managerdemo usescurios-dog,me2resh/curios-dog, andme2resh/curios-dog#179as a concrete repo. This is the exact leak pattern you flagged. Replace with a generic placeholder, e.g.your-org/your-projectandyour-org/your-project#179(same pattern you already use at line 1165 in the/code-reviewdemo). The caption at 1201 is fine.
(Note: references to github.com/me2resh/apexstack in the hero, install section, footer, and final CTA are correct — me2resh/apexstack IS the actual repo of this project. Those are not leaks.)
B3. Hero metric "17" is wrong.
site/index.html:923 — the hero metric block shows <strong>17</strong> under "Hooks · rules · agents · skills". Actual counts:
- hooks: 6
- rules: 6
- agents: 5
- skills: 5
- Total: 22
Change 17 → 22, or if the intent was to count something else (say, the four directories), adjust the label.
B4. Tree header file count and LOC are both wrong.
site/index.html:956 — the tree header shows 62 files · 5,480 LOC. Actual counts on current HEAD (excluding .git and .github):
- files: 64 (or 63 if you exclude the stray root
./.gitkeep) - LOC (all
.md+.yml+.sh+.json+.yaml+.html): 8,849 (or 7,385 if you excludesite/index.htmlitself, which may be the more honest framing — the site line-counts itself otherwise).
Pick a framing and update both numbers. I'd suggest 64 files · ~7,400 LOC (excluding the site file so the self-referential "5,480" doesn't race with every site edit).
B5. Agent naming drift — Sentinel and Scout don't exist as agents.
The ticket-manager/code-reviewer/security-reviewer/dependency-auditor/pr-manager set is your shipped roster (5 agents). But the CI pipelines and landing site reference two additional names that have no agent file:
- Sentinel (security agent) — appears in
golden-paths/pipelines/security.yml,golden-paths/pipelines/ci.yml(step name"🛡️ Sentinel: Security"),site/index.html:1227,1239(pipeline labels), and the PR description. But.claude/agents/security-reviewer.md:121names the agent Shield, and the landing tree at line 1000 correctly sayssecurity-reviewer.md # Shield — OWASP, injection, auth. - Scout (SEO agent) — referenced at
site/index.html:1019,1263and ingolden-paths/pipelines/seo-check.yml. Noscout.mdorseo-*.mdexists in.claude/agents/.
Pick one of two options:
a) Rename all Sentinel → Shield across ci.yml, security.yml, site/index.html:1227,1239, and the PR body. For Scout, either add a placeholder agent file (seo-auditor.md / Scout identity) so the pipeline references something real, or rebrand the seo-check.yml workflow as ownerless ("governance" or "content") since there's no agent.
b) Keep the Sentinel/Scout names and add the corresponding agent files to .claude/agents/ in this PR. Note this would also push the "5 agents" metric to 7 and every "5 sub-agents" claim in the README/CLAUDE.md/site/layer card/PR body would need updating too.
Option (a) is the smaller diff and recommended for a v0.1 ship.
B6. Ticket regex disagreement between local hooks and CI pipeline.
.claude/hooks/validate-branch-name.sh:30and.claude/hooks/validate-pr-create.sh:32,47use[A-Z]+-[0-9]+— any uppercase-letter prefix of length ≥ 1.golden-paths/pipelines/pr-title-check.yml:33uses[A-Z]{2,5}-\d+— 2 to 5 chars only.
Consequence: a team using PROJECT-123 (7 chars) passes the local hook but their PR fails the CI check. A team using X-1 (1 char) passes the hook but fails the CI. Align them — I'd recommend loosening the pipeline to [A-Z]{2,}-\d+ (min 2 to avoid false matches on ordinary uppercase letters followed by numbers) and updating the hooks to [A-Z]{2,}-[0-9]+ to match. Document the chosen minimum in .claude/rules/git-conventions.md (currently uses examples ABC-123, GH-45, ENG-99 — all ≥ 2 chars, which is fine).
Suggestions (non-blocking)
S1. Titlebar nav has a dead anchor. site/index.html:874 — <a href="#stack">stack</a> but there is no id="stack" on any section. Either remove the link, or add the id to the hero section (<section class="hero" id="stack">), or rename to #tree.
S2. JS copy script shadows a browser global. site/index.html:1451 — the script declares function confirm() { ... } inside the IIFE, which shadows window.confirm. It works (called as a void callback), but it's a code smell that'll bite someone debugging later. Rename to showCopied() or similar.
S3. Unused animation-delay class. site/index.html:832 defines .rise-6 but nothing in the HTML uses it. Dead CSS.
S4. Hook scripts have an undeclared runtime dependency on jq. All six hooks do echo "$INPUT" | jq -r '.tool_input.command // empty'. If a user adopts ApexStack on a machine without jq, every Bash tool call will emit a silent stderr line and $COMMAND will be empty, causing the hooks to no-op and silently fail to enforce. Two mitigations:
- Add a note in
docs/getting-started.mdunder prerequisites: "ApexStack hooks requirejq(install viabrew install jqorapt install jq)." - Have each hook fail loud if
jqis missing:command -v jq >/dev/null || { echo 'apexstack hook: jq required but not installed' >&2; exit 0; }(exit 0 because blocking every Bash call on a missing dep would be user-hostile).
S5. pre-push-gate.sh advertises framework validators but doesn't check anything. The output mentions sam validate, terraform validate as examples — fine for reminder semantics, but docs/getting-started.md should mention that the reminder is inert (no actual gate, just a stderr nudge) so adopters don't think it blocks.
S6. Stray root .gitkeep. find lists ./.gitkeep at repo root. Usually .gitkeep belongs inside an otherwise-empty directory. Remove it or move it into an empty dir it's actually keeping.
S7. Tree in landing omits .gitkeep and README.md inside golden-paths/pipelines/. Not critical — the tree is a visual aid, not a manifest — but the "62 files" claim fails the moment someone actually counts them against the tree as shown.
S8. workflows/sdlc.md entry criteria for Phase 1 (Planning) says "Design review complete (if UI involved)" but Phase 2 is "Technical Design". Minor sequencing nit: either swap the phase order or rename Phase 2 to clarify it's the technical design (distinct from UI design that happens earlier in product discovery).
Answers to Your Specific Questions
-
Generalisation completeness across the WHOLE PR — FAIL for the site, PASS for everything else. Three leaks:
LICENSEcopyright holder (B1), FlatMate in the/decidedemo (B2), curios-dog in the ticket-manager demo (B2). Everything under.claude/,golden-paths/,roles/,workflows/,templates/,onboarding.yaml,README.md,CLAUDE.md,docs/getting-started.mdis clean. -
Hook scripts correctness — PASS. All six scripts parse on Linux:
#!/bin/bash,grep -qE(no-P), POSIX classes,git branch --show-current(Git ≥ 2.22, well supported on all CI runners). One runtime caveat:jqis an undeclared dependency (see S4). -
settings.json schema — PASS. The
PreToolUse→matcher: Bash→ array ofhookswithtype: "command"+if: "Bash(...)"structure mirrors the in-production setup at/Users/ahmed/Projects/apexscript-org/.claude/settings.jsonwhich is proven to work. Additionally, every hook script self-gates on its own command pattern viagrep, so even if theif:matcher shape changed in a future release, the hooks would still only fire for their intended commands. Defense in depth — well done. -
GitHub Issues migration — PASS.
.claude/agents/ticket-manager.md:10-12leads with "GitHub Issues in the project's own GitHub repo".- The
gh issue createexample at lines 35-52 is correct syntax (heredoc body,--repo,--title,--body,--label). - The "Note for Teams Using a Different Tracker" section (lines 155-162) is the only place that mentions Linear/Jira as alternatives.
- Hook regex in
validate-branch-name.sh:30accepts([A-Z]+-[0-9]+|GH-[0-9]+|#[0-9]+)— Linear/Jira teams are supported. validate-pr-create.sh:32and line 47 also accept[A-Z]+-[0-9]+. Opt-in works.- (Caveat: the CI-side
pr-title-check.ymlregex disagrees — see B6.)
-
Landing site quality:
- Install command
git clone https://github.com/me2resh/apexstack.git ~/.apexstack— works (repo exists). - File tree — mostly accurate, but the header count "62 files · 5,480 LOC" is wrong (see B4). The tree body itself is faithful — I cross-checked every line against
find. - Metric numbers — 19 roles ✓, 17 ✗ (should be 22 — see B3), 7 pipelines ✓, 4 templates ✓.
- Dark theme — works via
@media (prefers-color-scheme: dark)at line 48, overrides all CSS variables. - Page load — no missing scripts; one external dep (Google Fonts) with
preconnecthints, graceful fallback toui-monospace, 'SF Mono', Menlo, Consolas, monospace. JS inlined at the bottom. - Terminal demos — mostly plausible, but two leak real project names (see B2). The filename
src/handlers/api/get-trending-questions.tsat line 1152 is invented-looking enough to read as an example, not a leak. - Accessibility — good: skip link (858),
<main id="main">(884), semantic landmarks,aria-labelon primary nav and metrics,:focus-visible(90),prefers-reduced-motion: reduce(834) disables both.riseanimations and the hero cursor blink. One broken in-page anchor (S1).
- Install command
-
No hidden Linear references in
site/index.html— PASS. Grepped forlinear(case-insensitive): no matches. Nolinear-gradientin the CSS either — matches your design intent. -
README.md and CLAUDE.md updates — PASS. Both were updated in
8fa3c92to describe the.claude/andgolden-paths/pipelines/layers. README.md uses the same directory tree format; CLAUDE.md adds the "CLAUDE CODE INTEGRATION" and "CI/CD PIPELINES" sections at the bottom pointing at the right paths. Consistent with what shipped. -
Glossary in PR body — PASS. 12 entries, substantive and accurate. Note: the glossary line for the identities mentions "Sentinel" and "Scout" — you'll want to update this if you take option (a) for B5.
-
Hook executability — PASS.
git ls-files --stage .claude/hooks/shows all six.shfiles with mode100755. Git is tracking the +x bit correctly. -
AgDR for the initial release — Acceptable, not a blocker. For a v0.1 initial-release PR where the rationale is spread across commit messages, README, and the site's own "Opinionated by design" section, I wouldn't block. The Code Reviewer agent's standing rule is "detect decisions in the code" — for a single commit that decides everything at once by bootstrapping a repo, the AgDR-per-decision model doesn't fit cleanly. Recommend opening a follow-up issue to retroactively author three AgDRs: (a) "Hooks vs MCP for policy enforcement", (b) "Which 5 agents for v0.1 (and why not security scanner / test writer / release manager)", (c) "Landing site aesthetic — terminal-native brutalism vs generic dev-tools SaaS". Cheap insurance against future "why did we pick this" questions.
-
No CI on apexstack itself — Acceptable for v0.1, but start dogfooding soon. For v0.1 with only content files this is defensible — no TypeScript to typecheck, no tests to run. But: (a) you could copy
pr-title-check.ymlinto.github/workflows/right now — it's content-agnostic, (b) a markdown linter (markdownlint-cli2) and a shell linter (shellcheck on.claude/hooks/*.sh) would catch exactly the kind of drift this PR surfaced, (c) a link-check pipeline onsite/index.htmlwould have caught the dead#stackanchor. Recommend opening an issue for v0.2: "dogfood: add.github/workflows/to apexstack itself" listing pr-title-check, markdownlint, shellcheck, lychee.
Verdict
CHANGES REQUESTED
Blocking items are B1 (LICENSE), B2 (site leaks — FlatMate + curios-dog), B3 (hero metric 17 → 22), B4 (tree header file count and LOC), B5 (Sentinel/Scout agent naming drift), and B6 (hook/pipeline regex disagreement). Every blocker is a small, localised fix — I'd estimate 20 minutes of work. Once those are in, this is a strong initial release.
Per instructions I am not approving here — relay the verdict to the CEO for final sign-off.
🤖 Reviewed by Rex (Code Reviewer Agent)
📌 Reviewed commit: 5847ceef77e6a9319c8a9a479681f0a3707e2086
…ixes
Three changes bundled:
1. Multi-project workspace mode — and it's the DEFAULT
- ApexStack now supports two modes set in onboarding.yaml under
`apexstack.mode`. Multi-project is the default. Single-project
is opt-in for the simple "I have one repo" case.
- Multi-project mode: an ops repo holds the registry
(apexstack.projects.yaml), per-project docs (projects/<name>/),
and optional live working copies (workspace/<name>/). Skills
aggregate across the registry.
- New files: apexstack.projects.yaml.example, workspace/README.md,
projects/README.md, docs/multi-project.md (full guide).
- Rationale: most engineering orgs have multiple repos. The single
-project case is the exception, not the rule. Defaulting to
multi-project matches what the typical adopter actually needs.
2. 8 new skills bringing the total to 13
- /idea — capture a new product idea to the backlog
- /handover — onboard an external repo with a structured assessment
- /projects — list managed projects with status
- /inbox — items needing your attention across the portfolio
- /status — git + CI snapshot per project
- /tasks — actionable task list with direct URLs, scored
- /roadmap — update or create the product roadmap
- /stakeholder-update — generate weekly/monthly/launch updates
- All 8 are mode-aware: aggregate across the registry in
multi-project mode (default), scope to current repo in
single-project mode.
3. Rex's 6 PR review blockers from commit 5847cee fixed
- B1: LICENSE copyright now "ApexStack contributors" (was the
wrong legal entity)
- B2: site/index.html demo blocks no longer leak FlatMate or
curios-dog refs — replaced with your-org/your-project
- B3: hero metric 17 → 30 (correct count of hooks 6 + rules 6 +
agents 5 + skills 13 once the new skills are added). Also
adjusted to highlight "operating modes" as the 4th metric.
- B4: file tree updated to reflect the actual structure including
new workspace/, projects/, the 8 new skills, and the multi-
project docs. Header updated to reflect new file count.
- B5: Sentinel renamed → Shield throughout pipelines + site (the
actual security agent file is security-reviewer.md / Shield).
Scout brand removed from seo-check.yml — SEO pipeline is now
just "SEO Check" with no agent identity (no scout.md exists).
- B6: hook regex consistency — both validate-branch-name.sh and
validate-pr-create.sh now use [A-Z]{2,10}-[0-9]+ matching the
pr-title-check.yml CI workflow. PROJECT-123 now passes both
hooks AND CI.
Plus content updates:
- Layers section in site grew from 4 to 5 layers (added the
workspace / multi-project layer as a full-width 5th card).
- Install section in site rewritten to lead with multi-project
setup (6 steps to set up the portfolio + a final opt-out step
for single-project).
- CLAUDE.md gained an "Operating Mode" section + a full 13-skill
table + multi-project paths in Quick Reference.
- README.md Quick Start rewritten to lead with multi-project
install, with single-project as an opt-out callout at the end.
Note: .claude/settings.local.json was created in the worktree
during this session but is intentionally NOT committed (per the
rule against committing local user settings).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
atlas-apex
left a comment
There was a problem hiding this comment.
Re-review (1/4)
ba892bc bundles three independent concerns under one SHA:
- Rex blocker fixes (B1 to B6)
- Multi-project mode as the default
- Eight new skills (
/handover,/idea,/inbox,/projects,/roadmap,/stakeholder-update,/status,/tasks)
The blocker fixes are largely good. Multi-project plus new skills are mostly good but introduce a fresh cluster of documentation contradictions about which mode is the default.
atlas-apex
left a comment
There was a problem hiding this comment.
Re-review (2/6): scope violation
ba892bc violates the "1 ticket = 1 PR" rule. Three concerns in one commit. Per .claude/rules/workflow-gates.md (One Ticket at a Time), each should have been its own commit/PR.
Recommendation: accept this time because the work is coherent with PR 1 initial-release framing, but call it out so the next round respects the rule. The 8 new skills alone is significant enough to warrant its own PR going forward.
atlas-apex
left a comment
There was a problem hiding this comment.
Re-review (3/6): previous blockers B1 to B6
- B1 LICENSE copyright: FIXED.
LICENSE:3now readsCopyright (c) 2026 ApexStack contributors. - B2 site leaks: FIXED. grep across
site/returns nothing for FlatMate / curios / movetwo. Only remainingme2resh/apexstackreferences are the actual repo URL. - B3 hero metric 17 for hooks/rules/agents/skills: FIXED. Now 30 at
site/index.html:923. Verified: 6 hooks + 6 rules + 5 agents + 13 skills = 30. - B4 tree header 62 files / 5,480 LOC: PARTIAL. LOC removed (good), but file count is wrong:
site/index.html:956claims 79 files, actualgit ls-filescount is 76. Off by 3. - B5 Sentinel/Scout drift: FIXED in user-facing surfaces. ONE stale comment remains:
golden-paths/pipelines/ci.yml:93still has# SENTINEL Security Scanningwhile the actual job uses Shield. NIT. - B6 ticket regex disagreement: FIXED.
validate-branch-name.sh:32,validate-pr-create.sh:34,49, andpr-title-check.yml:37now use[A-Z]{2,10}-[0-9]+(or\dequivalent), with explicit alignment comments.
Score: 5 fixed, 1 partial. B4 still wrong.
atlas-apex
left a comment
There was a problem hiding this comment.
Re-review (4/8): NEW blocker N1 default-mode contradiction (CRITICAL)
onboarding.yaml, CLAUDE.md, README.md, docs/multi-project.md, and site/index.html all say multi-project is the default. But these NEW files contradict that:
apexstack.projects.yaml.example:21-23says "Single-project mode is the default ... if this file does not exist, ApexStack treats the current directory as a single managed project"workspace/README.md:3,11,15says "in single-project mode (the default) you can ignore it" andmode: single-project # defaultprojects/README.md:9says "In single-project mode (the default), this directory is optional".claude/skills/projects/SKILL.md:29says "mode: single-project(or missing this is the default)"
A user landing on any of those four files will be told the opposite of what onboarding.yaml, the README, and the site claim. This is the most user-facing aspect of the multi-project work and has to be internally consistent before merge. Fix all four to say multi-project is the default.
* 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 th…
…ject feat(#1): initial ApexStack project
… SENTINEL nit (me2resh#3) Post-merge cleanup after the Rex re-review of PR #1. Four blockers and one nit — all small, all correctness fixes, no scope creep. N1 — Default-mode contradiction (CRITICAL) Four files claimed single-project was the default, contradicting CLAUDE.md, README, onboarding.yaml, docs/multi-project.md, the site, and 7 of 8 new skills (which all say multi-project is the default). The commit message for ba892bc literally said "multi-project mode (default)". These four were the stragglers; they now align: - apexstack.projects.yaml.example: header comment rewritten to say this file is the registry for the default multi-project mode, single-project is the opt-in - workspace/README.md: section order swapped (multi-project first as the default, single-project second as opt-in), intro paragraph reworded, mode comments in the onboarding.yaml snippet flipped - projects/README.md: opening paragraph flipped so multi-project is described as the default - .claude/skills/projects/SKILL.md: mode detection table row order flipped (multi-project "or missing" is now default), sections swapped so multi-project comes first as the default, single-project moved below as opt-in, the "how to flip back" prompt reworded N2 — Roles count - README.md:166 — "20 software development roles" → "19" (actual count: 7 Engineering + 3 Product + 3 Design + 3 Security + 3 Data = 19) N3 — Site file count - site/index.html:956 — dropped the "79 files · " prefix from the tree header line to kill the drift-prone number entirely. The descriptive tagline "the runnable + portfolio layers added in v0.1" stays. Any time a file is added to the repo, the header no longer lies. Stale SENTINEL comment - golden-paths/pipelines/ci.yml:93 — the security section comment said "SENTINEL — Security Scanning" but the job itself is named "Shield: Security" (the Sentinel→Shield rename from PR #1). Fixed the comment to match. What's NOT in this commit - N4 (PR body "5 skills" + Sentinel/Scout in glossary) — GitHub PR bodies are immutable after merge, so this is recorded in the tracking issue as NOT FIXABLE. - Retroactive AgDRs — intentionally skipped per the CEO's call (v0.1 doesn't need them backfilled). Verification grep -n "20 software development roles" README.md → no hits grep -n "79 files" site/index.html → no hits grep -n "SENTINEL" golden-paths/pipelines/ci.yml → no hits grep for "single-project is the default" in the 4 N1 files → no hits Closes me2resh/apexscript-org#99 Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…UDE.md (me2resh#7) Closes the biggest gap the PR #1 Rex audit flagged: all 19 role files exist and are well-specified, but nothing in the running system references them. CLAUDE.md listed them in a table but didn't @import anything. No workflow named a role file. No skill invoked a specific role. Users had to manually say "please read roles/engineering/qa-engineer.md and act as the QA Engineer" for anything to happen. Roles were passive reference docs. This PR makes them first-class participants. New file — .claude/rules/role-triggers.md (~107 lines) - Activation table for all 19 roles (department, file path, when to activate with 2-4 concrete conditions per role) - Activation protocol (read file → adopt identity → follow handoff rules → stay in role until task completes) - Auto-activation signal table (ticket label, PR path match, incident, PRD draft, etc. → which role activates) - Prompted activation examples ("act as the QA Engineer for me2resh#42") - Role boundary enforcement (CAN/CANNOT from each role file is strict — hand off when you hit a CANNOT) - Handoff artefact table (PRD, tech design, testable build, security findings, AC sign-off, etc.) — the contracts between roles - Explicit "aspirational → real" closing note explaining what this file makes concrete CLAUDE.md - ROLES section gains an "Activation" subsection explaining the model in three short paragraphs - New @.claude/rules/role-triggers.md import at the end of the subsection — the trigger table is loaded into every session so Claude always knows which role to activate on which signal workflows/sdlc.md — each phase gets a "Primary role" header - Phase 1 Planning → Tech Lead (+ Product Manager, Head of Eng on escalation) - Phase 2 Tech Design → Tech Lead (+ Head of Eng, UX/UI Designer) - Phase 3 Build → Backend / Frontend Engineer (+ Tech Lead coordinator) - Phase 4 Code Review → Tech Lead + Rex (+ Security Auditor on auth/crypto diff, UI Designer on UI diff) - Phase 5 QA → QA Engineer (MANDATORY — merged code is never Done without QA sign-off; expands the existing "QA gate" warning) - Phase 6 Deploy → Platform Engineer (+ SRE for runbook/rollback) - Phase 7 Monitor → SRE (+ Head of Eng escalation) - "Roles Summary" table at the bottom rewritten with every role as a markdown link to its file workflows/code-review.md — Roles table expanded - Author column now names the actual Backend/Frontend Engineer roles - Automated reviewer named explicitly (Rex agent) - Human approval gate = Tech Lead with a link to the role file - Conditional Security Auditor and UI Designer rows added with their trigger conditions - QA Engineer called out as "not a reviewer" to kill the common misconception that QA approves merges workflows/deployment.md — new Roles table at the top - Maps each deployment stage (CI/CD maintenance, staging, prod gate, incident response, post-deploy monitoring, security gate) to its activating role and trigger condition - Head of Eng sign-off noted for risky production promotions Skills — 5 files get an "Activated role" section between the intro and the process - /decide → Tech Lead (+ Head of Eng for arch-review threshold, Security Auditor if decision touches auth/secrets) - /write-spec → Product Manager (+ Head of Product escalation, UX/UI Designer for design-heavy features; hands off to Tech Lead at Tech Design phase) - /code-review → Rex + Tech Lead (+ conditional Security Auditor, conditional UI Designer) — the richest of the five because code review has the most conditional branches - /security-review → Shield + Security Auditor (+ Head of Security for strategic calls, Penetration Tester for active testing) - /roadmap → Head of Product (+ Product Analyst for data-driven reprioritisation) Not in scope for this PR - The 19 role files themselves are untouched — their content was already good per the audit - Skills /inbox, /status, /tasks, /projects, /idea, /handover, /audit-deps, /stakeholder-update don't get role activation sections because they're portfolio / CEO-facing or the role is implicit from the task context - No new roles added - The .claude/rules/role-triggers.md file is NOT imported from every workflow/skill — only CLAUDE.md imports it once, and individual files reference it relatively when they need to cite the activation protocol. Keeps the context footprint lean. Token budget - role-triggers.md is ~107 lines (~1.3k tokens). Loaded once per session via the CLAUDE.md @import. The 19 full role files are NOT pre-loaded — they're read on demand when their trigger fires (saves ~22k tokens that would otherwise sit idle). Multi-project neutrality - grep for flat-mate/curios/sharppick/movetwo/yumyum across all 10 changed files → 0 hits - All file paths use `roles/{department}/{role}.md` and `.claude/rules/role-triggers.md` — no org-specific references Refs me2resh/apexscript-org#100 Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ion skill + migration AgDR template (me2resh#72) * feat(me2resh#59): ticket-first gate for database migrations + /migration skill + migration AgDR template Ships Phase 1 of the migration-gate feature per the issue's own rollout plan. Phase 2 (sam/terraform plan introspection) is deferred — path-pattern heuristics cover 80% of the real cases and the remaining 20% can be handled by per-project `migration_paths` overrides. What ships: 1. `.claude/hooks/require-migration-ticket.sh` — new PreToolUse hook on Edit/Write/MultiEdit. If the target path matches a migration pattern, runs three gates: G1: active ticket marker exists (same me2resh#41 two-tier resolution — per-project preferred, ops-level fallback) G2: the referenced GitHub issue is OPEN and carries the `migration` label (default, overridable via `.claude/project-config.json` → `migration_label`) G3: the issue body contains a reference to a migration AgDR matching `docs/agdr/AgDR-\d+-.*migration.*\.md` Non-migration paths exit silently; the normal require-active-ticket hook then applies. Exempts .claude/, docs/, projects/*/docs/, *.md, and *.example up front. 2. `.claude/skills/migration/SKILL.md` — guided flow that produces both artefacts in one pass. Asks for migration type, affected tables, rollback plan (required non-empty), downtime estimate, cross-service consumers, data volume, testing plan, and observability. Writes the AgDR first (reversible local write), then creates the labelled GitHub issue, then back-fills the AgDR with the issue reference. Tells the user to run /start-ticket as the next step — doesn't do it automatically so the handoff stays explicit. 3. `templates/agdr-migration.md` — AgDR template with migration- specific sections: Rollback Plan (explicit, not buried under Consequences), Cross-Service Consumers, Testing Plan, and Observability. Built on the same shape as the base AgDR template (Context / Options Considered / Decision) so existing AgDR discipline carries over. Wiring: - `.claude/settings.json`: require-migration-ticket.sh registered BEFORE require-active-ticket.sh in the PreToolUse chain so migration-specific blocking messages surface first. - `.claude/rules/workflow-gates.md`: new gate 3a row + dedicated "Migration Gate" section with default path list and enforcement note. - `.claude/hooks/README.md`: new section 1a documenting the hook's three gates. Hook #1 (require-active-ticket) description updated to include the me2resh#41 two-tier resolution. - `CLAUDE.md`: hook count 16 → 17, skill count 31 → 32, templates table gains Migration AgDR row, skills table gains /migration row. - `workflows/sdlc.md`: new "Sub-Workflow: Database Migrations" section after Phase 3 Exit Criteria, showing the flow from /migration → /start-ticket → edit → dev smoke → staging → prod → monitor. Smoke-tested the hook with 11 path-matching cases in an isolated /tmp mock ops root (/tmp/smoke-59.sh). All pass — non-migration paths pass through, every migration-pattern path blocks when no marker exists. Gates 2 and 3 require live gh calls and are covered by real-usage flow (documented in the test output). Closes me2resh#59 * fix: shellcheck SC2221/SC2222/SC2254 + markdownlint MD032 on me2resh#59 - require-migration-ticket.sh: remove subsumed case patterns (*/projects/*/docs/* under */docs/*, */migrations/*/*.sql under */migrations/*.sql) and disable SC2254 on the intentional glob-expansion in the custom-paths case. - agdr-migration.md: add blank line before a bulleted list under 'Deploy-order constraint'. All 11 smoke cases still pass. --------- Co-authored-by: me2resh <ahmed.abdelaliem@gmail.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(#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 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…
`gh issue view` only resolves GitHub Issues, so the migration gate's label/body checks could never pass for a Jira-tracked migration ticket — it blocked every migration-file edit even with a labelled ticket and a migration AgDR present. When the active ticket key is Jira-style ([A-Z]+-[0-9]+), skip the GitHub call and instead verify a migration AgDR referencing the ticket exists locally under docs/agdr/ or the portfolio projects_dir. GitHub- tracked tickets fall through to the existing checks unchanged. ITH-94
* 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…
…169) Closes #168. The /release skill prescribed `release/vA.B.C` as the source-branch name and `release: vA.B.C` as the PR title for the dev → main release PR (per AgDR-0007). Both were rejected by the framework's own validators: validate-branch-name.sh required {type}/{TICKET-ID}-{description}; release/v1.2.0 has no ticket-id portion. validate-pr-create.sh required type(SCOPE): form with `release` not in pr.title_type_whitelist. The contradiction surfaced cutting v1.2.0 — the first release under the dev/main model. Three small changes: 1. .claude/hooks/validate-branch-name.sh — added an early-out branch that accepts ^release/vN.N.N(-rcN)?$ as a valid name. Narrow, intentional exception for the framework's release-cut convention; release branches don't carry a ticket-id because the release itself IS the ticket. 2. .claude/project-config.defaults.json — added "release" to pr.title_type_whitelist so a title like `release(#160): v1.2.0` passes validate-pr-create.sh's existing regex unchanged. 3. .claude/skills/release/SKILL.md step 4 — corrected the prescribed PR title to `release(#<release-ticket>): vA.B.C` so future /release invocations produce a title that satisfies the validators by construction. Tested: bash .claude/hooks/validate-branch-name.sh against: release/v1.2.0 → 0 (allowed, release-special-case) release/v1.2.0-rc1 → 0 (allowed, RC variant) release/v9.9.9 → 0 (allowed) release/foo → 2 (correctly blocked) release/v1 → 2 (correctly blocked) chore/GH-168-fix → 0 (allowed, standard pattern) feature/GH-1-x → 0 (allowed, standard pattern) Full hook test suite: 196/196 cases green across 12 test files. Refs: surfaced 2026-05-04 cutting the first release under AgDR-0007. Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…d check (#171) Closes #170. Completes the work started in #169 (closing #168). #169 added a release-pattern early-out to validate-branch-name.sh so release/vN.N.N branches pass the branch-name validator. But validate-pr-create.sh has its own independent branch-id check at line 273 that #169 didn't touch — and it still rejects release/v1.2.0 because that name doesn't contain a ticket-id substring. This is the same class of contradiction #168 fixed; the fix is the same shape. Add the same release-pattern early-out to the branch-id check in validate-pr-create.sh: - if the branch matches ^release/vN.N.N(-rcN)?$ → exempt (release branches don't carry ticket-ids; the release itself is the ticket) - otherwise → require a ticket-id substring as before #168's acceptance criterion 3 ("validate-pr-create.sh accepts a PR title `release(#160): v1.2.0` against the `release/v1.2.0` branch") was checked off based on the title regex alone but didn't catch the secondary branch-id check living in the same file. Surfaced trying to open the v1.2.0 release PR. Tested: bash .claude/hooks/validate-pr-create.sh against: release/v1.2.0 → 0 (allowed, exempt) release/v1.2.0-rc1 → 0 (allowed, RC variant) chore/GH-1-fix → 0 (allowed, has ticket-id) release/foo → 2 (correctly blocked) chore/no-ticket → 2 (correctly blocked) Refs #168 (the parent bug) + #169 (the partial fix). Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 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 instea…
…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…
* 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 th…
* chore(#109): project-configurable ticket / branch / commit / PR schema (#118)
* chore(#109): project-configurable ticket / branch / commit / PR schema
Lift the prefix / type whitelists hardcoded across skills, hooks, and
CI into a versioned JSON config read through a shared shell library.
Shipped defaults at .claude/project-config.defaults.json; per-fork
overrides at the optional .claude/project-config.json; one reader
(_lib-read-config.sh) that every consumer now uses.
Added:
- .claude/project-config.defaults.json (v1 schema)
- .claude/hooks/_lib-read-config.sh (shared reader)
- docs/project-config.md (schema reference + extension guide)
- docs/agdr/AgDR-0006-project-configurable-ticket-schema.md
Migrated (still pass with no config present via last-resort fallback):
- validate-branch-name.sh → .branch.type_whitelist
- validate-commit-format.sh → .commit.type_whitelist (legacy
`commit_types` top-level key honoured as backward-compat fallback)
- validate-pr-create.sh → .pr.title_type_whitelist
- /feature, /task, /bug skills reference the config in their Rules
sections; none hardcodes the list any more
Unlocks subsequent config-readers for #107 / #110 / #111 / #112 /
#113 / #114 / #115 — each extends the schema under its own subtree
without further changes to the loader.
https://github.com/me2resh/apexyard/issues/109
* fix(#109): satisfy markdownlint MD032 and MD060 on new docs
Auto-fix MD032 (blank lines around lists) in AgDR-0006 and format
table-separator rows with surrounding spaces (MD060) in both new
doc files. Content unchanged; CI green.
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#110): add block-private-refs-in-public-repos.sh hook (#119)
Adds a new PreToolUse hook that blocks gh issue/PR/comment creation and
gh api .../issues|/pulls calls targeting a public framework repo
(default: me2resh/apexyard + whatever `upstream` resolves to) when the
title or body references any registered private project from
apexyard.projects.yaml (by name, repo slug, owner/repo#N ticket ref, or
workspace path).
The hook is a sibling to check-secrets.sh — both scan outgoing content
for identifiers that should never leave the local environment. Skip
marker `<!-- private-refs: allow -->` in the body lets a deliberate
reference through with a visible warning.
Files touched:
- .claude/hooks/block-private-refs-in-public-repos.sh (new)
- .claude/hooks/tests/test_block_private_refs.sh (new)
- .claude/rules/leak-protection.md (new)
- .claude/settings.json (wire PreToolUse matchers for the 5 gh shapes)
- docs/rule-audit.md (append section 10 + bump counts)
Refs: https://github.com/me2resh/apexyard/issues/110
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#115): add warn-stale-review-markers.sh PostToolUse hook (#120)
Fires after `git push` to surface review markers that have gone stale
because new commits were pushed past an existing Rex / CEO / design
approval. The merge gate already catches this at `gh pr merge` time,
but only then -- this hook closes the gap by flagging it immediately
at push-time so the author isn't surprised at merge.
- `.claude/hooks/warn-stale-review-markers.sh`
- PostToolUse, non-blocking (PostToolUse exit 2 would push noise
into the conversation; this hook is purely informational).
- Resolves the PR HEAD via `gh pr view --json headRefOid` -- same
source-of-truth as the merge-gate hooks post-apexyard#47 / #55.
Falls back to local HEAD with a visible WARN when gh is offline.
- Silent on: no PR for branch, no markers, fresh markers,
failed push (detected via `rejected` / `failed to push` /
`fatal:` / `error:` markers in tool_response.stderr).
- Modes: `warn` (default) prints one stderr line per stale marker;
`delete` opts in to auto-removal via
`.claude/project-config.json` -> `review_markers.on_stale`.
TODO(apexyard#109): switch to the shared project-config reader
once it lands.
- `.claude/settings.json`
- Wires the hook on PostToolUse / Bash / `git push *`.
- `docs/rule-audit.md`
- Adds a row under section 3 (Code review & PR quality) and
bumps the mechanized count 26 -> 27 / total 73 -> 74.
- `.claude/hooks/tests/test_warn_stale_review_markers.sh`
- 8 cases: no PR, no markers, fresh markers, stale rex / ceo /
design (warn), delete mode, failed push. All pass locally.
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#111): upgrade pre-push-gate from advisory reminder to blocking check-runner (#121)
* chore(#111): upgrade pre-push-gate from reminder to blocking check-runner
Previously pre-push-gate.sh just printed a checklist of things to run
locally before pushing — it was advisory. The rule it enforces is a
HARD STOP per pr-workflow.md. That asymmetry meant agents routinely
pushed broken work and discovered it only when CI went red.
Replaces the reminder with a blocking runner that reads the list of
shell commands from project config (.pre_push.commands) and executes
them in sequence before a push is allowed through. First non-zero
exit blocks the push with exit 2 and prints the failing command plus
the last 20 lines of its output.
- Config key: .pre_push.commands[] — array of {name, run} objects.
Shipped default is an empty list (hook stays a no-op on repos that
haven't configured their checks yet, including the framework repo
itself until it wires its own CI).
- Emergency bypass: '<!-- pre-push: skip -->' in the HEAD commit
message. Grep-able on purpose so bypasses stay auditable.
- Fail-fast: once a command fails, the rest don't run. Parallel
execution is a follow-up polish.
- 7 test cases in .claude/hooks/tests/test_pre_push_gate.sh — all
pass on the shipped default + a minimal custom config.
Updates docs/rule-audit.md to flip "partial" → "yes" for the
"before git push" rule.
Integrates with the shared config reader landed in #109.
https://github.com/me2resh/apexyard/issues/111
* fix(#111): remove orphaned footnote reference from rule-audit
The previous advisory-mode footnote was superseded by pre-push-111
but its definition was accidentally kept, tripping markdownlint MD053
(unused reference definition). Drop it.
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#107): add validate-issue-structure.sh PreToolUse hook (#122)
Mechanically enforces the ticket body schema when an agent files raw
`gh issue create` calls instead of going through the interactive
/feature, /task, /bug skills. Matches bracketed title prefix
([Feature] / [Chore] / [Bug] / [Docs] / etc.) against
`.ticket.required_sections` in project-config, and blocks (exit 2)
when any required section is missing or empty. Skip marker
`<!-- validate-issue-structure: skip -->` bypasses with a visible
stderr WARN for legitimate off-template tickets (epics, meta-threads).
Changes:
- .claude/hooks/validate-issue-structure.sh — the hook; reads schema
via the shared _lib-read-config.sh, with inlined defaults for bare
checkouts predating the config-schema rollout. Handles
--body / --body-file / -F path.
- .claude/project-config.defaults.json — extends .ticket with
required_sections (Feature/Chore/Refactor/Testing/CI/Docs/Bug) and
skip_marker; other .ticket fields untouched.
- .claude/settings.json — new PreToolUse matcher on Bash(gh issue
create *) alongside the existing suggest-ticket-template.sh and
block-private-refs-in-public-repos.sh hooks.
- .claude/hooks/tests/test_validate_issue_structure.sh — 15 cases
covering pass + fail paths per prefix, empty section detection,
skip marker, unknown prefix, non-gh invocation, --body-file path.
- docs/rule-audit.md — new section 11 row, mechanized count +1.
Upstream ticket: https://github.com/me2resh/apexyard/issues/107
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#112): add require-agdr-for-arch-pr.sh PreToolUse hook (#123)
Closes the asymmetry noted in .claude/rules/agdr-decisions.md: every
other HARD STOP in the ruleset (merge approval, ticket-first,
migration-first) is mechanically enforced, but the /decide HARD STOP
was prose-only. The commit-time hook require-agdr-for-arch-changes.sh
catches one architectural change at commit; this new PR-time hook
catches the cumulative diff so reviewers always have a pointer to the
decision record.
- New hook at .claude/hooks/require-agdr-for-arch-pr.sh
- Fires on Bash(gh pr create *)
- Parses --title/--body/--body-file/-F <path>
- Resolves base branch from --base, else upstream/dev, origin/dev,
upstream/main, origin/main, main, master (in that order)
- Computes `git diff <merge-base>..HEAD --name-only`
- Triggers on any changed file matching .agdr_trigger_paths[], OR any
dep-file addition (package.json via jq key-set diff; other
dep files via a commented +/- line-count heuristic — version
bumps match +/- counts and do not fire)
- Blocks (exit 2) with a helpful message naming the triggers and
pointing at /decide if the body has no `AgDR-\d+-[a-z0-9-]+`
reference
- Skip marker `<!-- agdr: not-applicable -->` bypasses with a
visible WARN on stderr
- Silent exit 0 on non-gh commands, empty diffs, unresolvable base
- Wired via .claude/settings.json PreToolUse Bash(gh pr create *)
- Adds two new top-level keys to .claude/project-config.defaults.json:
agdr_trigger_paths (shell globs — domain/, infrastructure/,
migrations/, *.tf, .github/workflows/, etc.)
agdr_trigger_dep_files (literal basenames — package.json,
pyproject.toml, Cargo.toml, go.mod, Gemfile)
Hook has inline fallback defaults kept in sync.
- Adds docs/rule-audit.md entry in the AgDR section; bumps mechanized
count 26 to 27 and total rows 73 to 74.
- Adds .claude/hooks/tests/test_require_agdr_for_arch_pr.sh (7 cases;
all green): path-triggered without AgDR (block), with AgDR (pass),
dep-file added (block), version-only bump (no fire), skip marker
(pass + warn), non-matching diff (pass), non-gh command (no-op).
Closes https://github.com/me2resh/apexyard/issues/112
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#113): require Testing section in PR body (config-driven) (#124)
Extends validate-pr-create.sh with a required-sections check that
replaces the hardcoded Glossary-only grep. The list of required H2
headings is project-configurable via `.pr.required_sections[]`.
Shipped default is ["Testing", "Glossary"], matching the canonical PR
description shape in workflows/code-review.md.
- Each entry must appear as `## <Name>` (case-insensitive).
- Empty sections are tolerated at this layer (the issue-structure hook
#107 does stricter empty-content checks for issue bodies; for PR
bodies, empty sections are left to the reviewer's judgement).
- Skip marker `<!-- pr-sections: skip -->` bypasses with a visible
stderr WARN — for trivial PRs (lint-only fixes, version bumps)
where the full template is overkill.
- Reads from project config via the shared _lib-read-config.sh (#109).
Inline fallback matches shipped defaults so bare checkouts predating
#109 keep working.
- 8 test cases cover: all-sections pass, each missing section,
missing-both (both errors printed), skip marker, case-insensitive
headings, H3 rejection.
https://github.com/me2resh/apexyard/issues/113
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#114): enforce single Closes-keyword per PR body (#125)
* chore(#114): enforce single Closes-keyword per PR body
Caps distinct auto-closing references (close/closes/closed, fix/fixes/
fixed, resolve/resolves/resolved + N or owner/repo+N) at one per PR
body. Closes the loophole where the title validator limited the title
to one ticket but multiple Closes lines in the body would still auto-
close all of them on merge.
- Scans stripped of fenced code blocks so closing keywords inside a
code sample do not count.
- Distinct counting: the same number referenced twice (e.g. via Fixes
and Closes) counts as one.
- Cross-repo refs (owner/repo+N) count normally.
- Opt-in escape hatch: pr.allow_multiple_closes=true in
project-config disables the check for teams that deliberately batch
rollbacks or dependency bumps.
- Per-PR bypass: a multi-close-approved HTML comment in the body
prints a visible stderr WARN and lets that PR through. Grep-able
trace so bypasses are auditable.
- 10 test cases cover: one close passes, no-keyword passes, two
distinct block, three mixed block, same-number-twice passes, code-
fence-ignored, skip marker, cross-ref without keyword, opt-in
config, cross-repo close.
Reads configuration via the shared _lib-read-config.sh (apexyard+109).
https://github.com/me2resh/apexyard/issues/114
* fix(#114): strip inline backticks and tilde fences from close-count scan
Rex caught a self-reflexive bug in the initial commit: documentation
mentioning closing keywords inside inline backticks (say a PR body
that explains the new hook with examples) counted as real closes, and
a skip marker inside inline backticks silently bypassed the check.
Future PRs that document the feature would trip the same trap.
Fix the code-region stripper to cover:
- Triple-backtick fences (already handled)
- Tilde fences (new)
- Inline-backtick spans (new)
Also run the skip-marker check against the stripped body, so a marker
used purely as documentation no longer activates a real bypass.
Three new test cases pin the behaviour:
- closing keywords in inline backticks are ignored
- skip marker inside inline backticks does NOT bypass
- tilde fences also get stripped
13/13 tests pass.
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#108): add /tickets-batch skill for bulk-file flow (#127)
The fast happy path for filing 5–20 structured tickets in one intent
without dropping to raw `gh issue create` (non-conformant) or running
`/feature` 20 times serially (~100 turns of interview).
- .claude/skills/tickets-batch/SKILL.md — new skill spec. Asks
shared-context questions (priority, epic, area-labels, repo) ONCE
for the whole batch, then runs a ≤3-question micro-interview per
ticket (type, one-line purpose, optional clarification when the
inference is low-confidence). Confirms the full batch as a table,
then files each via specific `gh issue create` calls (never a
bulk JSON dump — the validator runs per-issue). Output conforms
to `.ticket.required_sections` by construction. Caps at 20
tickets per invocation.
- CLAUDE.md — added a row for /tickets-batch in the Available
Skills table; bumped the count references from 33 to 34.
Refs https://github.com/me2resh/apexyard/issues/108
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#117): add /fan-out skill + parallel-work rule doc (#128)
- Add `.claude/skills/fan-out/SKILL.md` — spawns N parallel Agent calls
in a single assistant message, with per-task agent type, worktree
isolation, and foreground/background mode. Caps at 5 concurrent
agents. Refuses fan-out when tasks share file write targets or have
sequential dependencies. Includes pre-spawn active-ticket safety
check and worktree merge-back flow that pauses on conflict.
- Add `.claude/rules/parallel-work.md` — trigger heuristic for when an
agent should proactively offer fan-out (>= 2 file-independent,
context-independent, individually substantial work items). Pairs
with the skill: rule says when, skill says how.
- Update `CLAUDE.md` — bump rules count to 9, skills count to 34, add
`/fan-out` row to the skills table.
Refs https://github.com/me2resh/apexyard/issues/117
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#116): adopt release-cut branch model (dev/main + tags) — framework only (#126)
* chore(#116): adopt release-cut branch model (dev/main + tags)
Formalises the dev/main split that already exists informally — dev is
the daily-work branch where every PR lands, main is release-only,
tagged with semver on each merge. Framework-only: managed projects
under apexyard governance stay trunk-based.
Added:
- AgDR-0007 — decision record (options table covers full git flow vs
trunk-only vs gitflow-lite; chose gitflow-lite)
- /release skill — diff dev against main, propose semver bump from
conventional commits, generate CHANGELOG, open release PR, tag
after merge
- docs/release-process.md — prose runbook for cutting a release
(manual fallback for the skill)
- .git.protected_branches in project-config.defaults.json
(main/master/dev/develop)
Modified:
- block-main-push.sh — now blocks direct pushes/commits to all
configured protected branches (was: hardcoded main/master). Reads
.git.protected_branches via the shared config reader (apexyard#109).
- CLAUDE.md — new section under Git Conventions explaining the
dev/main model + the framework-only scope. Skill table entry for
/release. Skills count bumped to 34.
- docs/multi-project.md — note that upstream/main is release-only
and the dev/main split is framework-only.
Non-consequences (per AgDR-0007):
- No release/* or hotfix/* branches. Hotfixes are normal patches
cut quickly. Revisit if multi-version maintenance becomes a need.
- No automatic on-merge issue closing for dev PRs. The release PR's
body aggregates all Closes references for the batch and triggers
auto-close en masse when it merges to main. Manual close in the
meantime.
- CI workflows trigger on pull_request regardless of base, so
dev-targeting PRs already get the full check matrix — no
workflow file edits needed.
https://github.com/me2resh/apexyard/issues/116
* fix(#116): satisfy markdownlint MD032 + MD060 in 116 docs
Auto-fix added blank lines around lists in AgDR-0007 (MD032) and
spaced the table separators in AgDR-0007 + multi-project.md (MD060).
Content unchanged.
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* fix(#106): CHANGELOG fallback in drift hook for squash-merged forks (#129)
* fix(#106): CHANGELOG fallback in drift hook for squash-merged forks
The v1.1.0 tag-reachability check (`git tag --merged main`) misfires on
forks that sync via GitHub's default squash-merge: the squash collapses
the upstream-tag commit into a synthetic SHA, the tag stops being
reachable, and the banner keeps firing forever.
Discovered live on the first real-world `/update` flow: ops fork
squash-merged the v1.1.0 sync PR, banner kept saying "v1.1.0 available"
even though the fork was content-caught-up.
This commit adds a CHANGELOG-content fallback that fires only when the
primary tag check fails. If the fork's main has a heading
`## [X.Y.Z]` matching the upstream tag's version, treat the release as
absorbed and stay silent. Tolerant grep (matches the apexyard CHANGELOG
format from v1.1.0 onward, with leading-`v` stripping for tag→heading
conversion).
The merge-commit and rebase paths are unchanged — primary tag check
still works for them, and the fallback never fires when it shouldn't.
Test coverage (5 cases):
- squash-merge fork caught up to v1.1.0 → silent (the fix)
- merge-commit fork caught up to v1.1.0 → silent (regression check)
- fork stopped at v1.0.0 → banner fires
- fork has its own newer tag → silent
- squash-merge but no CHANGELOG on fork → banner fires (no false silence)
Records the strategy update in docs/agdr/AgDR-0008-…md (extends
AgDR-0005's tag-based-drift design).
https://github.com/me2resh/apexyard/issues/106
* fix(#106): satisfy markdownlint MD032/MD060 + shellcheck SC2164
Rex flagged two CI-blocking issues on the original 106 commit:
- AgDR-0008 had three bulleted sub-lists in the Consequences section
without surrounding blank lines (MD032). Added blanks and padded the
one tight-pipe table separator (MD060).
- The 106 test fixture had five subshell `cd "$fk"` calls without
`|| exit 1` (SC2164). Added the guard to all five.
5/5 tests still pass after the fix. No semantic change to the hook
or the test logic.
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#130): add /validate-idea skill — lightweight pre-spec gate (#131)
A 10-minute, 5-question check that sits between /idea and /write-spec.
Designed for solo founders running ApexYard — not a heavyweight
methodology like event storming or Wardley mapping.
Five questions, asked one at a time:
1. Who is this specifically for?
2. What do they do today instead?
3. What's the smallest version that proves the value?
4. What would prove this is wrong? (kill criteria)
5. Build, buy, or rent?
Output: a one-page validation doc with a GREEN/YELLOW/RED verdict.
RED auto-updates the IDEA-NNN backlog row to WONTDO.
Integration:
/idea — adds an optional default-no "Validate now?" step after
capture (and after the optional GitHub Issue offer).
/handover — adds a conditional "this looks dormant, validate?"
step at the end of the integration plan, gated on the dormancy
heuristic (last commit > 90d AND zero open PRs AND no recent
issue activity). Healthy projects don't see the prompt.
CLAUDE.md skills count bumped to 35; new skills row added.
https://github.com/me2resh/apexyard/issues/130
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat: configurable voice prompts on assistant pause (AgDR-0009-voice-prompts-on-pause) (#135)
Stop hook that speaks the assistant's question aloud (Jarvis-style)
when it pauses for user input. Initial phase is macOS-only via `say`,
no voice input — user replies via keyboard.
Default OFF. Adopters opt in by overriding `voice_prompts.enabled` to
true in `.claude/project-config.json`.
Files:
- .claude/hooks/voice-prompt-on-pause.sh — Stop hook with config gate,
trigger heuristic (questions-only by default), markdown stripping,
sentence-boundary truncation, fire-and-forget say invocation
- .claude/hooks/tests/test_voice_prompt_on_pause.sh — 9 cases covering
disabled-default, enabled+question, enabled+statement, approved-pattern,
abc-menu, malformed-transcript, no-say-on-PATH, trigger-always,
markdown-stripping
- .claude/project-config.defaults.json — voice_prompts schema block
added (enabled, voice, max_chars, rate_wpm, trigger), default OFF
- .claude/settings.json — new Stop hook entry wired with the standard
ops-root resolver wrapper
- docs/agdr/AgDR-0009-voice-prompts-on-pause.md — design rationale,
options matrix (status quo / macOS say / cloud TTS / ML detection),
consequences, future phases
- docs/project-config.md — new "Voice prompts" section with override
examples and privacy notes
Test mode: hook respects VOICE_PROMPTS_SYNC=1 to run say synchronously
(test runners need this so assertions don't race against orphaned
background processes). Production invocations always run async.
Future phases (out of scope here, AgDR §"Future phases"):
- Phase 2: cross-platform TTS (Linux espeak, Windows SpeechSynthesizer)
- Phase 3: cloud TTS providers (OpenAI, ElevenLabs) — privacy AgDR-worthy
- Phase 4: voice input via Whisper-based STT
- Phase 5: per-message overrides
Refs: https://github.com/me2resh/apexyard/issues/134
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#141): add /debug skill — structured hypothesis-driven debugging (#142)
* feat(#141): add /debug skill — structured hypothesis-driven debugging
Adds a methodology skill that enforces five disciplines:
1. Capture the symptom precisely (exact URL, exact response, exact step)
2. Read the architecture before guessing (map every layer the request
touches, file by file)
3. Form a hypothesis ladder (3–5 candidates, each with an explicit
evidence test that confirms or refutes it)
4. Gather evidence first, fix second
5. Verify the fix against the original symptom evidence (re-run the
same `curl` / browser repro you used in step 4 — unit tests
verify code, not feature, correctness)
Stack appendices (Web, Desktop) carry stack-specific surface-evidence
requirements (step 1), architecture-surface maps (step 2), and
evidence-tests cookbooks (step 4). The methodology body stays portable
across stacks; appendices are where stack-specific knowledge accrues
over time.
Web appendix covers browser routing, framework configs (Next/Nuxt/Vite),
SPA-fallback layers, CDN, origin, the shared API client, backend
handlers, and auth providers. Desktop appendix covers Electron / Tauri
/ native-shell concerns: app entry points, IPC bridges, native modules,
auto-updater, sandbox / entitlements, code signing, crash reports.
Includes "When NOT to use" guidance so the methodology overhead doesn't
sandbag simple bugs (typos, off-by-ones, greenfield exploration).
Motivated by a real OAuth debug session in a managed project where
three sequential fixes chased adjacent symptoms because each was
hypothesis-then-fix without evidence in between. The skill is the
"never do that again" guardrail.
Closes #141
* fix(#141): scrub private project issue numbers from anti-pattern table
Rex review on PR #142 caught that line 155 of the skill's anti-pattern
table still named the originating PRs (#375, #377, #380) from the
private project where the methodology was first exercised. The PR body
and commit message were correctly abstracted earlier, but this in-file
reference slipped through — the leak-protection hook only scans gh
issue/pr writes, not staged file content, so mechanical enforcement
didn't catch it.
Replaced with "Three sequential PRs chasing the same symptom because
each was based on a different guess (no evidence test in between
cycles)" — same pedagogical value, zero attribution.
Refs #141
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* docs(#143): document split-portfolio mode + add /setup privacy gate (#144)
Adopters on GitHub Free with any private project hit a silent privacy bug
following today's docs: forking apexyard makes a public fork that you cannot
later flip to private (GitHub policy), and committing `apexyard.projects.yaml`
+ `projects/<name>/` to the fork publishes private project names + handover
findings on a public GitHub repo.
This PR documents the supported workaround — split-portfolio mode — and
adds an upfront privacy gate to the /setup skill so new adopters never
hit the trip-wire silently.
docs/multi-project.md:
- New "Two setup modes — pick the one that matches your privacy needs"
section before TL;DR, with a side-by-side table and the explicit
trip-wire callout.
- Existing TL;DR retitled "TL;DR — single-fork mode (default)" with a
one-line pointer to the split-portfolio section.
- New "Split-portfolio mode — public framework + private portfolio"
section between the existing setup steps and the directory-layout
section. Includes:
- The two-repo layout (~/ops/apexyard public + ~/ops/portfolio private)
- 7-step setup walkthrough with copy-pasteable commands
- Daily workflow + upstream sync notes (both unchanged)
- Trade-offs (two repos to maintain, two clones per machine, one
upstream-sync conflict path on `projects/README.md`)
- "Migrating from single-fork to split-portfolio" recovery flow with
the explicit warning that GitHub Issue / PR edit history survives a
force-push and must be redacted separately
.claude/skills/setup/SKILL.md:
- New Step 2a: privacy gate — asks "are any projects private?" before
proposing the config. Branches on the answer:
- All public → single-fork mode
- GitHub Pro / Team / Enterprise → single-fork mode (private
forks of public repos
are supported on those
plans)
- Any private + GitHub Free → split-portfolio mode
- New Step 2b: walks through the split-portfolio setup interactively
(private repo create, sibling clone, gitignore + symlink) when the
privacy gate triggers.
- Detection: `test -L apexyard.projects.yaml` short-circuits Step 2b
for adopters already in split mode.
- Explicit "do NOT auto-migrate" rule for adopters already in single-fork
mode with private names already pushed — that path is destructive
(force-push history rewrite + redact issue/PR bodies + delete backup
branch) and warrants a deliberate, eyes-open run, not a /setup side
effect.
Out of scope for this PR (tracked separately on #143):
- `portfolio:` config block in `onboarding.yaml` schema
- Skill audit + refactor to honour configured `registry` / `projects_dir`
/ `ideas_backlog` paths instead of hardcoded fork-relative paths
- `/split-portfolio` migration helper skill that automates the recovery
flow currently documented manually
This is the docs-and-setup-question minimum-viable starter — the
framework code refactor is mechanical and lands as a follow-up.
Refs #143
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#145): portfolio config + self-healing + /split-portfolio helper (#147)
* feat(me2resh/apexyard#145): portfolio config block + self-healing + /split-portfolio helper
Closes the framework primitive deferred from me2resh/apexyard#144. Adds
first-class config-driven path resolution for the portfolio registry,
projects dir, and ideas backlog, with self-healing surfacing of broken
config at session start, plus a new /split-portfolio skill that automates
the destructive recovery flow.
Schema, helper, and hook:
- .claude/project-config.defaults.json: new portfolio: block
(registry, projects_dir, ideas_backlog) with defaults matching
today's single-fork layout
- .claude/hooks/_lib-portfolio-paths.sh: new sourceable helper exposing
portfolio_registry, portfolio_projects_dir, portfolio_ideas_backlog,
portfolio_validate, portfolio_clear_cache. Resolves relative paths
against the ops-fork root.
- .claude/hooks/check-portfolio-config.sh: new SessionStart hook —
silent on OK, one-line banner on broken config, never blocks session
- .claude/hooks/tests/test_portfolio_paths.sh: 13 cases covering
defaults, absolute/relative overrides, validate states, cache clear
Skill audit (18 SKILL.md files):
- Adds Path resolution callout pointing at the helper
- handover bash blocks now source helper and use $(portfolio_registry)
instead of literal apexyard.projects.yaml
- setup Step 2b now writes the portfolio: config block (recommended)
and validates via portfolio_validate before declaring success;
symlink approach kept as legacy fallback
New skill (.claude/skills/split-portfolio/SKILL.md):
- 10-step migration with explicit operator-confirmation gates at each
destructive step (force-push, body redaction, branch deletion)
- --verify mode: read-only state report (mode, paths, validate, drift)
- --dry-run mode: prints commands without executing
- Pre-flight refusals: already-private fork, paid GitHub plan, dirty
working tree, already-migrated state
- Step 9 writes the portfolio: config block (not symlinks) — symlink
fallback documented for adopters on older framework versions
- Step 9 surfaces the GitHub timeline-API survival caveat verbatim
- Idempotent re-runs: detects partial-migration state and resumes
Docs (docs/multi-project.md):
- Layout section describes both modes (config-block recommended,
symlink legacy) with self-healing notes
- Setup steps split into config-block mode and legacy symlink mode
- Migration section now points at /split-portfolio skill; manual
recipe preserved as fallback
AgDR (docs/agdr/AgDR-0010-portfolio-config-and-self-healing.md):
- Full Y-statement, options, decision, consequences, future phases
- Schema decision rationale: project-config.json over onboarding.yaml
because runtime path resolution belongs in project-config
Closes me2resh/apexyard#145
Refs me2resh/apexyard#146 (delivered same PR; closed manually post-merge
per the single-Closes-keyword rule in validate-pr-create.sh)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(me2resh/apexyard#145): markdownlint MD031 — blank lines around fences
CI's markdownlint-cli2 (v0.34.0) flagged the JSON + bash fenced code
blocks I added in setup/SKILL.md Step 2b without surrounding blank
lines. Added the required blanks. No content change.
Refs me2resh/apexyard#147
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(me2resh/apexyard#148): correct privacy-gate wording — adopter action, not framework auto-publish (#149)
The privacy-gate wording introduced in PR #144 (and unchanged in PR #147)
attributed the publication to the framework rather than the adopter:
"the standard fork-and-commit setup will silently publish your private
project names on a public GitHub repo"
That's factually wrong. ApexYard never pushes anything without explicit
operator approval — the publication only happens when the adopter
themselves runs git push. The "silently publish" framing read as if the
framework auto-publishes, which is misleading and undermines trust in
the rest of the framework's safety claims.
Two prose-only edits, no code, no behavior change:
- .claude/skills/setup/SKILL.md Step 2a — replaced "will silently
publish ..." with adopter-action language ("you might accidentally
publish ... a stray git push after registering them — I won't push
without your approval, but the risk is on the adopter once the data
is committed locally")
- docs/multi-project.md trip-wire callout — replaced "silently publish
their portfolio names the moment they push" with "risk accidentally
publishing their portfolio names with a stray push (the framework
itself never pushes without operator approval, but once the registry
is committed locally the next push exposes it)"
Verified: `grep -r "silently publish" .claude/skills/ docs/` returns no
hits.
Closes me2resh/apexyard#148
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#150): bootstrap-skill exemption + Bash-write coverage (#152)
Closes the legitimate-bypass case (me2resh/apexyard#150) and the
illegitimate-bypass case (me2resh/apexyard#151) together so the
ticket-first gate is coherent. Shipping either alone would leave a
window where the framework is internally inconsistent — see AgDR-0011
for the full rationale.
Bootstrap exemption (me2resh/apexyard#150):
- .claude/session/active-bootstrap marker, written by /setup,
/handover, /update, /split-portfolio on entry; cleared on exit
- SessionStart sweep (clear-bootstrap-marker.sh) for stale markers
from interrupted sessions
- require-active-ticket.sh reads the marker and exempts skills on
the configured ticket.bootstrap_skills list
- bootstrap_skills list lives in .claude/project-config.defaults.json
(extendable per fork via .claude/project-config.json)
Bash-write coverage (me2resh/apexyard#151):
- new _lib-detect-bash-write.sh — heuristic detector for output
redirection, tee, sed -i, awk -i inplace, python/node/ruby
embedded interpreters
- require-active-ticket.sh + require-migration-ticket.sh now fire
on Bash in addition to Edit|Write|MultiEdit
- design choice: false-negatives preferred over false-positives
(the matcher errs toward "let through" rather than block legit
read-only commands)
Tests: 32 unit cases on the lib + 12 integration cases on the hook,
including the exact me2resh/apexyard#151 bypass repro from the issue
body.
Closes me2resh/apexyard#150
Will manually close me2resh/apexyard#151 post-merge per the
single-Closes-per-PR rule (precedent: AgDR-0010 / PR #147).
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#153): extend Bash-write matcher beyond first-version coverage (#155)
Closes me2resh/apexyard#153.
Extends `_lib-detect-bash-write.sh` (introduced for me2resh/apexyard#151
in PR #152) with the matcher families flagged by Rex's review of #152.
AgDR-0011 already frames the matcher as a living list extended on
observation; this commit just walks the list.
New matcher families:
- File-moving builtins: `cp`, `mv`, `rm`, `dd`, `install` (anchored at
command-start; `--help`/`--version` and `git rm`/`git mv` excluded)
- Archive / network writes: `tar -x` / `tar --extract`, `curl -o` /
`--output`, `wget -O` / `--output-document`
- Additional embedded interpreters: `perl -e`, `php -r` (keyword-gated
like python/node/ruby); `go run`, `deno run`/`deno script.ts`,
`bun run`/`bun script.ts` (categorical script runners)
- Python helpers: `pathlib.Path().touch()`, `shutil.copy*`,
`shutil.move`, `os.rename` added to the `python -c` and python
heredoc keyword list
- Heredoc variants for `ruby` and `node` (previously only python
heredoc was covered)
Extractor extensions:
- `cp` / `mv`: last positional arg
- `curl -o` / `--output`: file argument
- `wget -O` / `--output-document`: file argument
- `tar -x`, `go run`, `deno`, `bun`, `perl -e`, `php -r`: return empty
(caller applies gate categorically per AgDR-0011)
Test count rose from 32 to 86. Negative-class counterexamples cover
the trickiest false-positive surfaces: `tar -t` listing, `cp --help`,
`rm --version`, `git rm`, `curl` bare URL fetch, `wget` bare URL fetch,
`deno fmt`, `deno test`, `go build`. Existing
`test_require_active_ticket_bash.sh` regression suite still passes 12/12.
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(#154): mock gh in test sandboxes to remove live-tracker dependency (#156)
- add _lib-mock-gh.sh helper that installs a fake `gh` on the sandbox PATH;
intercepts `gh issue view <N> ... --json ...` and returns synthetic
`{"number":N,"state":"OPEN"}` (overridable per-num via mock_gh_set_state)
- wire the shim into test_single_closes_per_pr.sh and
test_validate_pr_required_sections.sh so the validator's CLOSED-issue
refusal no longer breaks the suite when upstream issues are closed
- both files previously failed every case (0/13 and 0/8) because their PR
titles reference #114 / #113, which are now CLOSED upstream
- post-fix: 13/13 and 8/8; full suite remains green
Closes me2resh/apexyard#154
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#132): structured CEO marker + same-turn merge in /approve-merge (#158)
* feat(#132): structured CEO marker + same-turn merge in /approve-merge
Closes me2resh/apexyard#132 (drop the "stop before merge" rule) and
me2resh/apexyard#48 (harden CEO marker against self-approval bypass)
together. The two threads compose — see AgDR-0012 for the full
rationale.
Streamline (#132):
- /approve-merge now runs `gh pr merge --squash --delete-branch`
in the same turn as the marker write, by default
- --no-merge opt-out preserves the deferred-merge case
- The discrete approval moment is the SKILL INVOCATION, not a
follow-up "now do the merge" message
Harden (#48):
- CEO marker is now a structured key/value file with required fields:
sha=<HEAD>
approved_by=user
skill_version=2
Validated by block-unreviewed-merge.sh; bare-SHA legacy markers
rejected with a clear "stale format" error pointing at /approve-merge
- The model's bare `echo SHA > <pr>-ceo.approved` bypass is now
mechanically rejected. Forging the structured fields requires a
deliberate, visible rule violation rather than a one-line accident
- Optional audit fields (approved_at, approval_summary) capture the
"what did the user say when they approved" trail
- Rex marker stays bare-SHA — different threat model (automated
reviewer, not human authorization moment)
pr-workflow.md reframed: "the load-bearing rule is explicit per-PR
approval, not two user messages." The merge is a deterministic
consequence of the approval invocation.
Tests: 12 cases on the hardened hook covering the new format end-to-end
(valid v2, missing rex/ceo, bare-SHA legacy rejected, missing
approved_by, wrong approved_by, skill_version=1, sha mismatch,
non-merge no-op, gh-api shape gated). Full suite: 205/205 across 13
test files.
Will manually close me2resh/apexyard#48 post-merge per the
single-Closes-per-PR rule (precedent: PR #152 / AgDR-0011).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#132): redact private project reference from AgDR-0012
Abstracted two references to a registered private project that named
the project's owner/repo. The leak-protection hook caught one in the
PR body; this fixup removes the matching references from the
AgDR-0012 file content (which would otherwise have shipped the names
to me2resh/apexyard public repo via the merge).
The pre-existing reference at .claude/rules/pr-workflow.md:130
(documenting #47) is untouched — it predates this PR and is already
on the public repo's history.
Refs me2resh/apexyard#132.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#132): markdownlint blanks-around-fences + typo fix
Two small fixups against red CI / Rex feedback:
- AgDR-0012 line 63: fenced code block now has a blank line before it
(MD031). markdownlint-cli2 0.13.0 was rejecting the indented fence
inside the bullet because the fence's preceding line was the bullet
text (no blank).
- approve-merge SKILL.md line 172: typo `deferes` → `defers` (Rex flag
on PR #158).
Refs me2resh/apexyard#132.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#157): remove voice-prompts feature + correct hook/skill counts (#161)
* chore(#157): remove voice-prompts-on-pause feature + correct hook/skill counts
Closes me2resh/apexyard#157 (sunset the voice-prompts feature) and
me2resh/apexyard#77 (hook count off-by-one in CHANGELOG / CLAUDE.md)
in one bundled PR. AgDR-0013 supersedes AgDR-0009 with the full
rationale; both AgDRs are preserved (decision records are append-only
history).
Removed (#157):
- .claude/hooks/voice-prompt-on-pause.sh
- .claude/hooks/tests/test_voice_prompt_on_pause.sh
- Stop matcher block in .claude/settings.json (became empty after
voice removal)
- voice_prompts block in .claude/project-config.defaults.json
- "## Voice prompts" section in docs/project-config.md
- voice_prompts mention in AgDR-0010 line 32 (replaced with
leak_protection / ticket as still-current example config blocks)
Preserved:
- docs/agdr/AgDR-0009-voice-prompts-on-pause.md — historical record;
new "Superseded by: AgDR-0013" header at the top
- AgDR-0010 line 115 reference to AgDR-0009 — still accurate as a
historical pattern reference
Counts corrected (#77):
- CHANGELOG.md v0.3.0 stats: "17 hooks" → "18 hooks" (historical
fix — at v0.3.0 there were actually 18 hooks)
- CLAUDE.md table line: "18 shell scripts" → "24 shell scripts"
(current count after this removal)
- CLAUDE.md table line: "35 slash commands" → "39 slash commands"
- CLAUDE.md "Available skills (34)" → "Available skills (39)"
- CLAUDE.md quick-reference "Skills (35 slash commands)" →
"(39 slash commands)"
Why bundled: #77's correct count depends on whether voice is still in
the framework. Shipping #77 before #157 would write a number that's
wrong by one again the moment #157 lands. Same shape as previous
bundles (PR #152 / AgDR-0011, PR #158 / AgDR-0012).
Why no adopter-facing changelog mention of the voice removal: the
feature never reached a tagged release on main. v1.1.0 didn't have
it; v1.2.0 won't have it. From the adopter's perspective there's
nothing to retire. AgDR-0013 captures the framework's internal record
for future contributors. See AgDR-0013 § "No adopter-facing changelog
mention".
Tests: full hook test suite green (196 cases across 12 files —
test_voice_prompt_on_pause.sh removed). No regressions.
Will manually close me2resh/apexyard#77 post-merge per the
single-Closes-per-PR rule (precedent: PRs #152, #158).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#157): unwire voice from settings + configs + docs + AgDR-0013
Continuation of d09d7b5 (the file deletions). Squash-merge will
collapse both into one PR commit. This commit captures:
- .claude/settings.json — Stop matcher block removed (was the only
hook in it; entire matcher gone)
- .claude/project-config.defaults.json — voice_prompts block + its
_comment removed
- docs/project-config.md — "## Voice prompts" section removed
- docs/agdr/AgDR-0009-voice-prompts-on-pause.md — "Superseded by"
header added at the top, content otherwise preserved as history
- docs/agdr/AgDR-0010-portfolio-config-and-self-healing.md — line 32
example reference swapped from voice_prompts to
leak_protection / ticket (still-current config blocks)
- docs/agdr/AgDR-0013-sunset-voice-prompts.md — new supersession AgDR
- CLAUDE.md — hook count 18 → 24, skill count 35 → 39 (three
occurrences each, all aligned to current reality)
- CHANGELOG.md v0.3.0 stats — "17 hooks" → "18 hooks" historical fix
(#77 acceptance criterion 1)
Refs me2resh/apexyard#157 + me2resh/apexyard#77.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#157): markdownlint MD028 + resolve stale AgDR-numbering ref
Three small fixups against Rex CHANGES-REQUESTED on PR #161:
- AgDR-0009 line 3: promote "Superseded by:" header out of a
blockquote. The original "I decided ..." canonical blockquote at
line 5 was being merged with the new supersession blockquote
(markdownlint MD028 — "no blanks inside blockquote").
- AgDR-0013 line 3: same shape — "Supersedes:" header now a plain
bold paragraph, canonical "I decided ..." blockquote untouched.
- approve-merge SKILL.md line ~187: stale conditional reference
"AgDR-0012 (or 0013 — depends on whether voice-removal lands
first)" — order is now resolved (12 = approve-merge bundle, 13 =
voice removal). Drop the parenthetical.
Refs me2resh/apexyard#157 + me2resh/apexyard#77.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#160): multi-tab terminal demo on the landing site (#162)
Landing-site `site/index.html` terminal demo previously played one
canonical flow ("one ticket, start to finish") on autoplay. With the
v1.2.0 skill surface expansion (4 new skills, 8+ new hooks), one flow
no longer represents the framework's breadth.
Adds tabs to the terminal chrome — four flows visitors can either let
auto-cycle or click directly:
1. one ticket — existing flow, unchanged content
2. /handover — adopt an external repo into the portfolio
3. /setup — first-run framework bootstrap on a fresh fork
4. /fan-out — spawn 3 parallel agents on independent tickets
Auto-advance: on completion of the active tab's script, the demo
pauses ~1.8s then advances to the next tab. Loops at the end. User
can interrupt by clicking any tab or hitting Replay.
Implementation:
- HTML chrome — single title span replaced with a tablist of 4 button
tabs. ARIA `role="tablist"` / `role="tab"` / `aria-selected` so
keyboard + screen-reader users get the same semantics as sighted
ones.
- CSS — new `.shell-demo__tabs` + `.shell-demo__tab` with an accent
underline on the active tab. Tabs scroll horizontally on narrow
viewports (mobile responsive).
- JS — refactored the existing IIFE from one `script` array to an
array of four. Added `setActiveTab()` for ARIA state, `play(idx)`
takes a tab index, end-of-script auto-advances to `(idx + 1) % N`
unless the user clicked away during the pause. Adds a new `cmd`
type alongside `you` for slash-command invocations (renders with
the same `>` prompt prefix). prefers-reduced-motion still bails
early and leaves the static seed visible.
- Static seed (the non-JS fallback) still shows tab 0's content, so
reduced-motion / no-JS visitors see the one-ticket flow as before.
Hero metrics also corrected to current reality (#77 / PR #161 covers
CLAUDE.md and CHANGELOG.md; this PR catches the same numbers in the
landing site):
Skills 32 → 39
Hooks 18 → 24
The tabs ship in v1.2.0 alongside the framework changes that make
the new flows worth showcasing.
Refs me2resh/apexyard#160 (release v1.2.0 + landing-site refresh).
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#163): default the split-portfolio sibling repo name to <fork>-portfolio (#164)
Closes me2resh/apexyard#163.
The split-portfolio mode docs and skills previously suggested
`your-org/ops` as the default name for the private sibling repo, and
`portfolio/` as the local clone directory. Both too generic — adopters
running multiple ops setups end up with `your-org/ops` collisions, and
a bare `portfolio/` dir gives no signal about which framework it
belongs to when it sits next to other unrelated `portfolio/` dirs.
`<fork>-portfolio` is now the default — keeps the relationship to the
public fork explicit on disk and on GitHub. If the fork is named
`your-org/apexyard`, the portfolio defaults to
`your-org/apexyard-portfolio`. If the fork was renamed (e.g. `cos`),
the portfolio defaults to `cos-portfolio`. Adopters with custom names
keep working — the `portfolio:` config block resolves whatever path
they configured.
Files updated:
docs/multi-project.md
- Layout diagrams use `apexyard-portfolio/` as the sibling
- Setup walkthrough Step 2 + Step 3 use `your-org/apexyard-portfolio`
and explain the `<fork>-portfolio` pattern
- Config-block + symlink path examples updated to
`../apexyard-portfolio/...`
- Daily workflow + cross-machine clone commands updated
- The two existing `your-org/ops` references that remain are
fork-rename examples (lines 52, 64) — kept as-is, since renaming
the fork to `ops` is still valid (the portfolio would then
default to `ops-portfolio`)
.claude/skills/setup/SKILL.md
- Step 2b's "default suggestion" for the private repo name is now
`your-org/<fork>-portfolio`, computed dynamically from the
fork's repo name via `gh repo view --json name -q .name` so the
suggestion is correct even when the fork was renamed
- Clone command no longer needs a second arg — the repo name IS
the directory name
- Config-block paths updated to `../apexyard-portfolio/...`
.claude/skills/split-portfolio/SKILL.md
- Step 3's suggested-name template is now `<account>/<fork>-portfolio`
with the same dynamic-fork-name resolution
Mechanism unchanged. The `portfolio:` config block in
`.claude/project-config.json` still takes any path; this PR is purely
default-suggestion + example prose.
No tests required (skills are markdown instructions; no automated
coverage today).
Refs `apexyard.projects.yaml.example` uses "ops repo" as a generic
term meaning "the operational management fork" — not a name — kept
as-is.
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#165): skills reference page on the landing site + changelog link (#167)
Closes me2resh/apexyard#165.
Adds a public, browseable index of every apexyard slash command
alongside a one-click changelog link from the homepage nav.
site/skills.html (new):
- Lists all 39 skills currently shipping in .claude/skills/
- Each entry: slash command, argument hint, description (taken
verbatim from the SKILL.md frontmatter so the page matches the
runtime exactly)
- 10 categories: Setup & onboarding, Daily ops, Tickets & ideas,
Specs & decisions, Code review & merge, Architecture & dev tools,
Production-readiness audits, Workflow primitives, Communications,
Deprecated
- Same brutalist-terminal design tokens as the homepage — JetBrains
Mono, paper-cream background, single warning-red accent, sharp
corners. Inlined CSS to keep the static-only no-build-step
convention; design vars duplicated rather than extracted to a
shared file (~18 vars; cheap to keep in sync).
- Mobile responsive — skill grid collapses to single-column under
720px; titlebar nav hides non-CTA items on narrow viewports.
- Reduced-motion friendly (no animation in the first place).
- Internal anchor TOC at the top so the page scans in seconds.
site/index.html (nav addition):
- Added two nav links to the titlebar between "what's in the box"
and the github CTA:
• skills → ./skills.html
• changelog → https://github.com/me2resh/apexyard/releases
- The changelog link points at the GitHub releases page (not the
raw CHANGELOG.md file) so it auto-resolves to the latest tagged
release on each visit. v1.2.0 lands and the link is already there.
No new dependencies, no build step, no JS for the skills page. The
existing site convention (one-html-file-per-route, inlined CSS,
optional progressive-enhancement JS) is preserved.
Refs me2resh/apexyard#160 (release v1.2.0 + landing-site refresh) —
this is the second site-side deliverable for that ticket; the release
tag itself follows once me2resh/apexyard#159 (testing) closes.
Follow-up worth a separate ticket: a small generator script that
walks .claude/skills/*/SKILL.md, parses YAML frontmatter, and emits
the skills.html sections automatically. Out of scope for v1.2.0 —
first version is hand-curated and will need maintenance until that
generator lands.
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#168): accept release/vN.N.N branches + release(...) PR titles (#169)
Closes me2resh/apexyard#168.
The /release skill prescribed `release/vA.B.C` as the source-branch
name and `release: vA.B.C` as the PR title for the dev → main release
PR (per AgDR-0007). Both were rejected by the framework's own
validators:
validate-branch-name.sh required {type}/{TICKET-ID}-{description};
release/v1.2.0 has no ticket-id portion.
validate-pr-create.sh required type(SCOPE): form with `release` not
in pr.title_type_whitelist.
The contradiction surfaced cutting v1.2.0 — the first release under
the dev/main model.
Three small changes:
1. .claude/hooks/validate-branch-name.sh — added an early-out branch
that accepts ^release/vN.N.N(-rcN)?$ as a valid name. Narrow,
intentional exception for the framework's release-cut convention;
release branches don't carry a ticket-id because the release itself
IS the ticket.
2. .claude/project-config.defaults.json — added "release" to
pr.title_type_whitelist so a title like `release(#160): v1.2.0`
passes validate-pr-create.sh's existing regex unchanged.
3. .claude/skills/release/SKILL.md step 4 — corrected the prescribed
PR title to `release(#<release-ticket>): vA.B.C` so future /release
invocations produce a title that satisfies the validators by
construction.
Tested:
bash .claude/hooks/validate-branch-name.sh against:
release/v1.2.0 → 0 (allowed, release-special-case)
release/v1.2.0-rc1 → 0 (allowed, RC variant)
release/v9.9.9 → 0 (allowed)
release/foo → 2 (correctly blocked)
release/v1 → 2 (correctly blocked)
chore/GH-168-fix → 0 (allowed, standard pattern)
feature/GH-1-x → 0 (allowed, standard pattern)
Full hook test suite: 196/196 cases green across 12 test files.
Refs: surfaced 2026-05-04 cutting the first release under AgDR-0007.
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#170): exempt release/vN.N.N from validate-pr-create's branch-id check (#171)
Closes me2resh/apexyard#170.
Completes the work started in #169 (closing #168). #169 added a
release-pattern early-out to validate-branch-name.sh so release/vN.N.N
branches pass the branch-name validator. But validate-pr-create.sh has
its own independent branch-id check at line 273 that #169 didn't
touch — and it still rejects release/v1.2.0 because that name doesn't
contain a ticket-id substring.
This is the same class of contradiction #168 fixed; the fix is the
same shape. Add the same release-pattern early-out to the branch-id
check in validate-pr-create.sh:
- if the branch matches ^release/vN.N.N(-rcN)?$ → exempt (release
branches don't carry ticket-ids; the release itself is the ticket)
- otherwise → require a ticket-id substring as before
#168's acceptance criterion 3 ("validate-pr-create.sh accepts a PR
title `release(#160): v1.2.0` against the `release/v1.2.0` branch")
was checked off based on the title regex alone but didn't catch the
secondary branch-id check living in the same file. Surfaced trying
to open the v1.2.0 release PR.
Tested:
bash .claude/hooks/validate-pr-create.sh against:
release/v1.2.0 → 0 (allowed, exempt)
release/v1.2.0-rc1 → 0 (allowed, RC variant)
chore/GH-1-fix → 0 (allowed, has ticket-id)
release/foo → 2 (correctly blocked)
chore/no-ticket → 2 (correctly blocked)
Refs me2resh/apexyard#168 (the parent bug) + #169 (the partial fix).
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#173): sync CHANGELOG.md from main → dev (#174)
Closes me2resh/apexyard#173.
The release-cut model (AgDR-0007 / #116) squash-merges
release/vN.N.N → main, which means the v1.2.0 CHANGELOG section that
landed on main via PR #172 was never propagated back to dev. This
commit copies main's CHANGELOG.md verbatim onto dev so the v1.2.0
section is now present on both branches.
The diff is exactly the v1.2.0 entry being prepended; no other lines
change.
Without this sync, the next release PR cut from dev would build a
v1.3.0 section on top of v1.1.0, silently dropping v1.2.0 from dev's
running history. The corollary skill-level fix (option B in the
ticket) — updating /release to source the previous CHANGELOG from
upstream/main — is filed as a follow-up and out of scope for this PR.
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#181): /agdr skill — searchable AgDR library (#186)
Adds /agdr — a portfolio-wide index for Agent Decision Records.
Walks apexyard.projects.yaml, reads each project's docs/agdr/*.md
(local clone if available, else gh api fallback), parses the optional
YAML frontmatter for category + projects, and answers four queries:
- /agdr browse list across the portfolio, grouped by category
- /agdr search <term> full-text grep across all bodies, returns
<project>/AgDR-NNNN paths + matching paragraph
- /agdr show <id> print a specific record, disambiguates duplicates
- /agdr stats counts per category (the marketing-slide tile,
now backed by real data)
Six-category taxonomy: architecture | tech-stack | security | patterns
| integrations | other. Legacy AgDRs without frontmatter remain
first-class — they bucket as `other` and are flagged in browse so
operators can migrate at their own pace.
Backwards-compatible template change: templates/agdr.md gains an
optional `category:` (and optional `projects:`) line in the existing
frontmatter block. Omitting the line keeps every existing AgDR valid;
the skill defaults to `other` when missing.
Doc note in workflows/sdlc.md § Phase 2 points at /agdr search for
"have we decided this before?" lookups before drafting a design.
Smoke test in .claude/hooks/tests/test_agdr_skill.sh covers the
parser the spec specifies — frontmatter extraction, category
bucketing (including the legacy default-to-other path), id reading,
stats aggregation, and search match counts. 18 assertions, all green;
12 pre-existing test suites also green (no regressions).
Closes #181
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(me2resh/apexyard#183): /launch-check trend tracking (#185)
- Persist each run as JSON under <projects_dir>/<name>/launch-check/runs/
(timestamp + branch + commit + per-dimension scores + verdict + top_risks)
- Append "Trend (last 5 runs)" section to the per-run summary when at least 2
prior runs exist — markdown table + ASCII score chart
- Add /launch-check trend mode for read-only trend rendering (no full audit)
- Auto-derive notes column from score-delta vs previous run
(e.g. "Security +12, Analytics +10")
- Opt-in commit via .launch-check-history-tracked marker; gitignored by default
- New helper script render-trend.sh + test_launch_check_trend.sh (21 cases)
- Resolve projects dir via portfolio_projects_dir helper (no hardcoded path)
- Schema is forward-compatible — extra fields preserved on framework upgrade
AgDR-0014 documents the chart format / schema / opt-in choices.
Closes me2resh/apexyard#183
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#182): /status --briefing + bin/apexyard status CLI shim (#187)
* feat(me2resh/apexyard#182): /status --briefing + bin/apexyard CLI shim
Make slide 6's `$ apexyard status` invocation real — the smallest of three
slide-reality tickets, demonstrating the marketing-to-implementation
pattern before #181 / #183 land.
- New helper at .claude/skills/status/briefing.sh — computes the 4-line
"where am I" briefing (active workspace, active ticket, branch,
role-set) so the logic is testable in isolation and runnable from a
plain shell. Workspace inference walks up from cwd to the ops-fork
root (same algorithm as _lib-portfolio-paths.sh and /start-ticket).
- Ticket reads from .claude/session/tickets/<workspace> first, falls
back to .claude/session/current-ticket. Role-set is inferred from
the active ticket's GitHub labels (v1: backend / frontend / qa /
security / platform / sre / data / ux / ui / product / tech-lead,
plus the long forms). No match emits the explicit "<none — inferred
per task>" placeholder so the four-line shape is constant.
- New CLI shim bin/apexyard delegates `apexyard status` to the same
helper after walking up to find the ops-fork root. Works from any
workspace/<name>/ clone or the fork itself; symlink onto PATH to
install (no shadowing of the `claude` binary).
- /status SKILL.md gains a "Briefing mode" section documenting the
--briefing / -b flag; default /status output is unchanged.
- docs/multi-project.md "Daily workflow" demonstrates the new
invocation alongside /inbox and /status.
- New smoke tests at .claude/hooks/tests/test_status_briefing.sh — 8
cases covering ops-root cwd, workspace cwd, unknown cwd, ops-fallback
marker, per-project marker priority, label-based role inference, the
no-matching-label path, and the constant-four-line shape.
Closes me2resh/apexyard#182
* fix(me2resh/apexyard#182): replace curios-dog with example-app in SKILL.md output sample (leak fix)
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* docs(#178): LSP integration spike — measurement + recommendation (#184)
* docs(#178): LSP spike — token-savings measurement + integration findings
- Phase 1: estimated token cost A vs B for three representative queries on a real TS Lambda backend (~9,750 LOC); shallow semantic queries see ~3-23x input-token savings, multi-hop traces see ~1.4x
- Phase 2: verified Claude Code shipped first-party LSP support in v2.0.74 (Dec 2025), gated behind ENABLE_LSP_TOOL=1, wired in via the plugin system (.lsp.json); MCP-wrapped LSP shims (cclsp, lsp-mcp) remain as fallback
- Phase 3: recommends Option 3 — interactive clone-first prompt at end of /handover, preserving today's "never auto-clone" principle while making the deep-dive path discoverable
- Phase 4: AgDR Y-statement and option matrix sketched; full AgDR is a follow-up if the spike says go
- Recommendation: GO. Adopt the built-in tool, document the opt-in path, change /handover to offer clone-first interactively. No code changes beyond /handover SKILL.md; no novel integration to maintain
Closes me2resh/apexyard#178
* docs: fix markdownlint MD031/MD032 in spike report
Add blank lines around lists and a fenced code block to satisfy
markdownlint-cli2 in CI. Pure formatting, no content change.
Refs me2resh/apexyard#178
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* docs(me2resh/apexyard#190): annotate LSP-aware skills with opt-in callouts (#191)
- Add identical-shape "LSP-aware (optional, recommended)" callout near
the top of each of the four code-aware SKILL.md files.
- Per-skill savings number is the only variation:
- /code-review: ~3-15× cheaper for semantic queries
- /threat-model: ~3-15× shallow, ~1.4-5× multi-hop traces
- /security-review: ~3-15× cheaper for semantic querie…
* chore(#109): project-configurable ticket / branch / commit / PR schema (#118)
* chore(#109): project-configurable ticket / branch / commit / PR schema
Lift the prefix / type whitelists hardcoded across skills, hooks, and
CI into a versioned JSON config read through a shared shell library.
Shipped defaults at .claude/project-config.defaults.json; per-fork
overrides at the optional .claude/project-config.json; one reader
(_lib-read-config.sh) that every consumer now uses.
Added:
- .claude/project-config.defaults.json (v1 schema)
- .claude/hooks/_lib-read-config.sh (shared reader)
- docs/project-config.md (schema reference + extension guide)
- docs/agdr/AgDR-0006-project-configurable-ticket-schema.md
Migrated (still pass with no config present via last-resort fallback):
- validate-branch-name.sh → .branch.type_whitelist
- validate-commit-format.sh → .commit.type_whitelist (legacy
`commit_types` top-level key honoured as backward-compat fallback)
- validate-pr-create.sh → .pr.title_type_whitelist
- /feature, /task, /bug skills reference the config in their Rules
sections; none hardcodes the list any more
Unlocks subsequent config-readers for #107 / #110 / #111 / #112 /
#113 / #114 / #115 — each extends the schema under its own subtree
without further changes to the loader.
https://github.com/me2resh/apexyard/issues/109
* fix(#109): satisfy markdownlint MD032 and MD060 on new docs
Auto-fix MD032 (blank lines around lists) in AgDR-0006 and format
table-separator rows with surrounding spaces (MD060) in both new
doc files. Content unchanged; CI green.
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#110): add block-private-refs-in-public-repos.sh hook (#119)
Adds a new PreToolUse hook that blocks gh issue/PR/comment creation and
gh api .../issues|/pulls calls targeting a public framework repo
(default: me2resh/apexyard + whatever `upstream` resolves to) when the
title or body references any registered private project from
apexyard.projects.yaml (by name, repo slug, owner/repo#N ticket ref, or
workspace path).
The hook is a sibling to check-secrets.sh — both scan outgoing content
for identifiers that should never leave the local environment. Skip
marker `<!-- private-refs: allow -->` in the body lets a deliberate
reference through with a visible warning.
Files touched:
- .claude/hooks/block-private-refs-in-public-repos.sh (new)
- .claude/hooks/tests/test_block_private_refs.sh (new)
- .claude/rules/leak-protection.md (new)
- .claude/settings.json (wire PreToolUse matchers for the 5 gh shapes)
- docs/rule-audit.md (append section 10 + bump counts)
Refs: https://github.com/me2resh/apexyard/issues/110
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#115): add warn-stale-review-markers.sh PostToolUse hook (#120)
Fires after `git push` to surface review markers that have gone stale
because new commits were pushed past an existing Rex / CEO / design
approval. The merge gate already catches this at `gh pr merge` time,
but only then -- this hook closes the gap by flagging it immediately
at push-time so the author isn't surprised at merge.
- `.claude/hooks/warn-stale-review-markers.sh`
- PostToolUse, non-blocking (PostToolUse exit 2 would push noise
into the conversation; this hook is purely informational).
- Resolves the PR HEAD via `gh pr view --json headRefOid` -- same
source-of-truth as the merge-gate hooks post-apexyard#47 / #55.
Falls back to local HEAD with a visible WARN when gh is offline.
- Silent on: no PR for branch, no markers, fresh markers,
failed push (detected via `rejected` / `failed to push` /
`fatal:` / `error:` markers in tool_response.stderr).
- Modes: `warn` (default) prints one stderr line per stale marker;
`delete` opts in to auto-removal via
`.claude/project-config.json` -> `review_markers.on_stale`.
TODO(apexyard#109): switch to the shared project-config reader
once it lands.
- `.claude/settings.json`
- Wires the hook on PostToolUse / Bash / `git push *`.
- `docs/rule-audit.md`
- Adds a row under section 3 (Code review & PR quality) and
bumps the mechanized count 26 -> 27 / total 73 -> 74.
- `.claude/hooks/tests/test_warn_stale_review_markers.sh`
- 8 cases: no PR, no markers, fresh markers, stale rex / ceo /
design (warn), delete mode, failed push. All pass locally.
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#111): upgrade pre-push-gate from advisory reminder to blocking check-runner (#121)
* chore(#111): upgrade pre-push-gate from reminder to blocking check-runner
Previously pre-push-gate.sh just printed a checklist of things to run
locally before pushing — it was advisory. The rule it enforces is a
HARD STOP per pr-workflow.md. That asymmetry meant agents routinely
pushed broken work and discovered it only when CI went red.
Replaces the reminder with a blocking runner that reads the list of
shell commands from project config (.pre_push.commands) and executes
them in sequence before a push is allowed through. First non-zero
exit blocks the push with exit 2 and prints the failing command plus
the last 20 lines of its output.
- Config key: .pre_push.commands[] — array of {name, run} objects.
Shipped default is an empty list (hook stays a no-op on repos that
haven't configured their checks yet, including the framework repo
itself until it wires its own CI).
- Emergency bypass: '<!-- pre-push: skip -->' in the HEAD commit
message. Grep-able on purpose so bypasses stay auditable.
- Fail-fast: once a command fails, the rest don't run. Parallel
execution is a follow-up polish.
- 7 test cases in .claude/hooks/tests/test_pre_push_gate.sh — all
pass on the shipped default + a minimal custom config.
Updates docs/rule-audit.md to flip "partial" → "yes" for the
"before git push" rule.
Integrates with the shared config reader landed in #109.
https://github.com/me2resh/apexyard/issues/111
* fix(#111): remove orphaned footnote reference from rule-audit
The previous advisory-mode footnote was superseded by pre-push-111
but its definition was accidentally kept, tripping markdownlint MD053
(unused reference definition). Drop it.
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#107): add validate-issue-structure.sh PreToolUse hook (#122)
Mechanically enforces the ticket body schema when an agent files raw
`gh issue create` calls instead of going through the interactive
/feature, /task, /bug skills. Matches bracketed title prefix
([Feature] / [Chore] / [Bug] / [Docs] / etc.) against
`.ticket.required_sections` in project-config, and blocks (exit 2)
when any required section is missing or empty. Skip marker
`<!-- validate-issue-structure: skip -->` bypasses with a visible
stderr WARN for legitimate off-template tickets (epics, meta-threads).
Changes:
- .claude/hooks/validate-issue-structure.sh — the hook; reads schema
via the shared _lib-read-config.sh, with inlined defaults for bare
checkouts predating the config-schema rollout. Handles
--body / --body-file / -F path.
- .claude/project-config.defaults.json — extends .ticket with
required_sections (Feature/Chore/Refactor/Testing/CI/Docs/Bug) and
skip_marker; other .ticket fields untouched.
- .claude/settings.json — new PreToolUse matcher on Bash(gh issue
create *) alongside the existing suggest-ticket-template.sh and
block-private-refs-in-public-repos.sh hooks.
- .claude/hooks/tests/test_validate_issue_structure.sh — 15 cases
covering pass + fail paths per prefix, empty section detection,
skip marker, unknown prefix, non-gh invocation, --body-file path.
- docs/rule-audit.md — new section 11 row, mechanized count +1.
Upstream ticket: https://github.com/me2resh/apexyard/issues/107
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#112): add require-agdr-for-arch-pr.sh PreToolUse hook (#123)
Closes the asymmetry noted in .claude/rules/agdr-decisions.md: every
other HARD STOP in the ruleset (merge approval, ticket-first,
migration-first) is mechanically enforced, but the /decide HARD STOP
was prose-only. The commit-time hook require-agdr-for-arch-changes.sh
catches one architectural change at commit; this new PR-time hook
catches the cumulative diff so reviewers always have a pointer to the
decision record.
- New hook at .claude/hooks/require-agdr-for-arch-pr.sh
- Fires on Bash(gh pr create *)
- Parses --title/--body/--body-file/-F <path>
- Resolves base branch from --base, else upstream/dev, origin/dev,
upstream/main, origin/main, main, master (in that order)
- Computes `git diff <merge-base>..HEAD --name-only`
- Triggers on any changed file matching .agdr_trigger_paths[], OR any
dep-file addition (package.json via jq key-set diff; other
dep files via a commented +/- line-count heuristic — version
bumps match +/- counts and do not fire)
- Blocks (exit 2) with a helpful message naming the triggers and
pointing at /decide if the body has no `AgDR-\d+-[a-z0-9-]+`
reference
- Skip marker `<!-- agdr: not-applicable -->` bypasses with a
visible WARN on stderr
- Silent exit 0 on non-gh commands, empty diffs, unresolvable base
- Wired via .claude/settings.json PreToolUse Bash(gh pr create *)
- Adds two new top-level keys to .claude/project-config.defaults.json:
agdr_trigger_paths (shell globs — domain/, infrastructure/,
migrations/, *.tf, .github/workflows/, etc.)
agdr_trigger_dep_files (literal basenames — package.json,
pyproject.toml, Cargo.toml, go.mod, Gemfile)
Hook has inline fallback defaults kept in sync.
- Adds docs/rule-audit.md entry in the AgDR section; bumps mechanized
count 26 to 27 and total rows 73 to 74.
- Adds .claude/hooks/tests/test_require_agdr_for_arch_pr.sh (7 cases;
all green): path-triggered without AgDR (block), with AgDR (pass),
dep-file added (block), version-only bump (no fire), skip marker
(pass + warn), non-matching diff (pass), non-gh command (no-op).
Closes https://github.com/me2resh/apexyard/issues/112
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#113): require Testing section in PR body (config-driven) (#124)
Extends validate-pr-create.sh with a required-sections check that
replaces the hardcoded Glossary-only grep. The list of required H2
headings is project-configurable via `.pr.required_sections[]`.
Shipped default is ["Testing", "Glossary"], matching the canonical PR
description shape in workflows/code-review.md.
- Each entry must appear as `## <Name>` (case-insensitive).
- Empty sections are tolerated at this layer (the issue-structure hook
#107 does stricter empty-content checks for issue bodies; for PR
bodies, empty sections are left to the reviewer's judgement).
- Skip marker `<!-- pr-sections: skip -->` bypasses with a visible
stderr WARN — for trivial PRs (lint-only fixes, version bumps)
where the full template is overkill.
- Reads from project config via the shared _lib-read-config.sh (#109).
Inline fallback matches shipped defaults so bare checkouts predating
#109 keep working.
- 8 test cases cover: all-sections pass, each missing section,
missing-both (both errors printed), skip marker, case-insensitive
headings, H3 rejection.
https://github.com/me2resh/apexyard/issues/113
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#114): enforce single Closes-keyword per PR body (#125)
* chore(#114): enforce single Closes-keyword per PR body
Caps distinct auto-closing references (close/closes/closed, fix/fixes/
fixed, resolve/resolves/resolved + N or owner/repo+N) at one per PR
body. Closes the loophole where the title validator limited the title
to one ticket but multiple Closes lines in the body would still auto-
close all of them on merge.
- Scans stripped of fenced code blocks so closing keywords inside a
code sample do not count.
- Distinct counting: the same number referenced twice (e.g. via Fixes
and Closes) counts as one.
- Cross-repo refs (owner/repo+N) count normally.
- Opt-in escape hatch: pr.allow_multiple_closes=true in
project-config disables the check for teams that deliberately batch
rollbacks or dependency bumps.
- Per-PR bypass: a multi-close-approved HTML comment in the body
prints a visible stderr WARN and lets that PR through. Grep-able
trace so bypasses are auditable.
- 10 test cases cover: one close passes, no-keyword passes, two
distinct block, three mixed block, same-number-twice passes, code-
fence-ignored, skip marker, cross-ref without keyword, opt-in
config, cross-repo close.
Reads configuration via the shared _lib-read-config.sh (apexyard+109).
https://github.com/me2resh/apexyard/issues/114
* fix(#114): strip inline backticks and tilde fences from close-count scan
Rex caught a self-reflexive bug in the initial commit: documentation
mentioning closing keywords inside inline backticks (say a PR body
that explains the new hook with examples) counted as real closes, and
a skip marker inside inline backticks silently bypassed the check.
Future PRs that document the feature would trip the same trap.
Fix the code-region stripper to cover:
- Triple-backtick fences (already handled)
- Tilde fences (new)
- Inline-backtick spans (new)
Also run the skip-marker check against the stripped body, so a marker
used purely as documentation no longer activates a real bypass.
Three new test cases pin the behaviour:
- closing keywords in inline backticks are ignored
- skip marker inside inline backticks does NOT bypass
- tilde fences also get stripped
13/13 tests pass.
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#108): add /tickets-batch skill for bulk-file flow (#127)
The fast happy path for filing 5–20 structured tickets in one intent
without dropping to raw `gh issue create` (non-conformant) or running
`/feature` 20 times serially (~100 turns of interview).
- .claude/skills/tickets-batch/SKILL.md — new skill spec. Asks
shared-context questions (priority, epic, area-labels, repo) ONCE
for the whole batch, then runs a ≤3-question micro-interview per
ticket (type, one-line purpose, optional clarification when the
inference is low-confidence). Confirms the full batch as a table,
then files each via specific `gh issue create` calls (never a
bulk JSON dump — the validator runs per-issue). Output conforms
to `.ticket.required_sections` by construction. Caps at 20
tickets per invocation.
- CLAUDE.md — added a row for /tickets-batch in the Available
Skills table; bumped the count references from 33 to 34.
Refs https://github.com/me2resh/apexyard/issues/108
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#117): add /fan-out skill + parallel-work rule doc (#128)
- Add `.claude/skills/fan-out/SKILL.md` — spawns N parallel Agent calls
in a single assistant message, with per-task agent type, worktree
isolation, and foreground/background mode. Caps at 5 concurrent
agents. Refuses fan-out when tasks share file write targets or have
sequential dependencies. Includes pre-spawn active-ticket safety
check and worktree merge-back flow that pauses on conflict.
- Add `.claude/rules/parallel-work.md` — trigger heuristic for when an
agent should proactively offer fan-out (>= 2 file-independent,
context-independent, individually substantial work items). Pairs
with the skill: rule says when, skill says how.
- Update `CLAUDE.md` — bump rules count to 9, skills count to 34, add
`/fan-out` row to the skills table.
Refs https://github.com/me2resh/apexyard/issues/117
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#116): adopt release-cut branch model (dev/main + tags) — framework only (#126)
* chore(#116): adopt release-cut branch model (dev/main + tags)
Formalises the dev/main split that already exists informally — dev is
the daily-work branch where every PR lands, main is release-only,
tagged with semver on each merge. Framework-only: managed projects
under apexyard governance stay trunk-based.
Added:
- AgDR-0007 — decision record (options table covers full git flow vs
trunk-only vs gitflow-lite; chose gitflow-lite)
- /release skill — diff dev against main, propose semver bump from
conventional commits, generate CHANGELOG, open release PR, tag
after merge
- docs/release-process.md — prose runbook for cutting a release
(manual fallback for the skill)
- .git.protected_branches in project-config.defaults.json
(main/master/dev/develop)
Modified:
- block-main-push.sh — now blocks direct pushes/commits to all
configured protected branches (was: hardcoded main/master). Reads
.git.protected_branches via the shared config reader (apexyard#109).
- CLAUDE.md — new section under Git Conventions explaining the
dev/main model + the framework-only scope. Skill table entry for
/release. Skills count bumped to 34.
- docs/multi-project.md — note that upstream/main is release-only
and the dev/main split is framework-only.
Non-consequences (per AgDR-0007):
- No release/* or hotfix/* branches. Hotfixes are normal patches
cut quickly. Revisit if multi-version maintenance becomes a need.
- No automatic on-merge issue closing for dev PRs. The release PR's
body aggregates all Closes references for the batch and triggers
auto-close en masse when it merges to main. Manual close in the
meantime.
- CI workflows trigger on pull_request regardless of base, so
dev-targeting PRs already get the full check matrix — no
workflow file edits needed.
https://github.com/me2resh/apexyard/issues/116
* fix(#116): satisfy markdownlint MD032 + MD060 in 116 docs
Auto-fix added blank lines around lists in AgDR-0007 (MD032) and
spaced the table separators in AgDR-0007 + multi-project.md (MD060).
Content unchanged.
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* fix(#106): CHANGELOG fallback in drift hook for squash-merged forks (#129)
* fix(#106): CHANGELOG fallback in drift hook for squash-merged forks
The v1.1.0 tag-reachability check (`git tag --merged main`) misfires on
forks that sync via GitHub's default squash-merge: the squash collapses
the upstream-tag commit into a synthetic SHA, the tag stops being
reachable, and the banner keeps firing forever.
Discovered live on the first real-world `/update` flow: ops fork
squash-merged the v1.1.0 sync PR, banner kept saying "v1.1.0 available"
even though the fork was content-caught-up.
This commit adds a CHANGELOG-content fallback that fires only when the
primary tag check fails. If the fork's main has a heading
`## [X.Y.Z]` matching the upstream tag's version, treat the release as
absorbed and stay silent. Tolerant grep (matches the apexyard CHANGELOG
format from v1.1.0 onward, with leading-`v` stripping for tag→heading
conversion).
The merge-commit and rebase paths are unchanged — primary tag check
still works for them, and the fallback never fires when it shouldn't.
Test coverage (5 cases):
- squash-merge fork caught up to v1.1.0 → silent (the fix)
- merge-commit fork caught up to v1.1.0 → silent (regression check)
- fork stopped at v1.0.0 → banner fires
- fork has its own newer tag → silent
- squash-merge but no CHANGELOG on fork → banner fires (no false silence)
Records the strategy update in docs/agdr/AgDR-0008-…md (extends
AgDR-0005's tag-based-drift design).
https://github.com/me2resh/apexyard/issues/106
* fix(#106): satisfy markdownlint MD032/MD060 + shellcheck SC2164
Rex flagged two CI-blocking issues on the original 106 commit:
- AgDR-0008 had three bulleted sub-lists in the Consequences section
without surrounding blank lines (MD032). Added blanks and padded the
one tight-pipe table separator (MD060).
- The 106 test fixture had five subshell `cd "$fk"` calls without
`|| exit 1` (SC2164). Added the guard to all five.
5/5 tests still pass after the fix. No semantic change to the hook
or the test logic.
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#130): add /validate-idea skill — lightweight pre-spec gate (#131)
A 10-minute, 5-question check that sits between /idea and /write-spec.
Designed for solo founders running ApexYard — not a heavyweight
methodology like event storming or Wardley mapping.
Five questions, asked one at a time:
1. Who is this specifically for?
2. What do they do today instead?
3. What's the smallest version that proves the value?
4. What would prove this is wrong? (kill criteria)
5. Build, buy, or rent?
Output: a one-page validation doc with a GREEN/YELLOW/RED verdict.
RED auto-updates the IDEA-NNN backlog row to WONTDO.
Integration:
/idea — adds an optional default-no "Validate now?" step after
capture (and after the optional GitHub Issue offer).
/handover — adds a conditional "this looks dormant, validate?"
step at the end of the integration plan, gated on the dormancy
heuristic (last commit > 90d AND zero open PRs AND no recent
issue activity). Healthy projects don't see the prompt.
CLAUDE.md skills count bumped to 35; new skills row added.
https://github.com/me2resh/apexyard/issues/130
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat: configurable voice prompts on assistant pause (AgDR-0009-voice-prompts-on-pause) (#135)
Stop hook that speaks the assistant's question aloud (Jarvis-style)
when it pauses for user input. Initial phase is macOS-only via `say`,
no voice input — user replies via keyboard.
Default OFF. Adopters opt in by overriding `voice_prompts.enabled` to
true in `.claude/project-config.json`.
Files:
- .claude/hooks/voice-prompt-on-pause.sh — Stop hook with config gate,
trigger heuristic (questions-only by default), markdown stripping,
sentence-boundary truncation, fire-and-forget say invocation
- .claude/hooks/tests/test_voice_prompt_on_pause.sh — 9 cases covering
disabled-default, enabled+question, enabled+statement, approved-pattern,
abc-menu, malformed-transcript, no-say-on-PATH, trigger-always,
markdown-stripping
- .claude/project-config.defaults.json — voice_prompts schema block
added (enabled, voice, max_chars, rate_wpm, trigger), default OFF
- .claude/settings.json — new Stop hook entry wired with the standard
ops-root resolver wrapper
- docs/agdr/AgDR-0009-voice-prompts-on-pause.md — design rationale,
options matrix (status quo / macOS say / cloud TTS / ML detection),
consequences, future phases
- docs/project-config.md — new "Voice prompts" section with override
examples and privacy notes
Test mode: hook respects VOICE_PROMPTS_SYNC=1 to run say synchronously
(test runners need this so assertions don't race against orphaned
background processes). Production invocations always run async.
Future phases (out of scope here, AgDR §"Future phases"):
- Phase 2: cross-platform TTS (Linux espeak, Windows SpeechSynthesizer)
- Phase 3: cloud TTS providers (OpenAI, ElevenLabs) — privacy AgDR-worthy
- Phase 4: voice input via Whisper-based STT
- Phase 5: per-message overrides
Refs: https://github.com/me2resh/apexyard/issues/134
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#141): add /debug skill — structured hypothesis-driven debugging (#142)
* feat(#141): add /debug skill — structured hypothesis-driven debugging
Adds a methodology skill that enforces five disciplines:
1. Capture the symptom precisely (exact URL, exact response, exact step)
2. Read the architecture before guessing (map every layer the request
touches, file by file)
3. Form a hypothesis ladder (3–5 candidates, each with an explicit
evidence test that confirms or refutes it)
4. Gather evidence first, fix second
5. Verify the fix against the original symptom evidence (re-run the
same `curl` / browser repro you used in step 4 — unit tests
verify code, not feature, correctness)
Stack appendices (Web, Desktop) carry stack-specific surface-evidence
requirements (step 1), architecture-surface maps (step 2), and
evidence-tests cookbooks (step 4). The methodology body stays portable
across stacks; appendices are where stack-specific knowledge accrues
over time.
Web appendix covers browser routing, framework configs (Next/Nuxt/Vite),
SPA-fallback layers, CDN, origin, the shared API client, backend
handlers, and auth providers. Desktop appendix covers Electron / Tauri
/ native-shell concerns: app entry points, IPC bridges, native modules,
auto-updater, sandbox / entitlements, code signing, crash reports.
Includes "When NOT to use" guidance so the methodology overhead doesn't
sandbag simple bugs (typos, off-by-ones, greenfield exploration).
Motivated by a real OAuth debug session in a managed project where
three sequential fixes chased adjacent symptoms because each was
hypothesis-then-fix without evidence in between. The skill is the
"never do that again" guardrail.
Closes #141
* fix(#141): scrub private project issue numbers from anti-pattern table
Rex review on PR #142 caught that line 155 of the skill's anti-pattern
table still named the originating PRs (#375, #377, #380) from the
private project where the methodology was first exercised. The PR body
and commit message were correctly abstracted earlier, but this in-file
reference slipped through — the leak-protection hook only scans gh
issue/pr writes, not staged file content, so mechanical enforcement
didn't catch it.
Replaced with "Three sequential PRs chasing the same symptom because
each was based on a different guess (no evidence test in between
cycles)" — same pedagogical value, zero attribution.
Refs #141
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* docs(#143): document split-portfolio mode + add /setup privacy gate (#144)
Adopters on GitHub Free with any private project hit a silent privacy bug
following today's docs: forking apexyard makes a public fork that you cannot
later flip to private (GitHub policy), and committing `apexyard.projects.yaml`
+ `projects/<name>/` to the fork publishes private project names + handover
findings on a public GitHub repo.
This PR documents the supported workaround — split-portfolio mode — and
adds an upfront privacy gate to the /setup skill so new adopters never
hit the trip-wire silently.
docs/multi-project.md:
- New "Two setup modes — pick the one that matches your privacy needs"
section before TL;DR, with a side-by-side table and the explicit
trip-wire callout.
- Existing TL;DR retitled "TL;DR — single-fork mode (default)" with a
one-line pointer to the split-portfolio section.
- New "Split-portfolio mode — public framework + private portfolio"
section between the existing setup steps and the directory-layout
section. Includes:
- The two-repo layout (~/ops/apexyard public + ~/ops/portfolio private)
- 7-step setup walkthrough with copy-pasteable commands
- Daily workflow + upstream sync notes (both unchanged)
- Trade-offs (two repos to maintain, two clones per machine, one
upstream-sync conflict path on `projects/README.md`)
- "Migrating from single-fork to split-portfolio" recovery flow with
the explicit warning that GitHub Issue / PR edit history survives a
force-push and must be redacted separately
.claude/skills/setup/SKILL.md:
- New Step 2a: privacy gate — asks "are any projects private?" before
proposing the config. Branches on the answer:
- All public → single-fork mode
- GitHub Pro / Team / Enterprise → single-fork mode (private
forks of public repos
are supported on those
plans)
- Any private + GitHub Free → split-portfolio mode
- New Step 2b: walks through the split-portfolio setup interactively
(private repo create, sibling clone, gitignore + symlink) when the
privacy gate triggers.
- Detection: `test -L apexyard.projects.yaml` short-circuits Step 2b
for adopters already in split mode.
- Explicit "do NOT auto-migrate" rule for adopters already in single-fork
mode with private names already pushed — that path is destructive
(force-push history rewrite + redact issue/PR bodies + delete backup
branch) and warrants a deliberate, eyes-open run, not a /setup side
effect.
Out of scope for this PR (tracked separately on #143):
- `portfolio:` config block in `onboarding.yaml` schema
- Skill audit + refactor to honour configured `registry` / `projects_dir`
/ `ideas_backlog` paths instead of hardcoded fork-relative paths
- `/split-portfolio` migration helper skill that automates the recovery
flow currently documented manually
This is the docs-and-setup-question minimum-viable starter — the
framework code refactor is mechanical and lands as a follow-up.
Refs #143
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#145): portfolio config + self-healing + /split-portfolio helper (#147)
* feat(me2resh/apexyard#145): portfolio config block + self-healing + /split-portfolio helper
Closes the framework primitive deferred from me2resh/apexyard#144. Adds
first-class config-driven path resolution for the portfolio registry,
projects dir, and ideas backlog, with self-healing surfacing of broken
config at session start, plus a new /split-portfolio skill that automates
the destructive recovery flow.
Schema, helper, and hook:
- .claude/project-config.defaults.json: new portfolio: block
(registry, projects_dir, ideas_backlog) with defaults matching
today's single-fork layout
- .claude/hooks/_lib-portfolio-paths.sh: new sourceable helper exposing
portfolio_registry, portfolio_projects_dir, portfolio_ideas_backlog,
portfolio_validate, portfolio_clear_cache. Resolves relative paths
against the ops-fork root.
- .claude/hooks/check-portfolio-config.sh: new SessionStart hook —
silent on OK, one-line banner on broken config, never blocks session
- .claude/hooks/tests/test_portfolio_paths.sh: 13 cases covering
defaults, absolute/relative overrides, validate states, cache clear
Skill audit (18 SKILL.md files):
- Adds Path resolution callout pointing at the helper
- handover bash blocks now source helper and use $(portfolio_registry)
instead of literal apexyard.projects.yaml
- setup Step 2b now writes the portfolio: config block (recommended)
and validates via portfolio_validate before declaring success;
symlink approach kept as legacy fallback
New skill (.claude/skills/split-portfolio/SKILL.md):
- 10-step migration with explicit operator-confirmation gates at each
destructive step (force-push, body redaction, branch deletion)
- --verify mode: read-only state report (mode, paths, validate, drift)
- --dry-run mode: prints commands without executing
- Pre-flight refusals: already-private fork, paid GitHub plan, dirty
working tree, already-migrated state
- Step 9 writes the portfolio: config block (not symlinks) — symlink
fallback documented for adopters on older framework versions
- Step 9 surfaces the GitHub timeline-API survival caveat verbatim
- Idempotent re-runs: detects partial-migration state and resumes
Docs (docs/multi-project.md):
- Layout section describes both modes (config-block recommended,
symlink legacy) with self-healing notes
- Setup steps split into config-block mode and legacy symlink mode
- Migration section now points at /split-portfolio skill; manual
recipe preserved as fallback
AgDR (docs/agdr/AgDR-0010-portfolio-config-and-self-healing.md):
- Full Y-statement, options, decision, consequences, future phases
- Schema decision rationale: project-config.json over onboarding.yaml
because runtime path resolution belongs in project-config
Closes me2resh/apexyard#145
Refs me2resh/apexyard#146 (delivered same PR; closed manually post-merge
per the single-Closes-keyword rule in validate-pr-create.sh)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(me2resh/apexyard#145): markdownlint MD031 — blank lines around fences
CI's markdownlint-cli2 (v0.34.0) flagged the JSON + bash fenced code
blocks I added in setup/SKILL.md Step 2b without surrounding blank
lines. Added the required blanks. No content change.
Refs me2resh/apexyard#147
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(me2resh/apexyard#148): correct privacy-gate wording — adopter action, not framework auto-publish (#149)
The privacy-gate wording introduced in PR #144 (and unchanged in PR #147)
attributed the publication to the framework rather than the adopter:
"the standard fork-and-commit setup will silently publish your private
project names on a public GitHub repo"
That's factually wrong. ApexYard never pushes anything without explicit
operator approval — the publication only happens when the adopter
themselves runs git push. The "silently publish" framing read as if the
framework auto-publishes, which is misleading and undermines trust in
the rest of the framework's safety claims.
Two prose-only edits, no code, no behavior change:
- .claude/skills/setup/SKILL.md Step 2a — replaced "will silently
publish ..." with adopter-action language ("you might accidentally
publish ... a stray git push after registering them — I won't push
without your approval, but the risk is on the adopter once the data
is committed locally")
- docs/multi-project.md trip-wire callout — replaced "silently publish
their portfolio names the moment they push" with "risk accidentally
publishing their portfolio names with a stray push (the framework
itself never pushes without operator approval, but once the registry
is committed locally the next push exposes it)"
Verified: `grep -r "silently publish" .claude/skills/ docs/` returns no
hits.
Closes me2resh/apexyard#148
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#150): bootstrap-skill exemption + Bash-write coverage (#152)
Closes the legitimate-bypass case (me2resh/apexyard#150) and the
illegitimate-bypass case (me2resh/apexyard#151) together so the
ticket-first gate is coherent. Shipping either alone would leave a
window where the framework is internally inconsistent — see AgDR-0011
for the full rationale.
Bootstrap exemption (me2resh/apexyard#150):
- .claude/session/active-bootstrap marker, written by /setup,
/handover, /update, /split-portfolio on entry; cleared on exit
- SessionStart sweep (clear-bootstrap-marker.sh) for stale markers
from interrupted sessions
- require-active-ticket.sh reads the marker and exempts skills on
the configured ticket.bootstrap_skills list
- bootstrap_skills list lives in .claude/project-config.defaults.json
(extendable per fork via .claude/project-config.json)
Bash-write coverage (me2resh/apexyard#151):
- new _lib-detect-bash-write.sh — heuristic detector for output
redirection, tee, sed -i, awk -i inplace, python/node/ruby
embedded interpreters
- require-active-ticket.sh + require-migration-ticket.sh now fire
on Bash in addition to Edit|Write|MultiEdit
- design choice: false-negatives preferred over false-positives
(the matcher errs toward "let through" rather than block legit
read-only commands)
Tests: 32 unit cases on the lib + 12 integration cases on the hook,
including the exact me2resh/apexyard#151 bypass repro from the issue
body.
Closes me2resh/apexyard#150
Will manually close me2resh/apexyard#151 post-merge per the
single-Closes-per-PR rule (precedent: AgDR-0010 / PR #147).
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#153): extend Bash-write matcher beyond first-version coverage (#155)
Closes me2resh/apexyard#153.
Extends `_lib-detect-bash-write.sh` (introduced for me2resh/apexyard#151
in PR #152) with the matcher families flagged by Rex's review of #152.
AgDR-0011 already frames the matcher as a living list extended on
observation; this commit just walks the list.
New matcher families:
- File-moving builtins: `cp`, `mv`, `rm`, `dd`, `install` (anchored at
command-start; `--help`/`--version` and `git rm`/`git mv` excluded)
- Archive / network writes: `tar -x` / `tar --extract`, `curl -o` /
`--output`, `wget -O` / `--output-document`
- Additional embedded interpreters: `perl -e`, `php -r` (keyword-gated
like python/node/ruby); `go run`, `deno run`/`deno script.ts`,
`bun run`/`bun script.ts` (categorical script runners)
- Python helpers: `pathlib.Path().touch()`, `shutil.copy*`,
`shutil.move`, `os.rename` added to the `python -c` and python
heredoc keyword list
- Heredoc variants for `ruby` and `node` (previously only python
heredoc was covered)
Extractor extensions:
- `cp` / `mv`: last positional arg
- `curl -o` / `--output`: file argument
- `wget -O` / `--output-document`: file argument
- `tar -x`, `go run`, `deno`, `bun`, `perl -e`, `php -r`: return empty
(caller applies gate categorically per AgDR-0011)
Test count rose from 32 to 86. Negative-class counterexamples cover
the trickiest false-positive surfaces: `tar -t` listing, `cp --help`,
`rm --version`, `git rm`, `curl` bare URL fetch, `wget` bare URL fetch,
`deno fmt`, `deno test`, `go build`. Existing
`test_require_active_ticket_bash.sh` regression suite still passes 12/12.
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(#154): mock gh in test sandboxes to remove live-tracker dependency (#156)
- add _lib-mock-gh.sh helper that installs a fake `gh` on the sandbox PATH;
intercepts `gh issue view <N> ... --json ...` and returns synthetic
`{"number":N,"state":"OPEN"}` (overridable per-num via mock_gh_set_state)
- wire the shim into test_single_closes_per_pr.sh and
test_validate_pr_required_sections.sh so the validator's CLOSED-issue
refusal no longer breaks the suite when upstream issues are closed
- both files previously failed every case (0/13 and 0/8) because their PR
titles reference #114 / #113, which are now CLOSED upstream
- post-fix: 13/13 and 8/8; full suite remains green
Closes me2resh/apexyard#154
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#132): structured CEO marker + same-turn merge in /approve-merge (#158)
* feat(#132): structured CEO marker + same-turn merge in /approve-merge
Closes me2resh/apexyard#132 (drop the "stop before merge" rule) and
me2resh/apexyard#48 (harden CEO marker against self-approval bypass)
together. The two threads compose — see AgDR-0012 for the full
rationale.
Streamline (#132):
- /approve-merge now runs `gh pr merge --squash --delete-branch`
in the same turn as the marker write, by default
- --no-merge opt-out preserves the deferred-merge case
- The discrete approval moment is the SKILL INVOCATION, not a
follow-up "now do the merge" message
Harden (#48):
- CEO marker is now a structured key/value file with required fields:
sha=<HEAD>
approved_by=user
skill_version=2
Validated by block-unreviewed-merge.sh; bare-SHA legacy markers
rejected with a clear "stale format" error pointing at /approve-merge
- The model's bare `echo SHA > <pr>-ceo.approved` bypass is now
mechanically rejected. Forging the structured fields requires a
deliberate, visible rule violation rather than a one-line accident
- Optional audit fields (approved_at, approval_summary) capture the
"what did the user say when they approved" trail
- Rex marker stays bare-SHA — different threat model (automated
reviewer, not human authorization moment)
pr-workflow.md reframed: "the load-bearing rule is explicit per-PR
approval, not two user messages." The merge is a deterministic
consequence of the approval invocation.
Tests: 12 cases on the hardened hook covering the new format end-to-end
(valid v2, missing rex/ceo, bare-SHA legacy rejected, missing
approved_by, wrong approved_by, skill_version=1, sha mismatch,
non-merge no-op, gh-api shape gated). Full suite: 205/205 across 13
test files.
Will manually close me2resh/apexyard#48 post-merge per the
single-Closes-per-PR rule (precedent: PR #152 / AgDR-0011).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#132): redact private project reference from AgDR-0012
Abstracted two references to a registered private project that named
the project's owner/repo. The leak-protection hook caught one in the
PR body; this fixup removes the matching references from the
AgDR-0012 file content (which would otherwise have shipped the names
to me2resh/apexyard public repo via the merge).
The pre-existing reference at .claude/rules/pr-workflow.md:130
(documenting #47) is untouched — it predates this PR and is already
on the public repo's history.
Refs me2resh/apexyard#132.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#132): markdownlint blanks-around-fences + typo fix
Two small fixups against red CI / Rex feedback:
- AgDR-0012 line 63: fenced code block now has a blank line before it
(MD031). markdownlint-cli2 0.13.0 was rejecting the indented fence
inside the bullet because the fence's preceding line was the bullet
text (no blank).
- approve-merge SKILL.md line 172: typo `deferes` → `defers` (Rex flag
on PR #158).
Refs me2resh/apexyard#132.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#157): remove voice-prompts feature + correct hook/skill counts (#161)
* chore(#157): remove voice-prompts-on-pause feature + correct hook/skill counts
Closes me2resh/apexyard#157 (sunset the voice-prompts feature) and
me2resh/apexyard#77 (hook count off-by-one in CHANGELOG / CLAUDE.md)
in one bundled PR. AgDR-0013 supersedes AgDR-0009 with the full
rationale; both AgDRs are preserved (decision records are append-only
history).
Removed (#157):
- .claude/hooks/voice-prompt-on-pause.sh
- .claude/hooks/tests/test_voice_prompt_on_pause.sh
- Stop matcher block in .claude/settings.json (became empty after
voice removal)
- voice_prompts block in .claude/project-config.defaults.json
- "## Voice prompts" section in docs/project-config.md
- voice_prompts mention in AgDR-0010 line 32 (replaced with
leak_protection / ticket as still-current example config blocks)
Preserved:
- docs/agdr/AgDR-0009-voice-prompts-on-pause.md — historical record;
new "Superseded by: AgDR-0013" header at the top
- AgDR-0010 line 115 reference to AgDR-0009 — still accurate as a
historical pattern reference
Counts corrected (#77):
- CHANGELOG.md v0.3.0 stats: "17 hooks" → "18 hooks" (historical
fix — at v0.3.0 there were actually 18 hooks)
- CLAUDE.md table line: "18 shell scripts" → "24 shell scripts"
(current count after this removal)
- CLAUDE.md table line: "35 slash commands" → "39 slash commands"
- CLAUDE.md "Available skills (34)" → "Available skills (39)"
- CLAUDE.md quick-reference "Skills (35 slash commands)" →
"(39 slash commands)"
Why bundled: #77's correct count depends on whether voice is still in
the framework. Shipping #77 before #157 would write a number that's
wrong by one again the moment #157 lands. Same shape as previous
bundles (PR #152 / AgDR-0011, PR #158 / AgDR-0012).
Why no adopter-facing changelog mention of the voice removal: the
feature never reached a tagged release on main. v1.1.0 didn't have
it; v1.2.0 won't have it. From the adopter's perspective there's
nothing to retire. AgDR-0013 captures the framework's internal record
for future contributors. See AgDR-0013 § "No adopter-facing changelog
mention".
Tests: full hook test suite green (196 cases across 12 files —
test_voice_prompt_on_pause.sh removed). No regressions.
Will manually close me2resh/apexyard#77 post-merge per the
single-Closes-per-PR rule (precedent: PRs #152, #158).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#157): unwire voice from settings + configs + docs + AgDR-0013
Continuation of d09d7b5 (the file deletions). Squash-merge will
collapse both into one PR commit. This commit captures:
- .claude/settings.json — Stop matcher block removed (was the only
hook in it; entire matcher gone)
- .claude/project-config.defaults.json — voice_prompts block + its
_comment removed
- docs/project-config.md — "## Voice prompts" section removed
- docs/agdr/AgDR-0009-voice-prompts-on-pause.md — "Superseded by"
header added at the top, content otherwise preserved as history
- docs/agdr/AgDR-0010-portfolio-config-and-self-healing.md — line 32
example reference swapped from voice_prompts to
leak_protection / ticket (still-current config blocks)
- docs/agdr/AgDR-0013-sunset-voice-prompts.md — new supersession AgDR
- CLAUDE.md — hook count 18 → 24, skill count 35 → 39 (three
occurrences each, all aligned to current reality)
- CHANGELOG.md v0.3.0 stats — "17 hooks" → "18 hooks" historical fix
(#77 acceptance criterion 1)
Refs me2resh/apexyard#157 + me2resh/apexyard#77.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#157): markdownlint MD028 + resolve stale AgDR-numbering ref
Three small fixups against Rex CHANGES-REQUESTED on PR #161:
- AgDR-0009 line 3: promote "Superseded by:" header out of a
blockquote. The original "I decided ..." canonical blockquote at
line 5 was being merged with the new supersession blockquote
(markdownlint MD028 — "no blanks inside blockquote").
- AgDR-0013 line 3: same shape — "Supersedes:" header now a plain
bold paragraph, canonical "I decided ..." blockquote untouched.
- approve-merge SKILL.md line ~187: stale conditional reference
"AgDR-0012 (or 0013 — depends on whether voice-removal lands
first)" — order is now resolved (12 = approve-merge bundle, 13 =
voice removal). Drop the parenthetical.
Refs me2resh/apexyard#157 + me2resh/apexyard#77.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#160): multi-tab terminal demo on the landing site (#162)
Landing-site `site/index.html` terminal demo previously played one
canonical flow ("one ticket, start to finish") on autoplay. With the
v1.2.0 skill surface expansion (4 new skills, 8+ new hooks), one flow
no longer represents the framework's breadth.
Adds tabs to the terminal chrome — four flows visitors can either let
auto-cycle or click directly:
1. one ticket — existing flow, unchanged content
2. /handover — adopt an external repo into the portfolio
3. /setup — first-run framework bootstrap on a fresh fork
4. /fan-out — spawn 3 parallel agents on independent tickets
Auto-advance: on completion of the active tab's script, the demo
pauses ~1.8s then advances to the next tab. Loops at the end. User
can interrupt by clicking any tab or hitting Replay.
Implementation:
- HTML chrome — single title span replaced with a tablist of 4 button
tabs. ARIA `role="tablist"` / `role="tab"` / `aria-selected` so
keyboard + screen-reader users get the same semantics as sighted
ones.
- CSS — new `.shell-demo__tabs` + `.shell-demo__tab` with an accent
underline on the active tab. Tabs scroll horizontally on narrow
viewports (mobile responsive).
- JS — refactored the existing IIFE from one `script` array to an
array of four. Added `setActiveTab()` for ARIA state, `play(idx)`
takes a tab index, end-of-script auto-advances to `(idx + 1) % N`
unless the user clicked away during the pause. Adds a new `cmd`
type alongside `you` for slash-command invocations (renders with
the same `>` prompt prefix). prefers-reduced-motion still bails
early and leaves the static seed visible.
- Static seed (the non-JS fallback) still shows tab 0's content, so
reduced-motion / no-JS visitors see the one-ticket flow as before.
Hero metrics also corrected to current reality (#77 / PR #161 covers
CLAUDE.md and CHANGELOG.md; this PR catches the same numbers in the
landing site):
Skills 32 → 39
Hooks 18 → 24
The tabs ship in v1.2.0 alongside the framework changes that make
the new flows worth showcasing.
Refs me2resh/apexyard#160 (release v1.2.0 + landing-site refresh).
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#163): default the split-portfolio sibling repo name to <fork>-portfolio (#164)
Closes me2resh/apexyard#163.
The split-portfolio mode docs and skills previously suggested
`your-org/ops` as the default name for the private sibling repo, and
`portfolio/` as the local clone directory. Both too generic — adopters
running multiple ops setups end up with `your-org/ops` collisions, and
a bare `portfolio/` dir gives no signal about which framework it
belongs to when it sits next to other unrelated `portfolio/` dirs.
`<fork>-portfolio` is now the default — keeps the relationship to the
public fork explicit on disk and on GitHub. If the fork is named
`your-org/apexyard`, the portfolio defaults to
`your-org/apexyard-portfolio`. If the fork was renamed (e.g. `cos`),
the portfolio defaults to `cos-portfolio`. Adopters with custom names
keep working — the `portfolio:` config block resolves whatever path
they configured.
Files updated:
docs/multi-project.md
- Layout diagrams use `apexyard-portfolio/` as the sibling
- Setup walkthrough Step 2 + Step 3 use `your-org/apexyard-portfolio`
and explain the `<fork>-portfolio` pattern
- Config-block + symlink path examples updated to
`../apexyard-portfolio/...`
- Daily workflow + cross-machine clone commands updated
- The two existing `your-org/ops` references that remain are
fork-rename examples (lines 52, 64) — kept as-is, since renaming
the fork to `ops` is still valid (the portfolio would then
default to `ops-portfolio`)
.claude/skills/setup/SKILL.md
- Step 2b's "default suggestion" for the private repo name is now
`your-org/<fork>-portfolio`, computed dynamically from the
fork's repo name via `gh repo view --json name -q .name` so the
suggestion is correct even when the fork was renamed
- Clone command no longer needs a second arg — the repo name IS
the directory name
- Config-block paths updated to `../apexyard-portfolio/...`
.claude/skills/split-portfolio/SKILL.md
- Step 3's suggested-name template is now `<account>/<fork>-portfolio`
with the same dynamic-fork-name resolution
Mechanism unchanged. The `portfolio:` config block in
`.claude/project-config.json` still takes any path; this PR is purely
default-suggestion + example prose.
No tests required (skills are markdown instructions; no automated
coverage today).
Refs `apexyard.projects.yaml.example` uses "ops repo" as a generic
term meaning "the operational management fork" — not a name — kept
as-is.
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#165): skills reference page on the landing site + changelog link (#167)
Closes me2resh/apexyard#165.
Adds a public, browseable index of every apexyard slash command
alongside a one-click changelog link from the homepage nav.
site/skills.html (new):
- Lists all 39 skills currently shipping in .claude/skills/
- Each entry: slash command, argument hint, description (taken
verbatim from the SKILL.md frontmatter so the page matches the
runtime exactly)
- 10 categories: Setup & onboarding, Daily ops, Tickets & ideas,
Specs & decisions, Code review & merge, Architecture & dev tools,
Production-readiness audits, Workflow primitives, Communications,
Deprecated
- Same brutalist-terminal design tokens as the homepage — JetBrains
Mono, paper-cream background, single warning-red accent, sharp
corners. Inlined CSS to keep the static-only no-build-step
convention; design vars duplicated rather than extracted to a
shared file (~18 vars; cheap to keep in sync).
- Mobile responsive — skill grid collapses to single-column under
720px; titlebar nav hides non-CTA items on narrow viewports.
- Reduced-motion friendly (no animation in the first place).
- Internal anchor TOC at the top so the page scans in seconds.
site/index.html (nav addition):
- Added two nav links to the titlebar between "what's in the box"
and the github CTA:
• skills → ./skills.html
• changelog → https://github.com/me2resh/apexyard/releases
- The changelog link points at the GitHub releases page (not the
raw CHANGELOG.md file) so it auto-resolves to the latest tagged
release on each visit. v1.2.0 lands and the link is already there.
No new dependencies, no build step, no JS for the skills page. The
existing site convention (one-html-file-per-route, inlined CSS,
optional progressive-enhancement JS) is preserved.
Refs me2resh/apexyard#160 (release v1.2.0 + landing-site refresh) —
this is the second site-side deliverable for that ticket; the release
tag itself follows once me2resh/apexyard#159 (testing) closes.
Follow-up worth a separate ticket: a small generator script that
walks .claude/skills/*/SKILL.md, parses YAML frontmatter, and emits
the skills.html sections automatically. Out of scope for v1.2.0 —
first version is hand-curated and will need maintenance until that
generator lands.
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#168): accept release/vN.N.N branches + release(...) PR titles (#169)
Closes me2resh/apexyard#168.
The /release skill prescribed `release/vA.B.C` as the source-branch
name and `release: vA.B.C` as the PR title for the dev → main release
PR (per AgDR-0007). Both were rejected by the framework's own
validators:
validate-branch-name.sh required {type}/{TICKET-ID}-{description};
release/v1.2.0 has no ticket-id portion.
validate-pr-create.sh required type(SCOPE): form with `release` not
in pr.title_type_whitelist.
The contradiction surfaced cutting v1.2.0 — the first release under
the dev/main model.
Three small changes:
1. .claude/hooks/validate-branch-name.sh — added an early-out branch
that accepts ^release/vN.N.N(-rcN)?$ as a valid name. Narrow,
intentional exception for the framework's release-cut convention;
release branches don't carry a ticket-id because the release itself
IS the ticket.
2. .claude/project-config.defaults.json — added "release" to
pr.title_type_whitelist so a title like `release(#160): v1.2.0`
passes validate-pr-create.sh's existing regex unchanged.
3. .claude/skills/release/SKILL.md step 4 — corrected the prescribed
PR title to `release(#<release-ticket>): vA.B.C` so future /release
invocations produce a title that satisfies the validators by
construction.
Tested:
bash .claude/hooks/validate-branch-name.sh against:
release/v1.2.0 → 0 (allowed, release-special-case)
release/v1.2.0-rc1 → 0 (allowed, RC variant)
release/v9.9.9 → 0 (allowed)
release/foo → 2 (correctly blocked)
release/v1 → 2 (correctly blocked)
chore/GH-168-fix → 0 (allowed, standard pattern)
feature/GH-1-x → 0 (allowed, standard pattern)
Full hook test suite: 196/196 cases green across 12 test files.
Refs: surfaced 2026-05-04 cutting the first release under AgDR-0007.
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#170): exempt release/vN.N.N from validate-pr-create's branch-id check (#171)
Closes me2resh/apexyard#170.
Completes the work started in #169 (closing #168). #169 added a
release-pattern early-out to validate-branch-name.sh so release/vN.N.N
branches pass the branch-name validator. But validate-pr-create.sh has
its own independent branch-id check at line 273 that #169 didn't
touch — and it still rejects release/v1.2.0 because that name doesn't
contain a ticket-id substring.
This is the same class of contradiction #168 fixed; the fix is the
same shape. Add the same release-pattern early-out to the branch-id
check in validate-pr-create.sh:
- if the branch matches ^release/vN.N.N(-rcN)?$ → exempt (release
branches don't carry ticket-ids; the release itself is the ticket)
- otherwise → require a ticket-id substring as before
#168's acceptance criterion 3 ("validate-pr-create.sh accepts a PR
title `release(#160): v1.2.0` against the `release/v1.2.0` branch")
was checked off based on the title regex alone but didn't catch the
secondary branch-id check living in the same file. Surfaced trying
to open the v1.2.0 release PR.
Tested:
bash .claude/hooks/validate-pr-create.sh against:
release/v1.2.0 → 0 (allowed, exempt)
release/v1.2.0-rc1 → 0 (allowed, RC variant)
chore/GH-1-fix → 0 (allowed, has ticket-id)
release/foo → 2 (correctly blocked)
chore/no-ticket → 2 (correctly blocked)
Refs me2resh/apexyard#168 (the parent bug) + #169 (the partial fix).
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#173): sync CHANGELOG.md from main → dev (#174)
Closes me2resh/apexyard#173.
The release-cut model (AgDR-0007 / #116) squash-merges
release/vN.N.N → main, which means the v1.2.0 CHANGELOG section that
landed on main via PR #172 was never propagated back to dev. This
commit copies main's CHANGELOG.md verbatim onto dev so the v1.2.0
section is now present on both branches.
The diff is exactly the v1.2.0 entry being prepended; no other lines
change.
Without this sync, the next release PR cut from dev would build a
v1.3.0 section on top of v1.1.0, silently dropping v1.2.0 from dev's
running history. The corollary skill-level fix (option B in the
ticket) — updating /release to source the previous CHANGELOG from
upstream/main — is filed as a follow-up and out of scope for this PR.
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#181): /agdr skill — searchable AgDR library (#186)
Adds /agdr — a portfolio-wide index for Agent Decision Records.
Walks apexyard.projects.yaml, reads each project's docs/agdr/*.md
(local clone if available, else gh api fallback), parses the optional
YAML frontmatter for category + projects, and answers four queries:
- /agdr browse list across the portfolio, grouped by category
- /agdr search <term> full-text grep across all bodies, returns
<project>/AgDR-NNNN paths + matching paragraph
- /agdr show <id> print a specific record, disambiguates duplicates
- /agdr stats counts per category (the marketing-slide tile,
now backed by real data)
Six-category taxonomy: architecture | tech-stack | security | patterns
| integrations | other. Legacy AgDRs without frontmatter remain
first-class — they bucket as `other` and are flagged in browse so
operators can migrate at their own pace.
Backwards-compatible template change: templates/agdr.md gains an
optional `category:` (and optional `projects:`) line in the existing
frontmatter block. Omitting the line keeps every existing AgDR valid;
the skill defaults to `other` when missing.
Doc note in workflows/sdlc.md § Phase 2 points at /agdr search for
"have we decided this before?" lookups before drafting a design.
Smoke test in .claude/hooks/tests/test_agdr_skill.sh covers the
parser the spec specifies — frontmatter extraction, category
bucketing (including the legacy default-to-other path), id reading,
stats aggregation, and search match counts. 18 assertions, all green;
12 pre-existing test suites also green (no regressions).
Closes #181
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(me2resh/apexyard#183): /launch-check trend tracking (#185)
- Persist each run as JSON under <projects_dir>/<name>/launch-check/runs/
(timestamp + branch + commit + per-dimension scores + verdict + top_risks)
- Append "Trend (last 5 runs)" section to the per-run summary when at least 2
prior runs exist — markdown table + ASCII score chart
- Add /launch-check trend mode for read-only trend rendering (no full audit)
- Auto-derive notes column from score-delta vs previous run
(e.g. "Security +12, Analytics +10")
- Opt-in commit via .launch-check-history-tracked marker; gitignored by default
- New helper script render-trend.sh + test_launch_check_trend.sh (21 cases)
- Resolve projects dir via portfolio_projects_dir helper (no hardcoded path)
- Schema is forward-compatible — extra fields preserved on framework upgrade
AgDR-0014 documents the chart format / schema / opt-in choices.
Closes me2resh/apexyard#183
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#182): /status --briefing + bin/apexyard status CLI shim (#187)
* feat(me2resh/apexyard#182): /status --briefing + bin/apexyard CLI shim
Make slide 6's `$ apexyard status` invocation real — the smallest of three
slide-reality tickets, demonstrating the marketing-to-implementation
pattern before #181 / #183 land.
- New helper at .claude/skills/status/briefing.sh — computes the 4-line
"where am I" briefing (active workspace, active ticket, branch,
role-set) so the logic is testable in isolation and runnable from a
plain shell. Workspace inference walks up from cwd to the ops-fork
root (same algorithm as _lib-portfolio-paths.sh and /start-ticket).
- Ticket reads from .claude/session/tickets/<workspace> first, falls
back to .claude/session/current-ticket. Role-set is inferred from
the active ticket's GitHub labels (v1: backend / frontend / qa /
security / platform / sre / data / ux / ui / product / tech-lead,
plus the long forms). No match emits the explicit "<none — inferred
per task>" placeholder so the four-line shape is constant.
- New CLI shim bin/apexyard delegates `apexyard status` to the same
helper after walking up to find the ops-fork root. Works from any
workspace/<name>/ clone or the fork itself; symlink onto PATH to
install (no shadowing of the `claude` binary).
- /status SKILL.md gains a "Briefing mode" section documenting the
--briefing / -b flag; default /status output is unchanged.
- docs/multi-project.md "Daily workflow" demonstrates the new
invocation alongside /inbox and /status.
- New smoke tests at .claude/hooks/tests/test_status_briefing.sh — 8
cases covering ops-root cwd, workspace cwd, unknown cwd, ops-fallback
marker, per-project marker priority, label-based role inference, the
no-matching-label path, and the constant-four-line shape.
Closes me2resh/apexyard#182
* fix(me2resh/apexyard#182): replace curios-dog with example-app in SKILL.md output sample (leak fix)
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* docs(#178): LSP integration spike — measurement + recommendation (#184)
* docs(#178): LSP spike — token-savings measurement + integration findings
- Phase 1: estimated token cost A vs B for three representative queries on a real TS Lambda backend (~9,750 LOC); shallow semantic queries see ~3-23x input-token savings, multi-hop traces see ~1.4x
- Phase 2: verified Claude Code shipped first-party LSP support in v2.0.74 (Dec 2025), gated behind ENABLE_LSP_TOOL=1, wired in via the plugin system (.lsp.json); MCP-wrapped LSP shims (cclsp, lsp-mcp) remain as fallback
- Phase 3: recommends Option 3 — interactive clone-first prompt at end of /handover, preserving today's "never auto-clone" principle while making the deep-dive path discoverable
- Phase 4: AgDR Y-statement and option matrix sketched; full AgDR is a follow-up if the spike says go
- Recommendation: GO. Adopt the built-in tool, document the opt-in path, change /handover to offer clone-first interactively. No code changes beyond /handover SKILL.md; no novel integration to maintain
Closes me2resh/apexyard#178
* docs: fix markdownlint MD031/MD032 in spike report
Add blank lines around lists and a fenced code block to satisfy
markdownlint-cli2 in CI. Pure formatting, no content change.
Refs me2resh/apexyard#178
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* docs(me2resh/apexyard#190): annotate LSP-aware skills with opt-in callouts (#191)
- Add identical-shape "LSP-aware (optional, recommended)" callout near
the top of each of the four code-aware SKILL.md files.
- Per-skill savings number is the only variation:
- /code-review: ~3-15× cheaper for semantic queries
- /threat-model: ~3-15× shallow, ~1.4-5× multi-hop traces
- /security-review: ~3-15× cheaper for semantic querie…
* chore(#109): project-configurable ticket / branch / commit / PR schema (#118)
* chore(#109): project-configurable ticket / branch / commit / PR schema
Lift the prefix / type whitelists hardcoded across skills, hooks, and
CI into a versioned JSON config read through a shared shell library.
Shipped defaults at .claude/project-config.defaults.json; per-fork
overrides at the optional .claude/project-config.json; one reader
(_lib-read-config.sh) that every consumer now uses.
Added:
- .claude/project-config.defaults.json (v1 schema)
- .claude/hooks/_lib-read-config.sh (shared reader)
- docs/project-config.md (schema reference + extension guide)
- docs/agdr/AgDR-0006-project-configurable-ticket-schema.md
Migrated (still pass with no config present via last-resort fallback):
- validate-branch-name.sh → .branch.type_whitelist
- validate-commit-format.sh → .commit.type_whitelist (legacy
`commit_types` top-level key honoured as backward-compat fallback)
- validate-pr-create.sh → .pr.title_type_whitelist
- /feature, /task, /bug skills reference the config in their Rules
sections; none hardcodes the list any more
Unlocks subsequent config-readers for #107 / #110 / #111 / #112 /
#113 / #114 / #115 — each extends the schema under its own subtree
without further changes to the loader.
https://github.com/me2resh/apexyard/issues/109
* fix(#109): satisfy markdownlint MD032 and MD060 on new docs
Auto-fix MD032 (blank lines around lists) in AgDR-0006 and format
table-separator rows with surrounding spaces (MD060) in both new
doc files. Content unchanged; CI green.
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#110): add block-private-refs-in-public-repos.sh hook (#119)
Adds a new PreToolUse hook that blocks gh issue/PR/comment creation and
gh api .../issues|/pulls calls targeting a public framework repo
(default: me2resh/apexyard + whatever `upstream` resolves to) when the
title or body references any registered private project from
apexyard.projects.yaml (by name, repo slug, owner/repo#N ticket ref, or
workspace path).
The hook is a sibling to check-secrets.sh — both scan outgoing content
for identifiers that should never leave the local environment. Skip
marker `<!-- private-refs: allow -->` in the body lets a deliberate
reference through with a visible warning.
Files touched:
- .claude/hooks/block-private-refs-in-public-repos.sh (new)
- .claude/hooks/tests/test_block_private_refs.sh (new)
- .claude/rules/leak-protection.md (new)
- .claude/settings.json (wire PreToolUse matchers for the 5 gh shapes)
- docs/rule-audit.md (append section 10 + bump counts)
Refs: https://github.com/me2resh/apexyard/issues/110
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#115): add warn-stale-review-markers.sh PostToolUse hook (#120)
Fires after `git push` to surface review markers that have gone stale
because new commits were pushed past an existing Rex / CEO / design
approval. The merge gate already catches this at `gh pr merge` time,
but only then -- this hook closes the gap by flagging it immediately
at push-time so the author isn't surprised at merge.
- `.claude/hooks/warn-stale-review-markers.sh`
- PostToolUse, non-blocking (PostToolUse exit 2 would push noise
into the conversation; this hook is purely informational).
- Resolves the PR HEAD via `gh pr view --json headRefOid` -- same
source-of-truth as the merge-gate hooks post-apexyard#47 / #55.
Falls back to local HEAD with a visible WARN when gh is offline.
- Silent on: no PR for branch, no markers, fresh markers,
failed push (detected via `rejected` / `failed to push` /
`fatal:` / `error:` markers in tool_response.stderr).
- Modes: `warn` (default) prints one stderr line per stale marker;
`delete` opts in to auto-removal via
`.claude/project-config.json` -> `review_markers.on_stale`.
TODO(apexyard#109): switch to the shared project-config reader
once it lands.
- `.claude/settings.json`
- Wires the hook on PostToolUse / Bash / `git push *`.
- `docs/rule-audit.md`
- Adds a row under section 3 (Code review & PR quality) and
bumps the mechanized count 26 -> 27 / total 73 -> 74.
- `.claude/hooks/tests/test_warn_stale_review_markers.sh`
- 8 cases: no PR, no markers, fresh markers, stale rex / ceo /
design (warn), delete mode, failed push. All pass locally.
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#111): upgrade pre-push-gate from advisory reminder to blocking check-runner (#121)
* chore(#111): upgrade pre-push-gate from reminder to blocking check-runner
Previously pre-push-gate.sh just printed a checklist of things to run
locally before pushing — it was advisory. The rule it enforces is a
HARD STOP per pr-workflow.md. That asymmetry meant agents routinely
pushed broken work and discovered it only when CI went red.
Replaces the reminder with a blocking runner that reads the list of
shell commands from project config (.pre_push.commands) and executes
them in sequence before a push is allowed through. First non-zero
exit blocks the push with exit 2 and prints the failing command plus
the last 20 lines of its output.
- Config key: .pre_push.commands[] — array of {name, run} objects.
Shipped default is an empty list (hook stays a no-op on repos that
haven't configured their checks yet, including the framework repo
itself until it wires its own CI).
- Emergency bypass: '<!-- pre-push: skip -->' in the HEAD commit
message. Grep-able on purpose so bypasses stay auditable.
- Fail-fast: once a command fails, the rest don't run. Parallel
execution is a follow-up polish.
- 7 test cases in .claude/hooks/tests/test_pre_push_gate.sh — all
pass on the shipped default + a minimal custom config.
Updates docs/rule-audit.md to flip "partial" → "yes" for the
"before git push" rule.
Integrates with the shared config reader landed in #109.
https://github.com/me2resh/apexyard/issues/111
* fix(#111): remove orphaned footnote reference from rule-audit
The previous advisory-mode footnote was superseded by pre-push-111
but its definition was accidentally kept, tripping markdownlint MD053
(unused reference definition). Drop it.
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#107): add validate-issue-structure.sh PreToolUse hook (#122)
Mechanically enforces the ticket body schema when an agent files raw
`gh issue create` calls instead of going through the interactive
/feature, /task, /bug skills. Matches bracketed title prefix
([Feature] / [Chore] / [Bug] / [Docs] / etc.) against
`.ticket.required_sections` in project-config, and blocks (exit 2)
when any required section is missing or empty. Skip marker
`<!-- validate-issue-structure: skip -->` bypasses with a visible
stderr WARN for legitimate off-template tickets (epics, meta-threads).
Changes:
- .claude/hooks/validate-issue-structure.sh — the hook; reads schema
via the shared _lib-read-config.sh, with inlined defaults for bare
checkouts predating the config-schema rollout. Handles
--body / --body-file / -F path.
- .claude/project-config.defaults.json — extends .ticket with
required_sections (Feature/Chore/Refactor/Testing/CI/Docs/Bug) and
skip_marker; other .ticket fields untouched.
- .claude/settings.json — new PreToolUse matcher on Bash(gh issue
create *) alongside the existing suggest-ticket-template.sh and
block-private-refs-in-public-repos.sh hooks.
- .claude/hooks/tests/test_validate_issue_structure.sh — 15 cases
covering pass + fail paths per prefix, empty section detection,
skip marker, unknown prefix, non-gh invocation, --body-file path.
- docs/rule-audit.md — new section 11 row, mechanized count +1.
Upstream ticket: https://github.com/me2resh/apexyard/issues/107
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#112): add require-agdr-for-arch-pr.sh PreToolUse hook (#123)
Closes the asymmetry noted in .claude/rules/agdr-decisions.md: every
other HARD STOP in the ruleset (merge approval, ticket-first,
migration-first) is mechanically enforced, but the /decide HARD STOP
was prose-only. The commit-time hook require-agdr-for-arch-changes.sh
catches one architectural change at commit; this new PR-time hook
catches the cumulative diff so reviewers always have a pointer to the
decision record.
- New hook at .claude/hooks/require-agdr-for-arch-pr.sh
- Fires on Bash(gh pr create *)
- Parses --title/--body/--body-file/-F <path>
- Resolves base branch from --base, else upstream/dev, origin/dev,
upstream/main, origin/main, main, master (in that order)
- Computes `git diff <merge-base>..HEAD --name-only`
- Triggers on any changed file matching .agdr_trigger_paths[], OR any
dep-file addition (package.json via jq key-set diff; other
dep files via a commented +/- line-count heuristic — version
bumps match +/- counts and do not fire)
- Blocks (exit 2) with a helpful message naming the triggers and
pointing at /decide if the body has no `AgDR-\d+-[a-z0-9-]+`
reference
- Skip marker `<!-- agdr: not-applicable -->` bypasses with a
visible WARN on stderr
- Silent exit 0 on non-gh commands, empty diffs, unresolvable base
- Wired via .claude/settings.json PreToolUse Bash(gh pr create *)
- Adds two new top-level keys to .claude/project-config.defaults.json:
agdr_trigger_paths (shell globs — domain/, infrastructure/,
migrations/, *.tf, .github/workflows/, etc.)
agdr_trigger_dep_files (literal basenames — package.json,
pyproject.toml, Cargo.toml, go.mod, Gemfile)
Hook has inline fallback defaults kept in sync.
- Adds docs/rule-audit.md entry in the AgDR section; bumps mechanized
count 26 to 27 and total rows 73 to 74.
- Adds .claude/hooks/tests/test_require_agdr_for_arch_pr.sh (7 cases;
all green): path-triggered without AgDR (block), with AgDR (pass),
dep-file added (block), version-only bump (no fire), skip marker
(pass + warn), non-matching diff (pass), non-gh command (no-op).
Closes https://github.com/me2resh/apexyard/issues/112
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#113): require Testing section in PR body (config-driven) (#124)
Extends validate-pr-create.sh with a required-sections check that
replaces the hardcoded Glossary-only grep. The list of required H2
headings is project-configurable via `.pr.required_sections[]`.
Shipped default is ["Testing", "Glossary"], matching the canonical PR
description shape in workflows/code-review.md.
- Each entry must appear as `## <Name>` (case-insensitive).
- Empty sections are tolerated at this layer (the issue-structure hook
#107 does stricter empty-content checks for issue bodies; for PR
bodies, empty sections are left to the reviewer's judgement).
- Skip marker `<!-- pr-sections: skip -->` bypasses with a visible
stderr WARN — for trivial PRs (lint-only fixes, version bumps)
where the full template is overkill.
- Reads from project config via the shared _lib-read-config.sh (#109).
Inline fallback matches shipped defaults so bare checkouts predating
#109 keep working.
- 8 test cases cover: all-sections pass, each missing section,
missing-both (both errors printed), skip marker, case-insensitive
headings, H3 rejection.
https://github.com/me2resh/apexyard/issues/113
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#114): enforce single Closes-keyword per PR body (#125)
* chore(#114): enforce single Closes-keyword per PR body
Caps distinct auto-closing references (close/closes/closed, fix/fixes/
fixed, resolve/resolves/resolved + N or owner/repo+N) at one per PR
body. Closes the loophole where the title validator limited the title
to one ticket but multiple Closes lines in the body would still auto-
close all of them on merge.
- Scans stripped of fenced code blocks so closing keywords inside a
code sample do not count.
- Distinct counting: the same number referenced twice (e.g. via Fixes
and Closes) counts as one.
- Cross-repo refs (owner/repo+N) count normally.
- Opt-in escape hatch: pr.allow_multiple_closes=true in
project-config disables the check for teams that deliberately batch
rollbacks or dependency bumps.
- Per-PR bypass: a multi-close-approved HTML comment in the body
prints a visible stderr WARN and lets that PR through. Grep-able
trace so bypasses are auditable.
- 10 test cases cover: one close passes, no-keyword passes, two
distinct block, three mixed block, same-number-twice passes, code-
fence-ignored, skip marker, cross-ref without keyword, opt-in
config, cross-repo close.
Reads configuration via the shared _lib-read-config.sh (apexyard+109).
https://github.com/me2resh/apexyard/issues/114
* fix(#114): strip inline backticks and tilde fences from close-count scan
Rex caught a self-reflexive bug in the initial commit: documentation
mentioning closing keywords inside inline backticks (say a PR body
that explains the new hook with examples) counted as real closes, and
a skip marker inside inline backticks silently bypassed the check.
Future PRs that document the feature would trip the same trap.
Fix the code-region stripper to cover:
- Triple-backtick fences (already handled)
- Tilde fences (new)
- Inline-backtick spans (new)
Also run the skip-marker check against the stripped body, so a marker
used purely as documentation no longer activates a real bypass.
Three new test cases pin the behaviour:
- closing keywords in inline backticks are ignored
- skip marker inside inline backticks does NOT bypass
- tilde fences also get stripped
13/13 tests pass.
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#108): add /tickets-batch skill for bulk-file flow (#127)
The fast happy path for filing 5–20 structured tickets in one intent
without dropping to raw `gh issue create` (non-conformant) or running
`/feature` 20 times serially (~100 turns of interview).
- .claude/skills/tickets-batch/SKILL.md — new skill spec. Asks
shared-context questions (priority, epic, area-labels, repo) ONCE
for the whole batch, then runs a ≤3-question micro-interview per
ticket (type, one-line purpose, optional clarification when the
inference is low-confidence). Confirms the full batch as a table,
then files each via specific `gh issue create` calls (never a
bulk JSON dump — the validator runs per-issue). Output conforms
to `.ticket.required_sections` by construction. Caps at 20
tickets per invocation.
- CLAUDE.md — added a row for /tickets-batch in the Available
Skills table; bumped the count references from 33 to 34.
Refs https://github.com/me2resh/apexyard/issues/108
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#117): add /fan-out skill + parallel-work rule doc (#128)
- Add `.claude/skills/fan-out/SKILL.md` — spawns N parallel Agent calls
in a single assistant message, with per-task agent type, worktree
isolation, and foreground/background mode. Caps at 5 concurrent
agents. Refuses fan-out when tasks share file write targets or have
sequential dependencies. Includes pre-spawn active-ticket safety
check and worktree merge-back flow that pauses on conflict.
- Add `.claude/rules/parallel-work.md` — trigger heuristic for when an
agent should proactively offer fan-out (>= 2 file-independent,
context-independent, individually substantial work items). Pairs
with the skill: rule says when, skill says how.
- Update `CLAUDE.md` — bump rules count to 9, skills count to 34, add
`/fan-out` row to the skills table.
Refs https://github.com/me2resh/apexyard/issues/117
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#116): adopt release-cut branch model (dev/main + tags) — framework only (#126)
* chore(#116): adopt release-cut branch model (dev/main + tags)
Formalises the dev/main split that already exists informally — dev is
the daily-work branch where every PR lands, main is release-only,
tagged with semver on each merge. Framework-only: managed projects
under apexyard governance stay trunk-based.
Added:
- AgDR-0007 — decision record (options table covers full git flow vs
trunk-only vs gitflow-lite; chose gitflow-lite)
- /release skill — diff dev against main, propose semver bump from
conventional commits, generate CHANGELOG, open release PR, tag
after merge
- docs/release-process.md — prose runbook for cutting a release
(manual fallback for the skill)
- .git.protected_branches in project-config.defaults.json
(main/master/dev/develop)
Modified:
- block-main-push.sh — now blocks direct pushes/commits to all
configured protected branches (was: hardcoded main/master). Reads
.git.protected_branches via the shared config reader (apexyard#109).
- CLAUDE.md — new section under Git Conventions explaining the
dev/main model + the framework-only scope. Skill table entry for
/release. Skills count bumped to 34.
- docs/multi-project.md — note that upstream/main is release-only
and the dev/main split is framework-only.
Non-consequences (per AgDR-0007):
- No release/* or hotfix/* branches. Hotfixes are normal patches
cut quickly. Revisit if multi-version maintenance becomes a need.
- No automatic on-merge issue closing for dev PRs. The release PR's
body aggregates all Closes references for the batch and triggers
auto-close en masse when it merges to main. Manual close in the
meantime.
- CI workflows trigger on pull_request regardless of base, so
dev-targeting PRs already get the full check matrix — no
workflow file edits needed.
https://github.com/me2resh/apexyard/issues/116
* fix(#116): satisfy markdownlint MD032 + MD060 in 116 docs
Auto-fix added blank lines around lists in AgDR-0007 (MD032) and
spaced the table separators in AgDR-0007 + multi-project.md (MD060).
Content unchanged.
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* fix(#106): CHANGELOG fallback in drift hook for squash-merged forks (#129)
* fix(#106): CHANGELOG fallback in drift hook for squash-merged forks
The v1.1.0 tag-reachability check (`git tag --merged main`) misfires on
forks that sync via GitHub's default squash-merge: the squash collapses
the upstream-tag commit into a synthetic SHA, the tag stops being
reachable, and the banner keeps firing forever.
Discovered live on the first real-world `/update` flow: ops fork
squash-merged the v1.1.0 sync PR, banner kept saying "v1.1.0 available"
even though the fork was content-caught-up.
This commit adds a CHANGELOG-content fallback that fires only when the
primary tag check fails. If the fork's main has a heading
`## [X.Y.Z]` matching the upstream tag's version, treat the release as
absorbed and stay silent. Tolerant grep (matches the apexyard CHANGELOG
format from v1.1.0 onward, with leading-`v` stripping for tag→heading
conversion).
The merge-commit and rebase paths are unchanged — primary tag check
still works for them, and the fallback never fires when it shouldn't.
Test coverage (5 cases):
- squash-merge fork caught up to v1.1.0 → silent (the fix)
- merge-commit fork caught up to v1.1.0 → silent (regression check)
- fork stopped at v1.0.0 → banner fires
- fork has its own newer tag → silent
- squash-merge but no CHANGELOG on fork → banner fires (no false silence)
Records the strategy update in docs/agdr/AgDR-0008-…md (extends
AgDR-0005's tag-based-drift design).
https://github.com/me2resh/apexyard/issues/106
* fix(#106): satisfy markdownlint MD032/MD060 + shellcheck SC2164
Rex flagged two CI-blocking issues on the original 106 commit:
- AgDR-0008 had three bulleted sub-lists in the Consequences section
without surrounding blank lines (MD032). Added blanks and padded the
one tight-pipe table separator (MD060).
- The 106 test fixture had five subshell `cd "$fk"` calls without
`|| exit 1` (SC2164). Added the guard to all five.
5/5 tests still pass after the fix. No semantic change to the hook
or the test logic.
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#130): add /validate-idea skill — lightweight pre-spec gate (#131)
A 10-minute, 5-question check that sits between /idea and /write-spec.
Designed for solo founders running ApexYard — not a heavyweight
methodology like event storming or Wardley mapping.
Five questions, asked one at a time:
1. Who is this specifically for?
2. What do they do today instead?
3. What's the smallest version that proves the value?
4. What would prove this is wrong? (kill criteria)
5. Build, buy, or rent?
Output: a one-page validation doc with a GREEN/YELLOW/RED verdict.
RED auto-updates the IDEA-NNN backlog row to WONTDO.
Integration:
/idea — adds an optional default-no "Validate now?" step after
capture (and after the optional GitHub Issue offer).
/handover — adds a conditional "this looks dormant, validate?"
step at the end of the integration plan, gated on the dormancy
heuristic (last commit > 90d AND zero open PRs AND no recent
issue activity). Healthy projects don't see the prompt.
CLAUDE.md skills count bumped to 35; new skills row added.
https://github.com/me2resh/apexyard/issues/130
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat: configurable voice prompts on assistant pause (AgDR-0009-voice-prompts-on-pause) (#135)
Stop hook that speaks the assistant's question aloud (Jarvis-style)
when it pauses for user input. Initial phase is macOS-only via `say`,
no voice input — user replies via keyboard.
Default OFF. Adopters opt in by overriding `voice_prompts.enabled` to
true in `.claude/project-config.json`.
Files:
- .claude/hooks/voice-prompt-on-pause.sh — Stop hook with config gate,
trigger heuristic (questions-only by default), markdown stripping,
sentence-boundary truncation, fire-and-forget say invocation
- .claude/hooks/tests/test_voice_prompt_on_pause.sh — 9 cases covering
disabled-default, enabled+question, enabled+statement, approved-pattern,
abc-menu, malformed-transcript, no-say-on-PATH, trigger-always,
markdown-stripping
- .claude/project-config.defaults.json — voice_prompts schema block
added (enabled, voice, max_chars, rate_wpm, trigger), default OFF
- .claude/settings.json — new Stop hook entry wired with the standard
ops-root resolver wrapper
- docs/agdr/AgDR-0009-voice-prompts-on-pause.md — design rationale,
options matrix (status quo / macOS say / cloud TTS / ML detection),
consequences, future phases
- docs/project-config.md — new "Voice prompts" section with override
examples and privacy notes
Test mode: hook respects VOICE_PROMPTS_SYNC=1 to run say synchronously
(test runners need this so assertions don't race against orphaned
background processes). Production invocations always run async.
Future phases (out of scope here, AgDR §"Future phases"):
- Phase 2: cross-platform TTS (Linux espeak, Windows SpeechSynthesizer)
- Phase 3: cloud TTS providers (OpenAI, ElevenLabs) — privacy AgDR-worthy
- Phase 4: voice input via Whisper-based STT
- Phase 5: per-message overrides
Refs: https://github.com/me2resh/apexyard/issues/134
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#141): add /debug skill — structured hypothesis-driven debugging (#142)
* feat(#141): add /debug skill — structured hypothesis-driven debugging
Adds a methodology skill that enforces five disciplines:
1. Capture the symptom precisely (exact URL, exact response, exact step)
2. Read the architecture before guessing (map every layer the request
touches, file by file)
3. Form a hypothesis ladder (3–5 candidates, each with an explicit
evidence test that confirms or refutes it)
4. Gather evidence first, fix second
5. Verify the fix against the original symptom evidence (re-run the
same `curl` / browser repro you used in step 4 — unit tests
verify code, not feature, correctness)
Stack appendices (Web, Desktop) carry stack-specific surface-evidence
requirements (step 1), architecture-surface maps (step 2), and
evidence-tests cookbooks (step 4). The methodology body stays portable
across stacks; appendices are where stack-specific knowledge accrues
over time.
Web appendix covers browser routing, framework configs (Next/Nuxt/Vite),
SPA-fallback layers, CDN, origin, the shared API client, backend
handlers, and auth providers. Desktop appendix covers Electron / Tauri
/ native-shell concerns: app entry points, IPC bridges, native modules,
auto-updater, sandbox / entitlements, code signing, crash reports.
Includes "When NOT to use" guidance so the methodology overhead doesn't
sandbag simple bugs (typos, off-by-ones, greenfield exploration).
Motivated by a real OAuth debug session in a managed project where
three sequential fixes chased adjacent symptoms because each was
hypothesis-then-fix without evidence in between. The skill is the
"never do that again" guardrail.
Closes #141
* fix(#141): scrub private project issue numbers from anti-pattern table
Rex review on PR #142 caught that line 155 of the skill's anti-pattern
table still named the originating PRs (#375, #377, #380) from the
private project where the methodology was first exercised. The PR body
and commit message were correctly abstracted earlier, but this in-file
reference slipped through — the leak-protection hook only scans gh
issue/pr writes, not staged file content, so mechanical enforcement
didn't catch it.
Replaced with "Three sequential PRs chasing the same symptom because
each was based on a different guess (no evidence test in between
cycles)" — same pedagogical value, zero attribution.
Refs #141
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* docs(#143): document split-portfolio mode + add /setup privacy gate (#144)
Adopters on GitHub Free with any private project hit a silent privacy bug
following today's docs: forking apexyard makes a public fork that you cannot
later flip to private (GitHub policy), and committing `apexyard.projects.yaml`
+ `projects/<name>/` to the fork publishes private project names + handover
findings on a public GitHub repo.
This PR documents the supported workaround — split-portfolio mode — and
adds an upfront privacy gate to the /setup skill so new adopters never
hit the trip-wire silently.
docs/multi-project.md:
- New "Two setup modes — pick the one that matches your privacy needs"
section before TL;DR, with a side-by-side table and the explicit
trip-wire callout.
- Existing TL;DR retitled "TL;DR — single-fork mode (default)" with a
one-line pointer to the split-portfolio section.
- New "Split-portfolio mode — public framework + private portfolio"
section between the existing setup steps and the directory-layout
section. Includes:
- The two-repo layout (~/ops/apexyard public + ~/ops/portfolio private)
- 7-step setup walkthrough with copy-pasteable commands
- Daily workflow + upstream sync notes (both unchanged)
- Trade-offs (two repos to maintain, two clones per machine, one
upstream-sync conflict path on `projects/README.md`)
- "Migrating from single-fork to split-portfolio" recovery flow with
the explicit warning that GitHub Issue / PR edit history survives a
force-push and must be redacted separately
.claude/skills/setup/SKILL.md:
- New Step 2a: privacy gate — asks "are any projects private?" before
proposing the config. Branches on the answer:
- All public → single-fork mode
- GitHub Pro / Team / Enterprise → single-fork mode (private
forks of public repos
are supported on those
plans)
- Any private + GitHub Free → split-portfolio mode
- New Step 2b: walks through the split-portfolio setup interactively
(private repo create, sibling clone, gitignore + symlink) when the
privacy gate triggers.
- Detection: `test -L apexyard.projects.yaml` short-circuits Step 2b
for adopters already in split mode.
- Explicit "do NOT auto-migrate" rule for adopters already in single-fork
mode with private names already pushed — that path is destructive
(force-push history rewrite + redact issue/PR bodies + delete backup
branch) and warrants a deliberate, eyes-open run, not a /setup side
effect.
Out of scope for this PR (tracked separately on #143):
- `portfolio:` config block in `onboarding.yaml` schema
- Skill audit + refactor to honour configured `registry` / `projects_dir`
/ `ideas_backlog` paths instead of hardcoded fork-relative paths
- `/split-portfolio` migration helper skill that automates the recovery
flow currently documented manually
This is the docs-and-setup-question minimum-viable starter — the
framework code refactor is mechanical and lands as a follow-up.
Refs #143
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#145): portfolio config + self-healing + /split-portfolio helper (#147)
* feat(me2resh/apexyard#145): portfolio config block + self-healing + /split-portfolio helper
Closes the framework primitive deferred from me2resh/apexyard#144. Adds
first-class config-driven path resolution for the portfolio registry,
projects dir, and ideas backlog, with self-healing surfacing of broken
config at session start, plus a new /split-portfolio skill that automates
the destructive recovery flow.
Schema, helper, and hook:
- .claude/project-config.defaults.json: new portfolio: block
(registry, projects_dir, ideas_backlog) with defaults matching
today's single-fork layout
- .claude/hooks/_lib-portfolio-paths.sh: new sourceable helper exposing
portfolio_registry, portfolio_projects_dir, portfolio_ideas_backlog,
portfolio_validate, portfolio_clear_cache. Resolves relative paths
against the ops-fork root.
- .claude/hooks/check-portfolio-config.sh: new SessionStart hook —
silent on OK, one-line banner on broken config, never blocks session
- .claude/hooks/tests/test_portfolio_paths.sh: 13 cases covering
defaults, absolute/relative overrides, validate states, cache clear
Skill audit (18 SKILL.md files):
- Adds Path resolution callout pointing at the helper
- handover bash blocks now source helper and use $(portfolio_registry)
instead of literal apexyard.projects.yaml
- setup Step 2b now writes the portfolio: config block (recommended)
and validates via portfolio_validate before declaring success;
symlink approach kept as legacy fallback
New skill (.claude/skills/split-portfolio/SKILL.md):
- 10-step migration with explicit operator-confirmation gates at each
destructive step (force-push, body redaction, branch deletion)
- --verify mode: read-only state report (mode, paths, validate, drift)
- --dry-run mode: prints commands without executing
- Pre-flight refusals: already-private fork, paid GitHub plan, dirty
working tree, already-migrated state
- Step 9 writes the portfolio: config block (not symlinks) — symlink
fallback documented for adopters on older framework versions
- Step 9 surfaces the GitHub timeline-API survival caveat verbatim
- Idempotent re-runs: detects partial-migration state and resumes
Docs (docs/multi-project.md):
- Layout section describes both modes (config-block recommended,
symlink legacy) with self-healing notes
- Setup steps split into config-block mode and legacy symlink mode
- Migration section now points at /split-portfolio skill; manual
recipe preserved as fallback
AgDR (docs/agdr/AgDR-0010-portfolio-config-and-self-healing.md):
- Full Y-statement, options, decision, consequences, future phases
- Schema decision rationale: project-config.json over onboarding.yaml
because runtime path resolution belongs in project-config
Closes me2resh/apexyard#145
Refs me2resh/apexyard#146 (delivered same PR; closed manually post-merge
per the single-Closes-keyword rule in validate-pr-create.sh)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(me2resh/apexyard#145): markdownlint MD031 — blank lines around fences
CI's markdownlint-cli2 (v0.34.0) flagged the JSON + bash fenced code
blocks I added in setup/SKILL.md Step 2b without surrounding blank
lines. Added the required blanks. No content change.
Refs me2resh/apexyard#147
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(me2resh/apexyard#148): correct privacy-gate wording — adopter action, not framework auto-publish (#149)
The privacy-gate wording introduced in PR #144 (and unchanged in PR #147)
attributed the publication to the framework rather than the adopter:
"the standard fork-and-commit setup will silently publish your private
project names on a public GitHub repo"
That's factually wrong. ApexYard never pushes anything without explicit
operator approval — the publication only happens when the adopter
themselves runs git push. The "silently publish" framing read as if the
framework auto-publishes, which is misleading and undermines trust in
the rest of the framework's safety claims.
Two prose-only edits, no code, no behavior change:
- .claude/skills/setup/SKILL.md Step 2a — replaced "will silently
publish ..." with adopter-action language ("you might accidentally
publish ... a stray git push after registering them — I won't push
without your approval, but the risk is on the adopter once the data
is committed locally")
- docs/multi-project.md trip-wire callout — replaced "silently publish
their portfolio names the moment they push" with "risk accidentally
publishing their portfolio names with a stray push (the framework
itself never pushes without operator approval, but once the registry
is committed locally the next push exposes it)"
Verified: `grep -r "silently publish" .claude/skills/ docs/` returns no
hits.
Closes me2resh/apexyard#148
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#150): bootstrap-skill exemption + Bash-write coverage (#152)
Closes the legitimate-bypass case (me2resh/apexyard#150) and the
illegitimate-bypass case (me2resh/apexyard#151) together so the
ticket-first gate is coherent. Shipping either alone would leave a
window where the framework is internally inconsistent — see AgDR-0011
for the full rationale.
Bootstrap exemption (me2resh/apexyard#150):
- .claude/session/active-bootstrap marker, written by /setup,
/handover, /update, /split-portfolio on entry; cleared on exit
- SessionStart sweep (clear-bootstrap-marker.sh) for stale markers
from interrupted sessions
- require-active-ticket.sh reads the marker and exempts skills on
the configured ticket.bootstrap_skills list
- bootstrap_skills list lives in .claude/project-config.defaults.json
(extendable per fork via .claude/project-config.json)
Bash-write coverage (me2resh/apexyard#151):
- new _lib-detect-bash-write.sh — heuristic detector for output
redirection, tee, sed -i, awk -i inplace, python/node/ruby
embedded interpreters
- require-active-ticket.sh + require-migration-ticket.sh now fire
on Bash in addition to Edit|Write|MultiEdit
- design choice: false-negatives preferred over false-positives
(the matcher errs toward "let through" rather than block legit
read-only commands)
Tests: 32 unit cases on the lib + 12 integration cases on the hook,
including the exact me2resh/apexyard#151 bypass repro from the issue
body.
Closes me2resh/apexyard#150
Will manually close me2resh/apexyard#151 post-merge per the
single-Closes-per-PR rule (precedent: AgDR-0010 / PR #147).
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#153): extend Bash-write matcher beyond first-version coverage (#155)
Closes me2resh/apexyard#153.
Extends `_lib-detect-bash-write.sh` (introduced for me2resh/apexyard#151
in PR #152) with the matcher families flagged by Rex's review of #152.
AgDR-0011 already frames the matcher as a living list extended on
observation; this commit just walks the list.
New matcher families:
- File-moving builtins: `cp`, `mv`, `rm`, `dd`, `install` (anchored at
command-start; `--help`/`--version` and `git rm`/`git mv` excluded)
- Archive / network writes: `tar -x` / `tar --extract`, `curl -o` /
`--output`, `wget -O` / `--output-document`
- Additional embedded interpreters: `perl -e`, `php -r` (keyword-gated
like python/node/ruby); `go run`, `deno run`/`deno script.ts`,
`bun run`/`bun script.ts` (categorical script runners)
- Python helpers: `pathlib.Path().touch()`, `shutil.copy*`,
`shutil.move`, `os.rename` added to the `python -c` and python
heredoc keyword list
- Heredoc variants for `ruby` and `node` (previously only python
heredoc was covered)
Extractor extensions:
- `cp` / `mv`: last positional arg
- `curl -o` / `--output`: file argument
- `wget -O` / `--output-document`: file argument
- `tar -x`, `go run`, `deno`, `bun`, `perl -e`, `php -r`: return empty
(caller applies gate categorically per AgDR-0011)
Test count rose from 32 to 86. Negative-class counterexamples cover
the trickiest false-positive surfaces: `tar -t` listing, `cp --help`,
`rm --version`, `git rm`, `curl` bare URL fetch, `wget` bare URL fetch,
`deno fmt`, `deno test`, `go build`. Existing
`test_require_active_ticket_bash.sh` regression suite still passes 12/12.
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(#154): mock gh in test sandboxes to remove live-tracker dependency (#156)
- add _lib-mock-gh.sh helper that installs a fake `gh` on the sandbox PATH;
intercepts `gh issue view <N> ... --json ...` and returns synthetic
`{"number":N,"state":"OPEN"}` (overridable per-num via mock_gh_set_state)
- wire the shim into test_single_closes_per_pr.sh and
test_validate_pr_required_sections.sh so the validator's CLOSED-issue
refusal no longer breaks the suite when upstream issues are closed
- both files previously failed every case (0/13 and 0/8) because their PR
titles reference #114 / #113, which are now CLOSED upstream
- post-fix: 13/13 and 8/8; full suite remains green
Closes me2resh/apexyard#154
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#132): structured CEO marker + same-turn merge in /approve-merge (#158)
* feat(#132): structured CEO marker + same-turn merge in /approve-merge
Closes me2resh/apexyard#132 (drop the "stop before merge" rule) and
me2resh/apexyard#48 (harden CEO marker against self-approval bypass)
together. The two threads compose — see AgDR-0012 for the full
rationale.
Streamline (#132):
- /approve-merge now runs `gh pr merge --squash --delete-branch`
in the same turn as the marker write, by default
- --no-merge opt-out preserves the deferred-merge case
- The discrete approval moment is the SKILL INVOCATION, not a
follow-up "now do the merge" message
Harden (#48):
- CEO marker is now a structured key/value file with required fields:
sha=<HEAD>
approved_by=user
skill_version=2
Validated by block-unreviewed-merge.sh; bare-SHA legacy markers
rejected with a clear "stale format" error pointing at /approve-merge
- The model's bare `echo SHA > <pr>-ceo.approved` bypass is now
mechanically rejected. Forging the structured fields requires a
deliberate, visible rule violation rather than a one-line accident
- Optional audit fields (approved_at, approval_summary) capture the
"what did the user say when they approved" trail
- Rex marker stays bare-SHA — different threat model (automated
reviewer, not human authorization moment)
pr-workflow.md reframed: "the load-bearing rule is explicit per-PR
approval, not two user messages." The merge is a deterministic
consequence of the approval invocation.
Tests: 12 cases on the hardened hook covering the new format end-to-end
(valid v2, missing rex/ceo, bare-SHA legacy rejected, missing
approved_by, wrong approved_by, skill_version=1, sha mismatch,
non-merge no-op, gh-api shape gated). Full suite: 205/205 across 13
test files.
Will manually close me2resh/apexyard#48 post-merge per the
single-Closes-per-PR rule (precedent: PR #152 / AgDR-0011).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#132): redact private project reference from AgDR-0012
Abstracted two references to a registered private project that named
the project's owner/repo. The leak-protection hook caught one in the
PR body; this fixup removes the matching references from the
AgDR-0012 file content (which would otherwise have shipped the names
to me2resh/apexyard public repo via the merge).
The pre-existing reference at .claude/rules/pr-workflow.md:130
(documenting #47) is untouched — it predates this PR and is already
on the public repo's history.
Refs me2resh/apexyard#132.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#132): markdownlint blanks-around-fences + typo fix
Two small fixups against red CI / Rex feedback:
- AgDR-0012 line 63: fenced code block now has a blank line before it
(MD031). markdownlint-cli2 0.13.0 was rejecting the indented fence
inside the bullet because the fence's preceding line was the bullet
text (no blank).
- approve-merge SKILL.md line 172: typo `deferes` → `defers` (Rex flag
on PR #158).
Refs me2resh/apexyard#132.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#157): remove voice-prompts feature + correct hook/skill counts (#161)
* chore(#157): remove voice-prompts-on-pause feature + correct hook/skill counts
Closes me2resh/apexyard#157 (sunset the voice-prompts feature) and
me2resh/apexyard#77 (hook count off-by-one in CHANGELOG / CLAUDE.md)
in one bundled PR. AgDR-0013 supersedes AgDR-0009 with the full
rationale; both AgDRs are preserved (decision records are append-only
history).
Removed (#157):
- .claude/hooks/voice-prompt-on-pause.sh
- .claude/hooks/tests/test_voice_prompt_on_pause.sh
- Stop matcher block in .claude/settings.json (became empty after
voice removal)
- voice_prompts block in .claude/project-config.defaults.json
- "## Voice prompts" section in docs/project-config.md
- voice_prompts mention in AgDR-0010 line 32 (replaced with
leak_protection / ticket as still-current example config blocks)
Preserved:
- docs/agdr/AgDR-0009-voice-prompts-on-pause.md — historical record;
new "Superseded by: AgDR-0013" header at the top
- AgDR-0010 line 115 reference to AgDR-0009 — still accurate as a
historical pattern reference
Counts corrected (#77):
- CHANGELOG.md v0.3.0 stats: "17 hooks" → "18 hooks" (historical
fix — at v0.3.0 there were actually 18 hooks)
- CLAUDE.md table line: "18 shell scripts" → "24 shell scripts"
(current count after this removal)
- CLAUDE.md table line: "35 slash commands" → "39 slash commands"
- CLAUDE.md "Available skills (34)" → "Available skills (39)"
- CLAUDE.md quick-reference "Skills (35 slash commands)" →
"(39 slash commands)"
Why bundled: #77's correct count depends on whether voice is still in
the framework. Shipping #77 before #157 would write a number that's
wrong by one again the moment #157 lands. Same shape as previous
bundles (PR #152 / AgDR-0011, PR #158 / AgDR-0012).
Why no adopter-facing changelog mention of the voice removal: the
feature never reached a tagged release on main. v1.1.0 didn't have
it; v1.2.0 won't have it. From the adopter's perspective there's
nothing to retire. AgDR-0013 captures the framework's internal record
for future contributors. See AgDR-0013 § "No adopter-facing changelog
mention".
Tests: full hook test suite green (196 cases across 12 files —
test_voice_prompt_on_pause.sh removed). No regressions.
Will manually close me2resh/apexyard#77 post-merge per the
single-Closes-per-PR rule (precedent: PRs #152, #158).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#157): unwire voice from settings + configs + docs + AgDR-0013
Continuation of d09d7b5 (the file deletions). Squash-merge will
collapse both into one PR commit. This commit captures:
- .claude/settings.json — Stop matcher block removed (was the only
hook in it; entire matcher gone)
- .claude/project-config.defaults.json — voice_prompts block + its
_comment removed
- docs/project-config.md — "## Voice prompts" section removed
- docs/agdr/AgDR-0009-voice-prompts-on-pause.md — "Superseded by"
header added at the top, content otherwise preserved as history
- docs/agdr/AgDR-0010-portfolio-config-and-self-healing.md — line 32
example reference swapped from voice_prompts to
leak_protection / ticket (still-current config blocks)
- docs/agdr/AgDR-0013-sunset-voice-prompts.md — new supersession AgDR
- CLAUDE.md — hook count 18 → 24, skill count 35 → 39 (three
occurrences each, all aligned to current reality)
- CHANGELOG.md v0.3.0 stats — "17 hooks" → "18 hooks" historical fix
(#77 acceptance criterion 1)
Refs me2resh/apexyard#157 + me2resh/apexyard#77.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#157): markdownlint MD028 + resolve stale AgDR-numbering ref
Three small fixups against Rex CHANGES-REQUESTED on PR #161:
- AgDR-0009 line 3: promote "Superseded by:" header out of a
blockquote. The original "I decided ..." canonical blockquote at
line 5 was being merged with the new supersession blockquote
(markdownlint MD028 — "no blanks inside blockquote").
- AgDR-0013 line 3: same shape — "Supersedes:" header now a plain
bold paragraph, canonical "I decided ..." blockquote untouched.
- approve-merge SKILL.md line ~187: stale conditional reference
"AgDR-0012 (or 0013 — depends on whether voice-removal lands
first)" — order is now resolved (12 = approve-merge bundle, 13 =
voice removal). Drop the parenthetical.
Refs me2resh/apexyard#157 + me2resh/apexyard#77.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#160): multi-tab terminal demo on the landing site (#162)
Landing-site `site/index.html` terminal demo previously played one
canonical flow ("one ticket, start to finish") on autoplay. With the
v1.2.0 skill surface expansion (4 new skills, 8+ new hooks), one flow
no longer represents the framework's breadth.
Adds tabs to the terminal chrome — four flows visitors can either let
auto-cycle or click directly:
1. one ticket — existing flow, unchanged content
2. /handover — adopt an external repo into the portfolio
3. /setup — first-run framework bootstrap on a fresh fork
4. /fan-out — spawn 3 parallel agents on independent tickets
Auto-advance: on completion of the active tab's script, the demo
pauses ~1.8s then advances to the next tab. Loops at the end. User
can interrupt by clicking any tab or hitting Replay.
Implementation:
- HTML chrome — single title span replaced with a tablist of 4 button
tabs. ARIA `role="tablist"` / `role="tab"` / `aria-selected` so
keyboard + screen-reader users get the same semantics as sighted
ones.
- CSS — new `.shell-demo__tabs` + `.shell-demo__tab` with an accent
underline on the active tab. Tabs scroll horizontally on narrow
viewports (mobile responsive).
- JS — refactored the existing IIFE from one `script` array to an
array of four. Added `setActiveTab()` for ARIA state, `play(idx)`
takes a tab index, end-of-script auto-advances to `(idx + 1) % N`
unless the user clicked away during the pause. Adds a new `cmd`
type alongside `you` for slash-command invocations (renders with
the same `>` prompt prefix). prefers-reduced-motion still bails
early and leaves the static seed visible.
- Static seed (the non-JS fallback) still shows tab 0's content, so
reduced-motion / no-JS visitors see the one-ticket flow as before.
Hero metrics also corrected to current reality (#77 / PR #161 covers
CLAUDE.md and CHANGELOG.md; this PR catches the same numbers in the
landing site):
Skills 32 → 39
Hooks 18 → 24
The tabs ship in v1.2.0 alongside the framework changes that make
the new flows worth showcasing.
Refs me2resh/apexyard#160 (release v1.2.0 + landing-site refresh).
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#163): default the split-portfolio sibling repo name to <fork>-portfolio (#164)
Closes me2resh/apexyard#163.
The split-portfolio mode docs and skills previously suggested
`your-org/ops` as the default name for the private sibling repo, and
`portfolio/` as the local clone directory. Both too generic — adopters
running multiple ops setups end up with `your-org/ops` collisions, and
a bare `portfolio/` dir gives no signal about which framework it
belongs to when it sits next to other unrelated `portfolio/` dirs.
`<fork>-portfolio` is now the default — keeps the relationship to the
public fork explicit on disk and on GitHub. If the fork is named
`your-org/apexyard`, the portfolio defaults to
`your-org/apexyard-portfolio`. If the fork was renamed (e.g. `cos`),
the portfolio defaults to `cos-portfolio`. Adopters with custom names
keep working — the `portfolio:` config block resolves whatever path
they configured.
Files updated:
docs/multi-project.md
- Layout diagrams use `apexyard-portfolio/` as the sibling
- Setup walkthrough Step 2 + Step 3 use `your-org/apexyard-portfolio`
and explain the `<fork>-portfolio` pattern
- Config-block + symlink path examples updated to
`../apexyard-portfolio/...`
- Daily workflow + cross-machine clone commands updated
- The two existing `your-org/ops` references that remain are
fork-rename examples (lines 52, 64) — kept as-is, since renaming
the fork to `ops` is still valid (the portfolio would then
default to `ops-portfolio`)
.claude/skills/setup/SKILL.md
- Step 2b's "default suggestion" for the private repo name is now
`your-org/<fork>-portfolio`, computed dynamically from the
fork's repo name via `gh repo view --json name -q .name` so the
suggestion is correct even when the fork was renamed
- Clone command no longer needs a second arg — the repo name IS
the directory name
- Config-block paths updated to `../apexyard-portfolio/...`
.claude/skills/split-portfolio/SKILL.md
- Step 3's suggested-name template is now `<account>/<fork>-portfolio`
with the same dynamic-fork-name resolution
Mechanism unchanged. The `portfolio:` config block in
`.claude/project-config.json` still takes any path; this PR is purely
default-suggestion + example prose.
No tests required (skills are markdown instructions; no automated
coverage today).
Refs `apexyard.projects.yaml.example` uses "ops repo" as a generic
term meaning "the operational management fork" — not a name — kept
as-is.
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#165): skills reference page on the landing site + changelog link (#167)
Closes me2resh/apexyard#165.
Adds a public, browseable index of every apexyard slash command
alongside a one-click changelog link from the homepage nav.
site/skills.html (new):
- Lists all 39 skills currently shipping in .claude/skills/
- Each entry: slash command, argument hint, description (taken
verbatim from the SKILL.md frontmatter so the page matches the
runtime exactly)
- 10 categories: Setup & onboarding, Daily ops, Tickets & ideas,
Specs & decisions, Code review & merge, Architecture & dev tools,
Production-readiness audits, Workflow primitives, Communications,
Deprecated
- Same brutalist-terminal design tokens as the homepage — JetBrains
Mono, paper-cream background, single warning-red accent, sharp
corners. Inlined CSS to keep the static-only no-build-step
convention; design vars duplicated rather than extracted to a
shared file (~18 vars; cheap to keep in sync).
- Mobile responsive — skill grid collapses to single-column under
720px; titlebar nav hides non-CTA items on narrow viewports.
- Reduced-motion friendly (no animation in the first place).
- Internal anchor TOC at the top so the page scans in seconds.
site/index.html (nav addition):
- Added two nav links to the titlebar between "what's in the box"
and the github CTA:
• skills → ./skills.html
• changelog → https://github.com/me2resh/apexyard/releases
- The changelog link points at the GitHub releases page (not the
raw CHANGELOG.md file) so it auto-resolves to the latest tagged
release on each visit. v1.2.0 lands and the link is already there.
No new dependencies, no build step, no JS for the skills page. The
existing site convention (one-html-file-per-route, inlined CSS,
optional progressive-enhancement JS) is preserved.
Refs me2resh/apexyard#160 (release v1.2.0 + landing-site refresh) —
this is the second site-side deliverable for that ticket; the release
tag itself follows once me2resh/apexyard#159 (testing) closes.
Follow-up worth a separate ticket: a small generator script that
walks .claude/skills/*/SKILL.md, parses YAML frontmatter, and emits
the skills.html sections automatically. Out of scope for v1.2.0 —
first version is hand-curated and will need maintenance until that
generator lands.
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#168): accept release/vN.N.N branches + release(...) PR titles (#169)
Closes me2resh/apexyard#168.
The /release skill prescribed `release/vA.B.C` as the source-branch
name and `release: vA.B.C` as the PR title for the dev → main release
PR (per AgDR-0007). Both were rejected by the framework's own
validators:
validate-branch-name.sh required {type}/{TICKET-ID}-{description};
release/v1.2.0 has no ticket-id portion.
validate-pr-create.sh required type(SCOPE): form with `release` not
in pr.title_type_whitelist.
The contradiction surfaced cutting v1.2.0 — the first release under
the dev/main model.
Three small changes:
1. .claude/hooks/validate-branch-name.sh — added an early-out branch
that accepts ^release/vN.N.N(-rcN)?$ as a valid name. Narrow,
intentional exception for the framework's release-cut convention;
release branches don't carry a ticket-id because the release itself
IS the ticket.
2. .claude/project-config.defaults.json — added "release" to
pr.title_type_whitelist so a title like `release(#160): v1.2.0`
passes validate-pr-create.sh's existing regex unchanged.
3. .claude/skills/release/SKILL.md step 4 — corrected the prescribed
PR title to `release(#<release-ticket>): vA.B.C` so future /release
invocations produce a title that satisfies the validators by
construction.
Tested:
bash .claude/hooks/validate-branch-name.sh against:
release/v1.2.0 → 0 (allowed, release-special-case)
release/v1.2.0-rc1 → 0 (allowed, RC variant)
release/v9.9.9 → 0 (allowed)
release/foo → 2 (correctly blocked)
release/v1 → 2 (correctly blocked)
chore/GH-168-fix → 0 (allowed, standard pattern)
feature/GH-1-x → 0 (allowed, standard pattern)
Full hook test suite: 196/196 cases green across 12 test files.
Refs: surfaced 2026-05-04 cutting the first release under AgDR-0007.
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#170): exempt release/vN.N.N from validate-pr-create's branch-id check (#171)
Closes me2resh/apexyard#170.
Completes the work started in #169 (closing #168). #169 added a
release-pattern early-out to validate-branch-name.sh so release/vN.N.N
branches pass the branch-name validator. But validate-pr-create.sh has
its own independent branch-id check at line 273 that #169 didn't
touch — and it still rejects release/v1.2.0 because that name doesn't
contain a ticket-id substring.
This is the same class of contradiction #168 fixed; the fix is the
same shape. Add the same release-pattern early-out to the branch-id
check in validate-pr-create.sh:
- if the branch matches ^release/vN.N.N(-rcN)?$ → exempt (release
branches don't carry ticket-ids; the release itself is the ticket)
- otherwise → require a ticket-id substring as before
#168's acceptance criterion 3 ("validate-pr-create.sh accepts a PR
title `release(#160): v1.2.0` against the `release/v1.2.0` branch")
was checked off based on the title regex alone but didn't catch the
secondary branch-id check living in the same file. Surfaced trying
to open the v1.2.0 release PR.
Tested:
bash .claude/hooks/validate-pr-create.sh against:
release/v1.2.0 → 0 (allowed, exempt)
release/v1.2.0-rc1 → 0 (allowed, RC variant)
chore/GH-1-fix → 0 (allowed, has ticket-id)
release/foo → 2 (correctly blocked)
chore/no-ticket → 2 (correctly blocked)
Refs me2resh/apexyard#168 (the parent bug) + #169 (the partial fix).
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#173): sync CHANGELOG.md from main → dev (#174)
Closes me2resh/apexyard#173.
The release-cut model (AgDR-0007 / #116) squash-merges
release/vN.N.N → main, which means the v1.2.0 CHANGELOG section that
landed on main via PR #172 was never propagated back to dev. This
commit copies main's CHANGELOG.md verbatim onto dev so the v1.2.0
section is now present on both branches.
The diff is exactly the v1.2.0 entry being prepended; no other lines
change.
Without this sync, the next release PR cut from dev would build a
v1.3.0 section on top of v1.1.0, silently dropping v1.2.0 from dev's
running history. The corollary skill-level fix (option B in the
ticket) — updating /release to source the previous CHANGELOG from
upstream/main — is filed as a follow-up and out of scope for this PR.
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#181): /agdr skill — searchable AgDR library (#186)
Adds /agdr — a portfolio-wide index for Agent Decision Records.
Walks apexyard.projects.yaml, reads each project's docs/agdr/*.md
(local clone if available, else gh api fallback), parses the optional
YAML frontmatter for category + projects, and answers four queries:
- /agdr browse list across the portfolio, grouped by category
- /agdr search <term> full-text grep across all bodies, returns
<project>/AgDR-NNNN paths + matching paragraph
- /agdr show <id> print a specific record, disambiguates duplicates
- /agdr stats counts per category (the marketing-slide tile,
now backed by real data)
Six-category taxonomy: architecture | tech-stack | security | patterns
| integrations | other. Legacy AgDRs without frontmatter remain
first-class — they bucket as `other` and are flagged in browse so
operators can migrate at their own pace.
Backwards-compatible template change: templates/agdr.md gains an
optional `category:` (and optional `projects:`) line in the existing
frontmatter block. Omitting the line keeps every existing AgDR valid;
the skill defaults to `other` when missing.
Doc note in workflows/sdlc.md § Phase 2 points at /agdr search for
"have we decided this before?" lookups before drafting a design.
Smoke test in .claude/hooks/tests/test_agdr_skill.sh covers the
parser the spec specifies — frontmatter extraction, category
bucketing (including the legacy default-to-other path), id reading,
stats aggregation, and search match counts. 18 assertions, all green;
12 pre-existing test suites also green (no regressions).
Closes #181
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(me2resh/apexyard#183): /launch-check trend tracking (#185)
- Persist each run as JSON under <projects_dir>/<name>/launch-check/runs/
(timestamp + branch + commit + per-dimension scores + verdict + top_risks)
- Append "Trend (last 5 runs)" section to the per-run summary when at least 2
prior runs exist — markdown table + ASCII score chart
- Add /launch-check trend mode for read-only trend rendering (no full audit)
- Auto-derive notes column from score-delta vs previous run
(e.g. "Security +12, Analytics +10")
- Opt-in commit via .launch-check-history-tracked marker; gitignored by default
- New helper script render-trend.sh + test_launch_check_trend.sh (21 cases)
- Resolve projects dir via portfolio_projects_dir helper (no hardcoded path)
- Schema is forward-compatible — extra fields preserved on framework upgrade
AgDR-0014 documents the chart format / schema / opt-in choices.
Closes me2resh/apexyard#183
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#182): /status --briefing + bin/apexyard status CLI shim (#187)
* feat(me2resh/apexyard#182): /status --briefing + bin/apexyard CLI shim
Make slide 6's `$ apexyard status` invocation real — the smallest of three
slide-reality tickets, demonstrating the marketing-to-implementation
pattern before #181 / #183 land.
- New helper at .claude/skills/status/briefing.sh — computes the 4-line
"where am I" briefing (active workspace, active ticket, branch,
role-set) so the logic is testable in isolation and runnable from a
plain shell. Workspace inference walks up from cwd to the ops-fork
root (same algorithm as _lib-portfolio-paths.sh and /start-ticket).
- Ticket reads from .claude/session/tickets/<workspace> first, falls
back to .claude/session/current-ticket. Role-set is inferred from
the active ticket's GitHub labels (v1: backend / frontend / qa /
security / platform / sre / data / ux / ui / product / tech-lead,
plus the long forms). No match emits the explicit "<none — inferred
per task>" placeholder so the four-line shape is constant.
- New CLI shim bin/apexyard delegates `apexyard status` to the same
helper after walking up to find the ops-fork root. Works from any
workspace/<name>/ clone or the fork itself; symlink onto PATH to
install (no shadowing of the `claude` binary).
- /status SKILL.md gains a "Briefing mode" section documenting the
--briefing / -b flag; default /status output is unchanged.
- docs/multi-project.md "Daily workflow" demonstrates the new
invocation alongside /inbox and /status.
- New smoke tests at .claude/hooks/tests/test_status_briefing.sh — 8
cases covering ops-root cwd, workspace cwd, unknown cwd, ops-fallback
marker, per-project marker priority, label-based role inference, the
no-matching-label path, and the constant-four-line shape.
Closes me2resh/apexyard#182
* fix(me2resh/apexyard#182): replace curios-dog with example-app in SKILL.md output sample (leak fix)
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* docs(#178): LSP integration spike — measurement + recommendation (#184)
* docs(#178): LSP spike — token-savings measurement + integration findings
- Phase 1: estimated token cost A vs B for three representative queries on a real TS Lambda backend (~9,750 LOC); shallow semantic queries see ~3-23x input-token savings, multi-hop traces see ~1.4x
- Phase 2: verified Claude Code shipped first-party LSP support in v2.0.74 (Dec 2025), gated behind ENABLE_LSP_TOOL=1, wired in via the plugin system (.lsp.json); MCP-wrapped LSP shims (cclsp, lsp-mcp) remain as fallback
- Phase 3: recommends Option 3 — interactive clone-first prompt at end of /handover, preserving today's "never auto-clone" principle while making the deep-dive path discoverable
- Phase 4: AgDR Y-statement and option matrix sketched; full AgDR is a follow-up if the spike says go
- Recommendation: GO. Adopt the built-in tool, document the opt-in path, change /handover to offer clone-first interactively. No code changes beyond /handover SKILL.md; no novel integration to maintain
Closes me2resh/apexyard#178
* docs: fix markdownlint MD031/MD032 in spike report
Add blank lines around lists and a fenced code block to satisfy
markdownlint-cli2 in CI. Pure formatting, no content change.
Refs me2resh/apexyard#178
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* docs(me2resh/apexyard#190): annotate LSP-aware skills with opt-in callouts (#191)
- Add identical-shape "LSP-aware (optional, recommended)" callout near
the top of each of the four code-aware SKILL.md files.
- Per-skill savings number is the only variation:
- /code-review: ~3-15× cheaper for semantic queries
- /threat-model: ~3-15× shallow, ~1.4-5× multi-hop traces
- /security-review: ~3-15× cheaper for semantic querie…
* chore(#109): project-configurable ticket / branch / commit / PR schema (#118)
* chore(#109): project-configurable ticket / branch / commit / PR schema
Lift the prefix / type whitelists hardcoded across skills, hooks, and
CI into a versioned JSON config read through a shared shell library.
Shipped defaults at .claude/project-config.defaults.json; per-fork
overrides at the optional .claude/project-config.json; one reader
(_lib-read-config.sh) that every consumer now uses.
Added:
- .claude/project-config.defaults.json (v1 schema)
- .claude/hooks/_lib-read-config.sh (shared reader)
- docs/project-config.md (schema reference + extension guide)
- docs/agdr/AgDR-0006-project-configurable-ticket-schema.md
Migrated (still pass with no config present via last-resort fallback):
- validate-branch-name.sh → .branch.type_whitelist
- validate-commit-format.sh → .commit.type_whitelist (legacy
`commit_types` top-level key honoured as backward-compat fallback)
- validate-pr-create.sh → .pr.title_type_whitelist
- /feature, /task, /bug skills reference the config in their Rules
sections; none hardcodes the list any more
Unlocks subsequent config-readers for #107 / #110 / #111 / #112 /
#113 / #114 / #115 — each extends the schema under its own subtree
without further changes to the loader.
https://github.com/me2resh/apexyard/issues/109
* fix(#109): satisfy markdownlint MD032 and MD060 on new docs
Auto-fix MD032 (blank lines around lists) in AgDR-0006 and format
table-separator rows with surrounding spaces (MD060) in both new
doc files. Content unchanged; CI green.
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#110): add block-private-refs-in-public-repos.sh hook (#119)
Adds a new PreToolUse hook that blocks gh issue/PR/comment creation and
gh api .../issues|/pulls calls targeting a public framework repo
(default: me2resh/apexyard + whatever `upstream` resolves to) when the
title or body references any registered private project from
apexyard.projects.yaml (by name, repo slug, owner/repo#N ticket ref, or
workspace path).
The hook is a sibling to check-secrets.sh — both scan outgoing content
for identifiers that should never leave the local environment. Skip
marker `<!-- private-refs: allow -->` in the body lets a deliberate
reference through with a visible warning.
Files touched:
- .claude/hooks/block-private-refs-in-public-repos.sh (new)
- .claude/hooks/tests/test_block_private_refs.sh (new)
- .claude/rules/leak-protection.md (new)
- .claude/settings.json (wire PreToolUse matchers for the 5 gh shapes)
- docs/rule-audit.md (append section 10 + bump counts)
Refs: https://github.com/me2resh/apexyard/issues/110
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#115): add warn-stale-review-markers.sh PostToolUse hook (#120)
Fires after `git push` to surface review markers that have gone stale
because new commits were pushed past an existing Rex / CEO / design
approval. The merge gate already catches this at `gh pr merge` time,
but only then -- this hook closes the gap by flagging it immediately
at push-time so the author isn't surprised at merge.
- `.claude/hooks/warn-stale-review-markers.sh`
- PostToolUse, non-blocking (PostToolUse exit 2 would push noise
into the conversation; this hook is purely informational).
- Resolves the PR HEAD via `gh pr view --json headRefOid` -- same
source-of-truth as the merge-gate hooks post-apexyard#47 / #55.
Falls back to local HEAD with a visible WARN when gh is offline.
- Silent on: no PR for branch, no markers, fresh markers,
failed push (detected via `rejected` / `failed to push` /
`fatal:` / `error:` markers in tool_response.stderr).
- Modes: `warn` (default) prints one stderr line per stale marker;
`delete` opts in to auto-removal via
`.claude/project-config.json` -> `review_markers.on_stale`.
TODO(apexyard#109): switch to the shared project-config reader
once it lands.
- `.claude/settings.json`
- Wires the hook on PostToolUse / Bash / `git push *`.
- `docs/rule-audit.md`
- Adds a row under section 3 (Code review & PR quality) and
bumps the mechanized count 26 -> 27 / total 73 -> 74.
- `.claude/hooks/tests/test_warn_stale_review_markers.sh`
- 8 cases: no PR, no markers, fresh markers, stale rex / ceo /
design (warn), delete mode, failed push. All pass locally.
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#111): upgrade pre-push-gate from advisory reminder to blocking check-runner (#121)
* chore(#111): upgrade pre-push-gate from reminder to blocking check-runner
Previously pre-push-gate.sh just printed a checklist of things to run
locally before pushing — it was advisory. The rule it enforces is a
HARD STOP per pr-workflow.md. That asymmetry meant agents routinely
pushed broken work and discovered it only when CI went red.
Replaces the reminder with a blocking runner that reads the list of
shell commands from project config (.pre_push.commands) and executes
them in sequence before a push is allowed through. First non-zero
exit blocks the push with exit 2 and prints the failing command plus
the last 20 lines of its output.
- Config key: .pre_push.commands[] — array of {name, run} objects.
Shipped default is an empty list (hook stays a no-op on repos that
haven't configured their checks yet, including the framework repo
itself until it wires its own CI).
- Emergency bypass: '<!-- pre-push: skip -->' in the HEAD commit
message. Grep-able on purpose so bypasses stay auditable.
- Fail-fast: once a command fails, the rest don't run. Parallel
execution is a follow-up polish.
- 7 test cases in .claude/hooks/tests/test_pre_push_gate.sh — all
pass on the shipped default + a minimal custom config.
Updates docs/rule-audit.md to flip "partial" → "yes" for the
"before git push" rule.
Integrates with the shared config reader landed in #109.
https://github.com/me2resh/apexyard/issues/111
* fix(#111): remove orphaned footnote reference from rule-audit
The previous advisory-mode footnote was superseded by pre-push-111
but its definition was accidentally kept, tripping markdownlint MD053
(unused reference definition). Drop it.
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#107): add validate-issue-structure.sh PreToolUse hook (#122)
Mechanically enforces the ticket body schema when an agent files raw
`gh issue create` calls instead of going through the interactive
/feature, /task, /bug skills. Matches bracketed title prefix
([Feature] / [Chore] / [Bug] / [Docs] / etc.) against
`.ticket.required_sections` in project-config, and blocks (exit 2)
when any required section is missing or empty. Skip marker
`<!-- validate-issue-structure: skip -->` bypasses with a visible
stderr WARN for legitimate off-template tickets (epics, meta-threads).
Changes:
- .claude/hooks/validate-issue-structure.sh — the hook; reads schema
via the shared _lib-read-config.sh, with inlined defaults for bare
checkouts predating the config-schema rollout. Handles
--body / --body-file / -F path.
- .claude/project-config.defaults.json — extends .ticket with
required_sections (Feature/Chore/Refactor/Testing/CI/Docs/Bug) and
skip_marker; other .ticket fields untouched.
- .claude/settings.json — new PreToolUse matcher on Bash(gh issue
create *) alongside the existing suggest-ticket-template.sh and
block-private-refs-in-public-repos.sh hooks.
- .claude/hooks/tests/test_validate_issue_structure.sh — 15 cases
covering pass + fail paths per prefix, empty section detection,
skip marker, unknown prefix, non-gh invocation, --body-file path.
- docs/rule-audit.md — new section 11 row, mechanized count +1.
Upstream ticket: https://github.com/me2resh/apexyard/issues/107
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#112): add require-agdr-for-arch-pr.sh PreToolUse hook (#123)
Closes the asymmetry noted in .claude/rules/agdr-decisions.md: every
other HARD STOP in the ruleset (merge approval, ticket-first,
migration-first) is mechanically enforced, but the /decide HARD STOP
was prose-only. The commit-time hook require-agdr-for-arch-changes.sh
catches one architectural change at commit; this new PR-time hook
catches the cumulative diff so reviewers always have a pointer to the
decision record.
- New hook at .claude/hooks/require-agdr-for-arch-pr.sh
- Fires on Bash(gh pr create *)
- Parses --title/--body/--body-file/-F <path>
- Resolves base branch from --base, else upstream/dev, origin/dev,
upstream/main, origin/main, main, master (in that order)
- Computes `git diff <merge-base>..HEAD --name-only`
- Triggers on any changed file matching .agdr_trigger_paths[], OR any
dep-file addition (package.json via jq key-set diff; other
dep files via a commented +/- line-count heuristic — version
bumps match +/- counts and do not fire)
- Blocks (exit 2) with a helpful message naming the triggers and
pointing at /decide if the body has no `AgDR-\d+-[a-z0-9-]+`
reference
- Skip marker `<!-- agdr: not-applicable -->` bypasses with a
visible WARN on stderr
- Silent exit 0 on non-gh commands, empty diffs, unresolvable base
- Wired via .claude/settings.json PreToolUse Bash(gh pr create *)
- Adds two new top-level keys to .claude/project-config.defaults.json:
agdr_trigger_paths (shell globs — domain/, infrastructure/,
migrations/, *.tf, .github/workflows/, etc.)
agdr_trigger_dep_files (literal basenames — package.json,
pyproject.toml, Cargo.toml, go.mod, Gemfile)
Hook has inline fallback defaults kept in sync.
- Adds docs/rule-audit.md entry in the AgDR section; bumps mechanized
count 26 to 27 and total rows 73 to 74.
- Adds .claude/hooks/tests/test_require_agdr_for_arch_pr.sh (7 cases;
all green): path-triggered without AgDR (block), with AgDR (pass),
dep-file added (block), version-only bump (no fire), skip marker
(pass + warn), non-matching diff (pass), non-gh command (no-op).
Closes https://github.com/me2resh/apexyard/issues/112
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#113): require Testing section in PR body (config-driven) (#124)
Extends validate-pr-create.sh with a required-sections check that
replaces the hardcoded Glossary-only grep. The list of required H2
headings is project-configurable via `.pr.required_sections[]`.
Shipped default is ["Testing", "Glossary"], matching the canonical PR
description shape in workflows/code-review.md.
- Each entry must appear as `## <Name>` (case-insensitive).
- Empty sections are tolerated at this layer (the issue-structure hook
#107 does stricter empty-content checks for issue bodies; for PR
bodies, empty sections are left to the reviewer's judgement).
- Skip marker `<!-- pr-sections: skip -->` bypasses with a visible
stderr WARN — for trivial PRs (lint-only fixes, version bumps)
where the full template is overkill.
- Reads from project config via the shared _lib-read-config.sh (#109).
Inline fallback matches shipped defaults so bare checkouts predating
#109 keep working.
- 8 test cases cover: all-sections pass, each missing section,
missing-both (both errors printed), skip marker, case-insensitive
headings, H3 rejection.
https://github.com/me2resh/apexyard/issues/113
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#114): enforce single Closes-keyword per PR body (#125)
* chore(#114): enforce single Closes-keyword per PR body
Caps distinct auto-closing references (close/closes/closed, fix/fixes/
fixed, resolve/resolves/resolved + N or owner/repo+N) at one per PR
body. Closes the loophole where the title validator limited the title
to one ticket but multiple Closes lines in the body would still auto-
close all of them on merge.
- Scans stripped of fenced code blocks so closing keywords inside a
code sample do not count.
- Distinct counting: the same number referenced twice (e.g. via Fixes
and Closes) counts as one.
- Cross-repo refs (owner/repo+N) count normally.
- Opt-in escape hatch: pr.allow_multiple_closes=true in
project-config disables the check for teams that deliberately batch
rollbacks or dependency bumps.
- Per-PR bypass: a multi-close-approved HTML comment in the body
prints a visible stderr WARN and lets that PR through. Grep-able
trace so bypasses are auditable.
- 10 test cases cover: one close passes, no-keyword passes, two
distinct block, three mixed block, same-number-twice passes, code-
fence-ignored, skip marker, cross-ref without keyword, opt-in
config, cross-repo close.
Reads configuration via the shared _lib-read-config.sh (apexyard+109).
https://github.com/me2resh/apexyard/issues/114
* fix(#114): strip inline backticks and tilde fences from close-count scan
Rex caught a self-reflexive bug in the initial commit: documentation
mentioning closing keywords inside inline backticks (say a PR body
that explains the new hook with examples) counted as real closes, and
a skip marker inside inline backticks silently bypassed the check.
Future PRs that document the feature would trip the same trap.
Fix the code-region stripper to cover:
- Triple-backtick fences (already handled)
- Tilde fences (new)
- Inline-backtick spans (new)
Also run the skip-marker check against the stripped body, so a marker
used purely as documentation no longer activates a real bypass.
Three new test cases pin the behaviour:
- closing keywords in inline backticks are ignored
- skip marker inside inline backticks does NOT bypass
- tilde fences also get stripped
13/13 tests pass.
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#108): add /tickets-batch skill for bulk-file flow (#127)
The fast happy path for filing 5–20 structured tickets in one intent
without dropping to raw `gh issue create` (non-conformant) or running
`/feature` 20 times serially (~100 turns of interview).
- .claude/skills/tickets-batch/SKILL.md — new skill spec. Asks
shared-context questions (priority, epic, area-labels, repo) ONCE
for the whole batch, then runs a ≤3-question micro-interview per
ticket (type, one-line purpose, optional clarification when the
inference is low-confidence). Confirms the full batch as a table,
then files each via specific `gh issue create` calls (never a
bulk JSON dump — the validator runs per-issue). Output conforms
to `.ticket.required_sections` by construction. Caps at 20
tickets per invocation.
- CLAUDE.md — added a row for /tickets-batch in the Available
Skills table; bumped the count references from 33 to 34.
Refs https://github.com/me2resh/apexyard/issues/108
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#117): add /fan-out skill + parallel-work rule doc (#128)
- Add `.claude/skills/fan-out/SKILL.md` — spawns N parallel Agent calls
in a single assistant message, with per-task agent type, worktree
isolation, and foreground/background mode. Caps at 5 concurrent
agents. Refuses fan-out when tasks share file write targets or have
sequential dependencies. Includes pre-spawn active-ticket safety
check and worktree merge-back flow that pauses on conflict.
- Add `.claude/rules/parallel-work.md` — trigger heuristic for when an
agent should proactively offer fan-out (>= 2 file-independent,
context-independent, individually substantial work items). Pairs
with the skill: rule says when, skill says how.
- Update `CLAUDE.md` — bump rules count to 9, skills count to 34, add
`/fan-out` row to the skills table.
Refs https://github.com/me2resh/apexyard/issues/117
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#116): adopt release-cut branch model (dev/main + tags) — framework only (#126)
* chore(#116): adopt release-cut branch model (dev/main + tags)
Formalises the dev/main split that already exists informally — dev is
the daily-work branch where every PR lands, main is release-only,
tagged with semver on each merge. Framework-only: managed projects
under apexyard governance stay trunk-based.
Added:
- AgDR-0007 — decision record (options table covers full git flow vs
trunk-only vs gitflow-lite; chose gitflow-lite)
- /release skill — diff dev against main, propose semver bump from
conventional commits, generate CHANGELOG, open release PR, tag
after merge
- docs/release-process.md — prose runbook for cutting a release
(manual fallback for the skill)
- .git.protected_branches in project-config.defaults.json
(main/master/dev/develop)
Modified:
- block-main-push.sh — now blocks direct pushes/commits to all
configured protected branches (was: hardcoded main/master). Reads
.git.protected_branches via the shared config reader (apexyard#109).
- CLAUDE.md — new section under Git Conventions explaining the
dev/main model + the framework-only scope. Skill table entry for
/release. Skills count bumped to 34.
- docs/multi-project.md — note that upstream/main is release-only
and the dev/main split is framework-only.
Non-consequences (per AgDR-0007):
- No release/* or hotfix/* branches. Hotfixes are normal patches
cut quickly. Revisit if multi-version maintenance becomes a need.
- No automatic on-merge issue closing for dev PRs. The release PR's
body aggregates all Closes references for the batch and triggers
auto-close en masse when it merges to main. Manual close in the
meantime.
- CI workflows trigger on pull_request regardless of base, so
dev-targeting PRs already get the full check matrix — no
workflow file edits needed.
https://github.com/me2resh/apexyard/issues/116
* fix(#116): satisfy markdownlint MD032 + MD060 in 116 docs
Auto-fix added blank lines around lists in AgDR-0007 (MD032) and
spaced the table separators in AgDR-0007 + multi-project.md (MD060).
Content unchanged.
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* fix(#106): CHANGELOG fallback in drift hook for squash-merged forks (#129)
* fix(#106): CHANGELOG fallback in drift hook for squash-merged forks
The v1.1.0 tag-reachability check (`git tag --merged main`) misfires on
forks that sync via GitHub's default squash-merge: the squash collapses
the upstream-tag commit into a synthetic SHA, the tag stops being
reachable, and the banner keeps firing forever.
Discovered live on the first real-world `/update` flow: ops fork
squash-merged the v1.1.0 sync PR, banner kept saying "v1.1.0 available"
even though the fork was content-caught-up.
This commit adds a CHANGELOG-content fallback that fires only when the
primary tag check fails. If the fork's main has a heading
`## [X.Y.Z]` matching the upstream tag's version, treat the release as
absorbed and stay silent. Tolerant grep (matches the apexyard CHANGELOG
format from v1.1.0 onward, with leading-`v` stripping for tag→heading
conversion).
The merge-commit and rebase paths are unchanged — primary tag check
still works for them, and the fallback never fires when it shouldn't.
Test coverage (5 cases):
- squash-merge fork caught up to v1.1.0 → silent (the fix)
- merge-commit fork caught up to v1.1.0 → silent (regression check)
- fork stopped at v1.0.0 → banner fires
- fork has its own newer tag → silent
- squash-merge but no CHANGELOG on fork → banner fires (no false silence)
Records the strategy update in docs/agdr/AgDR-0008-…md (extends
AgDR-0005's tag-based-drift design).
https://github.com/me2resh/apexyard/issues/106
* fix(#106): satisfy markdownlint MD032/MD060 + shellcheck SC2164
Rex flagged two CI-blocking issues on the original 106 commit:
- AgDR-0008 had three bulleted sub-lists in the Consequences section
without surrounding blank lines (MD032). Added blanks and padded the
one tight-pipe table separator (MD060).
- The 106 test fixture had five subshell `cd "$fk"` calls without
`|| exit 1` (SC2164). Added the guard to all five.
5/5 tests still pass after the fix. No semantic change to the hook
or the test logic.
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#130): add /validate-idea skill — lightweight pre-spec gate (#131)
A 10-minute, 5-question check that sits between /idea and /write-spec.
Designed for solo founders running ApexYard — not a heavyweight
methodology like event storming or Wardley mapping.
Five questions, asked one at a time:
1. Who is this specifically for?
2. What do they do today instead?
3. What's the smallest version that proves the value?
4. What would prove this is wrong? (kill criteria)
5. Build, buy, or rent?
Output: a one-page validation doc with a GREEN/YELLOW/RED verdict.
RED auto-updates the IDEA-NNN backlog row to WONTDO.
Integration:
/idea — adds an optional default-no "Validate now?" step after
capture (and after the optional GitHub Issue offer).
/handover — adds a conditional "this looks dormant, validate?"
step at the end of the integration plan, gated on the dormancy
heuristic (last commit > 90d AND zero open PRs AND no recent
issue activity). Healthy projects don't see the prompt.
CLAUDE.md skills count bumped to 35; new skills row added.
https://github.com/me2resh/apexyard/issues/130
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat: configurable voice prompts on assistant pause (AgDR-0009-voice-prompts-on-pause) (#135)
Stop hook that speaks the assistant's question aloud (Jarvis-style)
when it pauses for user input. Initial phase is macOS-only via `say`,
no voice input — user replies via keyboard.
Default OFF. Adopters opt in by overriding `voice_prompts.enabled` to
true in `.claude/project-config.json`.
Files:
- .claude/hooks/voice-prompt-on-pause.sh — Stop hook with config gate,
trigger heuristic (questions-only by default), markdown stripping,
sentence-boundary truncation, fire-and-forget say invocation
- .claude/hooks/tests/test_voice_prompt_on_pause.sh — 9 cases covering
disabled-default, enabled+question, enabled+statement, approved-pattern,
abc-menu, malformed-transcript, no-say-on-PATH, trigger-always,
markdown-stripping
- .claude/project-config.defaults.json — voice_prompts schema block
added (enabled, voice, max_chars, rate_wpm, trigger), default OFF
- .claude/settings.json — new Stop hook entry wired with the standard
ops-root resolver wrapper
- docs/agdr/AgDR-0009-voice-prompts-on-pause.md — design rationale,
options matrix (status quo / macOS say / cloud TTS / ML detection),
consequences, future phases
- docs/project-config.md — new "Voice prompts" section with override
examples and privacy notes
Test mode: hook respects VOICE_PROMPTS_SYNC=1 to run say synchronously
(test runners need this so assertions don't race against orphaned
background processes). Production invocations always run async.
Future phases (out of scope here, AgDR §"Future phases"):
- Phase 2: cross-platform TTS (Linux espeak, Windows SpeechSynthesizer)
- Phase 3: cloud TTS providers (OpenAI, ElevenLabs) — privacy AgDR-worthy
- Phase 4: voice input via Whisper-based STT
- Phase 5: per-message overrides
Refs: https://github.com/me2resh/apexyard/issues/134
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#141): add /debug skill — structured hypothesis-driven debugging (#142)
* feat(#141): add /debug skill — structured hypothesis-driven debugging
Adds a methodology skill that enforces five disciplines:
1. Capture the symptom precisely (exact URL, exact response, exact step)
2. Read the architecture before guessing (map every layer the request
touches, file by file)
3. Form a hypothesis ladder (3–5 candidates, each with an explicit
evidence test that confirms or refutes it)
4. Gather evidence first, fix second
5. Verify the fix against the original symptom evidence (re-run the
same `curl` / browser repro you used in step 4 — unit tests
verify code, not feature, correctness)
Stack appendices (Web, Desktop) carry stack-specific surface-evidence
requirements (step 1), architecture-surface maps (step 2), and
evidence-tests cookbooks (step 4). The methodology body stays portable
across stacks; appendices are where stack-specific knowledge accrues
over time.
Web appendix covers browser routing, framework configs (Next/Nuxt/Vite),
SPA-fallback layers, CDN, origin, the shared API client, backend
handlers, and auth providers. Desktop appendix covers Electron / Tauri
/ native-shell concerns: app entry points, IPC bridges, native modules,
auto-updater, sandbox / entitlements, code signing, crash reports.
Includes "When NOT to use" guidance so the methodology overhead doesn't
sandbag simple bugs (typos, off-by-ones, greenfield exploration).
Motivated by a real OAuth debug session in a managed project where
three sequential fixes chased adjacent symptoms because each was
hypothesis-then-fix without evidence in between. The skill is the
"never do that again" guardrail.
Closes #141
* fix(#141): scrub private project issue numbers from anti-pattern table
Rex review on PR #142 caught that line 155 of the skill's anti-pattern
table still named the originating PRs (#375, #377, #380) from the
private project where the methodology was first exercised. The PR body
and commit message were correctly abstracted earlier, but this in-file
reference slipped through — the leak-protection hook only scans gh
issue/pr writes, not staged file content, so mechanical enforcement
didn't catch it.
Replaced with "Three sequential PRs chasing the same symptom because
each was based on a different guess (no evidence test in between
cycles)" — same pedagogical value, zero attribution.
Refs #141
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* docs(#143): document split-portfolio mode + add /setup privacy gate (#144)
Adopters on GitHub Free with any private project hit a silent privacy bug
following today's docs: forking apexyard makes a public fork that you cannot
later flip to private (GitHub policy), and committing `apexyard.projects.yaml`
+ `projects/<name>/` to the fork publishes private project names + handover
findings on a public GitHub repo.
This PR documents the supported workaround — split-portfolio mode — and
adds an upfront privacy gate to the /setup skill so new adopters never
hit the trip-wire silently.
docs/multi-project.md:
- New "Two setup modes — pick the one that matches your privacy needs"
section before TL;DR, with a side-by-side table and the explicit
trip-wire callout.
- Existing TL;DR retitled "TL;DR — single-fork mode (default)" with a
one-line pointer to the split-portfolio section.
- New "Split-portfolio mode — public framework + private portfolio"
section between the existing setup steps and the directory-layout
section. Includes:
- The two-repo layout (~/ops/apexyard public + ~/ops/portfolio private)
- 7-step setup walkthrough with copy-pasteable commands
- Daily workflow + upstream sync notes (both unchanged)
- Trade-offs (two repos to maintain, two clones per machine, one
upstream-sync conflict path on `projects/README.md`)
- "Migrating from single-fork to split-portfolio" recovery flow with
the explicit warning that GitHub Issue / PR edit history survives a
force-push and must be redacted separately
.claude/skills/setup/SKILL.md:
- New Step 2a: privacy gate — asks "are any projects private?" before
proposing the config. Branches on the answer:
- All public → single-fork mode
- GitHub Pro / Team / Enterprise → single-fork mode (private
forks of public repos
are supported on those
plans)
- Any private + GitHub Free → split-portfolio mode
- New Step 2b: walks through the split-portfolio setup interactively
(private repo create, sibling clone, gitignore + symlink) when the
privacy gate triggers.
- Detection: `test -L apexyard.projects.yaml` short-circuits Step 2b
for adopters already in split mode.
- Explicit "do NOT auto-migrate" rule for adopters already in single-fork
mode with private names already pushed — that path is destructive
(force-push history rewrite + redact issue/PR bodies + delete backup
branch) and warrants a deliberate, eyes-open run, not a /setup side
effect.
Out of scope for this PR (tracked separately on #143):
- `portfolio:` config block in `onboarding.yaml` schema
- Skill audit + refactor to honour configured `registry` / `projects_dir`
/ `ideas_backlog` paths instead of hardcoded fork-relative paths
- `/split-portfolio` migration helper skill that automates the recovery
flow currently documented manually
This is the docs-and-setup-question minimum-viable starter — the
framework code refactor is mechanical and lands as a follow-up.
Refs #143
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#145): portfolio config + self-healing + /split-portfolio helper (#147)
* feat(me2resh/apexyard#145): portfolio config block + self-healing + /split-portfolio helper
Closes the framework primitive deferred from me2resh/apexyard#144. Adds
first-class config-driven path resolution for the portfolio registry,
projects dir, and ideas backlog, with self-healing surfacing of broken
config at session start, plus a new /split-portfolio skill that automates
the destructive recovery flow.
Schema, helper, and hook:
- .claude/project-config.defaults.json: new portfolio: block
(registry, projects_dir, ideas_backlog) with defaults matching
today's single-fork layout
- .claude/hooks/_lib-portfolio-paths.sh: new sourceable helper exposing
portfolio_registry, portfolio_projects_dir, portfolio_ideas_backlog,
portfolio_validate, portfolio_clear_cache. Resolves relative paths
against the ops-fork root.
- .claude/hooks/check-portfolio-config.sh: new SessionStart hook —
silent on OK, one-line banner on broken config, never blocks session
- .claude/hooks/tests/test_portfolio_paths.sh: 13 cases covering
defaults, absolute/relative overrides, validate states, cache clear
Skill audit (18 SKILL.md files):
- Adds Path resolution callout pointing at the helper
- handover bash blocks now source helper and use $(portfolio_registry)
instead of literal apexyard.projects.yaml
- setup Step 2b now writes the portfolio: config block (recommended)
and validates via portfolio_validate before declaring success;
symlink approach kept as legacy fallback
New skill (.claude/skills/split-portfolio/SKILL.md):
- 10-step migration with explicit operator-confirmation gates at each
destructive step (force-push, body redaction, branch deletion)
- --verify mode: read-only state report (mode, paths, validate, drift)
- --dry-run mode: prints commands without executing
- Pre-flight refusals: already-private fork, paid GitHub plan, dirty
working tree, already-migrated state
- Step 9 writes the portfolio: config block (not symlinks) — symlink
fallback documented for adopters on older framework versions
- Step 9 surfaces the GitHub timeline-API survival caveat verbatim
- Idempotent re-runs: detects partial-migration state and resumes
Docs (docs/multi-project.md):
- Layout section describes both modes (config-block recommended,
symlink legacy) with self-healing notes
- Setup steps split into config-block mode and legacy symlink mode
- Migration section now points at /split-portfolio skill; manual
recipe preserved as fallback
AgDR (docs/agdr/AgDR-0010-portfolio-config-and-self-healing.md):
- Full Y-statement, options, decision, consequences, future phases
- Schema decision rationale: project-config.json over onboarding.yaml
because runtime path resolution belongs in project-config
Closes me2resh/apexyard#145
Refs me2resh/apexyard#146 (delivered same PR; closed manually post-merge
per the single-Closes-keyword rule in validate-pr-create.sh)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(me2resh/apexyard#145): markdownlint MD031 — blank lines around fences
CI's markdownlint-cli2 (v0.34.0) flagged the JSON + bash fenced code
blocks I added in setup/SKILL.md Step 2b without surrounding blank
lines. Added the required blanks. No content change.
Refs me2resh/apexyard#147
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(me2resh/apexyard#148): correct privacy-gate wording — adopter action, not framework auto-publish (#149)
The privacy-gate wording introduced in PR #144 (and unchanged in PR #147)
attributed the publication to the framework rather than the adopter:
"the standard fork-and-commit setup will silently publish your private
project names on a public GitHub repo"
That's factually wrong. ApexYard never pushes anything without explicit
operator approval — the publication only happens when the adopter
themselves runs git push. The "silently publish" framing read as if the
framework auto-publishes, which is misleading and undermines trust in
the rest of the framework's safety claims.
Two prose-only edits, no code, no behavior change:
- .claude/skills/setup/SKILL.md Step 2a — replaced "will silently
publish ..." with adopter-action language ("you might accidentally
publish ... a stray git push after registering them — I won't push
without your approval, but the risk is on the adopter once the data
is committed locally")
- docs/multi-project.md trip-wire callout — replaced "silently publish
their portfolio names the moment they push" with "risk accidentally
publishing their portfolio names with a stray push (the framework
itself never pushes without operator approval, but once the registry
is committed locally the next push exposes it)"
Verified: `grep -r "silently publish" .claude/skills/ docs/` returns no
hits.
Closes me2resh/apexyard#148
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#150): bootstrap-skill exemption + Bash-write coverage (#152)
Closes the legitimate-bypass case (me2resh/apexyard#150) and the
illegitimate-bypass case (me2resh/apexyard#151) together so the
ticket-first gate is coherent. Shipping either alone would leave a
window where the framework is internally inconsistent — see AgDR-0011
for the full rationale.
Bootstrap exemption (me2resh/apexyard#150):
- .claude/session/active-bootstrap marker, written by /setup,
/handover, /update, /split-portfolio on entry; cleared on exit
- SessionStart sweep (clear-bootstrap-marker.sh) for stale markers
from interrupted sessions
- require-active-ticket.sh reads the marker and exempts skills on
the configured ticket.bootstrap_skills list
- bootstrap_skills list lives in .claude/project-config.defaults.json
(extendable per fork via .claude/project-config.json)
Bash-write coverage (me2resh/apexyard#151):
- new _lib-detect-bash-write.sh — heuristic detector for output
redirection, tee, sed -i, awk -i inplace, python/node/ruby
embedded interpreters
- require-active-ticket.sh + require-migration-ticket.sh now fire
on Bash in addition to Edit|Write|MultiEdit
- design choice: false-negatives preferred over false-positives
(the matcher errs toward "let through" rather than block legit
read-only commands)
Tests: 32 unit cases on the lib + 12 integration cases on the hook,
including the exact me2resh/apexyard#151 bypass repro from the issue
body.
Closes me2resh/apexyard#150
Will manually close me2resh/apexyard#151 post-merge per the
single-Closes-per-PR rule (precedent: AgDR-0010 / PR #147).
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#153): extend Bash-write matcher beyond first-version coverage (#155)
Closes me2resh/apexyard#153.
Extends `_lib-detect-bash-write.sh` (introduced for me2resh/apexyard#151
in PR #152) with the matcher families flagged by Rex's review of #152.
AgDR-0011 already frames the matcher as a living list extended on
observation; this commit just walks the list.
New matcher families:
- File-moving builtins: `cp`, `mv`, `rm`, `dd`, `install` (anchored at
command-start; `--help`/`--version` and `git rm`/`git mv` excluded)
- Archive / network writes: `tar -x` / `tar --extract`, `curl -o` /
`--output`, `wget -O` / `--output-document`
- Additional embedded interpreters: `perl -e`, `php -r` (keyword-gated
like python/node/ruby); `go run`, `deno run`/`deno script.ts`,
`bun run`/`bun script.ts` (categorical script runners)
- Python helpers: `pathlib.Path().touch()`, `shutil.copy*`,
`shutil.move`, `os.rename` added to the `python -c` and python
heredoc keyword list
- Heredoc variants for `ruby` and `node` (previously only python
heredoc was covered)
Extractor extensions:
- `cp` / `mv`: last positional arg
- `curl -o` / `--output`: file argument
- `wget -O` / `--output-document`: file argument
- `tar -x`, `go run`, `deno`, `bun`, `perl -e`, `php -r`: return empty
(caller applies gate categorically per AgDR-0011)
Test count rose from 32 to 86. Negative-class counterexamples cover
the trickiest false-positive surfaces: `tar -t` listing, `cp --help`,
`rm --version`, `git rm`, `curl` bare URL fetch, `wget` bare URL fetch,
`deno fmt`, `deno test`, `go build`. Existing
`test_require_active_ticket_bash.sh` regression suite still passes 12/12.
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(#154): mock gh in test sandboxes to remove live-tracker dependency (#156)
- add _lib-mock-gh.sh helper that installs a fake `gh` on the sandbox PATH;
intercepts `gh issue view <N> ... --json ...` and returns synthetic
`{"number":N,"state":"OPEN"}` (overridable per-num via mock_gh_set_state)
- wire the shim into test_single_closes_per_pr.sh and
test_validate_pr_required_sections.sh so the validator's CLOSED-issue
refusal no longer breaks the suite when upstream issues are closed
- both files previously failed every case (0/13 and 0/8) because their PR
titles reference #114 / #113, which are now CLOSED upstream
- post-fix: 13/13 and 8/8; full suite remains green
Closes me2resh/apexyard#154
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#132): structured CEO marker + same-turn merge in /approve-merge (#158)
* feat(#132): structured CEO marker + same-turn merge in /approve-merge
Closes me2resh/apexyard#132 (drop the "stop before merge" rule) and
me2resh/apexyard#48 (harden CEO marker against self-approval bypass)
together. The two threads compose — see AgDR-0012 for the full
rationale.
Streamline (#132):
- /approve-merge now runs `gh pr merge --squash --delete-branch`
in the same turn as the marker write, by default
- --no-merge opt-out preserves the deferred-merge case
- The discrete approval moment is the SKILL INVOCATION, not a
follow-up "now do the merge" message
Harden (#48):
- CEO marker is now a structured key/value file with required fields:
sha=<HEAD>
approved_by=user
skill_version=2
Validated by block-unreviewed-merge.sh; bare-SHA legacy markers
rejected with a clear "stale format" error pointing at /approve-merge
- The model's bare `echo SHA > <pr>-ceo.approved` bypass is now
mechanically rejected. Forging the structured fields requires a
deliberate, visible rule violation rather than a one-line accident
- Optional audit fields (approved_at, approval_summary) capture the
"what did the user say when they approved" trail
- Rex marker stays bare-SHA — different threat model (automated
reviewer, not human authorization moment)
pr-workflow.md reframed: "the load-bearing rule is explicit per-PR
approval, not two user messages." The merge is a deterministic
consequence of the approval invocation.
Tests: 12 cases on the hardened hook covering the new format end-to-end
(valid v2, missing rex/ceo, bare-SHA legacy rejected, missing
approved_by, wrong approved_by, skill_version=1, sha mismatch,
non-merge no-op, gh-api shape gated). Full suite: 205/205 across 13
test files.
Will manually close me2resh/apexyard#48 post-merge per the
single-Closes-per-PR rule (precedent: PR #152 / AgDR-0011).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#132): redact private project reference from AgDR-0012
Abstracted two references to a registered private project that named
the project's owner/repo. The leak-protection hook caught one in the
PR body; this fixup removes the matching references from the
AgDR-0012 file content (which would otherwise have shipped the names
to me2resh/apexyard public repo via the merge).
The pre-existing reference at .claude/rules/pr-workflow.md:130
(documenting #47) is untouched — it predates this PR and is already
on the public repo's history.
Refs me2resh/apexyard#132.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#132): markdownlint blanks-around-fences + typo fix
Two small fixups against red CI / Rex feedback:
- AgDR-0012 line 63: fenced code block now has a blank line before it
(MD031). markdownlint-cli2 0.13.0 was rejecting the indented fence
inside the bullet because the fence's preceding line was the bullet
text (no blank).
- approve-merge SKILL.md line 172: typo `deferes` → `defers` (Rex flag
on PR #158).
Refs me2resh/apexyard#132.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#157): remove voice-prompts feature + correct hook/skill counts (#161)
* chore(#157): remove voice-prompts-on-pause feature + correct hook/skill counts
Closes me2resh/apexyard#157 (sunset the voice-prompts feature) and
me2resh/apexyard#77 (hook count off-by-one in CHANGELOG / CLAUDE.md)
in one bundled PR. AgDR-0013 supersedes AgDR-0009 with the full
rationale; both AgDRs are preserved (decision records are append-only
history).
Removed (#157):
- .claude/hooks/voice-prompt-on-pause.sh
- .claude/hooks/tests/test_voice_prompt_on_pause.sh
- Stop matcher block in .claude/settings.json (became empty after
voice removal)
- voice_prompts block in .claude/project-config.defaults.json
- "## Voice prompts" section in docs/project-config.md
- voice_prompts mention in AgDR-0010 line 32 (replaced with
leak_protection / ticket as still-current example config blocks)
Preserved:
- docs/agdr/AgDR-0009-voice-prompts-on-pause.md — historical record;
new "Superseded by: AgDR-0013" header at the top
- AgDR-0010 line 115 reference to AgDR-0009 — still accurate as a
historical pattern reference
Counts corrected (#77):
- CHANGELOG.md v0.3.0 stats: "17 hooks" → "18 hooks" (historical
fix — at v0.3.0 there were actually 18 hooks)
- CLAUDE.md table line: "18 shell scripts" → "24 shell scripts"
(current count after this removal)
- CLAUDE.md table line: "35 slash commands" → "39 slash commands"
- CLAUDE.md "Available skills (34)" → "Available skills (39)"
- CLAUDE.md quick-reference "Skills (35 slash commands)" →
"(39 slash commands)"
Why bundled: #77's correct count depends on whether voice is still in
the framework. Shipping #77 before #157 would write a number that's
wrong by one again the moment #157 lands. Same shape as previous
bundles (PR #152 / AgDR-0011, PR #158 / AgDR-0012).
Why no adopter-facing changelog mention of the voice removal: the
feature never reached a tagged release on main. v1.1.0 didn't have
it; v1.2.0 won't have it. From the adopter's perspective there's
nothing to retire. AgDR-0013 captures the framework's internal record
for future contributors. See AgDR-0013 § "No adopter-facing changelog
mention".
Tests: full hook test suite green (196 cases across 12 files —
test_voice_prompt_on_pause.sh removed). No regressions.
Will manually close me2resh/apexyard#77 post-merge per the
single-Closes-per-PR rule (precedent: PRs #152, #158).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#157): unwire voice from settings + configs + docs + AgDR-0013
Continuation of d09d7b5 (the file deletions). Squash-merge will
collapse both into one PR commit. This commit captures:
- .claude/settings.json — Stop matcher block removed (was the only
hook in it; entire matcher gone)
- .claude/project-config.defaults.json — voice_prompts block + its
_comment removed
- docs/project-config.md — "## Voice prompts" section removed
- docs/agdr/AgDR-0009-voice-prompts-on-pause.md — "Superseded by"
header added at the top, content otherwise preserved as history
- docs/agdr/AgDR-0010-portfolio-config-and-self-healing.md — line 32
example reference swapped from voice_prompts to
leak_protection / ticket (still-current config blocks)
- docs/agdr/AgDR-0013-sunset-voice-prompts.md — new supersession AgDR
- CLAUDE.md — hook count 18 → 24, skill count 35 → 39 (three
occurrences each, all aligned to current reality)
- CHANGELOG.md v0.3.0 stats — "17 hooks" → "18 hooks" historical fix
(#77 acceptance criterion 1)
Refs me2resh/apexyard#157 + me2resh/apexyard#77.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#157): markdownlint MD028 + resolve stale AgDR-numbering ref
Three small fixups against Rex CHANGES-REQUESTED on PR #161:
- AgDR-0009 line 3: promote "Superseded by:" header out of a
blockquote. The original "I decided ..." canonical blockquote at
line 5 was being merged with the new supersession blockquote
(markdownlint MD028 — "no blanks inside blockquote").
- AgDR-0013 line 3: same shape — "Supersedes:" header now a plain
bold paragraph, canonical "I decided ..." blockquote untouched.
- approve-merge SKILL.md line ~187: stale conditional reference
"AgDR-0012 (or 0013 — depends on whether voice-removal lands
first)" — order is now resolved (12 = approve-merge bundle, 13 =
voice removal). Drop the parenthetical.
Refs me2resh/apexyard#157 + me2resh/apexyard#77.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#160): multi-tab terminal demo on the landing site (#162)
Landing-site `site/index.html` terminal demo previously played one
canonical flow ("one ticket, start to finish") on autoplay. With the
v1.2.0 skill surface expansion (4 new skills, 8+ new hooks), one flow
no longer represents the framework's breadth.
Adds tabs to the terminal chrome — four flows visitors can either let
auto-cycle or click directly:
1. one ticket — existing flow, unchanged content
2. /handover — adopt an external repo into the portfolio
3. /setup — first-run framework bootstrap on a fresh fork
4. /fan-out — spawn 3 parallel agents on independent tickets
Auto-advance: on completion of the active tab's script, the demo
pauses ~1.8s then advances to the next tab. Loops at the end. User
can interrupt by clicking any tab or hitting Replay.
Implementation:
- HTML chrome — single title span replaced with a tablist of 4 button
tabs. ARIA `role="tablist"` / `role="tab"` / `aria-selected` so
keyboard + screen-reader users get the same semantics as sighted
ones.
- CSS — new `.shell-demo__tabs` + `.shell-demo__tab` with an accent
underline on the active tab. Tabs scroll horizontally on narrow
viewports (mobile responsive).
- JS — refactored the existing IIFE from one `script` array to an
array of four. Added `setActiveTab()` for ARIA state, `play(idx)`
takes a tab index, end-of-script auto-advances to `(idx + 1) % N`
unless the user clicked away during the pause. Adds a new `cmd`
type alongside `you` for slash-command invocations (renders with
the same `>` prompt prefix). prefers-reduced-motion still bails
early and leaves the static seed visible.
- Static seed (the non-JS fallback) still shows tab 0's content, so
reduced-motion / no-JS visitors see the one-ticket flow as before.
Hero metrics also corrected to current reality (#77 / PR #161 covers
CLAUDE.md and CHANGELOG.md; this PR catches the same numbers in the
landing site):
Skills 32 → 39
Hooks 18 → 24
The tabs ship in v1.2.0 alongside the framework changes that make
the new flows worth showcasing.
Refs me2resh/apexyard#160 (release v1.2.0 + landing-site refresh).
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#163): default the split-portfolio sibling repo name to <fork>-portfolio (#164)
Closes me2resh/apexyard#163.
The split-portfolio mode docs and skills previously suggested
`your-org/ops` as the default name for the private sibling repo, and
`portfolio/` as the local clone directory. Both too generic — adopters
running multiple ops setups end up with `your-org/ops` collisions, and
a bare `portfolio/` dir gives no signal about which framework it
belongs to when it sits next to other unrelated `portfolio/` dirs.
`<fork>-portfolio` is now the default — keeps the relationship to the
public fork explicit on disk and on GitHub. If the fork is named
`your-org/apexyard`, the portfolio defaults to
`your-org/apexyard-portfolio`. If the fork was renamed (e.g. `cos`),
the portfolio defaults to `cos-portfolio`. Adopters with custom names
keep working — the `portfolio:` config block resolves whatever path
they configured.
Files updated:
docs/multi-project.md
- Layout diagrams use `apexyard-portfolio/` as the sibling
- Setup walkthrough Step 2 + Step 3 use `your-org/apexyard-portfolio`
and explain the `<fork>-portfolio` pattern
- Config-block + symlink path examples updated to
`../apexyard-portfolio/...`
- Daily workflow + cross-machine clone commands updated
- The two existing `your-org/ops` references that remain are
fork-rename examples (lines 52, 64) — kept as-is, since renaming
the fork to `ops` is still valid (the portfolio would then
default to `ops-portfolio`)
.claude/skills/setup/SKILL.md
- Step 2b's "default suggestion" for the private repo name is now
`your-org/<fork>-portfolio`, computed dynamically from the
fork's repo name via `gh repo view --json name -q .name` so the
suggestion is correct even when the fork was renamed
- Clone command no longer needs a second arg — the repo name IS
the directory name
- Config-block paths updated to `../apexyard-portfolio/...`
.claude/skills/split-portfolio/SKILL.md
- Step 3's suggested-name template is now `<account>/<fork>-portfolio`
with the same dynamic-fork-name resolution
Mechanism unchanged. The `portfolio:` config block in
`.claude/project-config.json` still takes any path; this PR is purely
default-suggestion + example prose.
No tests required (skills are markdown instructions; no automated
coverage today).
Refs `apexyard.projects.yaml.example` uses "ops repo" as a generic
term meaning "the operational management fork" — not a name — kept
as-is.
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#165): skills reference page on the landing site + changelog link (#167)
Closes me2resh/apexyard#165.
Adds a public, browseable index of every apexyard slash command
alongside a one-click changelog link from the homepage nav.
site/skills.html (new):
- Lists all 39 skills currently shipping in .claude/skills/
- Each entry: slash command, argument hint, description (taken
verbatim from the SKILL.md frontmatter so the page matches the
runtime exactly)
- 10 categories: Setup & onboarding, Daily ops, Tickets & ideas,
Specs & decisions, Code review & merge, Architecture & dev tools,
Production-readiness audits, Workflow primitives, Communications,
Deprecated
- Same brutalist-terminal design tokens as the homepage — JetBrains
Mono, paper-cream background, single warning-red accent, sharp
corners. Inlined CSS to keep the static-only no-build-step
convention; design vars duplicated rather than extracted to a
shared file (~18 vars; cheap to keep in sync).
- Mobile responsive — skill grid collapses to single-column under
720px; titlebar nav hides non-CTA items on narrow viewports.
- Reduced-motion friendly (no animation in the first place).
- Internal anchor TOC at the top so the page scans in seconds.
site/index.html (nav addition):
- Added two nav links to the titlebar between "what's in the box"
and the github CTA:
• skills → ./skills.html
• changelog → https://github.com/me2resh/apexyard/releases
- The changelog link points at the GitHub releases page (not the
raw CHANGELOG.md file) so it auto-resolves to the latest tagged
release on each visit. v1.2.0 lands and the link is already there.
No new dependencies, no build step, no JS for the skills page. The
existing site convention (one-html-file-per-route, inlined CSS,
optional progressive-enhancement JS) is preserved.
Refs me2resh/apexyard#160 (release v1.2.0 + landing-site refresh) —
this is the second site-side deliverable for that ticket; the release
tag itself follows once me2resh/apexyard#159 (testing) closes.
Follow-up worth a separate ticket: a small generator script that
walks .claude/skills/*/SKILL.md, parses YAML frontmatter, and emits
the skills.html sections automatically. Out of scope for v1.2.0 —
first version is hand-curated and will need maintenance until that
generator lands.
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#168): accept release/vN.N.N branches + release(...) PR titles (#169)
Closes me2resh/apexyard#168.
The /release skill prescribed `release/vA.B.C` as the source-branch
name and `release: vA.B.C` as the PR title for the dev → main release
PR (per AgDR-0007). Both were rejected by the framework's own
validators:
validate-branch-name.sh required {type}/{TICKET-ID}-{description};
release/v1.2.0 has no ticket-id portion.
validate-pr-create.sh required type(SCOPE): form with `release` not
in pr.title_type_whitelist.
The contradiction surfaced cutting v1.2.0 — the first release under
the dev/main model.
Three small changes:
1. .claude/hooks/validate-branch-name.sh — added an early-out branch
that accepts ^release/vN.N.N(-rcN)?$ as a valid name. Narrow,
intentional exception for the framework's release-cut convention;
release branches don't carry a ticket-id because the release itself
IS the ticket.
2. .claude/project-config.defaults.json — added "release" to
pr.title_type_whitelist so a title like `release(#160): v1.2.0`
passes validate-pr-create.sh's existing regex unchanged.
3. .claude/skills/release/SKILL.md step 4 — corrected the prescribed
PR title to `release(#<release-ticket>): vA.B.C` so future /release
invocations produce a title that satisfies the validators by
construction.
Tested:
bash .claude/hooks/validate-branch-name.sh against:
release/v1.2.0 → 0 (allowed, release-special-case)
release/v1.2.0-rc1 → 0 (allowed, RC variant)
release/v9.9.9 → 0 (allowed)
release/foo → 2 (correctly blocked)
release/v1 → 2 (correctly blocked)
chore/GH-168-fix → 0 (allowed, standard pattern)
feature/GH-1-x → 0 (allowed, standard pattern)
Full hook test suite: 196/196 cases green across 12 test files.
Refs: surfaced 2026-05-04 cutting the first release under AgDR-0007.
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#170): exempt release/vN.N.N from validate-pr-create's branch-id check (#171)
Closes me2resh/apexyard#170.
Completes the work started in #169 (closing #168). #169 added a
release-pattern early-out to validate-branch-name.sh so release/vN.N.N
branches pass the branch-name validator. But validate-pr-create.sh has
its own independent branch-id check at line 273 that #169 didn't
touch — and it still rejects release/v1.2.0 because that name doesn't
contain a ticket-id substring.
This is the same class of contradiction #168 fixed; the fix is the
same shape. Add the same release-pattern early-out to the branch-id
check in validate-pr-create.sh:
- if the branch matches ^release/vN.N.N(-rcN)?$ → exempt (release
branches don't carry ticket-ids; the release itself is the ticket)
- otherwise → require a ticket-id substring as before
#168's acceptance criterion 3 ("validate-pr-create.sh accepts a PR
title `release(#160): v1.2.0` against the `release/v1.2.0` branch")
was checked off based on the title regex alone but didn't catch the
secondary branch-id check living in the same file. Surfaced trying
to open the v1.2.0 release PR.
Tested:
bash .claude/hooks/validate-pr-create.sh against:
release/v1.2.0 → 0 (allowed, exempt)
release/v1.2.0-rc1 → 0 (allowed, RC variant)
chore/GH-1-fix → 0 (allowed, has ticket-id)
release/foo → 2 (correctly blocked)
chore/no-ticket → 2 (correctly blocked)
Refs me2resh/apexyard#168 (the parent bug) + #169 (the partial fix).
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#173): sync CHANGELOG.md from main → dev (#174)
Closes me2resh/apexyard#173.
The release-cut model (AgDR-0007 / #116) squash-merges
release/vN.N.N → main, which means the v1.2.0 CHANGELOG section that
landed on main via PR #172 was never propagated back to dev. This
commit copies main's CHANGELOG.md verbatim onto dev so the v1.2.0
section is now present on both branches.
The diff is exactly the v1.2.0 entry being prepended; no other lines
change.
Without this sync, the next release PR cut from dev would build a
v1.3.0 section on top of v1.1.0, silently dropping v1.2.0 from dev's
running history. The corollary skill-level fix (option B in the
ticket) — updating /release to source the previous CHANGELOG from
upstream/main — is filed as a follow-up and out of scope for this PR.
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#181): /agdr skill — searchable AgDR library (#186)
Adds /agdr — a portfolio-wide index for Agent Decision Records.
Walks apexyard.projects.yaml, reads each project's docs/agdr/*.md
(local clone if available, else gh api fallback), parses the optional
YAML frontmatter for category + projects, and answers four queries:
- /agdr browse list across the portfolio, grouped by category
- /agdr search <term> full-text grep across all bodies, returns
<project>/AgDR-NNNN paths + matching paragraph
- /agdr show <id> print a specific record, disambiguates duplicates
- /agdr stats counts per category (the marketing-slide tile,
now backed by real data)
Six-category taxonomy: architecture | tech-stack | security | patterns
| integrations | other. Legacy AgDRs without frontmatter remain
first-class — they bucket as `other` and are flagged in browse so
operators can migrate at their own pace.
Backwards-compatible template change: templates/agdr.md gains an
optional `category:` (and optional `projects:`) line in the existing
frontmatter block. Omitting the line keeps every existing AgDR valid;
the skill defaults to `other` when missing.
Doc note in workflows/sdlc.md § Phase 2 points at /agdr search for
"have we decided this before?" lookups before drafting a design.
Smoke test in .claude/hooks/tests/test_agdr_skill.sh covers the
parser the spec specifies — frontmatter extraction, category
bucketing (including the legacy default-to-other path), id reading,
stats aggregation, and search match counts. 18 assertions, all green;
12 pre-existing test suites also green (no regressions).
Closes #181
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(me2resh/apexyard#183): /launch-check trend tracking (#185)
- Persist each run as JSON under <projects_dir>/<name>/launch-check/runs/
(timestamp + branch + commit + per-dimension scores + verdict + top_risks)
- Append "Trend (last 5 runs)" section to the per-run summary when at least 2
prior runs exist — markdown table + ASCII score chart
- Add /launch-check trend mode for read-only trend rendering (no full audit)
- Auto-derive notes column from score-delta vs previous run
(e.g. "Security +12, Analytics +10")
- Opt-in commit via .launch-check-history-tracked marker; gitignored by default
- New helper script render-trend.sh + test_launch_check_trend.sh (21 cases)
- Resolve projects dir via portfolio_projects_dir helper (no hardcoded path)
- Schema is forward-compatible — extra fields preserved on framework upgrade
AgDR-0014 documents the chart format / schema / opt-in choices.
Closes me2resh/apexyard#183
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#182): /status --briefing + bin/apexyard status CLI shim (#187)
* feat(me2resh/apexyard#182): /status --briefing + bin/apexyard CLI shim
Make slide 6's `$ apexyard status` invocation real — the smallest of three
slide-reality tickets, demonstrating the marketing-to-implementation
pattern before #181 / #183 land.
- New helper at .claude/skills/status/briefing.sh — computes the 4-line
"where am I" briefing (active workspace, active ticket, branch,
role-set) so the logic is testable in isolation and runnable from a
plain shell. Workspace inference walks up from cwd to the ops-fork
root (same algorithm as _lib-portfolio-paths.sh and /start-ticket).
- Ticket reads from .claude/session/tickets/<workspace> first, falls
back to .claude/session/current-ticket. Role-set is inferred from
the active ticket's GitHub labels (v1: backend / frontend / qa /
security / platform / sre / data / ux / ui / product / tech-lead,
plus the long forms). No match emits the explicit "<none — inferred
per task>" placeholder so the four-line shape is constant.
- New CLI shim bin/apexyard delegates `apexyard status` to the same
helper after walking up to find the ops-fork root. Works from any
workspace/<name>/ clone or the fork itself; symlink onto PATH to
install (no shadowing of the `claude` binary).
- /status SKILL.md gains a "Briefing mode" section documenting the
--briefing / -b flag; default /status output is unchanged.
- docs/multi-project.md "Daily workflow" demonstrates the new
invocation alongside /inbox and /status.
- New smoke tests at .claude/hooks/tests/test_status_briefing.sh — 8
cases covering ops-root cwd, workspace cwd, unknown cwd, ops-fallback
marker, per-project marker priority, label-based role inference, the
no-matching-label path, and the constant-four-line shape.
Closes me2resh/apexyard#182
* fix(me2resh/apexyard#182): replace curios-dog with example-app in SKILL.md output sample (leak fix)
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* docs(#178): LSP integration spike — measurement + recommendation (#184)
* docs(#178): LSP spike — token-savings measurement + integration findings
- Phase 1: estimated token cost A vs B for three representative queries on a real TS Lambda backend (~9,750 LOC); shallow semantic queries see ~3-23x input-token savings, multi-hop traces see ~1.4x
- Phase 2: verified Claude Code shipped first-party LSP support in v2.0.74 (Dec 2025), gated behind ENABLE_LSP_TOOL=1, wired in via the plugin system (.lsp.json); MCP-wrapped LSP shims (cclsp, lsp-mcp) remain as fallback
- Phase 3: recommends Option 3 — interactive clone-first prompt at end of /handover, preserving today's "never auto-clone" principle while making the deep-dive path discoverable
- Phase 4: AgDR Y-statement and option matrix sketched; full AgDR is a follow-up if the spike says go
- Recommendation: GO. Adopt the built-in tool, document the opt-in path, change /handover to offer clone-first interactively. No code changes beyond /handover SKILL.md; no novel integration to maintain
Closes me2resh/apexyard#178
* docs: fix markdownlint MD031/MD032 in spike report
Add blank lines around lists and a fenced code block to satisfy
markdownlint-cli2 in CI. Pure formatting, no content change.
Refs me2resh/apexyard#178
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* docs(me2resh/apexyard#190): annotate LSP-aware skills with opt-in callouts (#191)
- Add identical-shape "LSP-aware (optional, recommended)" callout near
the top of each of the four code-aware SKILL.md files.
- Per-skill savings number is the only variation:
- /code-review: ~3-15× cheaper for semantic queries
- /threat-model: ~3-15× shallow, ~1.4-5× multi-hop traces
- /security-review: ~3-15× cheaper for semantic querie…
* chore(#109): project-configurable ticket / branch / commit / PR schema (#118)
* chore(#109): project-configurable ticket / branch / commit / PR schema
Lift the prefix / type whitelists hardcoded across skills, hooks, and
CI into a versioned JSON config read through a shared shell library.
Shipped defaults at .claude/project-config.defaults.json; per-fork
overrides at the optional .claude/project-config.json; one reader
(_lib-read-config.sh) that every consumer now uses.
Added:
- .claude/project-config.defaults.json (v1 schema)
- .claude/hooks/_lib-read-config.sh (shared reader)
- docs/project-config.md (schema reference + extension guide)
- docs/agdr/AgDR-0006-project-configurable-ticket-schema.md
Migrated (still pass with no config present via last-resort fallback):
- validate-branch-name.sh → .branch.type_whitelist
- validate-commit-format.sh → .commit.type_whitelist (legacy
`commit_types` top-level key honoured as backward-compat fallback)
- validate-pr-create.sh → .pr.title_type_whitelist
- /feature, /task, /bug skills reference the config in their Rules
sections; none hardcodes the list any more
Unlocks subsequent config-readers for #107 / #110 / #111 / #112 /
#113 / #114 / #115 — each extends the schema under its own subtree
without further changes to the loader.
https://github.com/me2resh/apexyard/issues/109
* fix(#109): satisfy markdownlint MD032 and MD060 on new docs
Auto-fix MD032 (blank lines around lists) in AgDR-0006 and format
table-separator rows with surrounding spaces (MD060) in both new
doc files. Content unchanged; CI green.
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#110): add block-private-refs-in-public-repos.sh hook (#119)
Adds a new PreToolUse hook that blocks gh issue/PR/comment creation and
gh api .../issues|/pulls calls targeting a public framework repo
(default: me2resh/apexyard + whatever `upstream` resolves to) when the
title or body references any registered private project from
apexyard.projects.yaml (by name, repo slug, owner/repo#N ticket ref, or
workspace path).
The hook is a sibling to check-secrets.sh — both scan outgoing content
for identifiers that should never leave the local environment. Skip
marker `<!-- private-refs: allow -->` in the body lets a deliberate
reference through with a visible warning.
Files touched:
- .claude/hooks/block-private-refs-in-public-repos.sh (new)
- .claude/hooks/tests/test_block_private_refs.sh (new)
- .claude/rules/leak-protection.md (new)
- .claude/settings.json (wire PreToolUse matchers for the 5 gh shapes)
- docs/rule-audit.md (append section 10 + bump counts)
Refs: https://github.com/me2resh/apexyard/issues/110
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#115): add warn-stale-review-markers.sh PostToolUse hook (#120)
Fires after `git push` to surface review markers that have gone stale
because new commits were pushed past an existing Rex / CEO / design
approval. The merge gate already catches this at `gh pr merge` time,
but only then -- this hook closes the gap by flagging it immediately
at push-time so the author isn't surprised at merge.
- `.claude/hooks/warn-stale-review-markers.sh`
- PostToolUse, non-blocking (PostToolUse exit 2 would push noise
into the conversation; this hook is purely informational).
- Resolves the PR HEAD via `gh pr view --json headRefOid` -- same
source-of-truth as the merge-gate hooks post-apexyard#47 / #55.
Falls back to local HEAD with a visible WARN when gh is offline.
- Silent on: no PR for branch, no markers, fresh markers,
failed push (detected via `rejected` / `failed to push` /
`fatal:` / `error:` markers in tool_response.stderr).
- Modes: `warn` (default) prints one stderr line per stale marker;
`delete` opts in to auto-removal via
`.claude/project-config.json` -> `review_markers.on_stale`.
TODO(apexyard#109): switch to the shared project-config reader
once it lands.
- `.claude/settings.json`
- Wires the hook on PostToolUse / Bash / `git push *`.
- `docs/rule-audit.md`
- Adds a row under section 3 (Code review & PR quality) and
bumps the mechanized count 26 -> 27 / total 73 -> 74.
- `.claude/hooks/tests/test_warn_stale_review_markers.sh`
- 8 cases: no PR, no markers, fresh markers, stale rex / ceo /
design (warn), delete mode, failed push. All pass locally.
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#111): upgrade pre-push-gate from advisory reminder to blocking check-runner (#121)
* chore(#111): upgrade pre-push-gate from reminder to blocking check-runner
Previously pre-push-gate.sh just printed a checklist of things to run
locally before pushing — it was advisory. The rule it enforces is a
HARD STOP per pr-workflow.md. That asymmetry meant agents routinely
pushed broken work and discovered it only when CI went red.
Replaces the reminder with a blocking runner that reads the list of
shell commands from project config (.pre_push.commands) and executes
them in sequence before a push is allowed through. First non-zero
exit blocks the push with exit 2 and prints the failing command plus
the last 20 lines of its output.
- Config key: .pre_push.commands[] — array of {name, run} objects.
Shipped default is an empty list (hook stays a no-op on repos that
haven't configured their checks yet, including the framework repo
itself until it wires its own CI).
- Emergency bypass: '<!-- pre-push: skip -->' in the HEAD commit
message. Grep-able on purpose so bypasses stay auditable.
- Fail-fast: once a command fails, the rest don't run. Parallel
execution is a follow-up polish.
- 7 test cases in .claude/hooks/tests/test_pre_push_gate.sh — all
pass on the shipped default + a minimal custom config.
Updates docs/rule-audit.md to flip "partial" → "yes" for the
"before git push" rule.
Integrates with the shared config reader landed in #109.
https://github.com/me2resh/apexyard/issues/111
* fix(#111): remove orphaned footnote reference from rule-audit
The previous advisory-mode footnote was superseded by pre-push-111
but its definition was accidentally kept, tripping markdownlint MD053
(unused reference definition). Drop it.
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#107): add validate-issue-structure.sh PreToolUse hook (#122)
Mechanically enforces the ticket body schema when an agent files raw
`gh issue create` calls instead of going through the interactive
/feature, /task, /bug skills. Matches bracketed title prefix
([Feature] / [Chore] / [Bug] / [Docs] / etc.) against
`.ticket.required_sections` in project-config, and blocks (exit 2)
when any required section is missing or empty. Skip marker
`<!-- validate-issue-structure: skip -->` bypasses with a visible
stderr WARN for legitimate off-template tickets (epics, meta-threads).
Changes:
- .claude/hooks/validate-issue-structure.sh — the hook; reads schema
via the shared _lib-read-config.sh, with inlined defaults for bare
checkouts predating the config-schema rollout. Handles
--body / --body-file / -F path.
- .claude/project-config.defaults.json — extends .ticket with
required_sections (Feature/Chore/Refactor/Testing/CI/Docs/Bug) and
skip_marker; other .ticket fields untouched.
- .claude/settings.json — new PreToolUse matcher on Bash(gh issue
create *) alongside the existing suggest-ticket-template.sh and
block-private-refs-in-public-repos.sh hooks.
- .claude/hooks/tests/test_validate_issue_structure.sh — 15 cases
covering pass + fail paths per prefix, empty section detection,
skip marker, unknown prefix, non-gh invocation, --body-file path.
- docs/rule-audit.md — new section 11 row, mechanized count +1.
Upstream ticket: https://github.com/me2resh/apexyard/issues/107
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#112): add require-agdr-for-arch-pr.sh PreToolUse hook (#123)
Closes the asymmetry noted in .claude/rules/agdr-decisions.md: every
other HARD STOP in the ruleset (merge approval, ticket-first,
migration-first) is mechanically enforced, but the /decide HARD STOP
was prose-only. The commit-time hook require-agdr-for-arch-changes.sh
catches one architectural change at commit; this new PR-time hook
catches the cumulative diff so reviewers always have a pointer to the
decision record.
- New hook at .claude/hooks/require-agdr-for-arch-pr.sh
- Fires on Bash(gh pr create *)
- Parses --title/--body/--body-file/-F <path>
- Resolves base branch from --base, else upstream/dev, origin/dev,
upstream/main, origin/main, main, master (in that order)
- Computes `git diff <merge-base>..HEAD --name-only`
- Triggers on any changed file matching .agdr_trigger_paths[], OR any
dep-file addition (package.json via jq key-set diff; other
dep files via a commented +/- line-count heuristic — version
bumps match +/- counts and do not fire)
- Blocks (exit 2) with a helpful message naming the triggers and
pointing at /decide if the body has no `AgDR-\d+-[a-z0-9-]+`
reference
- Skip marker `<!-- agdr: not-applicable -->` bypasses with a
visible WARN on stderr
- Silent exit 0 on non-gh commands, empty diffs, unresolvable base
- Wired via .claude/settings.json PreToolUse Bash(gh pr create *)
- Adds two new top-level keys to .claude/project-config.defaults.json:
agdr_trigger_paths (shell globs — domain/, infrastructure/,
migrations/, *.tf, .github/workflows/, etc.)
agdr_trigger_dep_files (literal basenames — package.json,
pyproject.toml, Cargo.toml, go.mod, Gemfile)
Hook has inline fallback defaults kept in sync.
- Adds docs/rule-audit.md entry in the AgDR section; bumps mechanized
count 26 to 27 and total rows 73 to 74.
- Adds .claude/hooks/tests/test_require_agdr_for_arch_pr.sh (7 cases;
all green): path-triggered without AgDR (block), with AgDR (pass),
dep-file added (block), version-only bump (no fire), skip marker
(pass + warn), non-matching diff (pass), non-gh command (no-op).
Closes https://github.com/me2resh/apexyard/issues/112
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#113): require Testing section in PR body (config-driven) (#124)
Extends validate-pr-create.sh with a required-sections check that
replaces the hardcoded Glossary-only grep. The list of required H2
headings is project-configurable via `.pr.required_sections[]`.
Shipped default is ["Testing", "Glossary"], matching the canonical PR
description shape in workflows/code-review.md.
- Each entry must appear as `## <Name>` (case-insensitive).
- Empty sections are tolerated at this layer (the issue-structure hook
#107 does stricter empty-content checks for issue bodies; for PR
bodies, empty sections are left to the reviewer's judgement).
- Skip marker `<!-- pr-sections: skip -->` bypasses with a visible
stderr WARN — for trivial PRs (lint-only fixes, version bumps)
where the full template is overkill.
- Reads from project config via the shared _lib-read-config.sh (#109).
Inline fallback matches shipped defaults so bare checkouts predating
#109 keep working.
- 8 test cases cover: all-sections pass, each missing section,
missing-both (both errors printed), skip marker, case-insensitive
headings, H3 rejection.
https://github.com/me2resh/apexyard/issues/113
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#114): enforce single Closes-keyword per PR body (#125)
* chore(#114): enforce single Closes-keyword per PR body
Caps distinct auto-closing references (close/closes/closed, fix/fixes/
fixed, resolve/resolves/resolved + N or owner/repo+N) at one per PR
body. Closes the loophole where the title validator limited the title
to one ticket but multiple Closes lines in the body would still auto-
close all of them on merge.
- Scans stripped of fenced code blocks so closing keywords inside a
code sample do not count.
- Distinct counting: the same number referenced twice (e.g. via Fixes
and Closes) counts as one.
- Cross-repo refs (owner/repo+N) count normally.
- Opt-in escape hatch: pr.allow_multiple_closes=true in
project-config disables the check for teams that deliberately batch
rollbacks or dependency bumps.
- Per-PR bypass: a multi-close-approved HTML comment in the body
prints a visible stderr WARN and lets that PR through. Grep-able
trace so bypasses are auditable.
- 10 test cases cover: one close passes, no-keyword passes, two
distinct block, three mixed block, same-number-twice passes, code-
fence-ignored, skip marker, cross-ref without keyword, opt-in
config, cross-repo close.
Reads configuration via the shared _lib-read-config.sh (apexyard+109).
https://github.com/me2resh/apexyard/issues/114
* fix(#114): strip inline backticks and tilde fences from close-count scan
Rex caught a self-reflexive bug in the initial commit: documentation
mentioning closing keywords inside inline backticks (say a PR body
that explains the new hook with examples) counted as real closes, and
a skip marker inside inline backticks silently bypassed the check.
Future PRs that document the feature would trip the same trap.
Fix the code-region stripper to cover:
- Triple-backtick fences (already handled)
- Tilde fences (new)
- Inline-backtick spans (new)
Also run the skip-marker check against the stripped body, so a marker
used purely as documentation no longer activates a real bypass.
Three new test cases pin the behaviour:
- closing keywords in inline backticks are ignored
- skip marker inside inline backticks does NOT bypass
- tilde fences also get stripped
13/13 tests pass.
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#108): add /tickets-batch skill for bulk-file flow (#127)
The fast happy path for filing 5–20 structured tickets in one intent
without dropping to raw `gh issue create` (non-conformant) or running
`/feature` 20 times serially (~100 turns of interview).
- .claude/skills/tickets-batch/SKILL.md — new skill spec. Asks
shared-context questions (priority, epic, area-labels, repo) ONCE
for the whole batch, then runs a ≤3-question micro-interview per
ticket (type, one-line purpose, optional clarification when the
inference is low-confidence). Confirms the full batch as a table,
then files each via specific `gh issue create` calls (never a
bulk JSON dump — the validator runs per-issue). Output conforms
to `.ticket.required_sections` by construction. Caps at 20
tickets per invocation.
- CLAUDE.md — added a row for /tickets-batch in the Available
Skills table; bumped the count references from 33 to 34.
Refs https://github.com/me2resh/apexyard/issues/108
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#117): add /fan-out skill + parallel-work rule doc (#128)
- Add `.claude/skills/fan-out/SKILL.md` — spawns N parallel Agent calls
in a single assistant message, with per-task agent type, worktree
isolation, and foreground/background mode. Caps at 5 concurrent
agents. Refuses fan-out when tasks share file write targets or have
sequential dependencies. Includes pre-spawn active-ticket safety
check and worktree merge-back flow that pauses on conflict.
- Add `.claude/rules/parallel-work.md` — trigger heuristic for when an
agent should proactively offer fan-out (>= 2 file-independent,
context-independent, individually substantial work items). Pairs
with the skill: rule says when, skill says how.
- Update `CLAUDE.md` — bump rules count to 9, skills count to 34, add
`/fan-out` row to the skills table.
Refs https://github.com/me2resh/apexyard/issues/117
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#116): adopt release-cut branch model (dev/main + tags) — framework only (#126)
* chore(#116): adopt release-cut branch model (dev/main + tags)
Formalises the dev/main split that already exists informally — dev is
the daily-work branch where every PR lands, main is release-only,
tagged with semver on each merge. Framework-only: managed projects
under apexyard governance stay trunk-based.
Added:
- AgDR-0007 — decision record (options table covers full git flow vs
trunk-only vs gitflow-lite; chose gitflow-lite)
- /release skill — diff dev against main, propose semver bump from
conventional commits, generate CHANGELOG, open release PR, tag
after merge
- docs/release-process.md — prose runbook for cutting a release
(manual fallback for the skill)
- .git.protected_branches in project-config.defaults.json
(main/master/dev/develop)
Modified:
- block-main-push.sh — now blocks direct pushes/commits to all
configured protected branches (was: hardcoded main/master). Reads
.git.protected_branches via the shared config reader (apexyard#109).
- CLAUDE.md — new section under Git Conventions explaining the
dev/main model + the framework-only scope. Skill table entry for
/release. Skills count bumped to 34.
- docs/multi-project.md — note that upstream/main is release-only
and the dev/main split is framework-only.
Non-consequences (per AgDR-0007):
- No release/* or hotfix/* branches. Hotfixes are normal patches
cut quickly. Revisit if multi-version maintenance becomes a need.
- No automatic on-merge issue closing for dev PRs. The release PR's
body aggregates all Closes references for the batch and triggers
auto-close en masse when it merges to main. Manual close in the
meantime.
- CI workflows trigger on pull_request regardless of base, so
dev-targeting PRs already get the full check matrix — no
workflow file edits needed.
https://github.com/me2resh/apexyard/issues/116
* fix(#116): satisfy markdownlint MD032 + MD060 in 116 docs
Auto-fix added blank lines around lists in AgDR-0007 (MD032) and
spaced the table separators in AgDR-0007 + multi-project.md (MD060).
Content unchanged.
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* fix(#106): CHANGELOG fallback in drift hook for squash-merged forks (#129)
* fix(#106): CHANGELOG fallback in drift hook for squash-merged forks
The v1.1.0 tag-reachability check (`git tag --merged main`) misfires on
forks that sync via GitHub's default squash-merge: the squash collapses
the upstream-tag commit into a synthetic SHA, the tag stops being
reachable, and the banner keeps firing forever.
Discovered live on the first real-world `/update` flow: ops fork
squash-merged the v1.1.0 sync PR, banner kept saying "v1.1.0 available"
even though the fork was content-caught-up.
This commit adds a CHANGELOG-content fallback that fires only when the
primary tag check fails. If the fork's main has a heading
`## [X.Y.Z]` matching the upstream tag's version, treat the release as
absorbed and stay silent. Tolerant grep (matches the apexyard CHANGELOG
format from v1.1.0 onward, with leading-`v` stripping for tag→heading
conversion).
The merge-commit and rebase paths are unchanged — primary tag check
still works for them, and the fallback never fires when it shouldn't.
Test coverage (5 cases):
- squash-merge fork caught up to v1.1.0 → silent (the fix)
- merge-commit fork caught up to v1.1.0 → silent (regression check)
- fork stopped at v1.0.0 → banner fires
- fork has its own newer tag → silent
- squash-merge but no CHANGELOG on fork → banner fires (no false silence)
Records the strategy update in docs/agdr/AgDR-0008-…md (extends
AgDR-0005's tag-based-drift design).
https://github.com/me2resh/apexyard/issues/106
* fix(#106): satisfy markdownlint MD032/MD060 + shellcheck SC2164
Rex flagged two CI-blocking issues on the original 106 commit:
- AgDR-0008 had three bulleted sub-lists in the Consequences section
without surrounding blank lines (MD032). Added blanks and padded the
one tight-pipe table separator (MD060).
- The 106 test fixture had five subshell `cd "$fk"` calls without
`|| exit 1` (SC2164). Added the guard to all five.
5/5 tests still pass after the fix. No semantic change to the hook
or the test logic.
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#130): add /validate-idea skill — lightweight pre-spec gate (#131)
A 10-minute, 5-question check that sits between /idea and /write-spec.
Designed for solo founders running ApexYard — not a heavyweight
methodology like event storming or Wardley mapping.
Five questions, asked one at a time:
1. Who is this specifically for?
2. What do they do today instead?
3. What's the smallest version that proves the value?
4. What would prove this is wrong? (kill criteria)
5. Build, buy, or rent?
Output: a one-page validation doc with a GREEN/YELLOW/RED verdict.
RED auto-updates the IDEA-NNN backlog row to WONTDO.
Integration:
/idea — adds an optional default-no "Validate now?" step after
capture (and after the optional GitHub Issue offer).
/handover — adds a conditional "this looks dormant, validate?"
step at the end of the integration plan, gated on the dormancy
heuristic (last commit > 90d AND zero open PRs AND no recent
issue activity). Healthy projects don't see the prompt.
CLAUDE.md skills count bumped to 35; new skills row added.
https://github.com/me2resh/apexyard/issues/130
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat: configurable voice prompts on assistant pause (AgDR-0009-voice-prompts-on-pause) (#135)
Stop hook that speaks the assistant's question aloud (Jarvis-style)
when it pauses for user input. Initial phase is macOS-only via `say`,
no voice input — user replies via keyboard.
Default OFF. Adopters opt in by overriding `voice_prompts.enabled` to
true in `.claude/project-config.json`.
Files:
- .claude/hooks/voice-prompt-on-pause.sh — Stop hook with config gate,
trigger heuristic (questions-only by default), markdown stripping,
sentence-boundary truncation, fire-and-forget say invocation
- .claude/hooks/tests/test_voice_prompt_on_pause.sh — 9 cases covering
disabled-default, enabled+question, enabled+statement, approved-pattern,
abc-menu, malformed-transcript, no-say-on-PATH, trigger-always,
markdown-stripping
- .claude/project-config.defaults.json — voice_prompts schema block
added (enabled, voice, max_chars, rate_wpm, trigger), default OFF
- .claude/settings.json — new Stop hook entry wired with the standard
ops-root resolver wrapper
- docs/agdr/AgDR-0009-voice-prompts-on-pause.md — design rationale,
options matrix (status quo / macOS say / cloud TTS / ML detection),
consequences, future phases
- docs/project-config.md — new "Voice prompts" section with override
examples and privacy notes
Test mode: hook respects VOICE_PROMPTS_SYNC=1 to run say synchronously
(test runners need this so assertions don't race against orphaned
background processes). Production invocations always run async.
Future phases (out of scope here, AgDR §"Future phases"):
- Phase 2: cross-platform TTS (Linux espeak, Windows SpeechSynthesizer)
- Phase 3: cloud TTS providers (OpenAI, ElevenLabs) — privacy AgDR-worthy
- Phase 4: voice input via Whisper-based STT
- Phase 5: per-message overrides
Refs: https://github.com/me2resh/apexyard/issues/134
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#141): add /debug skill — structured hypothesis-driven debugging (#142)
* feat(#141): add /debug skill — structured hypothesis-driven debugging
Adds a methodology skill that enforces five disciplines:
1. Capture the symptom precisely (exact URL, exact response, exact step)
2. Read the architecture before guessing (map every layer the request
touches, file by file)
3. Form a hypothesis ladder (3–5 candidates, each with an explicit
evidence test that confirms or refutes it)
4. Gather evidence first, fix second
5. Verify the fix against the original symptom evidence (re-run the
same `curl` / browser repro you used in step 4 — unit tests
verify code, not feature, correctness)
Stack appendices (Web, Desktop) carry stack-specific surface-evidence
requirements (step 1), architecture-surface maps (step 2), and
evidence-tests cookbooks (step 4). The methodology body stays portable
across stacks; appendices are where stack-specific knowledge accrues
over time.
Web appendix covers browser routing, framework configs (Next/Nuxt/Vite),
SPA-fallback layers, CDN, origin, the shared API client, backend
handlers, and auth providers. Desktop appendix covers Electron / Tauri
/ native-shell concerns: app entry points, IPC bridges, native modules,
auto-updater, sandbox / entitlements, code signing, crash reports.
Includes "When NOT to use" guidance so the methodology overhead doesn't
sandbag simple bugs (typos, off-by-ones, greenfield exploration).
Motivated by a real OAuth debug session in a managed project where
three sequential fixes chased adjacent symptoms because each was
hypothesis-then-fix without evidence in between. The skill is the
"never do that again" guardrail.
Closes #141
* fix(#141): scrub private project issue numbers from anti-pattern table
Rex review on PR #142 caught that line 155 of the skill's anti-pattern
table still named the originating PRs (#375, #377, #380) from the
private project where the methodology was first exercised. The PR body
and commit message were correctly abstracted earlier, but this in-file
reference slipped through — the leak-protection hook only scans gh
issue/pr writes, not staged file content, so mechanical enforcement
didn't catch it.
Replaced with "Three sequential PRs chasing the same symptom because
each was based on a different guess (no evidence test in between
cycles)" — same pedagogical value, zero attribution.
Refs #141
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* docs(#143): document split-portfolio mode + add /setup privacy gate (#144)
Adopters on GitHub Free with any private project hit a silent privacy bug
following today's docs: forking apexyard makes a public fork that you cannot
later flip to private (GitHub policy), and committing `apexyard.projects.yaml`
+ `projects/<name>/` to the fork publishes private project names + handover
findings on a public GitHub repo.
This PR documents the supported workaround — split-portfolio mode — and
adds an upfront privacy gate to the /setup skill so new adopters never
hit the trip-wire silently.
docs/multi-project.md:
- New "Two setup modes — pick the one that matches your privacy needs"
section before TL;DR, with a side-by-side table and the explicit
trip-wire callout.
- Existing TL;DR retitled "TL;DR — single-fork mode (default)" with a
one-line pointer to the split-portfolio section.
- New "Split-portfolio mode — public framework + private portfolio"
section between the existing setup steps and the directory-layout
section. Includes:
- The two-repo layout (~/ops/apexyard public + ~/ops/portfolio private)
- 7-step setup walkthrough with copy-pasteable commands
- Daily workflow + upstream sync notes (both unchanged)
- Trade-offs (two repos to maintain, two clones per machine, one
upstream-sync conflict path on `projects/README.md`)
- "Migrating from single-fork to split-portfolio" recovery flow with
the explicit warning that GitHub Issue / PR edit history survives a
force-push and must be redacted separately
.claude/skills/setup/SKILL.md:
- New Step 2a: privacy gate — asks "are any projects private?" before
proposing the config. Branches on the answer:
- All public → single-fork mode
- GitHub Pro / Team / Enterprise → single-fork mode (private
forks of public repos
are supported on those
plans)
- Any private + GitHub Free → split-portfolio mode
- New Step 2b: walks through the split-portfolio setup interactively
(private repo create, sibling clone, gitignore + symlink) when the
privacy gate triggers.
- Detection: `test -L apexyard.projects.yaml` short-circuits Step 2b
for adopters already in split mode.
- Explicit "do NOT auto-migrate" rule for adopters already in single-fork
mode with private names already pushed — that path is destructive
(force-push history rewrite + redact issue/PR bodies + delete backup
branch) and warrants a deliberate, eyes-open run, not a /setup side
effect.
Out of scope for this PR (tracked separately on #143):
- `portfolio:` config block in `onboarding.yaml` schema
- Skill audit + refactor to honour configured `registry` / `projects_dir`
/ `ideas_backlog` paths instead of hardcoded fork-relative paths
- `/split-portfolio` migration helper skill that automates the recovery
flow currently documented manually
This is the docs-and-setup-question minimum-viable starter — the
framework code refactor is mechanical and lands as a follow-up.
Refs #143
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#145): portfolio config + self-healing + /split-portfolio helper (#147)
* feat(me2resh/apexyard#145): portfolio config block + self-healing + /split-portfolio helper
Closes the framework primitive deferred from me2resh/apexyard#144. Adds
first-class config-driven path resolution for the portfolio registry,
projects dir, and ideas backlog, with self-healing surfacing of broken
config at session start, plus a new /split-portfolio skill that automates
the destructive recovery flow.
Schema, helper, and hook:
- .claude/project-config.defaults.json: new portfolio: block
(registry, projects_dir, ideas_backlog) with defaults matching
today's single-fork layout
- .claude/hooks/_lib-portfolio-paths.sh: new sourceable helper exposing
portfolio_registry, portfolio_projects_dir, portfolio_ideas_backlog,
portfolio_validate, portfolio_clear_cache. Resolves relative paths
against the ops-fork root.
- .claude/hooks/check-portfolio-config.sh: new SessionStart hook —
silent on OK, one-line banner on broken config, never blocks session
- .claude/hooks/tests/test_portfolio_paths.sh: 13 cases covering
defaults, absolute/relative overrides, validate states, cache clear
Skill audit (18 SKILL.md files):
- Adds Path resolution callout pointing at the helper
- handover bash blocks now source helper and use $(portfolio_registry)
instead of literal apexyard.projects.yaml
- setup Step 2b now writes the portfolio: config block (recommended)
and validates via portfolio_validate before declaring success;
symlink approach kept as legacy fallback
New skill (.claude/skills/split-portfolio/SKILL.md):
- 10-step migration with explicit operator-confirmation gates at each
destructive step (force-push, body redaction, branch deletion)
- --verify mode: read-only state report (mode, paths, validate, drift)
- --dry-run mode: prints commands without executing
- Pre-flight refusals: already-private fork, paid GitHub plan, dirty
working tree, already-migrated state
- Step 9 writes the portfolio: config block (not symlinks) — symlink
fallback documented for adopters on older framework versions
- Step 9 surfaces the GitHub timeline-API survival caveat verbatim
- Idempotent re-runs: detects partial-migration state and resumes
Docs (docs/multi-project.md):
- Layout section describes both modes (config-block recommended,
symlink legacy) with self-healing notes
- Setup steps split into config-block mode and legacy symlink mode
- Migration section now points at /split-portfolio skill; manual
recipe preserved as fallback
AgDR (docs/agdr/AgDR-0010-portfolio-config-and-self-healing.md):
- Full Y-statement, options, decision, consequences, future phases
- Schema decision rationale: project-config.json over onboarding.yaml
because runtime path resolution belongs in project-config
Closes me2resh/apexyard#145
Refs me2resh/apexyard#146 (delivered same PR; closed manually post-merge
per the single-Closes-keyword rule in validate-pr-create.sh)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(me2resh/apexyard#145): markdownlint MD031 — blank lines around fences
CI's markdownlint-cli2 (v0.34.0) flagged the JSON + bash fenced code
blocks I added in setup/SKILL.md Step 2b without surrounding blank
lines. Added the required blanks. No content change.
Refs me2resh/apexyard#147
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(me2resh/apexyard#148): correct privacy-gate wording — adopter action, not framework auto-publish (#149)
The privacy-gate wording introduced in PR #144 (and unchanged in PR #147)
attributed the publication to the framework rather than the adopter:
"the standard fork-and-commit setup will silently publish your private
project names on a public GitHub repo"
That's factually wrong. ApexYard never pushes anything without explicit
operator approval — the publication only happens when the adopter
themselves runs git push. The "silently publish" framing read as if the
framework auto-publishes, which is misleading and undermines trust in
the rest of the framework's safety claims.
Two prose-only edits, no code, no behavior change:
- .claude/skills/setup/SKILL.md Step 2a — replaced "will silently
publish ..." with adopter-action language ("you might accidentally
publish ... a stray git push after registering them — I won't push
without your approval, but the risk is on the adopter once the data
is committed locally")
- docs/multi-project.md trip-wire callout — replaced "silently publish
their portfolio names the moment they push" with "risk accidentally
publishing their portfolio names with a stray push (the framework
itself never pushes without operator approval, but once the registry
is committed locally the next push exposes it)"
Verified: `grep -r "silently publish" .claude/skills/ docs/` returns no
hits.
Closes me2resh/apexyard#148
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#150): bootstrap-skill exemption + Bash-write coverage (#152)
Closes the legitimate-bypass case (me2resh/apexyard#150) and the
illegitimate-bypass case (me2resh/apexyard#151) together so the
ticket-first gate is coherent. Shipping either alone would leave a
window where the framework is internally inconsistent — see AgDR-0011
for the full rationale.
Bootstrap exemption (me2resh/apexyard#150):
- .claude/session/active-bootstrap marker, written by /setup,
/handover, /update, /split-portfolio on entry; cleared on exit
- SessionStart sweep (clear-bootstrap-marker.sh) for stale markers
from interrupted sessions
- require-active-ticket.sh reads the marker and exempts skills on
the configured ticket.bootstrap_skills list
- bootstrap_skills list lives in .claude/project-config.defaults.json
(extendable per fork via .claude/project-config.json)
Bash-write coverage (me2resh/apexyard#151):
- new _lib-detect-bash-write.sh — heuristic detector for output
redirection, tee, sed -i, awk -i inplace, python/node/ruby
embedded interpreters
- require-active-ticket.sh + require-migration-ticket.sh now fire
on Bash in addition to Edit|Write|MultiEdit
- design choice: false-negatives preferred over false-positives
(the matcher errs toward "let through" rather than block legit
read-only commands)
Tests: 32 unit cases on the lib + 12 integration cases on the hook,
including the exact me2resh/apexyard#151 bypass repro from the issue
body.
Closes me2resh/apexyard#150
Will manually close me2resh/apexyard#151 post-merge per the
single-Closes-per-PR rule (precedent: AgDR-0010 / PR #147).
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#153): extend Bash-write matcher beyond first-version coverage (#155)
Closes me2resh/apexyard#153.
Extends `_lib-detect-bash-write.sh` (introduced for me2resh/apexyard#151
in PR #152) with the matcher families flagged by Rex's review of #152.
AgDR-0011 already frames the matcher as a living list extended on
observation; this commit just walks the list.
New matcher families:
- File-moving builtins: `cp`, `mv`, `rm`, `dd`, `install` (anchored at
command-start; `--help`/`--version` and `git rm`/`git mv` excluded)
- Archive / network writes: `tar -x` / `tar --extract`, `curl -o` /
`--output`, `wget -O` / `--output-document`
- Additional embedded interpreters: `perl -e`, `php -r` (keyword-gated
like python/node/ruby); `go run`, `deno run`/`deno script.ts`,
`bun run`/`bun script.ts` (categorical script runners)
- Python helpers: `pathlib.Path().touch()`, `shutil.copy*`,
`shutil.move`, `os.rename` added to the `python -c` and python
heredoc keyword list
- Heredoc variants for `ruby` and `node` (previously only python
heredoc was covered)
Extractor extensions:
- `cp` / `mv`: last positional arg
- `curl -o` / `--output`: file argument
- `wget -O` / `--output-document`: file argument
- `tar -x`, `go run`, `deno`, `bun`, `perl -e`, `php -r`: return empty
(caller applies gate categorically per AgDR-0011)
Test count rose from 32 to 86. Negative-class counterexamples cover
the trickiest false-positive surfaces: `tar -t` listing, `cp --help`,
`rm --version`, `git rm`, `curl` bare URL fetch, `wget` bare URL fetch,
`deno fmt`, `deno test`, `go build`. Existing
`test_require_active_ticket_bash.sh` regression suite still passes 12/12.
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(#154): mock gh in test sandboxes to remove live-tracker dependency (#156)
- add _lib-mock-gh.sh helper that installs a fake `gh` on the sandbox PATH;
intercepts `gh issue view <N> ... --json ...` and returns synthetic
`{"number":N,"state":"OPEN"}` (overridable per-num via mock_gh_set_state)
- wire the shim into test_single_closes_per_pr.sh and
test_validate_pr_required_sections.sh so the validator's CLOSED-issue
refusal no longer breaks the suite when upstream issues are closed
- both files previously failed every case (0/13 and 0/8) because their PR
titles reference #114 / #113, which are now CLOSED upstream
- post-fix: 13/13 and 8/8; full suite remains green
Closes me2resh/apexyard#154
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#132): structured CEO marker + same-turn merge in /approve-merge (#158)
* feat(#132): structured CEO marker + same-turn merge in /approve-merge
Closes me2resh/apexyard#132 (drop the "stop before merge" rule) and
me2resh/apexyard#48 (harden CEO marker against self-approval bypass)
together. The two threads compose — see AgDR-0012 for the full
rationale.
Streamline (#132):
- /approve-merge now runs `gh pr merge --squash --delete-branch`
in the same turn as the marker write, by default
- --no-merge opt-out preserves the deferred-merge case
- The discrete approval moment is the SKILL INVOCATION, not a
follow-up "now do the merge" message
Harden (#48):
- CEO marker is now a structured key/value file with required fields:
sha=<HEAD>
approved_by=user
skill_version=2
Validated by block-unreviewed-merge.sh; bare-SHA legacy markers
rejected with a clear "stale format" error pointing at /approve-merge
- The model's bare `echo SHA > <pr>-ceo.approved` bypass is now
mechanically rejected. Forging the structured fields requires a
deliberate, visible rule violation rather than a one-line accident
- Optional audit fields (approved_at, approval_summary) capture the
"what did the user say when they approved" trail
- Rex marker stays bare-SHA — different threat model (automated
reviewer, not human authorization moment)
pr-workflow.md reframed: "the load-bearing rule is explicit per-PR
approval, not two user messages." The merge is a deterministic
consequence of the approval invocation.
Tests: 12 cases on the hardened hook covering the new format end-to-end
(valid v2, missing rex/ceo, bare-SHA legacy rejected, missing
approved_by, wrong approved_by, skill_version=1, sha mismatch,
non-merge no-op, gh-api shape gated). Full suite: 205/205 across 13
test files.
Will manually close me2resh/apexyard#48 post-merge per the
single-Closes-per-PR rule (precedent: PR #152 / AgDR-0011).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#132): redact private project reference from AgDR-0012
Abstracted two references to a registered private project that named
the project's owner/repo. The leak-protection hook caught one in the
PR body; this fixup removes the matching references from the
AgDR-0012 file content (which would otherwise have shipped the names
to me2resh/apexyard public repo via the merge).
The pre-existing reference at .claude/rules/pr-workflow.md:130
(documenting #47) is untouched — it predates this PR and is already
on the public repo's history.
Refs me2resh/apexyard#132.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#132): markdownlint blanks-around-fences + typo fix
Two small fixups against red CI / Rex feedback:
- AgDR-0012 line 63: fenced code block now has a blank line before it
(MD031). markdownlint-cli2 0.13.0 was rejecting the indented fence
inside the bullet because the fence's preceding line was the bullet
text (no blank).
- approve-merge SKILL.md line 172: typo `deferes` → `defers` (Rex flag
on PR #158).
Refs me2resh/apexyard#132.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#157): remove voice-prompts feature + correct hook/skill counts (#161)
* chore(#157): remove voice-prompts-on-pause feature + correct hook/skill counts
Closes me2resh/apexyard#157 (sunset the voice-prompts feature) and
me2resh/apexyard#77 (hook count off-by-one in CHANGELOG / CLAUDE.md)
in one bundled PR. AgDR-0013 supersedes AgDR-0009 with the full
rationale; both AgDRs are preserved (decision records are append-only
history).
Removed (#157):
- .claude/hooks/voice-prompt-on-pause.sh
- .claude/hooks/tests/test_voice_prompt_on_pause.sh
- Stop matcher block in .claude/settings.json (became empty after
voice removal)
- voice_prompts block in .claude/project-config.defaults.json
- "## Voice prompts" section in docs/project-config.md
- voice_prompts mention in AgDR-0010 line 32 (replaced with
leak_protection / ticket as still-current example config blocks)
Preserved:
- docs/agdr/AgDR-0009-voice-prompts-on-pause.md — historical record;
new "Superseded by: AgDR-0013" header at the top
- AgDR-0010 line 115 reference to AgDR-0009 — still accurate as a
historical pattern reference
Counts corrected (#77):
- CHANGELOG.md v0.3.0 stats: "17 hooks" → "18 hooks" (historical
fix — at v0.3.0 there were actually 18 hooks)
- CLAUDE.md table line: "18 shell scripts" → "24 shell scripts"
(current count after this removal)
- CLAUDE.md table line: "35 slash commands" → "39 slash commands"
- CLAUDE.md "Available skills (34)" → "Available skills (39)"
- CLAUDE.md quick-reference "Skills (35 slash commands)" →
"(39 slash commands)"
Why bundled: #77's correct count depends on whether voice is still in
the framework. Shipping #77 before #157 would write a number that's
wrong by one again the moment #157 lands. Same shape as previous
bundles (PR #152 / AgDR-0011, PR #158 / AgDR-0012).
Why no adopter-facing changelog mention of the voice removal: the
feature never reached a tagged release on main. v1.1.0 didn't have
it; v1.2.0 won't have it. From the adopter's perspective there's
nothing to retire. AgDR-0013 captures the framework's internal record
for future contributors. See AgDR-0013 § "No adopter-facing changelog
mention".
Tests: full hook test suite green (196 cases across 12 files —
test_voice_prompt_on_pause.sh removed). No regressions.
Will manually close me2resh/apexyard#77 post-merge per the
single-Closes-per-PR rule (precedent: PRs #152, #158).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#157): unwire voice from settings + configs + docs + AgDR-0013
Continuation of d09d7b5 (the file deletions). Squash-merge will
collapse both into one PR commit. This commit captures:
- .claude/settings.json — Stop matcher block removed (was the only
hook in it; entire matcher gone)
- .claude/project-config.defaults.json — voice_prompts block + its
_comment removed
- docs/project-config.md — "## Voice prompts" section removed
- docs/agdr/AgDR-0009-voice-prompts-on-pause.md — "Superseded by"
header added at the top, content otherwise preserved as history
- docs/agdr/AgDR-0010-portfolio-config-and-self-healing.md — line 32
example reference swapped from voice_prompts to
leak_protection / ticket (still-current config blocks)
- docs/agdr/AgDR-0013-sunset-voice-prompts.md — new supersession AgDR
- CLAUDE.md — hook count 18 → 24, skill count 35 → 39 (three
occurrences each, all aligned to current reality)
- CHANGELOG.md v0.3.0 stats — "17 hooks" → "18 hooks" historical fix
(#77 acceptance criterion 1)
Refs me2resh/apexyard#157 + me2resh/apexyard#77.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#157): markdownlint MD028 + resolve stale AgDR-numbering ref
Three small fixups against Rex CHANGES-REQUESTED on PR #161:
- AgDR-0009 line 3: promote "Superseded by:" header out of a
blockquote. The original "I decided ..." canonical blockquote at
line 5 was being merged with the new supersession blockquote
(markdownlint MD028 — "no blanks inside blockquote").
- AgDR-0013 line 3: same shape — "Supersedes:" header now a plain
bold paragraph, canonical "I decided ..." blockquote untouched.
- approve-merge SKILL.md line ~187: stale conditional reference
"AgDR-0012 (or 0013 — depends on whether voice-removal lands
first)" — order is now resolved (12 = approve-merge bundle, 13 =
voice removal). Drop the parenthetical.
Refs me2resh/apexyard#157 + me2resh/apexyard#77.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#160): multi-tab terminal demo on the landing site (#162)
Landing-site `site/index.html` terminal demo previously played one
canonical flow ("one ticket, start to finish") on autoplay. With the
v1.2.0 skill surface expansion (4 new skills, 8+ new hooks), one flow
no longer represents the framework's breadth.
Adds tabs to the terminal chrome — four flows visitors can either let
auto-cycle or click directly:
1. one ticket — existing flow, unchanged content
2. /handover — adopt an external repo into the portfolio
3. /setup — first-run framework bootstrap on a fresh fork
4. /fan-out — spawn 3 parallel agents on independent tickets
Auto-advance: on completion of the active tab's script, the demo
pauses ~1.8s then advances to the next tab. Loops at the end. User
can interrupt by clicking any tab or hitting Replay.
Implementation:
- HTML chrome — single title span replaced with a tablist of 4 button
tabs. ARIA `role="tablist"` / `role="tab"` / `aria-selected` so
keyboard + screen-reader users get the same semantics as sighted
ones.
- CSS — new `.shell-demo__tabs` + `.shell-demo__tab` with an accent
underline on the active tab. Tabs scroll horizontally on narrow
viewports (mobile responsive).
- JS — refactored the existing IIFE from one `script` array to an
array of four. Added `setActiveTab()` for ARIA state, `play(idx)`
takes a tab index, end-of-script auto-advances to `(idx + 1) % N`
unless the user clicked away during the pause. Adds a new `cmd`
type alongside `you` for slash-command invocations (renders with
the same `>` prompt prefix). prefers-reduced-motion still bails
early and leaves the static seed visible.
- Static seed (the non-JS fallback) still shows tab 0's content, so
reduced-motion / no-JS visitors see the one-ticket flow as before.
Hero metrics also corrected to current reality (#77 / PR #161 covers
CLAUDE.md and CHANGELOG.md; this PR catches the same numbers in the
landing site):
Skills 32 → 39
Hooks 18 → 24
The tabs ship in v1.2.0 alongside the framework changes that make
the new flows worth showcasing.
Refs me2resh/apexyard#160 (release v1.2.0 + landing-site refresh).
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#163): default the split-portfolio sibling repo name to <fork>-portfolio (#164)
Closes me2resh/apexyard#163.
The split-portfolio mode docs and skills previously suggested
`your-org/ops` as the default name for the private sibling repo, and
`portfolio/` as the local clone directory. Both too generic — adopters
running multiple ops setups end up with `your-org/ops` collisions, and
a bare `portfolio/` dir gives no signal about which framework it
belongs to when it sits next to other unrelated `portfolio/` dirs.
`<fork>-portfolio` is now the default — keeps the relationship to the
public fork explicit on disk and on GitHub. If the fork is named
`your-org/apexyard`, the portfolio defaults to
`your-org/apexyard-portfolio`. If the fork was renamed (e.g. `cos`),
the portfolio defaults to `cos-portfolio`. Adopters with custom names
keep working — the `portfolio:` config block resolves whatever path
they configured.
Files updated:
docs/multi-project.md
- Layout diagrams use `apexyard-portfolio/` as the sibling
- Setup walkthrough Step 2 + Step 3 use `your-org/apexyard-portfolio`
and explain the `<fork>-portfolio` pattern
- Config-block + symlink path examples updated to
`../apexyard-portfolio/...`
- Daily workflow + cross-machine clone commands updated
- The two existing `your-org/ops` references that remain are
fork-rename examples (lines 52, 64) — kept as-is, since renaming
the fork to `ops` is still valid (the portfolio would then
default to `ops-portfolio`)
.claude/skills/setup/SKILL.md
- Step 2b's "default suggestion" for the private repo name is now
`your-org/<fork>-portfolio`, computed dynamically from the
fork's repo name via `gh repo view --json name -q .name` so the
suggestion is correct even when the fork was renamed
- Clone command no longer needs a second arg — the repo name IS
the directory name
- Config-block paths updated to `../apexyard-portfolio/...`
.claude/skills/split-portfolio/SKILL.md
- Step 3's suggested-name template is now `<account>/<fork>-portfolio`
with the same dynamic-fork-name resolution
Mechanism unchanged. The `portfolio:` config block in
`.claude/project-config.json` still takes any path; this PR is purely
default-suggestion + example prose.
No tests required (skills are markdown instructions; no automated
coverage today).
Refs `apexyard.projects.yaml.example` uses "ops repo" as a generic
term meaning "the operational management fork" — not a name — kept
as-is.
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#165): skills reference page on the landing site + changelog link (#167)
Closes me2resh/apexyard#165.
Adds a public, browseable index of every apexyard slash command
alongside a one-click changelog link from the homepage nav.
site/skills.html (new):
- Lists all 39 skills currently shipping in .claude/skills/
- Each entry: slash command, argument hint, description (taken
verbatim from the SKILL.md frontmatter so the page matches the
runtime exactly)
- 10 categories: Setup & onboarding, Daily ops, Tickets & ideas,
Specs & decisions, Code review & merge, Architecture & dev tools,
Production-readiness audits, Workflow primitives, Communications,
Deprecated
- Same brutalist-terminal design tokens as the homepage — JetBrains
Mono, paper-cream background, single warning-red accent, sharp
corners. Inlined CSS to keep the static-only no-build-step
convention; design vars duplicated rather than extracted to a
shared file (~18 vars; cheap to keep in sync).
- Mobile responsive — skill grid collapses to single-column under
720px; titlebar nav hides non-CTA items on narrow viewports.
- Reduced-motion friendly (no animation in the first place).
- Internal anchor TOC at the top so the page scans in seconds.
site/index.html (nav addition):
- Added two nav links to the titlebar between "what's in the box"
and the github CTA:
• skills → ./skills.html
• changelog → https://github.com/me2resh/apexyard/releases
- The changelog link points at the GitHub releases page (not the
raw CHANGELOG.md file) so it auto-resolves to the latest tagged
release on each visit. v1.2.0 lands and the link is already there.
No new dependencies, no build step, no JS for the skills page. The
existing site convention (one-html-file-per-route, inlined CSS,
optional progressive-enhancement JS) is preserved.
Refs me2resh/apexyard#160 (release v1.2.0 + landing-site refresh) —
this is the second site-side deliverable for that ticket; the release
tag itself follows once me2resh/apexyard#159 (testing) closes.
Follow-up worth a separate ticket: a small generator script that
walks .claude/skills/*/SKILL.md, parses YAML frontmatter, and emits
the skills.html sections automatically. Out of scope for v1.2.0 —
first version is hand-curated and will need maintenance until that
generator lands.
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#168): accept release/vN.N.N branches + release(...) PR titles (#169)
Closes me2resh/apexyard#168.
The /release skill prescribed `release/vA.B.C` as the source-branch
name and `release: vA.B.C` as the PR title for the dev → main release
PR (per AgDR-0007). Both were rejected by the framework's own
validators:
validate-branch-name.sh required {type}/{TICKET-ID}-{description};
release/v1.2.0 has no ticket-id portion.
validate-pr-create.sh required type(SCOPE): form with `release` not
in pr.title_type_whitelist.
The contradiction surfaced cutting v1.2.0 — the first release under
the dev/main model.
Three small changes:
1. .claude/hooks/validate-branch-name.sh — added an early-out branch
that accepts ^release/vN.N.N(-rcN)?$ as a valid name. Narrow,
intentional exception for the framework's release-cut convention;
release branches don't carry a ticket-id because the release itself
IS the ticket.
2. .claude/project-config.defaults.json — added "release" to
pr.title_type_whitelist so a title like `release(#160): v1.2.0`
passes validate-pr-create.sh's existing regex unchanged.
3. .claude/skills/release/SKILL.md step 4 — corrected the prescribed
PR title to `release(#<release-ticket>): vA.B.C` so future /release
invocations produce a title that satisfies the validators by
construction.
Tested:
bash .claude/hooks/validate-branch-name.sh against:
release/v1.2.0 → 0 (allowed, release-special-case)
release/v1.2.0-rc1 → 0 (allowed, RC variant)
release/v9.9.9 → 0 (allowed)
release/foo → 2 (correctly blocked)
release/v1 → 2 (correctly blocked)
chore/GH-168-fix → 0 (allowed, standard pattern)
feature/GH-1-x → 0 (allowed, standard pattern)
Full hook test suite: 196/196 cases green across 12 test files.
Refs: surfaced 2026-05-04 cutting the first release under AgDR-0007.
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#170): exempt release/vN.N.N from validate-pr-create's branch-id check (#171)
Closes me2resh/apexyard#170.
Completes the work started in #169 (closing #168). #169 added a
release-pattern early-out to validate-branch-name.sh so release/vN.N.N
branches pass the branch-name validator. But validate-pr-create.sh has
its own independent branch-id check at line 273 that #169 didn't
touch — and it still rejects release/v1.2.0 because that name doesn't
contain a ticket-id substring.
This is the same class of contradiction #168 fixed; the fix is the
same shape. Add the same release-pattern early-out to the branch-id
check in validate-pr-create.sh:
- if the branch matches ^release/vN.N.N(-rcN)?$ → exempt (release
branches don't carry ticket-ids; the release itself is the ticket)
- otherwise → require a ticket-id substring as before
#168's acceptance criterion 3 ("validate-pr-create.sh accepts a PR
title `release(#160): v1.2.0` against the `release/v1.2.0` branch")
was checked off based on the title regex alone but didn't catch the
secondary branch-id check living in the same file. Surfaced trying
to open the v1.2.0 release PR.
Tested:
bash .claude/hooks/validate-pr-create.sh against:
release/v1.2.0 → 0 (allowed, exempt)
release/v1.2.0-rc1 → 0 (allowed, RC variant)
chore/GH-1-fix → 0 (allowed, has ticket-id)
release/foo → 2 (correctly blocked)
chore/no-ticket → 2 (correctly blocked)
Refs me2resh/apexyard#168 (the parent bug) + #169 (the partial fix).
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#173): sync CHANGELOG.md from main → dev (#174)
Closes me2resh/apexyard#173.
The release-cut model (AgDR-0007 / #116) squash-merges
release/vN.N.N → main, which means the v1.2.0 CHANGELOG section that
landed on main via PR #172 was never propagated back to dev. This
commit copies main's CHANGELOG.md verbatim onto dev so the v1.2.0
section is now present on both branches.
The diff is exactly the v1.2.0 entry being prepended; no other lines
change.
Without this sync, the next release PR cut from dev would build a
v1.3.0 section on top of v1.1.0, silently dropping v1.2.0 from dev's
running history. The corollary skill-level fix (option B in the
ticket) — updating /release to source the previous CHANGELOG from
upstream/main — is filed as a follow-up and out of scope for this PR.
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#181): /agdr skill — searchable AgDR library (#186)
Adds /agdr — a portfolio-wide index for Agent Decision Records.
Walks apexyard.projects.yaml, reads each project's docs/agdr/*.md
(local clone if available, else gh api fallback), parses the optional
YAML frontmatter for category + projects, and answers four queries:
- /agdr browse list across the portfolio, grouped by category
- /agdr search <term> full-text grep across all bodies, returns
<project>/AgDR-NNNN paths + matching paragraph
- /agdr show <id> print a specific record, disambiguates duplicates
- /agdr stats counts per category (the marketing-slide tile,
now backed by real data)
Six-category taxonomy: architecture | tech-stack | security | patterns
| integrations | other. Legacy AgDRs without frontmatter remain
first-class — they bucket as `other` and are flagged in browse so
operators can migrate at their own pace.
Backwards-compatible template change: templates/agdr.md gains an
optional `category:` (and optional `projects:`) line in the existing
frontmatter block. Omitting the line keeps every existing AgDR valid;
the skill defaults to `other` when missing.
Doc note in workflows/sdlc.md § Phase 2 points at /agdr search for
"have we decided this before?" lookups before drafting a design.
Smoke test in .claude/hooks/tests/test_agdr_skill.sh covers the
parser the spec specifies — frontmatter extraction, category
bucketing (including the legacy default-to-other path), id reading,
stats aggregation, and search match counts. 18 assertions, all green;
12 pre-existing test suites also green (no regressions).
Closes #181
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(me2resh/apexyard#183): /launch-check trend tracking (#185)
- Persist each run as JSON under <projects_dir>/<name>/launch-check/runs/
(timestamp + branch + commit + per-dimension scores + verdict + top_risks)
- Append "Trend (last 5 runs)" section to the per-run summary when at least 2
prior runs exist — markdown table + ASCII score chart
- Add /launch-check trend mode for read-only trend rendering (no full audit)
- Auto-derive notes column from score-delta vs previous run
(e.g. "Security +12, Analytics +10")
- Opt-in commit via .launch-check-history-tracked marker; gitignored by default
- New helper script render-trend.sh + test_launch_check_trend.sh (21 cases)
- Resolve projects dir via portfolio_projects_dir helper (no hardcoded path)
- Schema is forward-compatible — extra fields preserved on framework upgrade
AgDR-0014 documents the chart format / schema / opt-in choices.
Closes me2resh/apexyard#183
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#182): /status --briefing + bin/apexyard status CLI shim (#187)
* feat(me2resh/apexyard#182): /status --briefing + bin/apexyard CLI shim
Make slide 6's `$ apexyard status` invocation real — the smallest of three
slide-reality tickets, demonstrating the marketing-to-implementation
pattern before #181 / #183 land.
- New helper at .claude/skills/status/briefing.sh — computes the 4-line
"where am I" briefing (active workspace, active ticket, branch,
role-set) so the logic is testable in isolation and runnable from a
plain shell. Workspace inference walks up from cwd to the ops-fork
root (same algorithm as _lib-portfolio-paths.sh and /start-ticket).
- Ticket reads from .claude/session/tickets/<workspace> first, falls
back to .claude/session/current-ticket. Role-set is inferred from
the active ticket's GitHub labels (v1: backend / frontend / qa /
security / platform / sre / data / ux / ui / product / tech-lead,
plus the long forms). No match emits the explicit "<none — inferred
per task>" placeholder so the four-line shape is constant.
- New CLI shim bin/apexyard delegates `apexyard status` to the same
helper after walking up to find the ops-fork root. Works from any
workspace/<name>/ clone or the fork itself; symlink onto PATH to
install (no shadowing of the `claude` binary).
- /status SKILL.md gains a "Briefing mode" section documenting the
--briefing / -b flag; default /status output is unchanged.
- docs/multi-project.md "Daily workflow" demonstrates the new
invocation alongside /inbox and /status.
- New smoke tests at .claude/hooks/tests/test_status_briefing.sh — 8
cases covering ops-root cwd, workspace cwd, unknown cwd, ops-fallback
marker, per-project marker priority, label-based role inference, the
no-matching-label path, and the constant-four-line shape.
Closes me2resh/apexyard#182
* fix(me2resh/apexyard#182): replace curios-dog with example-app in SKILL.md output sample (leak fix)
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* docs(#178): LSP integration spike — measurement + recommendation (#184)
* docs(#178): LSP spike — token-savings measurement + integration findings
- Phase 1: estimated token cost A vs B for three representative queries on a real TS Lambda backend (~9,750 LOC); shallow semantic queries see ~3-23x input-token savings, multi-hop traces see ~1.4x
- Phase 2: verified Claude Code shipped first-party LSP support in v2.0.74 (Dec 2025), gated behind ENABLE_LSP_TOOL=1, wired in via the plugin system (.lsp.json); MCP-wrapped LSP shims (cclsp, lsp-mcp) remain as fallback
- Phase 3: recommends Option 3 — interactive clone-first prompt at end of /handover, preserving today's "never auto-clone" principle while making the deep-dive path discoverable
- Phase 4: AgDR Y-statement and option matrix sketched; full AgDR is a follow-up if the spike says go
- Recommendation: GO. Adopt the built-in tool, document the opt-in path, change /handover to offer clone-first interactively. No code changes beyond /handover SKILL.md; no novel integration to maintain
Closes me2resh/apexyard#178
* docs: fix markdownlint MD031/MD032 in spike report
Add blank lines around lists and a fenced code block to satisfy
markdownlint-cli2 in CI. Pure formatting, no content change.
Refs me2resh/apexyard#178
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* docs(me2resh/apexyard#190): annotate LSP-aware skills with opt-in callouts (#191)
- Add identical-shape "LSP-aware (optional, recommended)" callout near
the top of each of the four code-aware SKILL.md files.
- Per-skill savings number is the only variation:
- /code-review: ~3-15× cheaper for semantic queries
- /threat-model: ~3-15× shallow, ~1.4-5× multi-hop traces
- /security-review: ~3-15× cheaper for semantic querie…
* chore(#109): project-configurable ticket / branch / commit / PR schema (#118)
* chore(#109): project-configurable ticket / branch / commit / PR schema
Lift the prefix / type whitelists hardcoded across skills, hooks, and
CI into a versioned JSON config read through a shared shell library.
Shipped defaults at .claude/project-config.defaults.json; per-fork
overrides at the optional .claude/project-config.json; one reader
(_lib-read-config.sh) that every consumer now uses.
Added:
- .claude/project-config.defaults.json (v1 schema)
- .claude/hooks/_lib-read-config.sh (shared reader)
- docs/project-config.md (schema reference + extension guide)
- docs/agdr/AgDR-0006-project-configurable-ticket-schema.md
Migrated (still pass with no config present via last-resort fallback):
- validate-branch-name.sh → .branch.type_whitelist
- validate-commit-format.sh → .commit.type_whitelist (legacy
`commit_types` top-level key honoured as backward-compat fallback)
- validate-pr-create.sh → .pr.title_type_whitelist
- /feature, /task, /bug skills reference the config in their Rules
sections; none hardcodes the list any more
Unlocks subsequent config-readers for #107 / #110 / #111 / #112 /
#113 / #114 / #115 — each extends the schema under its own subtree
without further changes to the loader.
https://github.com/me2resh/apexyard/issues/109
* fix(#109): satisfy markdownlint MD032 and MD060 on new docs
Auto-fix MD032 (blank lines around lists) in AgDR-0006 and format
table-separator rows with surrounding spaces (MD060) in both new
doc files. Content unchanged; CI green.
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#110): add block-private-refs-in-public-repos.sh hook (#119)
Adds a new PreToolUse hook that blocks gh issue/PR/comment creation and
gh api .../issues|/pulls calls targeting a public framework repo
(default: me2resh/apexyard + whatever `upstream` resolves to) when the
title or body references any registered private project from
apexyard.projects.yaml (by name, repo slug, owner/repo#N ticket ref, or
workspace path).
The hook is a sibling to check-secrets.sh — both scan outgoing content
for identifiers that should never leave the local environment. Skip
marker `<!-- private-refs: allow -->` in the body lets a deliberate
reference through with a visible warning.
Files touched:
- .claude/hooks/block-private-refs-in-public-repos.sh (new)
- .claude/hooks/tests/test_block_private_refs.sh (new)
- .claude/rules/leak-protection.md (new)
- .claude/settings.json (wire PreToolUse matchers for the 5 gh shapes)
- docs/rule-audit.md (append section 10 + bump counts)
Refs: https://github.com/me2resh/apexyard/issues/110
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#115): add warn-stale-review-markers.sh PostToolUse hook (#120)
Fires after `git push` to surface review markers that have gone stale
because new commits were pushed past an existing Rex / CEO / design
approval. The merge gate already catches this at `gh pr merge` time,
but only then -- this hook closes the gap by flagging it immediately
at push-time so the author isn't surprised at merge.
- `.claude/hooks/warn-stale-review-markers.sh`
- PostToolUse, non-blocking (PostToolUse exit 2 would push noise
into the conversation; this hook is purely informational).
- Resolves the PR HEAD via `gh pr view --json headRefOid` -- same
source-of-truth as the merge-gate hooks post-apexyard#47 / #55.
Falls back to local HEAD with a visible WARN when gh is offline.
- Silent on: no PR for branch, no markers, fresh markers,
failed push (detected via `rejected` / `failed to push` /
`fatal:` / `error:` markers in tool_response.stderr).
- Modes: `warn` (default) prints one stderr line per stale marker;
`delete` opts in to auto-removal via
`.claude/project-config.json` -> `review_markers.on_stale`.
TODO(apexyard#109): switch to the shared project-config reader
once it lands.
- `.claude/settings.json`
- Wires the hook on PostToolUse / Bash / `git push *`.
- `docs/rule-audit.md`
- Adds a row under section 3 (Code review & PR quality) and
bumps the mechanized count 26 -> 27 / total 73 -> 74.
- `.claude/hooks/tests/test_warn_stale_review_markers.sh`
- 8 cases: no PR, no markers, fresh markers, stale rex / ceo /
design (warn), delete mode, failed push. All pass locally.
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#111): upgrade pre-push-gate from advisory reminder to blocking check-runner (#121)
* chore(#111): upgrade pre-push-gate from reminder to blocking check-runner
Previously pre-push-gate.sh just printed a checklist of things to run
locally before pushing — it was advisory. The rule it enforces is a
HARD STOP per pr-workflow.md. That asymmetry meant agents routinely
pushed broken work and discovered it only when CI went red.
Replaces the reminder with a blocking runner that reads the list of
shell commands from project config (.pre_push.commands) and executes
them in sequence before a push is allowed through. First non-zero
exit blocks the push with exit 2 and prints the failing command plus
the last 20 lines of its output.
- Config key: .pre_push.commands[] — array of {name, run} objects.
Shipped default is an empty list (hook stays a no-op on repos that
haven't configured their checks yet, including the framework repo
itself until it wires its own CI).
- Emergency bypass: '<!-- pre-push: skip -->' in the HEAD commit
message. Grep-able on purpose so bypasses stay auditable.
- Fail-fast: once a command fails, the rest don't run. Parallel
execution is a follow-up polish.
- 7 test cases in .claude/hooks/tests/test_pre_push_gate.sh — all
pass on the shipped default + a minimal custom config.
Updates docs/rule-audit.md to flip "partial" → "yes" for the
"before git push" rule.
Integrates with the shared config reader landed in #109.
https://github.com/me2resh/apexyard/issues/111
* fix(#111): remove orphaned footnote reference from rule-audit
The previous advisory-mode footnote was superseded by pre-push-111
but its definition was accidentally kept, tripping markdownlint MD053
(unused reference definition). Drop it.
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#107): add validate-issue-structure.sh PreToolUse hook (#122)
Mechanically enforces the ticket body schema when an agent files raw
`gh issue create` calls instead of going through the interactive
/feature, /task, /bug skills. Matches bracketed title prefix
([Feature] / [Chore] / [Bug] / [Docs] / etc.) against
`.ticket.required_sections` in project-config, and blocks (exit 2)
when any required section is missing or empty. Skip marker
`<!-- validate-issue-structure: skip -->` bypasses with a visible
stderr WARN for legitimate off-template tickets (epics, meta-threads).
Changes:
- .claude/hooks/validate-issue-structure.sh — the hook; reads schema
via the shared _lib-read-config.sh, with inlined defaults for bare
checkouts predating the config-schema rollout. Handles
--body / --body-file / -F path.
- .claude/project-config.defaults.json — extends .ticket with
required_sections (Feature/Chore/Refactor/Testing/CI/Docs/Bug) and
skip_marker; other .ticket fields untouched.
- .claude/settings.json — new PreToolUse matcher on Bash(gh issue
create *) alongside the existing suggest-ticket-template.sh and
block-private-refs-in-public-repos.sh hooks.
- .claude/hooks/tests/test_validate_issue_structure.sh — 15 cases
covering pass + fail paths per prefix, empty section detection,
skip marker, unknown prefix, non-gh invocation, --body-file path.
- docs/rule-audit.md — new section 11 row, mechanized count +1.
Upstream ticket: https://github.com/me2resh/apexyard/issues/107
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#112): add require-agdr-for-arch-pr.sh PreToolUse hook (#123)
Closes the asymmetry noted in .claude/rules/agdr-decisions.md: every
other HARD STOP in the ruleset (merge approval, ticket-first,
migration-first) is mechanically enforced, but the /decide HARD STOP
was prose-only. The commit-time hook require-agdr-for-arch-changes.sh
catches one architectural change at commit; this new PR-time hook
catches the cumulative diff so reviewers always have a pointer to the
decision record.
- New hook at .claude/hooks/require-agdr-for-arch-pr.sh
- Fires on Bash(gh pr create *)
- Parses --title/--body/--body-file/-F <path>
- Resolves base branch from --base, else upstream/dev, origin/dev,
upstream/main, origin/main, main, master (in that order)
- Computes `git diff <merge-base>..HEAD --name-only`
- Triggers on any changed file matching .agdr_trigger_paths[], OR any
dep-file addition (package.json via jq key-set diff; other
dep files via a commented +/- line-count heuristic — version
bumps match +/- counts and do not fire)
- Blocks (exit 2) with a helpful message naming the triggers and
pointing at /decide if the body has no `AgDR-\d+-[a-z0-9-]+`
reference
- Skip marker `<!-- agdr: not-applicable -->` bypasses with a
visible WARN on stderr
- Silent exit 0 on non-gh commands, empty diffs, unresolvable base
- Wired via .claude/settings.json PreToolUse Bash(gh pr create *)
- Adds two new top-level keys to .claude/project-config.defaults.json:
agdr_trigger_paths (shell globs — domain/, infrastructure/,
migrations/, *.tf, .github/workflows/, etc.)
agdr_trigger_dep_files (literal basenames — package.json,
pyproject.toml, Cargo.toml, go.mod, Gemfile)
Hook has inline fallback defaults kept in sync.
- Adds docs/rule-audit.md entry in the AgDR section; bumps mechanized
count 26 to 27 and total rows 73 to 74.
- Adds .claude/hooks/tests/test_require_agdr_for_arch_pr.sh (7 cases;
all green): path-triggered without AgDR (block), with AgDR (pass),
dep-file added (block), version-only bump (no fire), skip marker
(pass + warn), non-matching diff (pass), non-gh command (no-op).
Closes https://github.com/me2resh/apexyard/issues/112
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#113): require Testing section in PR body (config-driven) (#124)
Extends validate-pr-create.sh with a required-sections check that
replaces the hardcoded Glossary-only grep. The list of required H2
headings is project-configurable via `.pr.required_sections[]`.
Shipped default is ["Testing", "Glossary"], matching the canonical PR
description shape in workflows/code-review.md.
- Each entry must appear as `## <Name>` (case-insensitive).
- Empty sections are tolerated at this layer (the issue-structure hook
#107 does stricter empty-content checks for issue bodies; for PR
bodies, empty sections are left to the reviewer's judgement).
- Skip marker `<!-- pr-sections: skip -->` bypasses with a visible
stderr WARN — for trivial PRs (lint-only fixes, version bumps)
where the full template is overkill.
- Reads from project config via the shared _lib-read-config.sh (#109).
Inline fallback matches shipped defaults so bare checkouts predating
#109 keep working.
- 8 test cases cover: all-sections pass, each missing section,
missing-both (both errors printed), skip marker, case-insensitive
headings, H3 rejection.
https://github.com/me2resh/apexyard/issues/113
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#114): enforce single Closes-keyword per PR body (#125)
* chore(#114): enforce single Closes-keyword per PR body
Caps distinct auto-closing references (close/closes/closed, fix/fixes/
fixed, resolve/resolves/resolved + N or owner/repo+N) at one per PR
body. Closes the loophole where the title validator limited the title
to one ticket but multiple Closes lines in the body would still auto-
close all of them on merge.
- Scans stripped of fenced code blocks so closing keywords inside a
code sample do not count.
- Distinct counting: the same number referenced twice (e.g. via Fixes
and Closes) counts as one.
- Cross-repo refs (owner/repo+N) count normally.
- Opt-in escape hatch: pr.allow_multiple_closes=true in
project-config disables the check for teams that deliberately batch
rollbacks or dependency bumps.
- Per-PR bypass: a multi-close-approved HTML comment in the body
prints a visible stderr WARN and lets that PR through. Grep-able
trace so bypasses are auditable.
- 10 test cases cover: one close passes, no-keyword passes, two
distinct block, three mixed block, same-number-twice passes, code-
fence-ignored, skip marker, cross-ref without keyword, opt-in
config, cross-repo close.
Reads configuration via the shared _lib-read-config.sh (apexyard+109).
https://github.com/me2resh/apexyard/issues/114
* fix(#114): strip inline backticks and tilde fences from close-count scan
Rex caught a self-reflexive bug in the initial commit: documentation
mentioning closing keywords inside inline backticks (say a PR body
that explains the new hook with examples) counted as real closes, and
a skip marker inside inline backticks silently bypassed the check.
Future PRs that document the feature would trip the same trap.
Fix the code-region stripper to cover:
- Triple-backtick fences (already handled)
- Tilde fences (new)
- Inline-backtick spans (new)
Also run the skip-marker check against the stripped body, so a marker
used purely as documentation no longer activates a real bypass.
Three new test cases pin the behaviour:
- closing keywords in inline backticks are ignored
- skip marker inside inline backticks does NOT bypass
- tilde fences also get stripped
13/13 tests pass.
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#108): add /tickets-batch skill for bulk-file flow (#127)
The fast happy path for filing 5–20 structured tickets in one intent
without dropping to raw `gh issue create` (non-conformant) or running
`/feature` 20 times serially (~100 turns of interview).
- .claude/skills/tickets-batch/SKILL.md — new skill spec. Asks
shared-context questions (priority, epic, area-labels, repo) ONCE
for the whole batch, then runs a ≤3-question micro-interview per
ticket (type, one-line purpose, optional clarification when the
inference is low-confidence). Confirms the full batch as a table,
then files each via specific `gh issue create` calls (never a
bulk JSON dump — the validator runs per-issue). Output conforms
to `.ticket.required_sections` by construction. Caps at 20
tickets per invocation.
- CLAUDE.md — added a row for /tickets-batch in the Available
Skills table; bumped the count references from 33 to 34.
Refs https://github.com/me2resh/apexyard/issues/108
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#117): add /fan-out skill + parallel-work rule doc (#128)
- Add `.claude/skills/fan-out/SKILL.md` — spawns N parallel Agent calls
in a single assistant message, with per-task agent type, worktree
isolation, and foreground/background mode. Caps at 5 concurrent
agents. Refuses fan-out when tasks share file write targets or have
sequential dependencies. Includes pre-spawn active-ticket safety
check and worktree merge-back flow that pauses on conflict.
- Add `.claude/rules/parallel-work.md` — trigger heuristic for when an
agent should proactively offer fan-out (>= 2 file-independent,
context-independent, individually substantial work items). Pairs
with the skill: rule says when, skill says how.
- Update `CLAUDE.md` — bump rules count to 9, skills count to 34, add
`/fan-out` row to the skills table.
Refs https://github.com/me2resh/apexyard/issues/117
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* chore(#116): adopt release-cut branch model (dev/main + tags) — framework only (#126)
* chore(#116): adopt release-cut branch model (dev/main + tags)
Formalises the dev/main split that already exists informally — dev is
the daily-work branch where every PR lands, main is release-only,
tagged with semver on each merge. Framework-only: managed projects
under apexyard governance stay trunk-based.
Added:
- AgDR-0007 — decision record (options table covers full git flow vs
trunk-only vs gitflow-lite; chose gitflow-lite)
- /release skill — diff dev against main, propose semver bump from
conventional commits, generate CHANGELOG, open release PR, tag
after merge
- docs/release-process.md — prose runbook for cutting a release
(manual fallback for the skill)
- .git.protected_branches in project-config.defaults.json
(main/master/dev/develop)
Modified:
- block-main-push.sh — now blocks direct pushes/commits to all
configured protected branches (was: hardcoded main/master). Reads
.git.protected_branches via the shared config reader (apexyard#109).
- CLAUDE.md — new section under Git Conventions explaining the
dev/main model + the framework-only scope. Skill table entry for
/release. Skills count bumped to 34.
- docs/multi-project.md — note that upstream/main is release-only
and the dev/main split is framework-only.
Non-consequences (per AgDR-0007):
- No release/* or hotfix/* branches. Hotfixes are normal patches
cut quickly. Revisit if multi-version maintenance becomes a need.
- No automatic on-merge issue closing for dev PRs. The release PR's
body aggregates all Closes references for the batch and triggers
auto-close en masse when it merges to main. Manual close in the
meantime.
- CI workflows trigger on pull_request regardless of base, so
dev-targeting PRs already get the full check matrix — no
workflow file edits needed.
https://github.com/me2resh/apexyard/issues/116
* fix(#116): satisfy markdownlint MD032 + MD060 in 116 docs
Auto-fix added blank lines around lists in AgDR-0007 (MD032) and
spaced the table separators in AgDR-0007 + multi-project.md (MD060).
Content unchanged.
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* fix(#106): CHANGELOG fallback in drift hook for squash-merged forks (#129)
* fix(#106): CHANGELOG fallback in drift hook for squash-merged forks
The v1.1.0 tag-reachability check (`git tag --merged main`) misfires on
forks that sync via GitHub's default squash-merge: the squash collapses
the upstream-tag commit into a synthetic SHA, the tag stops being
reachable, and the banner keeps firing forever.
Discovered live on the first real-world `/update` flow: ops fork
squash-merged the v1.1.0 sync PR, banner kept saying "v1.1.0 available"
even though the fork was content-caught-up.
This commit adds a CHANGELOG-content fallback that fires only when the
primary tag check fails. If the fork's main has a heading
`## [X.Y.Z]` matching the upstream tag's version, treat the release as
absorbed and stay silent. Tolerant grep (matches the apexyard CHANGELOG
format from v1.1.0 onward, with leading-`v` stripping for tag→heading
conversion).
The merge-commit and rebase paths are unchanged — primary tag check
still works for them, and the fallback never fires when it shouldn't.
Test coverage (5 cases):
- squash-merge fork caught up to v1.1.0 → silent (the fix)
- merge-commit fork caught up to v1.1.0 → silent (regression check)
- fork stopped at v1.0.0 → banner fires
- fork has its own newer tag → silent
- squash-merge but no CHANGELOG on fork → banner fires (no false silence)
Records the strategy update in docs/agdr/AgDR-0008-…md (extends
AgDR-0005's tag-based-drift design).
https://github.com/me2resh/apexyard/issues/106
* fix(#106): satisfy markdownlint MD032/MD060 + shellcheck SC2164
Rex flagged two CI-blocking issues on the original 106 commit:
- AgDR-0008 had three bulleted sub-lists in the Consequences section
without surrounding blank lines (MD032). Added blanks and padded the
one tight-pipe table separator (MD060).
- The 106 test fixture had five subshell `cd "$fk"` calls without
`|| exit 1` (SC2164). Added the guard to all five.
5/5 tests still pass after the fix. No semantic change to the hook
or the test logic.
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#130): add /validate-idea skill — lightweight pre-spec gate (#131)
A 10-minute, 5-question check that sits between /idea and /write-spec.
Designed for solo founders running ApexYard — not a heavyweight
methodology like event storming or Wardley mapping.
Five questions, asked one at a time:
1. Who is this specifically for?
2. What do they do today instead?
3. What's the smallest version that proves the value?
4. What would prove this is wrong? (kill criteria)
5. Build, buy, or rent?
Output: a one-page validation doc with a GREEN/YELLOW/RED verdict.
RED auto-updates the IDEA-NNN backlog row to WONTDO.
Integration:
/idea — adds an optional default-no "Validate now?" step after
capture (and after the optional GitHub Issue offer).
/handover — adds a conditional "this looks dormant, validate?"
step at the end of the integration plan, gated on the dormancy
heuristic (last commit > 90d AND zero open PRs AND no recent
issue activity). Healthy projects don't see the prompt.
CLAUDE.md skills count bumped to 35; new skills row added.
https://github.com/me2resh/apexyard/issues/130
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat: configurable voice prompts on assistant pause (AgDR-0009-voice-prompts-on-pause) (#135)
Stop hook that speaks the assistant's question aloud (Jarvis-style)
when it pauses for user input. Initial phase is macOS-only via `say`,
no voice input — user replies via keyboard.
Default OFF. Adopters opt in by overriding `voice_prompts.enabled` to
true in `.claude/project-config.json`.
Files:
- .claude/hooks/voice-prompt-on-pause.sh — Stop hook with config gate,
trigger heuristic (questions-only by default), markdown stripping,
sentence-boundary truncation, fire-and-forget say invocation
- .claude/hooks/tests/test_voice_prompt_on_pause.sh — 9 cases covering
disabled-default, enabled+question, enabled+statement, approved-pattern,
abc-menu, malformed-transcript, no-say-on-PATH, trigger-always,
markdown-stripping
- .claude/project-config.defaults.json — voice_prompts schema block
added (enabled, voice, max_chars, rate_wpm, trigger), default OFF
- .claude/settings.json — new Stop hook entry wired with the standard
ops-root resolver wrapper
- docs/agdr/AgDR-0009-voice-prompts-on-pause.md — design rationale,
options matrix (status quo / macOS say / cloud TTS / ML detection),
consequences, future phases
- docs/project-config.md — new "Voice prompts" section with override
examples and privacy notes
Test mode: hook respects VOICE_PROMPTS_SYNC=1 to run say synchronously
(test runners need this so assertions don't race against orphaned
background processes). Production invocations always run async.
Future phases (out of scope here, AgDR §"Future phases"):
- Phase 2: cross-platform TTS (Linux espeak, Windows SpeechSynthesizer)
- Phase 3: cloud TTS providers (OpenAI, ElevenLabs) — privacy AgDR-worthy
- Phase 4: voice input via Whisper-based STT
- Phase 5: per-message overrides
Refs: https://github.com/me2resh/apexyard/issues/134
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#141): add /debug skill — structured hypothesis-driven debugging (#142)
* feat(#141): add /debug skill — structured hypothesis-driven debugging
Adds a methodology skill that enforces five disciplines:
1. Capture the symptom precisely (exact URL, exact response, exact step)
2. Read the architecture before guessing (map every layer the request
touches, file by file)
3. Form a hypothesis ladder (3–5 candidates, each with an explicit
evidence test that confirms or refutes it)
4. Gather evidence first, fix second
5. Verify the fix against the original symptom evidence (re-run the
same `curl` / browser repro you used in step 4 — unit tests
verify code, not feature, correctness)
Stack appendices (Web, Desktop) carry stack-specific surface-evidence
requirements (step 1), architecture-surface maps (step 2), and
evidence-tests cookbooks (step 4). The methodology body stays portable
across stacks; appendices are where stack-specific knowledge accrues
over time.
Web appendix covers browser routing, framework configs (Next/Nuxt/Vite),
SPA-fallback layers, CDN, origin, the shared API client, backend
handlers, and auth providers. Desktop appendix covers Electron / Tauri
/ native-shell concerns: app entry points, IPC bridges, native modules,
auto-updater, sandbox / entitlements, code signing, crash reports.
Includes "When NOT to use" guidance so the methodology overhead doesn't
sandbag simple bugs (typos, off-by-ones, greenfield exploration).
Motivated by a real OAuth debug session in a managed project where
three sequential fixes chased adjacent symptoms because each was
hypothesis-then-fix without evidence in between. The skill is the
"never do that again" guardrail.
Closes #141
* fix(#141): scrub private project issue numbers from anti-pattern table
Rex review on PR #142 caught that line 155 of the skill's anti-pattern
table still named the originating PRs (#375, #377, #380) from the
private project where the methodology was first exercised. The PR body
and commit message were correctly abstracted earlier, but this in-file
reference slipped through — the leak-protection hook only scans gh
issue/pr writes, not staged file content, so mechanical enforcement
didn't catch it.
Replaced with "Three sequential PRs chasing the same symptom because
each was based on a different guess (no evidence test in between
cycles)" — same pedagogical value, zero attribution.
Refs #141
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* docs(#143): document split-portfolio mode + add /setup privacy gate (#144)
Adopters on GitHub Free with any private project hit a silent privacy bug
following today's docs: forking apexyard makes a public fork that you cannot
later flip to private (GitHub policy), and committing `apexyard.projects.yaml`
+ `projects/<name>/` to the fork publishes private project names + handover
findings on a public GitHub repo.
This PR documents the supported workaround — split-portfolio mode — and
adds an upfront privacy gate to the /setup skill so new adopters never
hit the trip-wire silently.
docs/multi-project.md:
- New "Two setup modes — pick the one that matches your privacy needs"
section before TL;DR, with a side-by-side table and the explicit
trip-wire callout.
- Existing TL;DR retitled "TL;DR — single-fork mode (default)" with a
one-line pointer to the split-portfolio section.
- New "Split-portfolio mode — public framework + private portfolio"
section between the existing setup steps and the directory-layout
section. Includes:
- The two-repo layout (~/ops/apexyard public + ~/ops/portfolio private)
- 7-step setup walkthrough with copy-pasteable commands
- Daily workflow + upstream sync notes (both unchanged)
- Trade-offs (two repos to maintain, two clones per machine, one
upstream-sync conflict path on `projects/README.md`)
- "Migrating from single-fork to split-portfolio" recovery flow with
the explicit warning that GitHub Issue / PR edit history survives a
force-push and must be redacted separately
.claude/skills/setup/SKILL.md:
- New Step 2a: privacy gate — asks "are any projects private?" before
proposing the config. Branches on the answer:
- All public → single-fork mode
- GitHub Pro / Team / Enterprise → single-fork mode (private
forks of public repos
are supported on those
plans)
- Any private + GitHub Free → split-portfolio mode
- New Step 2b: walks through the split-portfolio setup interactively
(private repo create, sibling clone, gitignore + symlink) when the
privacy gate triggers.
- Detection: `test -L apexyard.projects.yaml` short-circuits Step 2b
for adopters already in split mode.
- Explicit "do NOT auto-migrate" rule for adopters already in single-fork
mode with private names already pushed — that path is destructive
(force-push history rewrite + redact issue/PR bodies + delete backup
branch) and warrants a deliberate, eyes-open run, not a /setup side
effect.
Out of scope for this PR (tracked separately on #143):
- `portfolio:` config block in `onboarding.yaml` schema
- Skill audit + refactor to honour configured `registry` / `projects_dir`
/ `ideas_backlog` paths instead of hardcoded fork-relative paths
- `/split-portfolio` migration helper skill that automates the recovery
flow currently documented manually
This is the docs-and-setup-question minimum-viable starter — the
framework code refactor is mechanical and lands as a follow-up.
Refs #143
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#145): portfolio config + self-healing + /split-portfolio helper (#147)
* feat(me2resh/apexyard#145): portfolio config block + self-healing + /split-portfolio helper
Closes the framework primitive deferred from me2resh/apexyard#144. Adds
first-class config-driven path resolution for the portfolio registry,
projects dir, and ideas backlog, with self-healing surfacing of broken
config at session start, plus a new /split-portfolio skill that automates
the destructive recovery flow.
Schema, helper, and hook:
- .claude/project-config.defaults.json: new portfolio: block
(registry, projects_dir, ideas_backlog) with defaults matching
today's single-fork layout
- .claude/hooks/_lib-portfolio-paths.sh: new sourceable helper exposing
portfolio_registry, portfolio_projects_dir, portfolio_ideas_backlog,
portfolio_validate, portfolio_clear_cache. Resolves relative paths
against the ops-fork root.
- .claude/hooks/check-portfolio-config.sh: new SessionStart hook —
silent on OK, one-line banner on broken config, never blocks session
- .claude/hooks/tests/test_portfolio_paths.sh: 13 cases covering
defaults, absolute/relative overrides, validate states, cache clear
Skill audit (18 SKILL.md files):
- Adds Path resolution callout pointing at the helper
- handover bash blocks now source helper and use $(portfolio_registry)
instead of literal apexyard.projects.yaml
- setup Step 2b now writes the portfolio: config block (recommended)
and validates via portfolio_validate before declaring success;
symlink approach kept as legacy fallback
New skill (.claude/skills/split-portfolio/SKILL.md):
- 10-step migration with explicit operator-confirmation gates at each
destructive step (force-push, body redaction, branch deletion)
- --verify mode: read-only state report (mode, paths, validate, drift)
- --dry-run mode: prints commands without executing
- Pre-flight refusals: already-private fork, paid GitHub plan, dirty
working tree, already-migrated state
- Step 9 writes the portfolio: config block (not symlinks) — symlink
fallback documented for adopters on older framework versions
- Step 9 surfaces the GitHub timeline-API survival caveat verbatim
- Idempotent re-runs: detects partial-migration state and resumes
Docs (docs/multi-project.md):
- Layout section describes both modes (config-block recommended,
symlink legacy) with self-healing notes
- Setup steps split into config-block mode and legacy symlink mode
- Migration section now points at /split-portfolio skill; manual
recipe preserved as fallback
AgDR (docs/agdr/AgDR-0010-portfolio-config-and-self-healing.md):
- Full Y-statement, options, decision, consequences, future phases
- Schema decision rationale: project-config.json over onboarding.yaml
because runtime path resolution belongs in project-config
Closes me2resh/apexyard#145
Refs me2resh/apexyard#146 (delivered same PR; closed manually post-merge
per the single-Closes-keyword rule in validate-pr-create.sh)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(me2resh/apexyard#145): markdownlint MD031 — blank lines around fences
CI's markdownlint-cli2 (v0.34.0) flagged the JSON + bash fenced code
blocks I added in setup/SKILL.md Step 2b without surrounding blank
lines. Added the required blanks. No content change.
Refs me2resh/apexyard#147
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(me2resh/apexyard#148): correct privacy-gate wording — adopter action, not framework auto-publish (#149)
The privacy-gate wording introduced in PR #144 (and unchanged in PR #147)
attributed the publication to the framework rather than the adopter:
"the standard fork-and-commit setup will silently publish your private
project names on a public GitHub repo"
That's factually wrong. ApexYard never pushes anything without explicit
operator approval — the publication only happens when the adopter
themselves runs git push. The "silently publish" framing read as if the
framework auto-publishes, which is misleading and undermines trust in
the rest of the framework's safety claims.
Two prose-only edits, no code, no behavior change:
- .claude/skills/setup/SKILL.md Step 2a — replaced "will silently
publish ..." with adopter-action language ("you might accidentally
publish ... a stray git push after registering them — I won't push
without your approval, but the risk is on the adopter once the data
is committed locally")
- docs/multi-project.md trip-wire callout — replaced "silently publish
their portfolio names the moment they push" with "risk accidentally
publishing their portfolio names with a stray push (the framework
itself never pushes without operator approval, but once the registry
is committed locally the next push exposes it)"
Verified: `grep -r "silently publish" .claude/skills/ docs/` returns no
hits.
Closes me2resh/apexyard#148
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#150): bootstrap-skill exemption + Bash-write coverage (#152)
Closes the legitimate-bypass case (me2resh/apexyard#150) and the
illegitimate-bypass case (me2resh/apexyard#151) together so the
ticket-first gate is coherent. Shipping either alone would leave a
window where the framework is internally inconsistent — see AgDR-0011
for the full rationale.
Bootstrap exemption (me2resh/apexyard#150):
- .claude/session/active-bootstrap marker, written by /setup,
/handover, /update, /split-portfolio on entry; cleared on exit
- SessionStart sweep (clear-bootstrap-marker.sh) for stale markers
from interrupted sessions
- require-active-ticket.sh reads the marker and exempts skills on
the configured ticket.bootstrap_skills list
- bootstrap_skills list lives in .claude/project-config.defaults.json
(extendable per fork via .claude/project-config.json)
Bash-write coverage (me2resh/apexyard#151):
- new _lib-detect-bash-write.sh — heuristic detector for output
redirection, tee, sed -i, awk -i inplace, python/node/ruby
embedded interpreters
- require-active-ticket.sh + require-migration-ticket.sh now fire
on Bash in addition to Edit|Write|MultiEdit
- design choice: false-negatives preferred over false-positives
(the matcher errs toward "let through" rather than block legit
read-only commands)
Tests: 32 unit cases on the lib + 12 integration cases on the hook,
including the exact me2resh/apexyard#151 bypass repro from the issue
body.
Closes me2resh/apexyard#150
Will manually close me2resh/apexyard#151 post-merge per the
single-Closes-per-PR rule (precedent: AgDR-0010 / PR #147).
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#153): extend Bash-write matcher beyond first-version coverage (#155)
Closes me2resh/apexyard#153.
Extends `_lib-detect-bash-write.sh` (introduced for me2resh/apexyard#151
in PR #152) with the matcher families flagged by Rex's review of #152.
AgDR-0011 already frames the matcher as a living list extended on
observation; this commit just walks the list.
New matcher families:
- File-moving builtins: `cp`, `mv`, `rm`, `dd`, `install` (anchored at
command-start; `--help`/`--version` and `git rm`/`git mv` excluded)
- Archive / network writes: `tar -x` / `tar --extract`, `curl -o` /
`--output`, `wget -O` / `--output-document`
- Additional embedded interpreters: `perl -e`, `php -r` (keyword-gated
like python/node/ruby); `go run`, `deno run`/`deno script.ts`,
`bun run`/`bun script.ts` (categorical script runners)
- Python helpers: `pathlib.Path().touch()`, `shutil.copy*`,
`shutil.move`, `os.rename` added to the `python -c` and python
heredoc keyword list
- Heredoc variants for `ruby` and `node` (previously only python
heredoc was covered)
Extractor extensions:
- `cp` / `mv`: last positional arg
- `curl -o` / `--output`: file argument
- `wget -O` / `--output-document`: file argument
- `tar -x`, `go run`, `deno`, `bun`, `perl -e`, `php -r`: return empty
(caller applies gate categorically per AgDR-0011)
Test count rose from 32 to 86. Negative-class counterexamples cover
the trickiest false-positive surfaces: `tar -t` listing, `cp --help`,
`rm --version`, `git rm`, `curl` bare URL fetch, `wget` bare URL fetch,
`deno fmt`, `deno test`, `go build`. Existing
`test_require_active_ticket_bash.sh` regression suite still passes 12/12.
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(#154): mock gh in test sandboxes to remove live-tracker dependency (#156)
- add _lib-mock-gh.sh helper that installs a fake `gh` on the sandbox PATH;
intercepts `gh issue view <N> ... --json ...` and returns synthetic
`{"number":N,"state":"OPEN"}` (overridable per-num via mock_gh_set_state)
- wire the shim into test_single_closes_per_pr.sh and
test_validate_pr_required_sections.sh so the validator's CLOSED-issue
refusal no longer breaks the suite when upstream issues are closed
- both files previously failed every case (0/13 and 0/8) because their PR
titles reference #114 / #113, which are now CLOSED upstream
- post-fix: 13/13 and 8/8; full suite remains green
Closes me2resh/apexyard#154
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#132): structured CEO marker + same-turn merge in /approve-merge (#158)
* feat(#132): structured CEO marker + same-turn merge in /approve-merge
Closes me2resh/apexyard#132 (drop the "stop before merge" rule) and
me2resh/apexyard#48 (harden CEO marker against self-approval bypass)
together. The two threads compose — see AgDR-0012 for the full
rationale.
Streamline (#132):
- /approve-merge now runs `gh pr merge --squash --delete-branch`
in the same turn as the marker write, by default
- --no-merge opt-out preserves the deferred-merge case
- The discrete approval moment is the SKILL INVOCATION, not a
follow-up "now do the merge" message
Harden (#48):
- CEO marker is now a structured key/value file with required fields:
sha=<HEAD>
approved_by=user
skill_version=2
Validated by block-unreviewed-merge.sh; bare-SHA legacy markers
rejected with a clear "stale format" error pointing at /approve-merge
- The model's bare `echo SHA > <pr>-ceo.approved` bypass is now
mechanically rejected. Forging the structured fields requires a
deliberate, visible rule violation rather than a one-line accident
- Optional audit fields (approved_at, approval_summary) capture the
"what did the user say when they approved" trail
- Rex marker stays bare-SHA — different threat model (automated
reviewer, not human authorization moment)
pr-workflow.md reframed: "the load-bearing rule is explicit per-PR
approval, not two user messages." The merge is a deterministic
consequence of the approval invocation.
Tests: 12 cases on the hardened hook covering the new format end-to-end
(valid v2, missing rex/ceo, bare-SHA legacy rejected, missing
approved_by, wrong approved_by, skill_version=1, sha mismatch,
non-merge no-op, gh-api shape gated). Full suite: 205/205 across 13
test files.
Will manually close me2resh/apexyard#48 post-merge per the
single-Closes-per-PR rule (precedent: PR #152 / AgDR-0011).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#132): redact private project reference from AgDR-0012
Abstracted two references to a registered private project that named
the project's owner/repo. The leak-protection hook caught one in the
PR body; this fixup removes the matching references from the
AgDR-0012 file content (which would otherwise have shipped the names
to me2resh/apexyard public repo via the merge).
The pre-existing reference at .claude/rules/pr-workflow.md:130
(documenting #47) is untouched — it predates this PR and is already
on the public repo's history.
Refs me2resh/apexyard#132.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#132): markdownlint blanks-around-fences + typo fix
Two small fixups against red CI / Rex feedback:
- AgDR-0012 line 63: fenced code block now has a blank line before it
(MD031). markdownlint-cli2 0.13.0 was rejecting the indented fence
inside the bullet because the fence's preceding line was the bullet
text (no blank).
- approve-merge SKILL.md line 172: typo `deferes` → `defers` (Rex flag
on PR #158).
Refs me2resh/apexyard#132.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#157): remove voice-prompts feature + correct hook/skill counts (#161)
* chore(#157): remove voice-prompts-on-pause feature + correct hook/skill counts
Closes me2resh/apexyard#157 (sunset the voice-prompts feature) and
me2resh/apexyard#77 (hook count off-by-one in CHANGELOG / CLAUDE.md)
in one bundled PR. AgDR-0013 supersedes AgDR-0009 with the full
rationale; both AgDRs are preserved (decision records are append-only
history).
Removed (#157):
- .claude/hooks/voice-prompt-on-pause.sh
- .claude/hooks/tests/test_voice_prompt_on_pause.sh
- Stop matcher block in .claude/settings.json (became empty after
voice removal)
- voice_prompts block in .claude/project-config.defaults.json
- "## Voice prompts" section in docs/project-config.md
- voice_prompts mention in AgDR-0010 line 32 (replaced with
leak_protection / ticket as still-current example config blocks)
Preserved:
- docs/agdr/AgDR-0009-voice-prompts-on-pause.md — historical record;
new "Superseded by: AgDR-0013" header at the top
- AgDR-0010 line 115 reference to AgDR-0009 — still accurate as a
historical pattern reference
Counts corrected (#77):
- CHANGELOG.md v0.3.0 stats: "17 hooks" → "18 hooks" (historical
fix — at v0.3.0 there were actually 18 hooks)
- CLAUDE.md table line: "18 shell scripts" → "24 shell scripts"
(current count after this removal)
- CLAUDE.md table line: "35 slash commands" → "39 slash commands"
- CLAUDE.md "Available skills (34)" → "Available skills (39)"
- CLAUDE.md quick-reference "Skills (35 slash commands)" →
"(39 slash commands)"
Why bundled: #77's correct count depends on whether voice is still in
the framework. Shipping #77 before #157 would write a number that's
wrong by one again the moment #157 lands. Same shape as previous
bundles (PR #152 / AgDR-0011, PR #158 / AgDR-0012).
Why no adopter-facing changelog mention of the voice removal: the
feature never reached a tagged release on main. v1.1.0 didn't have
it; v1.2.0 won't have it. From the adopter's perspective there's
nothing to retire. AgDR-0013 captures the framework's internal record
for future contributors. See AgDR-0013 § "No adopter-facing changelog
mention".
Tests: full hook test suite green (196 cases across 12 files —
test_voice_prompt_on_pause.sh removed). No regressions.
Will manually close me2resh/apexyard#77 post-merge per the
single-Closes-per-PR rule (precedent: PRs #152, #158).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#157): unwire voice from settings + configs + docs + AgDR-0013
Continuation of d09d7b5 (the file deletions). Squash-merge will
collapse both into one PR commit. This commit captures:
- .claude/settings.json — Stop matcher block removed (was the only
hook in it; entire matcher gone)
- .claude/project-config.defaults.json — voice_prompts block + its
_comment removed
- docs/project-config.md — "## Voice prompts" section removed
- docs/agdr/AgDR-0009-voice-prompts-on-pause.md — "Superseded by"
header added at the top, content otherwise preserved as history
- docs/agdr/AgDR-0010-portfolio-config-and-self-healing.md — line 32
example reference swapped from voice_prompts to
leak_protection / ticket (still-current config blocks)
- docs/agdr/AgDR-0013-sunset-voice-prompts.md — new supersession AgDR
- CLAUDE.md — hook count 18 → 24, skill count 35 → 39 (three
occurrences each, all aligned to current reality)
- CHANGELOG.md v0.3.0 stats — "17 hooks" → "18 hooks" historical fix
(#77 acceptance criterion 1)
Refs me2resh/apexyard#157 + me2resh/apexyard#77.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#157): markdownlint MD028 + resolve stale AgDR-numbering ref
Three small fixups against Rex CHANGES-REQUESTED on PR #161:
- AgDR-0009 line 3: promote "Superseded by:" header out of a
blockquote. The original "I decided ..." canonical blockquote at
line 5 was being merged with the new supersession blockquote
(markdownlint MD028 — "no blanks inside blockquote").
- AgDR-0013 line 3: same shape — "Supersedes:" header now a plain
bold paragraph, canonical "I decided ..." blockquote untouched.
- approve-merge SKILL.md line ~187: stale conditional reference
"AgDR-0012 (or 0013 — depends on whether voice-removal lands
first)" — order is now resolved (12 = approve-merge bundle, 13 =
voice removal). Drop the parenthetical.
Refs me2resh/apexyard#157 + me2resh/apexyard#77.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#160): multi-tab terminal demo on the landing site (#162)
Landing-site `site/index.html` terminal demo previously played one
canonical flow ("one ticket, start to finish") on autoplay. With the
v1.2.0 skill surface expansion (4 new skills, 8+ new hooks), one flow
no longer represents the framework's breadth.
Adds tabs to the terminal chrome — four flows visitors can either let
auto-cycle or click directly:
1. one ticket — existing flow, unchanged content
2. /handover — adopt an external repo into the portfolio
3. /setup — first-run framework bootstrap on a fresh fork
4. /fan-out — spawn 3 parallel agents on independent tickets
Auto-advance: on completion of the active tab's script, the demo
pauses ~1.8s then advances to the next tab. Loops at the end. User
can interrupt by clicking any tab or hitting Replay.
Implementation:
- HTML chrome — single title span replaced with a tablist of 4 button
tabs. ARIA `role="tablist"` / `role="tab"` / `aria-selected` so
keyboard + screen-reader users get the same semantics as sighted
ones.
- CSS — new `.shell-demo__tabs` + `.shell-demo__tab` with an accent
underline on the active tab. Tabs scroll horizontally on narrow
viewports (mobile responsive).
- JS — refactored the existing IIFE from one `script` array to an
array of four. Added `setActiveTab()` for ARIA state, `play(idx)`
takes a tab index, end-of-script auto-advances to `(idx + 1) % N`
unless the user clicked away during the pause. Adds a new `cmd`
type alongside `you` for slash-command invocations (renders with
the same `>` prompt prefix). prefers-reduced-motion still bails
early and leaves the static seed visible.
- Static seed (the non-JS fallback) still shows tab 0's content, so
reduced-motion / no-JS visitors see the one-ticket flow as before.
Hero metrics also corrected to current reality (#77 / PR #161 covers
CLAUDE.md and CHANGELOG.md; this PR catches the same numbers in the
landing site):
Skills 32 → 39
Hooks 18 → 24
The tabs ship in v1.2.0 alongside the framework changes that make
the new flows worth showcasing.
Refs me2resh/apexyard#160 (release v1.2.0 + landing-site refresh).
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#163): default the split-portfolio sibling repo name to <fork>-portfolio (#164)
Closes me2resh/apexyard#163.
The split-portfolio mode docs and skills previously suggested
`your-org/ops` as the default name for the private sibling repo, and
`portfolio/` as the local clone directory. Both too generic — adopters
running multiple ops setups end up with `your-org/ops` collisions, and
a bare `portfolio/` dir gives no signal about which framework it
belongs to when it sits next to other unrelated `portfolio/` dirs.
`<fork>-portfolio` is now the default — keeps the relationship to the
public fork explicit on disk and on GitHub. If the fork is named
`your-org/apexyard`, the portfolio defaults to
`your-org/apexyard-portfolio`. If the fork was renamed (e.g. `cos`),
the portfolio defaults to `cos-portfolio`. Adopters with custom names
keep working — the `portfolio:` config block resolves whatever path
they configured.
Files updated:
docs/multi-project.md
- Layout diagrams use `apexyard-portfolio/` as the sibling
- Setup walkthrough Step 2 + Step 3 use `your-org/apexyard-portfolio`
and explain the `<fork>-portfolio` pattern
- Config-block + symlink path examples updated to
`../apexyard-portfolio/...`
- Daily workflow + cross-machine clone commands updated
- The two existing `your-org/ops` references that remain are
fork-rename examples (lines 52, 64) — kept as-is, since renaming
the fork to `ops` is still valid (the portfolio would then
default to `ops-portfolio`)
.claude/skills/setup/SKILL.md
- Step 2b's "default suggestion" for the private repo name is now
`your-org/<fork>-portfolio`, computed dynamically from the
fork's repo name via `gh repo view --json name -q .name` so the
suggestion is correct even when the fork was renamed
- Clone command no longer needs a second arg — the repo name IS
the directory name
- Config-block paths updated to `../apexyard-portfolio/...`
.claude/skills/split-portfolio/SKILL.md
- Step 3's suggested-name template is now `<account>/<fork>-portfolio`
with the same dynamic-fork-name resolution
Mechanism unchanged. The `portfolio:` config block in
`.claude/project-config.json` still takes any path; this PR is purely
default-suggestion + example prose.
No tests required (skills are markdown instructions; no automated
coverage today).
Refs `apexyard.projects.yaml.example` uses "ops repo" as a generic
term meaning "the operational management fork" — not a name — kept
as-is.
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#165): skills reference page on the landing site + changelog link (#167)
Closes me2resh/apexyard#165.
Adds a public, browseable index of every apexyard slash command
alongside a one-click changelog link from the homepage nav.
site/skills.html (new):
- Lists all 39 skills currently shipping in .claude/skills/
- Each entry: slash command, argument hint, description (taken
verbatim from the SKILL.md frontmatter so the page matches the
runtime exactly)
- 10 categories: Setup & onboarding, Daily ops, Tickets & ideas,
Specs & decisions, Code review & merge, Architecture & dev tools,
Production-readiness audits, Workflow primitives, Communications,
Deprecated
- Same brutalist-terminal design tokens as the homepage — JetBrains
Mono, paper-cream background, single warning-red accent, sharp
corners. Inlined CSS to keep the static-only no-build-step
convention; design vars duplicated rather than extracted to a
shared file (~18 vars; cheap to keep in sync).
- Mobile responsive — skill grid collapses to single-column under
720px; titlebar nav hides non-CTA items on narrow viewports.
- Reduced-motion friendly (no animation in the first place).
- Internal anchor TOC at the top so the page scans in seconds.
site/index.html (nav addition):
- Added two nav links to the titlebar between "what's in the box"
and the github CTA:
• skills → ./skills.html
• changelog → https://github.com/me2resh/apexyard/releases
- The changelog link points at the GitHub releases page (not the
raw CHANGELOG.md file) so it auto-resolves to the latest tagged
release on each visit. v1.2.0 lands and the link is already there.
No new dependencies, no build step, no JS for the skills page. The
existing site convention (one-html-file-per-route, inlined CSS,
optional progressive-enhancement JS) is preserved.
Refs me2resh/apexyard#160 (release v1.2.0 + landing-site refresh) —
this is the second site-side deliverable for that ticket; the release
tag itself follows once me2resh/apexyard#159 (testing) closes.
Follow-up worth a separate ticket: a small generator script that
walks .claude/skills/*/SKILL.md, parses YAML frontmatter, and emits
the skills.html sections automatically. Out of scope for v1.2.0 —
first version is hand-curated and will need maintenance until that
generator lands.
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#168): accept release/vN.N.N branches + release(...) PR titles (#169)
Closes me2resh/apexyard#168.
The /release skill prescribed `release/vA.B.C` as the source-branch
name and `release: vA.B.C` as the PR title for the dev → main release
PR (per AgDR-0007). Both were rejected by the framework's own
validators:
validate-branch-name.sh required {type}/{TICKET-ID}-{description};
release/v1.2.0 has no ticket-id portion.
validate-pr-create.sh required type(SCOPE): form with `release` not
in pr.title_type_whitelist.
The contradiction surfaced cutting v1.2.0 — the first release under
the dev/main model.
Three small changes:
1. .claude/hooks/validate-branch-name.sh — added an early-out branch
that accepts ^release/vN.N.N(-rcN)?$ as a valid name. Narrow,
intentional exception for the framework's release-cut convention;
release branches don't carry a ticket-id because the release itself
IS the ticket.
2. .claude/project-config.defaults.json — added "release" to
pr.title_type_whitelist so a title like `release(#160): v1.2.0`
passes validate-pr-create.sh's existing regex unchanged.
3. .claude/skills/release/SKILL.md step 4 — corrected the prescribed
PR title to `release(#<release-ticket>): vA.B.C` so future /release
invocations produce a title that satisfies the validators by
construction.
Tested:
bash .claude/hooks/validate-branch-name.sh against:
release/v1.2.0 → 0 (allowed, release-special-case)
release/v1.2.0-rc1 → 0 (allowed, RC variant)
release/v9.9.9 → 0 (allowed)
release/foo → 2 (correctly blocked)
release/v1 → 2 (correctly blocked)
chore/GH-168-fix → 0 (allowed, standard pattern)
feature/GH-1-x → 0 (allowed, standard pattern)
Full hook test suite: 196/196 cases green across 12 test files.
Refs: surfaced 2026-05-04 cutting the first release under AgDR-0007.
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#170): exempt release/vN.N.N from validate-pr-create's branch-id check (#171)
Closes me2resh/apexyard#170.
Completes the work started in #169 (closing #168). #169 added a
release-pattern early-out to validate-branch-name.sh so release/vN.N.N
branches pass the branch-name validator. But validate-pr-create.sh has
its own independent branch-id check at line 273 that #169 didn't
touch — and it still rejects release/v1.2.0 because that name doesn't
contain a ticket-id substring.
This is the same class of contradiction #168 fixed; the fix is the
same shape. Add the same release-pattern early-out to the branch-id
check in validate-pr-create.sh:
- if the branch matches ^release/vN.N.N(-rcN)?$ → exempt (release
branches don't carry ticket-ids; the release itself is the ticket)
- otherwise → require a ticket-id substring as before
#168's acceptance criterion 3 ("validate-pr-create.sh accepts a PR
title `release(#160): v1.2.0` against the `release/v1.2.0` branch")
was checked off based on the title regex alone but didn't catch the
secondary branch-id check living in the same file. Surfaced trying
to open the v1.2.0 release PR.
Tested:
bash .claude/hooks/validate-pr-create.sh against:
release/v1.2.0 → 0 (allowed, exempt)
release/v1.2.0-rc1 → 0 (allowed, RC variant)
chore/GH-1-fix → 0 (allowed, has ticket-id)
release/foo → 2 (correctly blocked)
chore/no-ticket → 2 (correctly blocked)
Refs me2resh/apexyard#168 (the parent bug) + #169 (the partial fix).
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#173): sync CHANGELOG.md from main → dev (#174)
Closes me2resh/apexyard#173.
The release-cut model (AgDR-0007 / #116) squash-merges
release/vN.N.N → main, which means the v1.2.0 CHANGELOG section that
landed on main via PR #172 was never propagated back to dev. This
commit copies main's CHANGELOG.md verbatim onto dev so the v1.2.0
section is now present on both branches.
The diff is exactly the v1.2.0 entry being prepended; no other lines
change.
Without this sync, the next release PR cut from dev would build a
v1.3.0 section on top of v1.1.0, silently dropping v1.2.0 from dev's
running history. The corollary skill-level fix (option B in the
ticket) — updating /release to source the previous CHANGELOG from
upstream/main — is filed as a follow-up and out of scope for this PR.
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#181): /agdr skill — searchable AgDR library (#186)
Adds /agdr — a portfolio-wide index for Agent Decision Records.
Walks apexyard.projects.yaml, reads each project's docs/agdr/*.md
(local clone if available, else gh api fallback), parses the optional
YAML frontmatter for category + projects, and answers four queries:
- /agdr browse list across the portfolio, grouped by category
- /agdr search <term> full-text grep across all bodies, returns
<project>/AgDR-NNNN paths + matching paragraph
- /agdr show <id> print a specific record, disambiguates duplicates
- /agdr stats counts per category (the marketing-slide tile,
now backed by real data)
Six-category taxonomy: architecture | tech-stack | security | patterns
| integrations | other. Legacy AgDRs without frontmatter remain
first-class — they bucket as `other` and are flagged in browse so
operators can migrate at their own pace.
Backwards-compatible template change: templates/agdr.md gains an
optional `category:` (and optional `projects:`) line in the existing
frontmatter block. Omitting the line keeps every existing AgDR valid;
the skill defaults to `other` when missing.
Doc note in workflows/sdlc.md § Phase 2 points at /agdr search for
"have we decided this before?" lookups before drafting a design.
Smoke test in .claude/hooks/tests/test_agdr_skill.sh covers the
parser the spec specifies — frontmatter extraction, category
bucketing (including the legacy default-to-other path), id reading,
stats aggregation, and search match counts. 18 assertions, all green;
12 pre-existing test suites also green (no regressions).
Closes #181
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(me2resh/apexyard#183): /launch-check trend tracking (#185)
- Persist each run as JSON under <projects_dir>/<name>/launch-check/runs/
(timestamp + branch + commit + per-dimension scores + verdict + top_risks)
- Append "Trend (last 5 runs)" section to the per-run summary when at least 2
prior runs exist — markdown table + ASCII score chart
- Add /launch-check trend mode for read-only trend rendering (no full audit)
- Auto-derive notes column from score-delta vs previous run
(e.g. "Security +12, Analytics +10")
- Opt-in commit via .launch-check-history-tracked marker; gitignored by default
- New helper script render-trend.sh + test_launch_check_trend.sh (21 cases)
- Resolve projects dir via portfolio_projects_dir helper (no hardcoded path)
- Schema is forward-compatible — extra fields preserved on framework upgrade
AgDR-0014 documents the chart format / schema / opt-in choices.
Closes me2resh/apexyard#183
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* feat(#182): /status --briefing + bin/apexyard status CLI shim (#187)
* feat(me2resh/apexyard#182): /status --briefing + bin/apexyard CLI shim
Make slide 6's `$ apexyard status` invocation real — the smallest of three
slide-reality tickets, demonstrating the marketing-to-implementation
pattern before #181 / #183 land.
- New helper at .claude/skills/status/briefing.sh — computes the 4-line
"where am I" briefing (active workspace, active ticket, branch,
role-set) so the logic is testable in isolation and runnable from a
plain shell. Workspace inference walks up from cwd to the ops-fork
root (same algorithm as _lib-portfolio-paths.sh and /start-ticket).
- Ticket reads from .claude/session/tickets/<workspace> first, falls
back to .claude/session/current-ticket. Role-set is inferred from
the active ticket's GitHub labels (v1: backend / frontend / qa /
security / platform / sre / data / ux / ui / product / tech-lead,
plus the long forms). No match emits the explicit "<none — inferred
per task>" placeholder so the four-line shape is constant.
- New CLI shim bin/apexyard delegates `apexyard status` to the same
helper after walking up to find the ops-fork root. Works from any
workspace/<name>/ clone or the fork itself; symlink onto PATH to
install (no shadowing of the `claude` binary).
- /status SKILL.md gains a "Briefing mode" section documenting the
--briefing / -b flag; default /status output is unchanged.
- docs/multi-project.md "Daily workflow" demonstrates the new
invocation alongside /inbox and /status.
- New smoke tests at .claude/hooks/tests/test_status_briefing.sh — 8
cases covering ops-root cwd, workspace cwd, unknown cwd, ops-fallback
marker, per-project marker priority, label-based role inference, the
no-matching-label path, and the constant-four-line shape.
Closes me2resh/apexyard#182
* fix(me2resh/apexyard#182): replace curios-dog with example-app in SKILL.md output sample (leak fix)
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* docs(#178): LSP integration spike — measurement + recommendation (#184)
* docs(#178): LSP spike — token-savings measurement + integration findings
- Phase 1: estimated token cost A vs B for three representative queries on a real TS Lambda backend (~9,750 LOC); shallow semantic queries see ~3-23x input-token savings, multi-hop traces see ~1.4x
- Phase 2: verified Claude Code shipped first-party LSP support in v2.0.74 (Dec 2025), gated behind ENABLE_LSP_TOOL=1, wired in via the plugin system (.lsp.json); MCP-wrapped LSP shims (cclsp, lsp-mcp) remain as fallback
- Phase 3: recommends Option 3 — interactive clone-first prompt at end of /handover, preserving today's "never auto-clone" principle while making the deep-dive path discoverable
- Phase 4: AgDR Y-statement and option matrix sketched; full AgDR is a follow-up if the spike says go
- Recommendation: GO. Adopt the built-in tool, document the opt-in path, change /handover to offer clone-first interactively. No code changes beyond /handover SKILL.md; no novel integration to maintain
Closes me2resh/apexyard#178
* docs: fix markdownlint MD031/MD032 in spike report
Add blank lines around lists and a fenced code block to satisfy
markdownlint-cli2 in CI. Pure formatting, no content change.
Refs me2resh/apexyard#178
---------
Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
* docs(me2resh/apexyard#190): annotate LSP-aware skills with opt-in callouts (#191)
- Add identical-shape "LSP-aware (optional, recommended)" callout near
the top of each of the four code-aware SKILL.md files.
- Per-skill savings number is the only variation:
- /code-review: ~3-15× cheaper for semantic queries
- /threat-model: ~3-15× shallow, ~1.4-5× multi-hop traces
- /security-review: ~3-15× cheaper for semantic querie…
Summary
onboarding.yaml) for easy team customisationCLAUDE.mdentry point that integrates with Claude Codesite/index.html.claude/runnable layer: 6 hooks, 6 modular rule files, 5 sub-agents, 5 slash-command skills, hook wiringgolden-paths/pipelines/: 7 drop-in GitHub Actions CI workflowsWhat's Included
Roles (19 total)
Workflows (3)
Templates (4)
Configuration
onboarding.yaml— company name, team members, tech stack, workflow preferencesCLAUDE.md— stack entry point for Claude Code integration.claude/runnable layer (NEW)Until this layer landed, ApexStack shipped only the passive surface — markdown for roles, workflows, and templates. There was nothing a user could actually invoke. The
.claude/directory closes that gap with the four primitives Claude Code reads at the project root:hooks/block-git-add-all.sh,block-main-push.sh,check-secrets.sh,pre-push-gate.sh,validate-branch-name.sh,validate-pr-create.sh— enforce nogit add -A, no direct push to main, no committed secrets, valid branch / PR title formatrules/agdr-decisions.md,code-standards.md,git-conventions.md,pr-quality.md,pr-workflow.md,workflow-gates.md— modular rule files imported via@.claude/rules/*fromCLAUDE.mdagents/skills//decide,/code-review,/security-review,/audit-deps,/write-spec— slash commands users can typesettings.jsonPreToolUseevents on the Bash toolgolden-paths/pipelines/(NEW)Seven drop-in GitHub Actions workflows that integrate the agents into a project's CI:
ci.yml— combined pipeline (Rex + Sentinel + Guardian)code-quality.yml— Rex (TypeScript, ESLint, Prettier, tests, build)security.yml— Sentinel (Semgrep SAST, npm audit, TruffleHog secrets, CodeQL)dependency-audit.yml— Guardian (vulnerabilities, outdated, licenses)pr-title-check.yml— enforces a ticket ID in every PR titlereview-check.yml— blocks merge if Rex hasn't reviewed the latest commitseo-check.yml— Scout (content SEO score)Each is self-contained and can be copied to
.github/workflows/independently. Seegolden-paths/pipelines/README.mdfor full documentation.Verification
Glossary
CLAUDE.mdvia@imports, encoding a specific area of standards/decide) the user types into Claude Code that runs a specific script or workflow🤖 Generated with Claude Code