fix(#310): resolve tracker config from ops-fork root, not workspace clone#313
Conversation
When the operator works inside a managed-project workspace clone at workspace/<project>/, hooks resolved .claude/project-config.json via `git rev-parse --show-toplevel`, which returns the PROJECT clone's git root — not the ops fork. The project clone usually has no framework config (or a different one), so tracker.kind silently defaulted to "gh" even when the operator configured Linear / Jira / Asana / custom at the ops-fork level. Linear/Jira-shaped IDs (PROJ-42, ENG-7, etc.) were then rejected as "missing GitHub issue". - _lib-read-config.sh: walk up to the ops-fork anchor (.apexyard-fork marker for split-portfolio v2, or onboarding.yaml + apexyard.projects.yaml pair for v1) via _lib-ops-root.sh BEFORE falling back to git rev-parse. Result is cached per-process. One-place fix that transparently cures every config / tracker consumer downstream. - validate-pr-create.sh + verify-commit-refs.sh: their direct `jq` reads of .claude/project-config.json (for .tracker_repo) and the in-hook lib sourcing now route through a CONFIG_ROOT resolved the same way. Source the libs via HOOK_DIR rather than REPO_ROOT so cwd inside a workspace doesn't break sibling-lib resolution either. - require-skill-for-issue-create.sh: already correctly used _lib-ops-root.sh for marker resolution; its config_get calls now pick up the fix transparently via the lib change. Regression test at .claude/hooks/tests/test_workspace_tracker_resolution.sh covers seven cases: config_get + tracker_kind from inside a workspace clone, the three named hooks all dispatching the configured tracker (jira) instead of falling back to a stub gh that always fails, require-skill-for-issue-create.sh still loading patterns from ops-fork-rooted defaults, regression from ops-fork root itself, and the no-ops-fork-anchor fallback path. Closes #310
atlas-apex
left a comment
There was a problem hiding this comment.
Code Review: PR #313 — APPROVED
Commit: a67096faccb73171d0c75bd27cc6cc06c4434060
Self-PR (cannot
--approvevia gh). Verdict is APPROVED; comment carries the review.
Summary
Targeted one-place fix to _lib-read-config.sh::_config_repo_root that restores the tracker-agnostic guarantee (AgDR-0033) for adopters working inside workspace/<project>/ clones. The lib now resolves the config-holding directory via the ops-fork anchor walk (_lib-ops-root.sh → v2 .apexyard-fork marker or v1 onboarding.yaml + apexyard.projects.yaml pair), with a git rev-parse --show-toplevel fallback to preserve bare-clone / CI-sandbox semantics. Two consumer hooks that bypass the lib with direct jq reads of .tracker_repo (validate-pr-create.sh, verify-commit-refs.sh) get the same ops-fork-walk treatment plus lib sourcing relocated from REPO_ROOT/.claude/hooks/ to HOOK_DIR. One new regression suite (7 cases) plus 13 regression suites green.
Checklist Results
- Architecture & Design: Pass
- Code Quality: Pass
- Testing: Pass
- Security: Pass (no surface change)
- Performance: Pass (per-process cache added; walk is cheap)
- PR Description & Glossary: Pass (5 entries,
Closes #310, Testing section) - Technical Decisions (AgDR):N/A (mechanical fix; AgDR-0033 + AgDR-0041 cover the surrounding decisions)
- Adopter Handbooks: N/A (no handbook findings)
Verification performed
- Re-ran
test_workspace_tracker_resolution.shlocally at HEAD: 7/7 PASS including the failure-mode-discriminating canary cases (gh-stub-that-fails in cases 3 + 4 — accidentally falling through toghraises exit 99; the test asserts the absence of 99, not just rc=0). - Re-ran the highest-risk regression suites:
test_tracker_aware_hooks.sh17/17,test_validate_pr_create_head.sh8/8,test_verify_commit_refs_upstream.sh12/12,test_validate_pr_required_sections.sh8/8,test_portfolio_paths.sh38/38. All green. - Audited the lib's call surface:
_config_repo_rootis private (only_config_defaults_file+_config_overrides_filecall it). No external consumer depends on the old return value — Hyrum-safe by construction. When the operator is at the ops-fork root, the new walk finds the v1 / v2 anchor in zero iterations; the return value matches whatgit rev-parse --show-toplevelwould have returned. - Graceful-degradation path verified:
BASH_SOURCE[0]anchors lib resolution to the lib's own location. If_lib-ops-root.shis missing on disk, the[ -f ]test skips the source and the legacygit rev-parsefallback fires. Both layers degrade silently — no hard fail.
Issues Found
None blocking.
Suggestions (non-blocking)
-
require-agdr-for-arch-pr.shretains the legacy$REPO_ROOT/.claude/hooks/_lib-read-config.shsourcing pattern (line 252-254). From inside a workspace clone, that path won't resolve and.agdr_trigger_paths[]/.agdr_trigger_dep_files[]overrides will silently be ignored — the hook falls through to the inline hardcoded defaults at line 260+. Impact is much smaller than #310's bug (no PRs incorrectly blocked, just adopter overrides ignored), so this is genuinely out of scope per the PR's explicit "Out of scope" section. Worth a separate ticket as a finishing pass — sameHOOK_DIR-sourcing +_lib-ops-root.shtreatment as the two hooks fixed here. -
Case 3 prose is slightly misleading: the embedded comment says "we instead exercise an explicit #N reference under a jira config" but the actual fixture uses
Closes PROJ-42, which the hook's REFS regex (only matches#N) doesn't extract. The test passes for the right reason (no tracker call happens because no #N ref is found), but the comment reads as if the hook actively dispatchesjira. Suggested tightening: change the comment to "the hook detects no #N ref, exits 0, never touches the gh stub — proving the bug-doesn't-fire on tracker-agnostic refs." Minor doc nit, not a test fault. -
_CONFIG_ROOT_CACHEuses a presence test instead of a$PWD-keyed cache: if a single hook subprocess somehowcd'd mid-run between calls toconfig_get, the cache wouldn't invalidate. Hooks are one-shot subprocesses in practice and don'tcdafter init, so this is fine — flagging only for awareness if anyone reaches for this pattern later.
AgDR check
No new AgDR needed. The change is a mechanical fix to a single private function. Surrounding architectural decisions (the ops-fork anchor primitive, the marker-vs-pair walk semantics, the tracker-dispatch contract) are already captured in _lib-ops-root.sh's docstring, AgDR-0033 (_lib-tracker.sh), and AgDR-0041 (ops-fork anchor sweep).
Standard gates
- Glossary: 5 entries — Pass.
Closes #310: present — Pass.- No secrets in diff.
- No private-project refs — test fixtures use
test-org/test-fork/test-org/test-project(generic stand-ins). - Branch
fix/GH-310-tracker-config-workspace-resolutionfollows convention.
Verdict
APPROVED
Operator action needed: this PR was opened by the marker-writer (me), so the marker write at .claude/session/reviews/313-rex.approved is sandbox-blocked. Run on the host:
printf 'a67096faccb73171d0c75bd27cc6cc06c4434060\n' \
> /Users/ahmed/Projects/apexstack/.claude/session/reviews/313-rex.approved41-byte file (40 hex + newline); the merge gate compares against gh pr view 313 --json headRefOid.
Reviewed by Rex (Code Reviewer Agent)
Reviewed commit: a67096faccb73171d0c75bd27cc6cc06c4434060
When the operator works inside a managed-project workspace clone at workspace/<project>/, hooks resolved .claude/project-config.json via `git rev-parse --show-toplevel`, which returns the PROJECT clone's git root — not the ops fork. The project clone usually has no framework config (or a different one), so tracker.kind silently defaulted to "gh" even when the operator configured Linear / Jira / Asana / custom at the ops-fork level. Linear/Jira-shaped IDs (PROJ-42, ENG-7, etc.) were then rejected as "missing GitHub issue". - _lib-read-config.sh: walk up to the ops-fork anchor (.apexyard-fork marker for split-portfolio v2, or onboarding.yaml + apexyard.projects.yaml pair for v1) via _lib-ops-root.sh BEFORE falling back to git rev-parse. Result is cached per-process. One-place fix that transparently cures every config / tracker consumer downstream. - validate-pr-create.sh + verify-commit-refs.sh: their direct `jq` reads of .claude/project-config.json (for .tracker_repo) and the in-hook lib sourcing now route through a CONFIG_ROOT resolved the same way. Source the libs via HOOK_DIR rather than REPO_ROOT so cwd inside a workspace doesn't break sibling-lib resolution either. - require-skill-for-issue-create.sh: already correctly used _lib-ops-root.sh for marker resolution; its config_get calls now pick up the fix transparently via the lib change. Regression test at .claude/hooks/tests/test_workspace_tracker_resolution.sh covers seven cases: config_get + tracker_kind from inside a workspace clone, the three named hooks all dispatching the configured tracker (jira) instead of falling back to a stub gh that always fails, require-skill-for-issue-create.sh still loading patterns from ops-fork-rooted defaults, regression from ops-fork root itself, and the no-ops-fork-anchor fallback path. Closes #310 Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Summary
_lib-read-config.sh::_config_repo_root— the lib resolved the config-dir viagit rev-parse --show-toplevel, which inside aworkspace/<project>/clone returns the project clone (no framework config there) instead of the ops-fork root (where.claude/project-config.jsonactually lives). Result:tracker.kindsilently defaulted to"gh"and Linear / Jira / Asana / custom ticket IDs were rejected as "missing GitHub issue"._config_repo_rootnow walks up ops-fork anchors via_lib-ops-root.shfirst (recognises both v2.apexyard-forkand legacy v1onboarding.yaml + apexyard.projects.yamlpair), falls back togit rev-parseonly when no anchor is found. Cures every downstreamconfig_getconsumer transparently.jqreads (validate-pr-create.sh,verify-commit-refs.sh) — they now resolveCONFIG_ROOTvia the same ops-fork walk and source their libs fromHOOK_DIRso the path works regardless of cwd.require-skill-for-issue-create.shwas already mostly correct (marker resolution walked correctly); itsconfig_getcalls inherit the lib fix transparently — no further changes needed.Why
A managed project running ApexYard under a non-GitHub tracker (Linear, Jira, Asana, custom) was hard-blocked from opening PRs the moment the operator
cd'd intoworkspace/<project>/. The framework hooks fired against the project clone's git root, found no.claude/project-config.jsonthere, fell back to the GitHub-default tracker, and 404'd on every legitimatePROJ-123/ENG-45ticket ID. The documented workaround (settracker.kind = "none") disabled ticket-existence verification entirely — a much broader trade-off than the bug warranted.This is a small, mechanical fix that restores the tracker-agnostic guarantee promised by
_lib-tracker.sh(AgDR-0033) for workspace-clone workflows.Testing
test_workspace_tracker_resolution.sh— 7 cases coveringconfig_get+tracker_kindfrom a workspace clone, three named hooks dispatching the configured tracker notgh,require-skill-for-issue-createloading patterns from ops-fork-rooted defaults, regression from ops-fork root, no-ops-fork fallbacktest_tracker_aware_hooks.sh— 17/17test_ops_root.sh— 8/8test_validate_pr_create_head.sh— 8/8test_validate_pr_create_upstream.sh— 7/7test_verify_commit_refs_upstream.sh— 12/12test_require_skill_for_issue_create.sh— 18/18test_validate_branch_name_pushref.sh— 15/15test_validate_pr_required_sections.sh— 8/8test_single_closes_per_pr.sh— 13/13test_portfolio_paths.sh— 38/38test_detect_deprecated_config.sh— 7/7test_require_active_ticket_bash.sh— 12/12test_validate_issue_structure.sh— 24/24Out of scope
git rev-parse --show-toplevelto source_lib-ops-root.sh— the SessionStart wrapper sweep ([Refactor] Sweep SessionStart hooks to use _lib-ops-root.sh — fix split-portfolio v2 invisibility #302) was a separate concern. This PR fixes only the hooks that READ tracker / config; the one-place lib fix covers their consumers transparently.kindvalues — out of scope._lib-tracker.sh/_lib-read-config.sh— internal-only fix; existing call sites unchanged.Glossary
apexyard.projects.yaml,onboarding.yaml, and.claude/project-config.jsonlive) — distinct from the managed-project clones underworkspace/<name>/workspace/<name>/for code work. Has its own.git/but no framework config.claude/project-config.json(gh/linear/jira/asana/custom/none); each kind maps to a CLI invocation pattern via_lib-tracker.sh. See AgDR-0033_lib-ops-root.shthat walks up from$PWDlooking for either.apexyard-fork(v2) oronboarding.yaml + apexyard.projects.yaml(legacy v1). The right primitive when a hook needs to find framework state regardless of cwdCLAUDE.mdskill table,docs/multi-project.mdskill behaviour table,.claude/project-config.defaults.json. Not relevant here — this PR doesn't touch any of themCloses #310