Skip to content

fix(root): stop the _common.sh root search climbing out of a multi-app project (#420) - #487

Merged
MendixMau merged 8 commits into
masterfrom
fix/issue-420
Oct 20, 2026
Merged

MendixMau merged 8 commits into
masterfrom
fix/issue-420

Conversation

@MendixMau

@MendixMau MendixMau commented Oct 20, 2026 •

Copy link
Copy Markdown
Owner

Direct lane

What changed and why (one paragraph): In a multi-app folder (apps/<X>/X.mpr), instruments that source project-bin/_common.sh resolved PROJECT_ROOT outside the project. Tier 2 and tier 3 of the root search accepted a model only at <root>/ or <root>/app/. The project matched neither, so tier 3 walked up from $PWD and took the first ancestor holding any .mpr; with none above, it fell back to the toolkit. Look-ledger seen rows then landed in that other folder's .claude/loop/look/. The fix, as the issue proposes:

  • Third layout. _mxtk_has_model probes apps/*/ alongside the root and app/. Tier 2 uses it too.
  • Walk stops at the project folder. The shared mxtk_walk_root stops at the first folder that either holds a model or is a project folder: it has an installed bin/_common.sh, or a .claude/ beside a PROJECT.md (the pair init-project.sh writes), and is not $HOME. The toolkit's own copy lives in project-bin/, so the toolkit is not a project folder.
    • A bare .claude/ is deliberately not enough. The issue suggested .claude/ alone, but Claude Code creates that folder wherever it runs. A stray empty /tmp/.claude on the test machine stopped the walk at /tmp, which broke test-coverage-check-shape.sh's self-parent fallback (rc 4). The second commit narrows the marker and adds R11 for it.
  • find_mpr accepts the same layout. It returns the model when exactly one app sits under apps/. With several it refuses to guess, as it already did for two root models: it lists them and names MPR_FILE=apps/<X>/<X>.mpr.
  • bin/check-requirements.sh kept a private copy of the old walk that probed the root only, so on a two-tree or multi-app project --fix could install tests/e2e into a stray ancestor. It now sources _common.sh and uses mxtk_walk_root, as doctor.sh already sources _common.sh.

What stays as before:

  • Single-tree and two-tree resolution, from the root or any subfolder.
  • A $PWD inside one app's folder (apps/A/javasource) resolves to that app's folder, as before.
  • MPR_FILE and PROJECT_ROOT overrides.
  • The fallback when nothing is found.

The "multi-app model key" the issue mentions (ledger state keyed per model) is not part of this. With several apps, project-level state stays at the project folder, which is the fix for the misrouting.

Review round (third commit, then a merge of master with #475). find_mpr accepted apps/<X>/, but four consumers still probed for the model at the root and under app/ only. They now know the multi-app layout too:

  • model-stamp.sh check. A staged model in a multi-app folder no longer exits 1 with "no .mpr resolves". The no-model path is kept, and with several apps nothing staged still passes, so a multi-app folder does not block every commit. Staged files under one apps/<X>/ are checked against that app. Staged files across several apps are refused with "commit one app at a time" (git reset apps/<Y>, then MPR_FILE=apps/<X>/<X>.mpr ./bin/verify-model.sh, then commit). With MPR_FILE set in a multi-app folder, check refuses when any staged model path lies outside dirname "$MPR_FILE", and names those paths. At 9dd85db, MPR_FILE=apps/A/A.mpr with A and B staged verified A and let B through unverified, because the stamp covers one app's paths; new case R13e reproduced that on 9dd85db and is refused now. The probe uses _mxtk_has_model, guarded with type, with an inline fallback for a held-back _common.sh.
  • lint-gate.sh. It exited 2, so exec.sh logged lint as "could not run". It now lints a single app and refuses several, naming MPR=apps/<X>/<X>.mpr.
  • doctor.sh. Both probes were fixed; several apps are listed with the MPR_FILE hint. --property-sets honours MPR_FILE; otherwise it says which app it checked (the first) and names the others.
  • metrics-ledger.sh. The fallback used when _common.sh is missing now probes apps/*/ too.

Also in this round:

Fixes #420

Impact

Row Answer
Process No new human step or gate. In a multi-app folder with several apps, find_mpr callers (exec.sh, snapshot-mpr.sh, …) now stop with "N .mpr files … set MPR_FILE=apps//.mpr". Before, they said "no .mpr found" or, worse, used a stray ancestor model.
Dependencies Touches the shared project-bin/_common.sh (root resolution and find_mpr; sourced by every installed instrument and the look-ledger hook), project-bin/model-stamp.sh, project-bin/lint-gate.sh, project-bin/metrics-ledger.sh, bin/doctor.sh, bin/check-requirements.sh and bin/sync-project.sh (one known_fix_note line). A hand-edited bin/_common.sh is held back by sync-project.sh, and so is every older script that depends on it, so those projects do not get #420 from a plain sync. The remedy is sync-project.sh <project> --only-bin _common.sh --upgrade-bin _common.sh, which backs up the old copy first; the sync drift warning now names #420 and that command. An unedited copy is refreshed by sync as usual. No new tools: ls/dirname globbing as before, bash 3.2 safe. New public helper mxtk_walk_root. No order dependency on #484/#485 apart from the shared ## Unreleased CHANGELOG spot.
Context cost none: no routed or always-loaded file touched
Behaviour n/a: scripts only
Runtime Per directory on the walk, one extra ls glob (apps/*/*.mpr) and two [ -f/-d ] tests. The walk now usually ends earlier, at the project folder instead of /. Not measurable at script start.

Field evidence: the issue's repro, taken verbatim as the fixture's R1/R2: a project at x/stray/proj with apps/A/A.mpr and a stray .mpr in x/stray. On master the installed copy and the toolkit copy both resolve x/stray (and, with the stray removed, the toolkit folder). On this branch both resolve x/stray/proj. The #414 PR body records the same tier-3 symptom on a lab-project copy under /tmp, where it had to be worked around with an explicit PROJECT_ROOT. No client project was used.

  • No client data anywhere in the diff: whole-word grep of the added lines for the denylist terms and home paths gives 0; LEAKGUARD_ALLOW_NO_DENYLIST=1 bin/check-no-client-data.sh is clean
  • Size cap: 7 files plus 1 new fixture and CHANGELOG; about 190 changed lines outside the fixture
  • Test tier reached: T1.
    • New tests/wave2/test-common-root-multiapp.sh project-bin/_common.sh: 36/36. Against master, 24 fail; the 12 that pass on master are the unchanged-behaviour guards. The cases added in the review round:
      • R12/R13: model-stamp.sh with one app and with two, including R13e (A and B staged, MPR_FILE=A: refused) and R13f (only A staged and stamped, MPR_FILE=A: passes).
      • R14: lint-gate.sh.
      • R15: doctor.sh, including R15c (--property-sets with two apps names the app it checked).
      • R16: a model-less worktree under .claude/worktrees/<w> resolves to the worktree, not the main project. This is a deliberate consequence of the bin/_common.sh marker and is stated in the CHANGELOG.
    • Every wave2 fixture that references _common.sh or check-requirements.sh: 38/38 pass (first round).
    • Review round, consumer fixtures, all pass: test-model-stamp 35, test-install-claude-permissions 77, test-mx-metadata-note 40, test-property-sets 61, test-lint-gate-accept 20, test-lint-gate-new-findings 9, test-lint-receipt-revert 55, test-exec-lint-autorestore 17, test-exec-lock-watch 84, test-sec014-notice 24, test-exec-gate-log 24, test-doctor-docker-probe 18, test-doctor-gate-selftest 28, test-doctor-mxcli-arch, test-doctor-stale-hooks 7, test-metrics-ledger 42, test-exec-change-class 21, test-look-ledger 11, test-common-windows 10, test-coverage-check-shape 8/8, test-affected-tests 138, test-sync-only-bin 40, test-bug12-sync 61, test-sync-past-stock 22.
    • Re-run after the second review round, all pass: test-common-root-multiapp 36, test-model-stamp 35, test-doctor-docker-probe 18, test-doctor-gate-selftest 28, test-doctor-mxcli-arch, test-doctor-stale-hooks 7, test-property-sets 61.
    • test-look-ledger-hook-repair fails H7 (a JSON re-format of a "für" value) the same way on an untouched origin/master export in this environment. It is not caused by this PR.
    • Checks: bin/check-scripts.sh, bin/check-portability.sh, bin/render-routing.sh --check and bin/check-pr-discipline.sh origin/master are all clean.
  • For a new/changed instrument: no parser is involved. Layouts: single tree, two-tree (app/), multi-app (apps/<X>/), nested multi-app folders, and a model-less project, each with a stray ancestor .mpr. Platforms: only ls globs, dirname, [ -f/-d ] and a $HOME string compare (Git Bash reports $HOME and pwd in the same /c/Users/... form); bash 3.2 compatible. Producer: .claude/ is written by init-project.sh / mxcli init, and bin/_common.sh by init-project.sh's install step
  • CHANGELOG.md line appended, credited to MendixMau
  • No new bug entries

Follow-up, not in this PR (for the coordinator to file). The installed copy and the toolkit copy can disagree when $PWD is inside app/ or apps/<A>/. The installed copy (<project>/bin/x.sh) resolves through tier 2, the script's own parent, which is the project. The shared toolkit copy resolves through tier 3, which walks up from $PWD and stops at the first folder holding a model; from inside apps/A/ that is apps/A, and from inside app/ it is app. The fixture's R8 pins today's tier-3 answer. Project state (ledgers, stamps) can therefore land in two places depending on which copy ran. A fix needs a decision on whether state follows the model (a per-model key) or the project.

https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw

https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw


Generated by Claude Code

…p project (#420)

Tier 2 and tier 3 accepted a model only at <root>/ or <root>/app/. A
multi-app folder (apps/<X>/X.mpr) matched neither, so tier 3 walked up
from $PWD and took the first ancestor holding any .mpr (or fell back to
the toolkit), and look-ledger rows landed in another folder's ledger.

- project-bin/_common.sh: _mxtk_has_model probes apps/*/ as a third
  layout; mxtk_walk_root stops at the first folder that holds a model
  or is a project folder (.claude/ other than $HOME's, or an installed
  bin/_common.sh). find_mpr finds a single apps/<X>/ model and refuses
  to guess between several, naming MPR_FILE=apps/<X>/<X>.mpr.
- bin/check-requirements.sh: its private root-only copy of the walk is
  replaced by mxtk_walk_root, so --fix no longer installs tests/e2e
  into a stray ancestor.
- tests/wave2/test-common-root-multiapp.sh: 20 cases over the three
  layouts with a stray ancestor .mpr, nested multi-app folders, a
  model-less project and check-requirements; 12 fail on master.

Single-tree and two-tree resolution are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw
Claude Code creates .claude/ in whatever folder it runs in; a stray
/tmp/.claude on the test machine stopped the walk there and broke
test-coverage-check-shape.sh's self-parent fallback. A project folder
is now an installed bin/_common.sh, or .claude/ beside PROJECT.md (the
pair init-project.sh writes). Fixture gains R11 (bare .claude/ above an
unmarked project: the old fallback and the model above still apply).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw
…nd metrics-ledger the apps/ layout (#420)

find_mpr accepted apps/<X>/ but four consumers still probed root and app/
only: model-stamp.sh check refused a staged model with "no .mpr resolves",
lint-gate.sh exited 2 (exec.sh logged lint as "could not run"), doctor.sh
reported no model, and metrics-ledger.sh's fallback found none.

- model-stamp.sh: _mxtk_has_model (type-guarded, inline fallback for a
  held-back _common.sh). With several apps and nothing model-related staged
  `check` still passes, so a multi-app folder does not block every commit;
  staged files under one apps/<X>/ are checked against that app; staged
  across several refuse and name MPR_FILE.
- lint-gate.sh: one app linted; several refused naming MPR=apps/<X>/<X>.mpr.
- doctor.sh: both probes; several apps are listed with the MPR_FILE hint.
- metrics-ledger.sh: the no-_common.sh fallback probes apps/*/.
- sync-project.sh: known_fix_note for a held-back _common.sh names #420
  and the --only-bin _common.sh --upgrade-bin _common.sh remedy.
- check-requirements.sh: fail with exit 2 if _common.sh cannot be loaded.
- test-common-root-multiapp.sh: R12-R16 (model-stamp, lint-gate, doctor,
  a model-less worktree under .claude/worktrees/); 33/33, 22 fail on master.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw
Keeps both ## Unreleased CHANGELOG entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw
…ames the checked app (#420)

Review: the several-apps refusal suggested MPR_FILE=apps/<X>/<X>.mpr, and
doing that verified A while B's staged changes went through unverified
(the stamp covers one model's paths).

- model-stamp.sh: the refusal now says commit one app at a time (git
  reset apps/<Y>, MPR_FILE=... ./bin/verify-model.sh, commit). In a
  multi-app folder with MPR_FILE set, `check` refuses when any staged
  model path is outside dirname(MPR_FILE) and names those paths.
- doctor.sh --property-sets: honours MPR_FILE; otherwise says which app
  it checked and names the others.
- test-common-root-multiapp.sh: R13e (A+B staged, MPR_FILE=A: refused;
  passed unverified at the previous head), R13f (only A staged and
  stamped: passes), R15c. 36/36.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw
@MendixMau
MendixMau merged commit 4516164 into master Oct 20, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug] _common.sh root search climbs to any ancestor .mpr in a multi-app layout, so ledgers land in the wrong folder

2 participants