Given / When / Then
Given a single-fork ApexYard adopter (registry + projects in the public fork) decides to migrate to split-portfolio mode for privacy
When they invoke /split-portfolio to perform the migration
Then the skill moves ONLY apexyard.projects.yaml + projects/ to the new private sibling repo. It does NOT handle onboarding.yaml (company name / mission / team list), does NOT move workspace/<project>/ clones, does NOT write the .apexyard-fork marker, and does NOT add the v2 config keys (portfolio.onboarding, portfolio.workspace_dir, portfolio.custom_skills_dir, portfolio.custom_handbooks_dir).
Result: adopter lands on a v1 layout. They have to then ALSO run /update to detect the v1 layout (step 8a) and complete the v2 migration. That's a two-step migration for what should be one. The root cause: .claude/skills/split-portfolio/SKILL.md predates framework #242 (which introduced the v2 layout in /update and /setup) and was never updated.
Repro
Suggested fix — copy-onboarding, move-workspace semantics
Design call from the audit follow-up (2026-05-20): for onboarding.yaml we want COPY semantics, not move. The sibling private repo holds the authoritative source of truth, but the public fork keeps a copy on disk (gitignored + git rm --cached) as a safe fallback for legacy tooling that walks the public-fork root directly. For workspace/<name>/ we keep MOVE semantics (clones are potentially gigabytes; doubling disk usage makes no sense). This refines AgDR-0021 § "v1→v2 migration semantics".
Scope of the fix spans 4 places — all four must land together to keep the framework consistent:
-
.claude/skills/split-portfolio/SKILL.md — extend Step 5 (or add 5a-5d) to also:
- Copy (
cp -p) onboarding.yaml to the sibling private repo
- Untrack the public-fork copy:
git rm --cached onboarding.yaml
- Add
onboarding.yaml to the public fork's .gitignore
- Move
workspace/<name>/ contents to the sibling (skipping workspace/README.md framework artefact, same as /update step 8a)
- Add
workspace to public-fork .gitignore
- Write the
.apexyard-fork marker at the public-fork root
- Add the four v2 config keys to
.claude/project-config.json (onboarding, workspace_dir, custom_skills_dir, custom_handbooks_dir)
- Use per-file-class confirmation pattern (
Copy onboarding.yaml? [Y/n] / Move workspace/? [Y/n]) — operator can defer either
-
.claude/skills/update/SKILL.md step 8a (existing v1→v2 migration) — change mv onboarding.yaml ... to cp -p + git rm --cached + .gitignore add. Keep workspace/ as move. Update the migration-confirmation prose to reflect copy-vs-move per file class.
-
docs/agdr/AgDR-0021-split-portfolio-v2-path-resolution.md — extend with a new section "v1→v2 migration semantics" that codifies the copy-onboarding / move-workspace decision. Document: why copy (legacy-tool fallback + safety + sibling-is-canonical), why not for workspace (size).
-
.claude/hooks/tests/test_split_portfolio_v2_migration.sh — update Case 1 assertions:
- Before:
[ ! -f "$SB/public/onboarding.yaml" ] (file moved out)
- After:
[ -f "$SB/public/onboarding.yaml" ] AND [ -f "$SB/private/onboarding.yaml" ] AND diff "$SB/public/onboarding.yaml" "$SB/private/onboarding.yaml" (both exist, identical content)
- Plus assert public-fork
onboarding.yaml is untracked: git -C "$SB/public" ls-files --error-unmatch onboarding.yaml should fail
- Plus assert public-fork
.gitignore has onboarding.yaml
workspace/ assertions stay as-is (move semantics unchanged)
- Update Case 2 (idempotence) to handle the new copy path
- Add a Case 4 specifically pinning the copy-not-move semantics for
onboarding.yaml
Out of scope
- Per-team scoring weights for what counts as "v1" detection (the existing detection logic
portfolio_is_v2 + the "has portfolio block, no .apexyard-fork marker" pre-v2 detection — both stay unchanged; only the migration body changes)
- Migrating other files (e.g.
apexyard.projects.yaml) from move to copy — it's already in the sibling per v1 design; no change
- Re-tracking the public-fork
onboarding.yaml in any future flow — once gitignored, it stays gitignored
Why this is P1
Non-GitHub-Pro adopters with any private project ARE the target audience for split-portfolio mode (per docs/multi-project.md § "Two setup modes"). The current behaviour produces a v1 layout that leaks the company name, mission, team list, and managed-project workspace clones onto the public fork until the operator separately runs /update. The window between /split-portfolio and /update is exactly where adopters could push the partial state and leak. Same shape as the precedent that's already happened in the framework's history. The copy-semantics refinement makes the migration safer too (no destructive mv for the canonical file).
Refs #312 (audit findings — Gap 1 of 4 from /update + /setup audit on 2026-05-20)
Given / When / Then
Given a single-fork ApexYard adopter (registry + projects in the public fork) decides to migrate to split-portfolio mode for privacy
When they invoke
/split-portfolioto perform the migrationThen the skill moves ONLY
apexyard.projects.yaml+projects/to the new private sibling repo. It does NOT handleonboarding.yaml(company name / mission / team list), does NOT moveworkspace/<project>/clones, does NOT write the.apexyard-forkmarker, and does NOT add the v2 config keys (portfolio.onboarding,portfolio.workspace_dir,portfolio.custom_skills_dir,portfolio.custom_handbooks_dir).Result: adopter lands on a v1 layout. They have to then ALSO run
/updateto detect the v1 layout (step 8a) and complete the v2 migration. That's a two-step migration for what should be one. The root cause:.claude/skills/split-portfolio/SKILL.mdpredates framework #242 (which introduced the v2 layout in/updateand/setup) and was never updated.Repro
onboarding.yaml+workspace/some-project/+apexyard.projects.yaml+projects/some-project/all in the public fork/split-portfolio(full destructive migration)onboarding.yamlSTILL fully in the public fork (no copy in sibling)workspace/<project>/STILL in the public fork.apexyard-forkmarker MISSING at the public fork root.claude/project-config.jsonmissing the four v2 keys/update— observe step 8a detect v1 and offer migration. That second step IS the bug surface.Suggested fix — copy-onboarding, move-workspace semantics
Design call from the audit follow-up (2026-05-20): for
onboarding.yamlwe want COPY semantics, not move. The sibling private repo holds the authoritative source of truth, but the public fork keeps a copy on disk (gitignored +git rm --cached) as a safe fallback for legacy tooling that walks the public-fork root directly. Forworkspace/<name>/we keep MOVE semantics (clones are potentially gigabytes; doubling disk usage makes no sense). This refinesAgDR-0021§ "v1→v2 migration semantics".Scope of the fix spans 4 places — all four must land together to keep the framework consistent:
.claude/skills/split-portfolio/SKILL.md— extend Step 5 (or add 5a-5d) to also:cp -p)onboarding.yamlto the sibling private repogit rm --cached onboarding.yamlonboarding.yamlto the public fork's.gitignoreworkspace/<name>/contents to the sibling (skippingworkspace/README.mdframework artefact, same as/updatestep 8a)workspaceto public-fork.gitignore.apexyard-forkmarker at the public-fork root.claude/project-config.json(onboarding,workspace_dir,custom_skills_dir,custom_handbooks_dir)Copy onboarding.yaml? [Y/n]/Move workspace/? [Y/n]) — operator can defer either.claude/skills/update/SKILL.mdstep 8a (existing v1→v2 migration) — changemv onboarding.yaml ...tocp -p+git rm --cached+.gitignoreadd. Keepworkspace/as move. Update the migration-confirmation prose to reflect copy-vs-move per file class.docs/agdr/AgDR-0021-split-portfolio-v2-path-resolution.md— extend with a new section "v1→v2 migration semantics" that codifies the copy-onboarding / move-workspace decision. Document: why copy (legacy-tool fallback + safety + sibling-is-canonical), why not for workspace (size)..claude/hooks/tests/test_split_portfolio_v2_migration.sh— update Case 1 assertions:[ ! -f "$SB/public/onboarding.yaml" ](file moved out)[ -f "$SB/public/onboarding.yaml" ]AND[ -f "$SB/private/onboarding.yaml" ]ANDdiff "$SB/public/onboarding.yaml" "$SB/private/onboarding.yaml"(both exist, identical content)onboarding.yamlis untracked:git -C "$SB/public" ls-files --error-unmatch onboarding.yamlshould fail.gitignorehasonboarding.yamlworkspace/assertions stay as-is (move semantics unchanged)onboarding.yamlOut of scope
portfolio_is_v2+ the "has portfolio block, no.apexyard-forkmarker" pre-v2 detection — both stay unchanged; only the migration body changes)apexyard.projects.yaml) from move to copy — it's already in the sibling per v1 design; no changeonboarding.yamlin any future flow — once gitignored, it stays gitignoredWhy this is P1
Non-GitHub-Pro adopters with any private project ARE the target audience for split-portfolio mode (per
docs/multi-project.md§ "Two setup modes"). The current behaviour produces a v1 layout that leaks the company name, mission, team list, and managed-project workspace clones onto the public fork until the operator separately runs/update. The window between/split-portfolioand/updateis exactly where adopters could push the partial state and leak. Same shape as the precedent that's already happened in the framework's history. The copy-semantics refinement makes the migration safer too (no destructivemvfor the canonical file).Refs #312 (audit findings — Gap 1 of 4 from
/update+/setupaudit on 2026-05-20)