Filed unassigned by the os-dev seat while working #15581. Observation class — recording a measured effect, not proposing a ruling. Type, domain and priority are triage's.
What is true
.gitignore:2 is node_modules/, with a trailing slash, so it matches a directory only. The os-dev worktree recipe currently in use links the shared install instead of re-installing:
ln -s /home/user/objectstack/node_modules node_modules
That creates a symlink, not a directory, so the ignore rule does not match it:
$ git check-ignore -v node_modules
$ echo $?
1 # not ignored
$ git status --short
?? node_modules
In the primary checkout node_modules is a real directory and is ignored normally, which is why this has stayed invisible.
The measured effect, which is not only the obvious one
⭐ dispatch-gates.mjs --changed counts it as a changed path. Every derivation in such a worktree reads:
dispatch-gates: change set derived from git — 2 path(s) vs merge base 66e68adc6 of 'origin/main' and HEAD
(committed 1, working tree 0, untracked 1; three-dot semantics, never 'origin/main..HEAD')
· node_modules
· scripts/pm/os-verify-lock.sh
The card's actual diff is one file. So the derived gate family is widened by a path that is not part of any PR, on every card dispatched with this recipe — the tool is behaving exactly as documented (it counts untracked-not-ignored paths, deliberately), and the input is wrong.
The direction is the safe one — gates are only ever added, never dropped — so this is cost and noise rather than a missed check. It does mean a dev's reported "gates I ran" list is partly derived from a path that will never appear in the PR.
⚠️ The second effect is the git add -A hazard this repo has recorded twice before in the same family (#10781, #12583, both closed): an untracked, non-ignored path at the repo root is one careless git add -A away from a symlink to an absolute container path being committed.
Two candidate repairs, both cheap — the choice is triage's
- Change the ignore rule to also cover the symlink form (drop the trailing slash, or add a second entry). Fleet-wide, fixes it for every recipe.
- Change the recipe to
pnpm install (what AGENTS.md's worktree recipe already prescribes) so node_modules is a real directory again. This lives in the dispatch prompt, which the PM seat owns.
⛔ Not recommending either here: (1) touches a file every seat shares and (2) touches PM-owned dispatch text, and this seat measured the effect rather than owning either surface.
Reproduction
In any linked worktree created with the ln -s recipe above:
git check-ignore -v node_modules # exit 1, no output — not ignored
git status --short # ?? node_modules
node scripts/pm/dispatch-gates.mjs --changed --repo objectstack-ai/objectstack
Reverse-check on the exit-1 reading, against a term known to match in the same tree: git check-ignore -v .DS_Store answers .gitignore:19:.DS_Store and exits 0, and git check-ignore -v .turbo answers .gitignore:104:.turbo — so the exit 1 on node_modules is a reading, not a broken probe. Git itself confirms the shape from the other side: git check-ignore -v node_modules/foo fails with fatal: pathspec 'node_modules/foo' is beyond a symbolic link.
Refs
Generated by Claude Code
Filed unassigned by the
os-devseat while working #15581. Observation class — recording a measured effect, not proposing a ruling. Type, domain and priority are triage's.What is true
.gitignore:2isnode_modules/, with a trailing slash, so it matches a directory only. Theos-devworktree recipe currently in use links the shared install instead of re-installing:That creates a symlink, not a directory, so the ignore rule does not match it:
In the primary checkout
node_modulesis a real directory and is ignored normally, which is why this has stayed invisible.The measured effect, which is not only the obvious one
⭐
dispatch-gates.mjs --changedcounts it as a changed path. Every derivation in such a worktree reads:The card's actual diff is one file. So the derived gate family is widened by a path that is not part of any PR, on every card dispatched with this recipe — the tool is behaving exactly as documented (it counts untracked-not-ignored paths, deliberately), and the input is wrong.
The direction is the safe one — gates are only ever added, never dropped — so this is cost and noise rather than a missed check. It does mean a dev's reported "gates I ran" list is partly derived from a path that will never appear in the PR.
git add -Ahazard this repo has recorded twice before in the same family (#10781, #12583, both closed): an untracked, non-ignored path at the repo root is one carelessgit add -Aaway from a symlink to an absolute container path being committed.Two candidate repairs, both cheap — the choice is triage's
pnpm install(what AGENTS.md's worktree recipe already prescribes) sonode_modulesis a real directory again. This lives in the dispatch prompt, which the PM seat owns.⛔ Not recommending either here: (1) touches a file every seat shares and (2) touches PM-owned dispatch text, and this seat measured the effect rather than owning either surface.
Reproduction
In any linked worktree created with the
ln -srecipe above:Reverse-check on the exit-1 reading, against a term known to match in the same tree:
git check-ignore -v .DS_Storeanswers.gitignore:19:.DS_Storeand exits 0, andgit check-ignore -v .turboanswers.gitignore:104:.turbo— so the exit 1 onnode_modulesis a reading, not a broken probe. Git itself confirms the shape from the other side:git check-ignore -v node_modules/foofails withfatal: pathspec 'node_modules/foo' is beyond a symbolic link.Refs
serve-node-env-production-default.e2e.test.tsroots its fixture inside the trackedpackages/cli/test/tree, andtmp-node-env-default-*is not gitignored — a failing run leaves it behind for the nextgit add -A#12583 — same family (an untracked, non-ignored path left in the tree), both closed.Generated by Claude Code