fix(#317): /split-portfolio produces v2 layout + copy-onboarding semantics#335
Conversation
…e workspace)
`/split-portfolio` previously landed adopters on the split-portfolio v1
layout (registry + projects/ in the sibling repo, but onboarding.yaml +
workspace/ still in the public fork). Adopters then had to run `/update`
to reach v2 — a two-step migration where one is sufficient.
Add Steps 9a–9d that produce the v2 layout in the same skill invocation:
- 9a: Copy onboarding.yaml to the sibling (not move) — sibling becomes
canonical, public-fork copy stays as a gitignored snapshot for the
legacy ops-root walk-up fallback. `cp -p` + `git rm --cached`.
- 9b: Move workspace/<name>/ contents to the sibling (mv — clones are
gigabytes, doubling disk has no benefit). workspace/README.md kept.
- 9c: Extend .gitignore with onboarding.yaml + workspace.
- 9d: Write .apexyard-fork marker + four v2 config-block keys
(portfolio.{onboarding,workspace_dir,custom_skills_dir,custom_handbooks_dir}).
Each sub-step is independently idempotent and per-file-class confirmable,
so an operator can copy onboarding.yaml now and defer workspace/ (or vice
versa). The pre-flight checklist + idempotency table are updated to
surface the new behaviour.
Refs #317 / AgDR-0021 § H.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The v1→v2 migration in /update step 8a moves onboarding.yaml and workspace/ from the public fork to the sibling private repo. v2's initial implementation used `mv` for both — but onboarding.yaml is small, doubles as a legacy ops-root walk-up fallback anchor, and benefits from staying on disk as a gitignored snapshot. Switch onboarding.yaml from `mv` to `cp -p` + `git rm --cached`. The sibling-repo copy becomes the canonical source of truth; the public- fork copy is untracked (so commits don't ship it) and gitignored (so it can't drift back in). workspace/ stays as `mv` — clones are gigabytes, doubling disk has no benefit. The conflict branch (both copies present pre-migration) is now idempotent: it untracks the public-fork copy via `git rm --cached || true` instead of failing. Adopters re-running /update after a partial migration land on the same v2 layout. Confirmation prose updated: "Copy onboarding.yaml? [Y/n]" / "Move workspace/? [Y/n]" surfaces the per-file-class verb. Refs #317 / AgDR-0021 § H. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ile class) Add section H "v1→v2 migration semantics" that codifies the refined per- file-class verbs. The earlier `mv`-for-both design had two failure modes observed in practice: it broke the legacy ops-root walk-up fallback when the `.apexyard-fork` marker was accidentally removed, and it left `/split-portfolio` producing a v1 layout (requiring a follow-up `/update`). The refined decision: - `onboarding.yaml` → COPY (`cp -p`) + `git rm --cached` + `.gitignore`. Sibling becomes canonical; public-fork copy stays as a gitignored snapshot for the legacy ops-root fallback. KB-scale so doubling disk is free. - `workspace/<name>/` → MOVE (`mv`). Gigabytes of clones; doubling disk has no compensating benefit. workspace/README.md stays in public per § G. Option-D table updated to surface the per-file-class choice as the chosen path (with the initial design retained as a "superseded" entry for posterity). Section H includes the post-state ASCII diagram, the list of files where the refinement lands, and an explicit "what we are NOT doing" section to pre-empt follow-up questions. Refs #317. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The migration recipe now copies `onboarding.yaml` to the sibling private repo (instead of moving it) so the public-fork copy remains as a gitignored snapshot that the legacy ops-root walk-up can still find. Pin that contract with tests. Changes: - run_migration: switch from `mv onboarding.yaml` to `cp -p` + `git rm --cached`. Add an idempotence branch that untracks even when the sibling-repo copy is already present. - Case 1 assertions: invert the public-fork check (file must STILL be present, not absent). Add three new assertions — content identity via `cmp -s`, untracked status via `git ls-files`, snapshot-retained prose. - Case 2 (idempotence): unchanged in shape; still asserts the file listing is stable on re-run. Now exercises the copy + untrack path. - Case 4 (new): explicit copy-not-move semantics check. Modifies the public-fork snapshot AFTER migration and asserts the sibling-repo copy is unaffected — proves the two are independent files, not hard links or move-with-symlink artefacts. Also asserts `.gitignore` carries `onboarding.yaml`. Test counts: 13 → 18 passes (5 new assertions; meets the ≥ 5 contract in the ticket). Refs #317 / AgDR-0021 § H. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
atlas-apex
left a comment
There was a problem hiding this comment.
Code Review: PR #335
Commit: 11f3bcf419a040ef4aa30990780ba11f68e2a27a
Summary
Refines the split-portfolio v2 migration semantics from mv-for-both to per-file-class verbs: onboarding.yaml is COPIED (cp -p + git rm --cached + gitignore), workspace/<name>/ is MOVED. Closes the gap where /split-portfolio produced a v1 layout that required a follow-up /update to reach v2. Adds Steps 9a–9d to /split-portfolio, aligns /update step 8a with the new copy semantics, codifies the call in AgDR-0021 § H, and extends the test from 13/13 → 18/18.
Checklist Results
- ✅ Architecture & Design: Pass — per-file-class semantics is the right shape for the file-size + legacy-fallback asymmetry
- ✅ Code Quality: Pass — shell recipes are idempotent, jq
// $xshort-circuit is clean, no double-write between Step 9 + 9d - ✅ Testing: Pass — 5 new assertions land Case 1 + Case 4; Case 2 (idempotence) preserved
- ✅ Security: Pass — no secrets, no private refs
- ✅ Performance: N/A — recipe-level shell, no hot path
- ✅ PR Description & Glossary: Pass — 5 glossary entries,
Closes #317present, Given/When/Then framing is honest about the failure modes - ✅ Technical Decisions (AgDR): Pass — AgDR-0021 § H added; Option D table refreshed with chosen + superseded rows clearly separated; Decision § D summary updated coherently; honest engagement with both failure modes of the prior
mv-for-both design - ✅ Adopter Handbooks: N/A —
migration-safety.md(blocking) is scoped to DB migration files; this PR touches filesystem-migration shell recipes inside skill markdown, not DB migration patterns. Other always-load handbooks don't apply to shell/markdown.
Issues Found
None blocking.
Suggestions (non-blocking)
-
docs/multi-project.mdprose still describesmvsemantics foronboarding.yaml. The author flagged this as out-of-scope in the PR body. Accepting that, because the load-bearing change is the skill behaviour + AgDR (skill recipes do the work; the manual recipe inmulti-project.mdis a fallback). AgDR-0021 § H is now the source of truth that the diverging prose can be measured against. Recommend filing a[Docs]follow-up via/taskbefore this session closes so it doesn't get forgotten — the affected lines are 180, 516, 528-530, 533, 539, 544, 558, 570 indocs/multi-project.md. -
Asymmetric "both files present" branch between the two skills.
/updateSKILL.md step 8a is idempotent on the both-present branch (doesgit rm --cachedand continues — lines 520-524 of/update/SKILL.md)./split-portfolioSKILL.md step 9a halts withexit 1(lines 318-323 of/split-portfolio/SKILL.md). The asymmetry is reasonable —/split-portfoliois a one-shot destructive migration where the sibling was just initialised so an unexpected pre-existingonboarding.yamlthere is genuinely surprising;/updateruns against an already-populated sibling where idempotence is the right shape. But the call isn't documented anywhere. A one-line comment in either SKILL.md or AgDR-0021 § H acknowledging the deliberate asymmetry would save future-us a context-rebuild. -
Test helper uses
/update-style idempotent recipe, not the/split-portfolioexit-1 recipe. The test filename sayssplit_portfolio_v2_migrationbutrun_migration(lines 106-165 of the test) pins/updatestep 8a. Not a correctness issue (the two recipes are 95% identical), but the file-header comment could be one sentence clearer about which recipe is under test.
Verdict
APPROVED (submitted as comment — gh pr review --approve blocked by GitHub: "Cannot approve your own pull request")
CI fully green (4/4: Verify Ticket ID, lychee, markdownlint-cli2, shellcheck). Glossary present, Closes #317, no secrets, no private-project refs, no worktree leak detectable in the diff.
🤖 Reviewed by Rex (Code Reviewer Agent)
📌 Reviewed commit: 11f3bcf419a040ef4aa30990780ba11f68e2a27a
…21 § H copy-onboarding semantics (#350) PR #335 (closes #317) refined the v1→v2 migration semantics: onboarding.yaml now uses **copy** (cp -p + git rm --cached + .gitignore add) while workspace/ keeps **move** semantics. The framework spec (split-portfolio skill, update skill, AgDR-0021 § H, test_split_portfolio_v2_migration.sh) landed aligned. This doc — the user-facing migration guide — was deliberately left out of #335 to keep that PR's scope tight; the manual recipe still described mv semantics for onboarding.yaml. Today every adopter who follows the by-hand recipe gets a 4-day-stale shape. Aligns 3 specific places per AgDR-0021 § H: - The /update example prompt (line 533) — "Move onboarding.yaml" → "Copy onboarding.yaml (sibling becomes canonical; public fork keeps a gitignored snapshot for legacy tooling)". The "Files MOVED, not copied — destructive" tagline replaced with a mixed-semantics explanation. - The per-file-class confirmation prose (line 544) — "(move onboarding.yaml? Y/N; move workspace/? Y/N)" → "(copy onboarding.yaml? Y/N; move workspace/? Y/N)". Adds an inline reference to AgDR-0021 § H for the rationale. - The by-hand recipe (line 562) — replaces "mv onboarding.yaml ..." with "cp -p onboarding.yaml ... + git rm --cached" per the canonical pattern. Adds an inline note that .gitignore must include onboarding.yaml so the kept-snapshot doesn't get committed. Untouched: the high-level intent paragraphs (lines 180, 528) use "moves" as conceptual language (v2 layout puts portfolio data in the private repo) with the per-file-class semantics specified immediately below. Reading the whole section, no ambiguity. Closes #336 Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
…ntics (#335) * fix(#317): /split-portfolio — produce v2 layout (copy onboarding, move workspace) `/split-portfolio` previously landed adopters on the split-portfolio v1 layout (registry + projects/ in the sibling repo, but onboarding.yaml + workspace/ still in the public fork). Adopters then had to run `/update` to reach v2 — a two-step migration where one is sufficient. Add Steps 9a–9d that produce the v2 layout in the same skill invocation: - 9a: Copy onboarding.yaml to the sibling (not move) — sibling becomes canonical, public-fork copy stays as a gitignored snapshot for the legacy ops-root walk-up fallback. `cp -p` + `git rm --cached`. - 9b: Move workspace/<name>/ contents to the sibling (mv — clones are gigabytes, doubling disk has no benefit). workspace/README.md kept. - 9c: Extend .gitignore with onboarding.yaml + workspace. - 9d: Write .apexyard-fork marker + four v2 config-block keys (portfolio.{onboarding,workspace_dir,custom_skills_dir,custom_handbooks_dir}). Each sub-step is independently idempotent and per-file-class confirmable, so an operator can copy onboarding.yaml now and defer workspace/ (or vice versa). The pre-flight checklist + idempotency table are updated to surface the new behaviour. Refs #317 / AgDR-0021 § H. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(#317): /update step 8a — switch onboarding.yaml from mv to cp The v1→v2 migration in /update step 8a moves onboarding.yaml and workspace/ from the public fork to the sibling private repo. v2's initial implementation used `mv` for both — but onboarding.yaml is small, doubles as a legacy ops-root walk-up fallback anchor, and benefits from staying on disk as a gitignored snapshot. Switch onboarding.yaml from `mv` to `cp -p` + `git rm --cached`. The sibling-repo copy becomes the canonical source of truth; the public- fork copy is untracked (so commits don't ship it) and gitignored (so it can't drift back in). workspace/ stays as `mv` — clones are gigabytes, doubling disk has no benefit. The conflict branch (both copies present pre-migration) is now idempotent: it untracks the public-fork copy via `git rm --cached || true` instead of failing. Adopters re-running /update after a partial migration land on the same v2 layout. Confirmation prose updated: "Copy onboarding.yaml? [Y/n]" / "Move workspace/? [Y/n]" surfaces the per-file-class verb. Refs #317 / AgDR-0021 § H. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(#317): AgDR-0021 — v1→v2 migration semantics (copy vs move per file class) Add section H "v1→v2 migration semantics" that codifies the refined per- file-class verbs. The earlier `mv`-for-both design had two failure modes observed in practice: it broke the legacy ops-root walk-up fallback when the `.apexyard-fork` marker was accidentally removed, and it left `/split-portfolio` producing a v1 layout (requiring a follow-up `/update`). The refined decision: - `onboarding.yaml` → COPY (`cp -p`) + `git rm --cached` + `.gitignore`. Sibling becomes canonical; public-fork copy stays as a gitignored snapshot for the legacy ops-root fallback. KB-scale so doubling disk is free. - `workspace/<name>/` → MOVE (`mv`). Gigabytes of clones; doubling disk has no compensating benefit. workspace/README.md stays in public per § G. Option-D table updated to surface the per-file-class choice as the chosen path (with the initial design retained as a "superseded" entry for posterity). Section H includes the post-state ASCII diagram, the list of files where the refinement lands, and an explicit "what we are NOT doing" section to pre-empt follow-up questions. Refs #317. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(#317): extend v1→v2 migration test for copy-onboarding semantics The migration recipe now copies `onboarding.yaml` to the sibling private repo (instead of moving it) so the public-fork copy remains as a gitignored snapshot that the legacy ops-root walk-up can still find. Pin that contract with tests. Changes: - run_migration: switch from `mv onboarding.yaml` to `cp -p` + `git rm --cached`. Add an idempotence branch that untracks even when the sibling-repo copy is already present. - Case 1 assertions: invert the public-fork check (file must STILL be present, not absent). Add three new assertions — content identity via `cmp -s`, untracked status via `git ls-files`, snapshot-retained prose. - Case 2 (idempotence): unchanged in shape; still asserts the file listing is stable on re-run. Now exercises the copy + untrack path. - Case 4 (new): explicit copy-not-move semantics check. Modifies the public-fork snapshot AFTER migration and asserts the sibling-repo copy is unaffected — proves the two are independent files, not hard links or move-with-symlink artefacts. Also asserts `.gitignore` carries `onboarding.yaml`. Test counts: 13 → 18 passes (5 new assertions; meets the ≥ 5 contract in the ticket). Refs #317 / AgDR-0021 § H. 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>
…21 § H copy-onboarding semantics (#350) PR #335 (closes #317) refined the v1→v2 migration semantics: onboarding.yaml now uses **copy** (cp -p + git rm --cached + .gitignore add) while workspace/ keeps **move** semantics. The framework spec (split-portfolio skill, update skill, AgDR-0021 § H, test_split_portfolio_v2_migration.sh) landed aligned. This doc — the user-facing migration guide — was deliberately left out of #335 to keep that PR's scope tight; the manual recipe still described mv semantics for onboarding.yaml. Today every adopter who follows the by-hand recipe gets a 4-day-stale shape. Aligns 3 specific places per AgDR-0021 § H: - The /update example prompt (line 533) — "Move onboarding.yaml" → "Copy onboarding.yaml (sibling becomes canonical; public fork keeps a gitignored snapshot for legacy tooling)". The "Files MOVED, not copied — destructive" tagline replaced with a mixed-semantics explanation. - The per-file-class confirmation prose (line 544) — "(move onboarding.yaml? Y/N; move workspace/? Y/N)" → "(copy onboarding.yaml? Y/N; move workspace/? Y/N)". Adds an inline reference to AgDR-0021 § H for the rationale. - The by-hand recipe (line 562) — replaces "mv onboarding.yaml ..." with "cp -p onboarding.yaml ... + git rm --cached" per the canonical pattern. Adds an inline note that .gitignore must include onboarding.yaml so the kept-snapshot doesn't get committed. Untouched: the high-level intent paragraphs (lines 180, 528) use "moves" as conceptual language (v2 layout puts portfolio data in the private repo) with the per-file-class semantics specified immediately below. Reading the whole section, no ambiguity. Closes #336 Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
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 before this PR, the skill moved only
apexyard.projects.yaml+projects/to the new private sibling repo. It did NOT handleonboarding.yaml, did NOT moveworkspace/<project>/clones, did NOT write the.apexyard-forkmarker, and did NOT add the four v2 config keys. Adopters landed on a v1 layout and had to run/updateto complete the migration — two-step for what should be one.This PR closes that gap, and refines the migration semantics:
onboarding.yamlis now copied, not moved (per operator design call) so the sibling repo holds the canonical source of truth while the public fork keeps a gitignored snapshot for legacy tooling.workspace/keeps move semantics (gigs of clones — doubling disk makes no sense).Summary
/split-portfolioproduces a v2 layout from the first run — Steps 9a–9d added to the skill spec to copyonboarding.yaml, moveworkspace/, write the.apexyard-forkmarker, and add the four v2 config keys (onboarding,workspace_dir,custom_skills_dir,custom_handbooks_dir). Per-file-class confirmation pattern (operator can defer either move independently)./updatestep 8a aligned with the new semantics — for adopters already in v1 layout migrating via/update,onboarding.yamlnow usescp -p+git rm --cached+.gitignoreadd (wasmv). The conflict branch is now idempotent.workspace/keepsmv.mv-for-both design + refresh of Option D table + Decision § D summary.test_split_portfolio_v2_migration.shextended — Case 1 assertions inverted/extended for the copy semantics, new Case 4 explicitly pins copy-not-move, run_migration helper updated. Test count 13 → 18 (5 new assertions, meets the ≥5 contract).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 pre-PR behaviour produced a v1 layout that leaked the company name, mission, team list, and managed-project workspace clones onto the public fork until the operator separately ran/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 also makes the migration safer (no destructivemvfor the canonical file — sibling gets the canonical, public fork keeps a snapshot that's gitignored).Testing
18/18 pass (baseline 13/13; +5 new assertions covering copy-not-move semantics +
.gitignorepost-state + public-fork-untracked check).Out of scope (per the ticket)
docs/multi-project.mdmanual recipe update — the manual recipe in § "Migrating from split-portfolio v1 to v2" still describes the oldermvsemantics foronboarding.yaml. AgDR-0021 § H now diverges from that prose. The implementing agent flagged this — happy to file a follow-up[Docs]ticket for the prose refresh; it's a tiny edit but worth a separate concern from the framework-spec change in this PR.portfolio_is_v2, "has portfolio block, no marker" pre-v2 detection) stays unchangedonboarding.yaml— once gitignored, stays gitignoredNote on implementation hygiene
The implementing agent flagged ~10 minutes of early debugging on an APFS / filesystem-cache mismatch (initially editing the main checkout path instead of the worktree path). Cleanly resolved before the commits landed — worktree state is correct and matches the pushed branch. Same shape as the worktree-cwd quirk on prior PRs.
Glossary
projects/in private sibling;onboarding.yaml+workspace/in public forkprojects/+onboarding.yaml+workspace/all in private sibling; public fork carries only framework files +.apexyard-forkmarker.apexyard-forkmarkeronboarding.yaml + apexyard.projects.yaml)onboarding.yamlcopied (legacy-tool fallback + sibling-canonical),workspace/moved (gigs of clones; doubling disk makes no sense). Codified in AgDR-0021 § HCopy onboarding.yaml? [Y/n]andMove workspace/? [Y/n]separatelyCloses #317