Skip to content

[Bug] /split-portfolio produces v1 layout — adopters need a second /update; refine onboarding.yaml to copy semantics #317

@atlas-apex

Description

@atlas-apex

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

  • Single-fork adopter with onboarding.yaml + workspace/some-project/ + apexyard.projects.yaml + projects/some-project/ all in the public fork
  • Run /split-portfolio (full destructive migration)
  • Inspect post-state: onboarding.yaml STILL fully in the public fork (no copy in sibling)
  • workspace/<project>/ STILL in the public fork
  • .apexyard-fork marker MISSING at the public fork root
  • .claude/project-config.json missing the four v2 keys
  • Run /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.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:

  1. .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
  2. .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.

  3. 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).

  4. .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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High — material gap or user-impactingbugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions