Skip to content

refactor(#302): sweep all 40 hook wrappers to v2-anchor-aware shape#306

Merged
atlas-apex merged 4 commits into
devfrom
refactor/GH-302-sessionstart-v2-sweep
May 19, 2026
Merged

refactor(#302): sweep all 40 hook wrappers to v2-anchor-aware shape#306
atlas-apex merged 4 commits into
devfrom
refactor/GH-302-sessionstart-v2-sweep

Conversation

@atlas-apex

@atlas-apex atlas-apex commented May 19, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Swept all 40 hook wrappers in .claude/settings.json to the canonical v2-anchor-aware shape — every wrapper now finds the ops fork via either .apexyard-fork (v2) OR onboarding.yaml (legacy v1 fallback), with the empty-r guard that legacy shape lacked.
  • Refactored .claude/hooks/link-custom-skills.sh to source _lib-ops-root.sh with graceful-degradation fallback, replacing its private inline walk-up. One fewer place that needs to change when the anchor convention evolves.
  • Documented the canonical wrapper shape in AgDR-0041 so 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, so dirname "$0" returns .).
  • Cross-linked from 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.sh SessionStart entry that landed on origin/dev between worktree creation and merge), 30 PreToolUse, 2 PostToolUse, 1 UserPromptSubmit.

Why

The v2 split-portfolio layout (introduced in me2resh/apexyard#242) moved onboarding.yaml to the private sibling repo and added .apexyard-fork as the new presence-only marker for the public fork. _lib-ops-root.sh was updated then, but the inline walk-up boilerplate copy-pasted across all 40 settings.json hook wrappers wasn't — they still look for onboarding.yaml AND apexyard.projects.yaml as the legacy v1 anchor pair. The hooks themselves work fine (most source _lib-ops-root.sh internally), 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.json clean (validates JSON after the sweep)
  • Simulated v2-only fork layout (only .apexyard-fork present, no legacy pair) — walk finds the right dir on every wrapper
  • Simulated no-anchors layout — empty-r guard exits cleanly (legacy shape would infinite-loop; confirmed 50+ iterations against the old shape)
  • test_ops_root.sh 8/8
  • test_check_jq_installed.sh 5/5
  • test_link_custom_skills.sh 7/7 (case 7 added in fix-up — exercises the graceful-degradation fallback when _lib-ops-root.sh is absent; cases 1–6 now exercise the primary lib-sourcing path that the refactor introduces)
  • test_portfolio_paths.sh 38/38
  • test_split_portfolio_v2_migration.sh 13/13
  • test_check_upstream_drift.sh 5/5

Out of scope

  • Migrating every hook script to source _lib-ops-root.sh for its own internal walks. Many already do (~10); the rest do private inline walks that are correct (they read $PWD at hook-fire time, which is what the hook semantics expect). Sweeping those is a separate ticket if we want it.
  • Eliminating the wrapper-level walk entirely by making claude resolve hook paths from the ops-fork root. That would need an upstream change to the Claude Code runtime, not a framework change.

Glossary

Term Definition
v2 anchor The .apexyard-fork marker file at the ops-fork root (introduced in #242 for split-portfolio v2). Presence-only — content is ignored.
v1 anchor The legacy pair onboarding.yaml + apexyard.projects.yaml. Still recognised as fallback for un-migrated forks during the transition window.
Wrapper walk The bash one-liner inside each settings.json hook entry that walks up from $PWD to find the ops fork, then execs the actual hook script.
Canonical shape The standardised wrapper one-liner documented in AgDR-0041 — every new SessionStart / PreToolUse / PostToolUse hook should copy this verbatim.

Closes #302

me2resh and others added 3 commits May 19, 2026 21:27
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 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 #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 -f check per ancestor; negligible
  • PR Description & Glossary: Pass — 4 glossary entries, Closes #302 present, 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.md doesn'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:

  1. Prepend PR or Issue so the line reads PR #242 (split-portfolio v2) moved ….
  2. 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 -c entries in .claude/settings.json and 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.sh SessionStart entry that landed on origin/dev mid-flight is included (line 17 of the new settings.json).
  • I executed the new shape against cwd=/tmp (no anchors anywhere up the tree): the loop walks once, reduces r="/tmp" to r="", the -n "$r" guard fires, loop exits cleanly with r=""; 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 same cwd=/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")]"' prints 0=[bash] dirname0=[.]. The $0=bash claim is technically accurate. Option A's rejection is not just convenient framing — it's a hard property of bash -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 use git rev-parse --show-toplevel instead 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 in workspace/<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.yaml at 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)

  1. Test coverage gap on link-custom-skills.sh lib-sourcing path. The 6 test cases in test_link_custom_skills.sh build a sandbox that copies in _lib-portfolio-paths.sh and _lib-read-config.sh but 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.sh into the sandbox in make_fork() so the helper-sourced path is the one exercised.
    • Add a 7th case that creates a v2-only fork (.apexyard-fork only, no onboarding.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.
  2. 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.

  3. 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 in docs/multi-project.md to 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.

  4. 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 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 (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 $PWD via bash -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-lib sets copy_ops_root=0 so the cp "$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-fork and 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.yaml alone as the v1 anchor, while _lib-ops-root.sh (the in-hook resolver) requires BOTH onboarding.yaml AND apexyard.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

@atlas-apex atlas-apex merged commit 55b92ec into dev May 19, 2026
5 checks passed
@atlas-apex atlas-apex deleted the refactor/GH-302-sessionstart-v2-sweep branch May 19, 2026 20:52
atlas-apex added a commit that referenced this pull request May 28, 2026
…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>
me2resh added a commit that referenced this pull request Jun 5, 2026
…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>
me2resh added a commit that referenced this pull request Jun 5, 2026
…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>
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