Skip to content

fix(#317): /split-portfolio produces v2 layout + copy-onboarding semantics#335

Merged
atlas-apex merged 4 commits into
devfrom
fix/GH-317-split-portfolio-v2-copy-onboarding
May 20, 2026
Merged

fix(#317): /split-portfolio produces v2 layout + copy-onboarding semantics#335
atlas-apex merged 4 commits into
devfrom
fix/GH-317-split-portfolio-v2-copy-onboarding

Conversation

@atlas-apex

Copy link
Copy Markdown
Collaborator

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 before this PR, the skill moved only apexyard.projects.yaml + projects/ to the new private sibling repo. It did NOT handle onboarding.yaml, did NOT move workspace/<project>/ clones, did NOT write the .apexyard-fork marker, and did NOT add the four v2 config keys. Adopters landed on a v1 layout and had to run /update to complete the migration — two-step for what should be one.

This PR closes that gap, and refines the migration semantics: onboarding.yaml is 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-portfolio produces a v2 layout from the first run — Steps 9a–9d added to the skill spec to copy onboarding.yaml, move workspace/, write the .apexyard-fork marker, 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).
  • /update step 8a aligned with the new semantics — for adopters already in v1 layout migrating via /update, onboarding.yaml now uses cp -p + git rm --cached + .gitignore add (was mv). The conflict branch is now idempotent. workspace/ keeps mv.
  • AgDR-0021 § H — new section codifying the per-file-class semantics (copy onboarding for legacy-tool-fallback + safety + sibling-is-canonical; move workspace for size). Documents the two failure modes of the original mv-for-both design + refresh of Option D table + Decision § D summary.
  • test_split_portfolio_v2_migration.sh extended — 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-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 also makes the migration safer (no destructive mv for the canonical file — sibling gets the canonical, public fork keeps a snapshot that's gitignored).

Testing

bash .claude/hooks/tests/test_split_portfolio_v2_migration.sh

18/18 pass (baseline 13/13; +5 new assertions covering copy-not-move semantics + .gitignore post-state + public-fork-untracked check).

Out of scope (per the ticket)

  • docs/multi-project.md manual recipe update — the manual recipe in § "Migrating from split-portfolio v1 to v2" still describes the older mv semantics for onboarding.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.
  • Per-team scoring weights for v1-detection — the existing detection logic (portfolio_is_v2, "has portfolio block, no marker" pre-v2 detection) stays unchanged
  • Re-tracking the public-fork onboarding.yaml — once gitignored, stays gitignored

Note 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

Term Definition
v1 layout Pre-#242 split-portfolio shape: registry + projects/ in private sibling; onboarding.yaml + workspace/ in public fork
v2 layout Post-#242 split-portfolio shape: registry + projects/ + onboarding.yaml + workspace/ all in private sibling; public fork carries only framework files + .apexyard-fork marker
.apexyard-fork marker Presence-only file at the public-fork root that v2 hooks recognise as the ops-fork anchor (replaces the legacy v1 anchor pair of onboarding.yaml + apexyard.projects.yaml)
Copy-vs-move semantics Per-file-class call: onboarding.yaml copied (legacy-tool fallback + sibling-canonical), workspace/ moved (gigs of clones; doubling disk makes no sense). Codified in AgDR-0021 § H
Per-file-class confirmation Operator can defer either move independently — the skill asks Copy onboarding.yaml? [Y/n] and Move workspace/? [Y/n] separately

Closes #317

me2resh and others added 4 commits May 20, 2026 11:22
…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 atlas-apex left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 // $x short-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 #317 present, 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)

  1. docs/multi-project.md prose still describes mv semantics for onboarding.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 in multi-project.md is 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 /task before this session closes so it doesn't get forgotten — the affected lines are 180, 516, 528-530, 533, 539, 544, 558, 570 in docs/multi-project.md.

  2. Asymmetric "both files present" branch between the two skills. /update SKILL.md step 8a is idempotent on the both-present branch (does git rm --cached and continues — lines 520-524 of /update/SKILL.md). /split-portfolio SKILL.md step 9a halts with exit 1 (lines 318-323 of /split-portfolio/SKILL.md). The asymmetry is reasonable — /split-portfolio is a one-shot destructive migration where the sibling was just initialised so an unexpected pre-existing onboarding.yaml there is genuinely surprising; /update runs 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.

  3. Test helper uses /update-style idempotent recipe, not the /split-portfolio exit-1 recipe. The test filename says split_portfolio_v2_migration but run_migration (lines 106-165 of the test) pins /update step 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

@atlas-apex atlas-apex merged commit 4730349 into dev May 20, 2026
4 checks passed
@atlas-apex atlas-apex deleted the fix/GH-317-split-portfolio-v2-copy-onboarding branch May 20, 2026 10:41
atlas-apex added a commit that referenced this pull request May 20, 2026
…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>
me2resh added a commit that referenced this pull request Jun 5, 2026
…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>
me2resh added a commit that referenced this pull request Jun 5, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants