refactor(#302): sweep all 40 hook wrappers to v2-anchor-aware shape#306
Conversation
Every SessionStart, PreToolUse, and PostToolUse entry inlined a walk-up shell looking for onboarding.yaml ONLY. Under split- portfolio v2 (framework #242), onboarding.yaml lives in the private sibling repo and the public fork is anchored solely by the .apexyard-fork marker file. The v1-only walk-up: r=\$PWD;while [ ! -f \"\$r/onboarding.yaml\" ] && [ \"\$r\" != / ] ;do r=\${r%/*};done;exec \"\$r/.claude/hooks/<name>.sh\" Either silently exec's a non-existent /.claude/hooks/<name>.sh (no banner, no error) or, more visibly, infinite-loops because the ${r%/*} on empty string keeps returning empty while [ \"\" != / ] stays true. Sweep every wrapper to recognise BOTH anchors (.apexyard-fork OR onboarding.yaml) AND add an [ -n \"\$r\" ] guard against the empty-string infinite loop: r=\$PWD;while [ ! -f \"\$r/.apexyard-fork\" ] \\ && [ ! -f \"\$r/onboarding.yaml\" ] \\ && [ -n \"\$r\" ] && [ \"\$r\" != / ];do r=\${r%/*};done; exec \"\$r/.claude/hooks/<name>.sh\" This shape is consistent with the link-custom-skills.sh wrapper (already v2-aware, the only entry that was) and with the resolve_ops_root helper in _lib-ops-root.sh (which recognises both anchors). The wrapper's job is to find the dir containing .claude/hooks/<name>.sh; the hook itself does any further ops-root resolution via the lib if it needs framework state. Closes #302 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The hook had its own inline v2-aware walk-up. Source the shared helper instead and keep the inline walk as a graceful-degradation fallback — same shape as check-jq-installed.sh and clear-bootstrap-marker.sh. Cosmetic; behaviour unchanged. Refs #302 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AgDR-0041 documents the canonical SessionStart / PreToolUse / PostToolUse wrapper shape used by .claude/settings.json — both anchors checked, `[ -n \"\$r\" ]` guard against infinite-loop on v2 forks. New SessionStart hooks ship with this shape from day one; the AgDR is the citation Rex points at when a PR uses the legacy shape. Updated docs/multi-project.md § \"Where session-state files live\" to reflect v2 (was still saying the walk-up requires BOTH legacy files), and added a cross-link to the AgDR. Refs #302 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 #306
Commit: 0d6a1a5173f381980a1e7054389de92f9578d47e
Summary
Sweeps all 40 bash -c hook wrappers in .claude/settings.json to the canonical v2-anchor-aware shape (.apexyard-fork OR onboarding.yaml, plus the -n "$r" infinite-loop guard); refactors link-custom-skills.sh to source _lib-ops-root.sh with a strict-v1 graceful-degradation fallback; documents the canonical wrapper + hook-internal shapes in AgDR-0041 and cross-links from docs/multi-project.md.
The substance of the refactor is solid — the AgDR is honest, the wrapper shape is correct, the docs match the new contract. There is one blocking issue: the markdownlint CI check is RED, caused by a single MD018 violation in the new AgDR. That has to be fixed before merge per .claude/rules/pr-quality.md § "No Red CI Before Merge".
Checklist Results
- Architecture & Design: Pass — wrapper logic is correct under all three layouts (v2-only, v1-only, no-anchor)
- Code Quality: Pass — mechanical sweep is consistent across all 40 wrappers
- Testing: Pass with note — see Suggestions
- Security: Pass — no secrets, no privilege changes
- Performance: Pass — one extra
-fcheck per ancestor; negligible - PR Description & Glossary: Pass — 4 glossary entries,
Closes #302present, AgDR referenced by full slug - Technical Decisions (AgDR):Pass — AgDR-0041 is created and engages with alternatives
- Adopter Handbooks: N/A — diff is shell + JSON wrappers;
clean-architecture-layers.mddoesn't apply
Issues Found
⛔ BLOCKER — Red CI (markdownlint MD018)
The new docs/agdr/AgDR-0041-sessionstart-v2-anchor-sweep.md:13:1 fails markdownlint with:
MD018/no-missing-space-atx No space after hash on atx style heading
Context: "#242 (split-portfolio v2) move..."
Line 13 of the AgDR is the second paragraph of ## Context and starts with the literal text #242 (split-portfolio v2) moved …. markdownlint reads #242 at the start of a line as a malformed ATX heading (hash with no space after).
Per .claude/rules/pr-quality.md § "No Red CI Before Merge": "Never merge with red CI — even if the failure is pre-existing or unrelated." This one isn't pre-existing — it ships in the new file.
Fix: rewrite the line so #242 is not at column 1. Two cheap options:
- Prepend
PRorIssueso the line readsPR #242 (split-portfolio v2) moved …. - Use the cross-repo form
me2resh/apexyard#242 (split-portfolio v2) moved …. This matches the linking style used elsewhere in the AgDR (the Artifacts section already does this).
Either is a one-character/one-word change. After the fix, re-push and CI should go green.
(The same pattern in the Artifacts section — me2resh/apexyard#302 — is fine because the m comes first.)
Audit of the load-bearing claims
The review request asked me to be thorough on (a) shape correctness across all 40 wrappers, (b) AgDR honesty about the options, and (c) docs accuracy. Findings:
(a) Shape correctness — all 40 wrappers swept correctly.
- I verified 40
bash -centries in.claude/settings.jsonand all 40 contain.apexyard-fork,onboarding.yaml, AND the[ -n "$r" ]guard. Zero drift. - Section distribution: 7 SessionStart, 3 PreToolUse/Edit-Write-MultiEdit, 27 PreToolUse/Bash, 1 UserPromptSubmit, 2 PostToolUse. That sums to 40 — matches the headline claim, though the PR body says "29 PreToolUse" (it's 30: 3 + 27). Minor counting nit in prose; the bulk count is right.
- The
check-jq-installed.shSessionStart entry that landed onorigin/devmid-flight is included (line 17 of the newsettings.json). - I executed the new shape against
cwd=/tmp(no anchors anywhere up the tree): the loop walks once, reducesr="/tmp"tor="", the-n "$r"guard fires, loop exits cleanly withr="";exec "/.claude/hooks/<name>.sh"then fails fast with exit 126. Visible failure, no infinite loop. Then ran the OLD legacy shape (no-n "$r"guard, only[ "$r" != / ]) against the samecwd=/tmp: it infinite-loops as predicted. The AgDR's Consequence #3 is grounded in observed behaviour, not theory.
(b) AgDR honesty about options.
- Option A (source the lib from the wrapper): I confirmed
bash -c 'echo "0=[$0] dirname0=[$(dirname "$0")]"'prints0=[bash] dirname0=[.]. The$0=bashclaim is technically accurate. Option A's rejection is not just convenient framing — it's a hard property ofbash -c. Honest. - Option B (inline the v2-aware walk-up): chosen. The relaxation from BOTH-of-pair (lib) to either-marker (wrapper) is justified because the wrapper's only job is to locate the hooks dir; the hook itself re-resolves via the lib if it needs strict ops-root semantics. I audited the two SessionStart hooks that DON'T source
_lib-ops-root.sh(onboarding-check.sh,check-upstream-drift.sh) and both usegit rev-parse --show-toplevelinstead of relying on the wrapper's$r. So the laxness has no semantic effect there either. The reasoning holds for every wrapper in the sweep. - Option D (drop the wrapper walk, exec from
$PWD): correctly rejected — breaks when working inworkspace/<project>/. - Gap: the user's review request mentions a different "Option C" — eliminate the wrapper walk via a Claude Code runtime change. The PR body covers it in "Out of scope" ("would need an upstream change to the Claude Code runtime, not a framework change"), but the AgDR's Options table only lists a different Option C (single dispatch script). Worth adding a short row to the AgDR Options table for the upstream-runtime option, even just to record it as deferred — it's the most principled long-term path and an honest options table should name it. Non-blocking nit.
(c) Docs accuracy.
docs/multi-project.md§ "Where session-state files live" now correctly describes the dual-anchor model and removes the wrong "BOTH … AND …" copy that was specific to v1. The "BLOCKED: PR has no recorded code-reviewer approval" troubleshooting line is updated to "at least one of the ops-fork anchors". Accurate.- One minor precision issue: the new third paragraph says "The same dual-anchor rule applies to the .claude/settings.json hook wrappers themselves". The wrappers use the WEAKER v1 condition (single
onboarding.yaml), not the strict pair — the rule is.apexyard-fork OR onboarding.yamlat wrapper level, vs.apexyard-fork OR (onboarding.yaml AND apexyard.projects.yaml)at lib level. Calling them both "the dual-anchor rule" elides that asymmetry. Minor wording suggestion; not blocking.
Handbook Findings
handbooks/architecture/clean-architecture-layers.md (always-load) — diff is shell + JSON wrappers + an AgDR + docs. No domain / application / infrastructure boundary in scope. No findings.
Suggestions (non-blocking)
-
Test coverage gap on
link-custom-skills.shlib-sourcing path. The 6 test cases intest_link_custom_skills.shbuild a sandbox that copies in_lib-portfolio-paths.shand_lib-read-config.shbut not_lib-ops-root.sh. So all 6 cases exercise the graceful-degradation fallback (lines 62–71 of the hook), not the new primary path (lines 57–60,if [ -f "$HOOK_DIR/_lib-ops-root.sh" ]; then . _lib-ops-root.sh; ops_root=$(resolve_ops_root "$PWD") …). The fallback works (the test fixture writes BOTH anchors, so the strict-v1 fallback condition is satisfied), but the actual code path that ships in production isn't pinned by these tests. Two cheap fixes:- Copy
_lib-ops-root.shinto the sandbox inmake_fork()so the helper-sourced path is the one exercised. - Add a 7th case that creates a v2-only fork (
.apexyard-forkonly, noonboarding.yaml) and asserts the hook resolves correctly through the lib.
The bug this would catch is the helper-sourced path silently returning the wrong dir for a v2-only layout — exactly the failure class this PR is meant to prevent.
- Copy
-
AgDR Options table: add the upstream-runtime row. As noted under audit (b), the most principled solution is an upstream change to Claude Code that resolves hook paths relative to the fork root. It's acknowledged in the PR body's "Out of scope" but missing from the AgDR Options table. Adding a row ("Option E — push the walk into the Claude Code runtime — Pros: removes all wrappers; Cons: needs upstream cooperation, not a framework-side change. Defer.") would make the Options table feel complete and honest.
-
Docs precision on "dual-anchor rule" asymmetry. The wrapper's v1 acceptance condition (single
onboarding.yaml) is a deliberate relaxation from the lib's stricter v1 pair. Worth one sentence indocs/multi-project.mdto note that wrappers and the lib accept slightly different shapes, and why (wrapper only needs to find the hooks dir; lib establishes canonical ops-root). Non-blocking. -
PR body counting nit. "29 PreToolUse" → "30 PreToolUse" (3 Edit-matcher + 27 Bash-matcher = 30). Cosmetic; the headline total of 40 is correct.
Verdict
CHANGES REQUESTED
The substance of the refactor is good and the architectural reasoning in AgDR-0041 holds up to scrutiny. Blocking only on the red CI from the markdownlint MD018 violation in the new AgDR file (one-line fix). Once that's green, I'm happy to re-review and approve.
🤖 Reviewed by Rex (Code Reviewer Agent)
📌 Reviewed commit: 0d6a1a5173f381980a1e7054389de92f9578d47e
Rex review on PR #306: Blocker (red CI): - AgDR-0041 line 13 started with bare `#242 (split-portfolio v2)…` which markdownlint reads as a malformed ATX heading (MD018). Prefixed with `PR ` so the line no longer starts with `#`. Non-blocking nits (all applied): - Added Option E to the AgDR's Options Considered table — "push the walk into the Claude Code runtime". Documented as the genuinely cleanest path, deferred because it needs an upstream Claude Code runtime change. Closes the honesty gap Rex flagged. - `test_link_custom_skills.sh`: make_fork now copies `_lib-ops-root.sh` by default, so cases 1-6 exercise the refactor's primary lib-sourcing path. Added a `--no-ops-root-lib` flag and case 7 to exercise the graceful-degradation fallback. Cases now pin BOTH branches of the discovery shape. - `docs/multi-project.md`: added a callout sentence clarifying that wrapper-level v1 detection accepts `onboarding.yaml` alone, while `_lib-ops-root.sh` requires both `onboarding.yaml` AND `apexyard.projects.yaml`. Cross-references AgDR-0041 § Decision point 2 for the rationale. Test results: 7/7 pass in test_link_custom_skills.sh. 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 (re-review): PR #306
Commit: 823b2fc1f63de255f50d334f8f007b18b3e8fd96
Summary
Re-review of the fix-up commit 823b2fc addressing the blocker + 3 non-blocking nits from the previous review at 0d6a1a5. (Cannot --approve own PR; using --comment with explicit APPROVED verdict.)
Checklist Results
- ✅ Architecture & Design: Pass (unchanged from prior review)
- ✅ Code Quality: Pass
- ✅ Testing: Pass — now 7/7 cases, both lib-sourcing branches pinned
- ✅ Security: N/A
- ✅ Performance: N/A
- ✅ PR Description & Glossary: Pass (unchanged from prior review)
- ✅ Technical Decisions (AgDR):Pass — AgDR-0041 now strengthened with Option E
- ✅ Adopter Handbooks: N/A
Fix-up verification
1. MD018 blocker (red CI on prior HEAD) — RESOLVED.
docs/agdr/AgDR-0041-sessionstart-v2-anchor-sweep.md line 13 now reads PR #242 (split-portfolio v2) moved … instead of bare #242 …. CI markdownlint-cli2 SUCCESS on 823b2fc. All five CI checks green (lychee, markdownlint-cli2, Verify Ticket ID, shellcheck).
2. Option E (nit 1: honesty gap) — RESOLVED genuinely.
The new row engages with the runtime-resolution alternative, not lip-service:
- Description names the actual mechanism: "runtime resolves hook paths from the ops-fork root, not from
$PWDviabash -c" - Pros are honest: "Cleanest — no inline walk in any wrapper, no dispatch hop, no lib-sourcing dance. The runtime already knows where the project root is."
- Cons name the right blocker: "Needs an upstream Claude Code change (settings.json semantics + how hook commands are resolved). Out of this PR's scope."
- Defer rationale closes the loop: "Option B is the right shape until the runtime grows that ability."
This is the shape an AgDR-level options-considered row should take — names the cleaner path, names what's blocking it, names when to revisit.
3. Test case 7 (nit 2: fallback path coverage) — RESOLVED, test passes for the right reason.
Verified at HEAD 823b2fc (local worktree run):
PASS: case 1-6 ... (primary lib-sourcing path)
PASS: case 7: graceful-degradation fallback works when _lib-ops-root.sh is absent
Passed: 7 Failed: 0
The case 7 setup is correctly designed:
make_fork --no-ops-root-libsetscopy_ops_root=0so thecp "$LIB_OPS_ROOT_SRC" …line is skipped- Pre-flight assertion
[ ! -f "$SB/.claude/hooks/_lib-ops-root.sh" ]exits 1 with an explicit FAIL message if the lib were unexpectedly present (false-positive setup guard) - Symlink check + "linked 1 custom skill" output check confirms the inline fallback walk-up successfully resolved the ops root via
.apexyard-forkand the rest of the hook ran end-to-end
Cases 1-6 now genuinely exercise the lib-sourcing branch (primary path) since make_fork copies _lib-ops-root.sh by default. Together with case 7, both branches of the if [ -f "$HOOK_DIR/_lib-ops-root.sh" ] shape are pinned by tests.
The reviewer-flagged risk ("passes for the wrong reason — some other walk-up resolves the path") doesn't apply: the fallback inline walk-up is the only path that can resolve the ops root when the lib is absent.
4. Doc precision (nit 3) — RESOLVED.
The added callout in docs/multi-project.md § "Where session-state files live" correctly states:
The wrappers accept
onboarding.yamlalone as the v1 anchor, while_lib-ops-root.sh(the in-hook resolver) requires BOTHonboarding.yamlANDapexyard.projects.yaml.
Reading the surrounding prose end-to-end — dual-anchor paragraph → wrappers paragraph → laxness callout → "at least one of the ops-fork anchors" recovery instructions — the flow is coherent. The laxness is explained before the recovery instructions mention single-anchor sufficiency, so a reader doesn't trip on "wait, I thought you needed both?". Cross-reference to AgDR-0041 § Decision point 2 anchors the rationale for the future-curious reader.
Issues Found
None.
Suggestions
None — all four points cleanly addressed, CI green, tests strengthened.
Verdict
APPROVED
The fix-up commit cleanly resolves the blocker and substantively addresses all three nits — Option E is honest engagement, case 7 is a real fallback test (not just a "test exists" line item), and the doc callout makes the wrapper-vs-lib asymmetry visible to future-us. Solid response to review feedback; ready to merge once CEO approves.
🤖 Reviewed by Rex (Code Reviewer Agent)
📌 Reviewed commit: 823b2fc1f63de255f50d334f8f007b18b3e8fd96
…ion (#430) The test's Invariant 1 counted wrappers via `r=$PWD` which matched both v1 and v2 walkers; Invariant 2 used a hardcoded v1 walker as the "canonical wrapper" sample even after PR #306 swept all hooks to the v2 pin-first shape. Changes: - Count wrappers by CLAUDE_CODE_SESSION_ID (v2 preamble) instead of r=$PWD - Add Invariant 1b: grep for bare `bash -c 'r=$PWD;while` (pure v1 shape, no session-pin preamble) and fail if any are found — this catches the stale-walker regression class mechanically on future settings.json edits - Replace the hardcoded v1 WRAPPER with the canonical v2 shape (session-pin first, walk-up fallback) so Invariants 2 and 3 test the actual current wrapper behaviour Refs #414 Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…306) * refactor(#302): make settings.json walk-ups v2-anchor aware Every SessionStart, PreToolUse, and PostToolUse entry inlined a walk-up shell looking for onboarding.yaml ONLY. Under split- portfolio v2 (framework #242), onboarding.yaml lives in the private sibling repo and the public fork is anchored solely by the .apexyard-fork marker file. The v1-only walk-up: r=\$PWD;while [ ! -f \"\$r/onboarding.yaml\" ] && [ \"\$r\" != / ] ;do r=\${r%/*};done;exec \"\$r/.claude/hooks/<name>.sh\" Either silently exec's a non-existent /.claude/hooks/<name>.sh (no banner, no error) or, more visibly, infinite-loops because the ${r%/*} on empty string keeps returning empty while [ \"\" != / ] stays true. Sweep every wrapper to recognise BOTH anchors (.apexyard-fork OR onboarding.yaml) AND add an [ -n \"\$r\" ] guard against the empty-string infinite loop: r=\$PWD;while [ ! -f \"\$r/.apexyard-fork\" ] \\ && [ ! -f \"\$r/onboarding.yaml\" ] \\ && [ -n \"\$r\" ] && [ \"\$r\" != / ];do r=\${r%/*};done; exec \"\$r/.claude/hooks/<name>.sh\" This shape is consistent with the link-custom-skills.sh wrapper (already v2-aware, the only entry that was) and with the resolve_ops_root helper in _lib-ops-root.sh (which recognises both anchors). The wrapper's job is to find the dir containing .claude/hooks/<name>.sh; the hook itself does any further ops-root resolution via the lib if it needs framework state. Closes #302 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(#302): link-custom-skills uses _lib-ops-root.sh The hook had its own inline v2-aware walk-up. Source the shared helper instead and keep the inline walk as a graceful-degradation fallback — same shape as check-jq-installed.sh and clear-bootstrap-marker.sh. Cosmetic; behaviour unchanged. Refs #302 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(#302): AgDR-0041 + multi-project.md note on v2-aware wrappers AgDR-0041 documents the canonical SessionStart / PreToolUse / PostToolUse wrapper shape used by .claude/settings.json — both anchors checked, `[ -n \"\$r\" ]` guard against infinite-loop on v2 forks. New SessionStart hooks ship with this shape from day one; the AgDR is the citation Rex points at when a PR uses the legacy shape. Updated docs/multi-project.md § \"Where session-state files live\" to reflect v2 (was still saying the walk-up requires BOTH legacy files), and added a cross-link to the AgDR. Refs #302 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(#302): address Rex review nits — MD018 blocker + 3 non-blocking Rex review on PR #306: Blocker (red CI): - AgDR-0041 line 13 started with bare `#242 (split-portfolio v2)…` which markdownlint reads as a malformed ATX heading (MD018). Prefixed with `PR ` so the line no longer starts with `#`. Non-blocking nits (all applied): - Added Option E to the AgDR's Options Considered table — "push the walk into the Claude Code runtime". Documented as the genuinely cleanest path, deferred because it needs an upstream Claude Code runtime change. Closes the honesty gap Rex flagged. - `test_link_custom_skills.sh`: make_fork now copies `_lib-ops-root.sh` by default, so cases 1-6 exercise the refactor's primary lib-sourcing path. Added a `--no-ops-root-lib` flag and case 7 to exercise the graceful-degradation fallback. Cases now pin BOTH branches of the discovery shape. - `docs/multi-project.md`: added a callout sentence clarifying that wrapper-level v1 detection accepts `onboarding.yaml` alone, while `_lib-ops-root.sh` requires both `onboarding.yaml` AND `apexyard.projects.yaml`. Cross-references AgDR-0041 § Decision point 2 for the rationale. Test results: 7/7 pass in test_link_custom_skills.sh. 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>
…ion (#430) The test's Invariant 1 counted wrappers via `r=$PWD` which matched both v1 and v2 walkers; Invariant 2 used a hardcoded v1 walker as the "canonical wrapper" sample even after PR #306 swept all hooks to the v2 pin-first shape. Changes: - Count wrappers by CLAUDE_CODE_SESSION_ID (v2 preamble) instead of r=$PWD - Add Invariant 1b: grep for bare `bash -c 'r=$PWD;while` (pure v1 shape, no session-pin preamble) and fail if any are found — this catches the stale-walker regression class mechanically on future settings.json edits - Replace the hardcoded v1 WRAPPER with the canonical v2 shape (session-pin first, walk-up fallback) so Invariants 2 and 3 test the actual current wrapper behaviour Refs #414 Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
.claude/settings.jsonto the canonical v2-anchor-aware shape — every wrapper now finds the ops fork via either.apexyard-fork(v2) ORonboarding.yaml(legacy v1 fallback), with the empty-rguard that legacy shape lacked..claude/hooks/link-custom-skills.shto source_lib-ops-root.shwith graceful-degradation fallback, replacing its private inline walk-up. One fewer place that needs to change when the anchor convention evolves.AgDR-0041so future hook authors copy from the right template — including a clear options-table entry on why the seemingly-cleaner "source the lib from the wrapper" path (Option A) doesn't actually work (bash -c '…'invocations set$0=bash, sodirname "$0"returns.).docs/multi-project.md§ "Where session-state files live" so adopters reading the operator docs land on the same explanation.40 wrappers swept: 7 SessionStart (including the newly-shipped
check-jq-installed.shSessionStart entry that landed onorigin/devbetween worktree creation and merge), 30 PreToolUse, 2 PostToolUse, 1 UserPromptSubmit.Why
The v2 split-portfolio layout (introduced in
me2resh/apexyard#242) movedonboarding.yamlto the private sibling repo and added.apexyard-forkas the new presence-only marker for the public fork._lib-ops-root.shwas updated then, but the inline walk-up boilerplate copy-pasted across all 40settings.jsonhook wrappers wasn't — they still look foronboarding.yamlANDapexyard.projects.yamlas the legacy v1 anchor pair. The hooks themselves work fine (most source_lib-ops-root.shinternally), but the wrapper that finds the hooks file fails on a v2-only fork — the walk never terminates because neither file exists. #302 closes that gap.The legacy wrapper shape also has an
r != ""infinite-loop hole (when${r%/*}reduces to empty before hitting/); the canonical shape fixes that too via the explicit[ -n "$r" ]guard.Testing
jq . .claude/settings.jsonclean (validates JSON after the sweep).apexyard-forkpresent, no legacy pair) — walk finds the right dir on every wrapperrguard exits cleanly (legacy shape would infinite-loop; confirmed 50+ iterations against the old shape)test_ops_root.sh8/8test_check_jq_installed.sh5/5test_link_custom_skills.sh7/7 (case 7 added in fix-up — exercises the graceful-degradation fallback when_lib-ops-root.shis absent; cases 1–6 now exercise the primary lib-sourcing path that the refactor introduces)test_portfolio_paths.sh38/38test_split_portfolio_v2_migration.sh13/13test_check_upstream_drift.sh5/5Out of scope
_lib-ops-root.shfor its own internal walks. Many already do (~10); the rest do private inline walks that are correct (they read$PWDat hook-fire time, which is what the hook semantics expect). Sweeping those is a separate ticket if we want it.clauderesolve hook paths from the ops-fork root. That would need an upstream change to the Claude Code runtime, not a framework change.Glossary
.apexyard-forkmarker file at the ops-fork root (introduced in #242 for split-portfolio v2). Presence-only — content is ignored.onboarding.yaml+apexyard.projects.yaml. Still recognised as fallback for un-migrated forks during the transition window.settings.jsonhook entry that walks up from$PWDto find the ops fork, thenexecs the actual hook script.Closes #302