feat(#145): portfolio config + self-healing + /split-portfolio helper#147
Merged
atlas-apex merged 2 commits intoMay 3, 2026
Merged
Conversation
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes the framework primitive deferred from #144.
portfolio:config block in.claude/project-config.{defaults,}.jsonfor path resolution (registry, projects_dir, ideas_backlog). Defaults preserve today's single-fork behavior; adopters in split-portfolio mode override the keys._lib-portfolio-paths.shhelper library +portfolio_validate()for self-healing. New SessionStart hook surfaces a one-line banner if the resolved paths are broken — silent on success, never blocks.handover(now sources the helper instead of hardcodedapexyard.projects.yaml);setupStep 2b (writes the config block as the first-class path; symlinks remain documented as legacy fallback)./split-portfolioskill — automates the destructive recovery flow with explicit operator-confirmation gates at each destructive step.--verifymode reads-only;--dry-runprints commands without executing. Refuses on already-private fork, paid GitHub plan, dirty working tree, or already-migrated state. Step 9 writes the new config block (not symlinks); Step 9 surfaces the GitHub timeline-API survival caveat verbatim.project-config.jsonoveronboarding.yaml).Targets
devper the apexyard release-cut model.What changes
.claude/project-config.defaults.json.claude/hooks/_lib-portfolio-paths.sh.claude/hooks/check-portfolio-config.sh.claude/settings.json.claude/hooks/tests/test_portfolio_paths.sh.claude/skills/split-portfolio/SKILL.mdSKILL.md(callout insertion).claude/skills/handover/SKILL.md,.claude/skills/setup/SKILL.mddocs/multi-project.mddocs/agdr/AgDR-0010-portfolio-config-and-self-healing.md26 files changed, ~1,416 insertions, 18 deletions.
Why "in this PR" (the self-healing scope)
The original ticket #145 only asked for the schema + helper + skill audit. During design, the question came up: "is this usable AND self-healing?" The honest answer was "usable yes, self-healing only partially." The cheapest way to close that gap was four extra hunks landing in this same PR:
portfolio_validate()in the helper, the SessionStart banner that uses it, the/setupfinalization gate, and--verifymode in/split-portfolio. ~150 extra lines, all separable. The acceptance criteria on #145 were updated before code to make Rex's review mechanical.Closes vs Refs
This PR closes #145 (per the single-Closes-keyword rule in
validate-pr-create.sh). #146 is delivered in the same PR but closed manually post-merge with a "delivered in PR #X" comment — that's the framework's documented pattern when a single PR delivers two coupled tickets.Testing
Helper unit tests
bash .claude/hooks/tests/test_portfolio_paths.sh— 13 cases, all pass:projects:key, missing projects_dir, ideas_backlog with missing parent.portfolio_clear_cache()resets resolver state.Hook regression sweep
for t in .claude/hooks/tests/test_*.sh; do bash "$t"; done— 8/10 pass. Two pre-existing failures unrelated to this PR (test_single_closes_per_pr,test_validate_pr_required_sections) reference closed issue #114 in their fixtures and trip the network cross-ref check on a cleanupstream/devcheckout. Verified pre-existing by running against pristineupstream/devon a stash.Live smoke
portfolio.registry. Exits 0 (informational, never blocks).Manual smoke for
/split-portfolioNot run end-to-end against a real public fork — destructive operations, requires throwaway repos. Documented in the skill's AC list as "manual smoke test" rather than CI-tested. The skill's pre-flight refusals + idempotency mean a partial run is recoverable.
Glossary
portfolio:config block.claude/project-config.{defaults,}.jsonwith three fields (registry,projects_dir,ideas_backlog) that resolve to absolute paths via the helper library. Defaults match the single-fork layout; adopters in split-portfolio mode override to point at a sibling repo._lib-portfolio-paths.shportfolio_registry,portfolio_projects_dir,portfolio_ideas_backlog,portfolio_validate,portfolio_clear_cache. Resolves relative paths against the ops-fork root (the directory containingonboarding.yaml + apexyard.projects.yaml). Cached per-process.portfolio_validateprojects:; projects_dir is a directory; ideas_backlog exists or is creatable. Returns0on OK,1with "broken: " on failure.SessionStarthook (check-portfolio-config.sh) callsportfolio_validateonce per session start. Silent on OK. One-line banner naming the broken field + suggested fix on failure. Never blocks the session — informational only.apexyard.projects.yamlreferences in bash blocks./split-portfolioskill--verifymode for state reports without destructive ops.--dry-runprints commands without executing. Refuses on already-migrated, paid plan, dirty tree.gh issue edit/gh pr edit, the original body content remains accessible via the GitHub timeline API forever. Body redaction hides the content from casual viewers + search engines, but full purge requires deleting the repo. The/split-portfolioskill surfaces this caveat verbatim at Step 8.validate-pr-create.sh(introduced #114). PR bodies must use exactly oneCloses #N-style keyword to prevent surprise auto-closures. When a single PR delivers two coupled tickets, close one withCloses, reference the other withRefs, and close the second manually post-merge.Closes #145
Refs #146 (closed manually post-merge)