codev doctor silently audits the wrong directory tree when run from anywhere under packages/, because findWorkspaceRoot() accepts a bare directory named codev as a codev-instance marker — and this monorepo has a package called packages/codev.
Found while running codev doctor during work on #1238 (PR #1243). Reproduces on main.
Repro
It is entirely about cwd:
cd <repo-root>/packages/codev && codev doctor # ⚠ "Architect state files ... are not gitignored"
cd <repo-root> && codev doctor # ✓ Project structure OK
.gitignore:15-16 correctly carries codev/state/*.md + !codev/state/*_thread.md, and git check-ignore -q codev/state/__doctor-probe__.md exits 0 from either repo root. The warning is bogus — but auditStateFileIgnore is not at fault; it is being handed the wrong root.
Note the trap: cd packages/codev is the documented place to work (CLAUDE.md → Directory Map: "pnpm build / pnpm test → run from packages/codev/"), so this is the normal cwd for anyone building or testing, not an exotic one.
Mechanism
findWorkspaceRoot() (packages/codev/src/commands/doctor.ts:443-455) walks up from cwd:
while (current !== dirname(current)) {
if (existsSync(resolve(current, 'codev'))) return current; // ← the bug
if (existsSync(resolve(current, '.git'))) return current;
current = dirname(current);
}
Starting at packages/codev:
packages/codev/codev — absent, packages/codev/.git — absent → walk up
packages/ → resolve('packages', 'codev') matches packages/codev, the npm package directory, not a codev instance → returns <repo-root>/packages
Every downstream check then receives workspaceRoot = <repo-root>/packages. auditStateFileIgnore runs its probe with cwd=packages/, so the path resolves to packages/codev/state/__doctor-probe__.md, which the root-anchored codev/state/*.md rule correctly does not match → exit ≠ 0 → warning.
Evidence
Calling the built auditStateFileIgnore directly with explicit roots (with node_modules installed, so this is not an artifact of an unbuilt tree):
workspaceRoot passed |
result |
<worktree> |
[] |
<worktree>/packages |
⚠ warns |
/…/cluesmith/codev (main checkout) |
[] |
/…/cluesmith/codev/packages |
⚠ warns — reproduces on main |
Blast radius — wider than the one warning
The bogus gitignore warning is just the visible symptom. workspaceRoot from findWorkspaceRoot() feeds every project-scoped check in doctor(), all of which then silently audit packages/ instead of the repo:
These mostly fail quietly — a check that finds nothing to audit reports ✓. So codev doctor run from packages/ can print a clean bill of health for a tree it never looked at. That is worse than the false warning, because it is invisible.
Fix direction
Don't accept a bare codev directory as the instance marker. Two complementary guards, plus a worktree caveat:
1. Require an instance signature. A real codev instance contains at least one of:
specs/ plans/ reviews/ resources/ protocols/ roles/ consult-types/
Verified against the two directories in question:
|
real codev/ |
packages/codev/ |
specs/, plans/, reviews/, resources/, roles/, consult-types/, protocols/ |
present |
absent |
templates/ |
present |
also present ⚠ |
package.json, src/, dist/, tsconfig.json |
absent |
present |
Two traps worth encoding:
- Do not use
templates/ as a signature — packages/codev/templates/ exists, so it collides and would not fix the bug.
- Do not require
protocols/ — per the four-tier resolver, a project that customizes no protocol legitimately has no codev/protocols/ on disk (CLAUDE.md calls this out explicitly). It belongs in an any-of set, never an all-of.
2. Add a negative signal. A candidate codev/ containing package.json is an npm package, not an instance — reject it outright. Cheap and decisive here.
3. Prefer the .git root when both markers match, as suggested — but note .git is a file, not a directory, inside a git worktree (.builders/<id>/.git is a 71-byte gitlink). The current existsSync handles that correctly; any refactor to statSync(...).isDirectory() would silently break every builder worktree, which is most of where Codev runs.
Ordering that resolves this case correctly: prefer a directory that has both a signature-passing codev/ and .git; else the nearest signature-passing codev/; else the .git root. From packages/codev, step 1 skips packages/ (fails the signature) and lands on the repo root — the right answer.
Scope
Deliberately not fixed in PR #1243 (#1238's session-log retention) to keep that PR scoped; filing separately at the architect's direction.
codev doctorsilently audits the wrong directory tree when run from anywhere underpackages/, becausefindWorkspaceRoot()accepts a bare directory namedcodevas a codev-instance marker — and this monorepo has a package calledpackages/codev.Found while running
codev doctorduring work on #1238 (PR #1243). Reproduces onmain.Repro
It is entirely about cwd:
.gitignore:15-16correctly carriescodev/state/*.md+!codev/state/*_thread.md, andgit check-ignore -q codev/state/__doctor-probe__.mdexits 0 from either repo root. The warning is bogus — butauditStateFileIgnoreis not at fault; it is being handed the wrong root.Note the trap:
cd packages/codevis the documented place to work (CLAUDE.md→ Directory Map: "pnpm build / pnpm test → run frompackages/codev/"), so this is the normal cwd for anyone building or testing, not an exotic one.Mechanism
findWorkspaceRoot()(packages/codev/src/commands/doctor.ts:443-455) walks up from cwd:Starting at
packages/codev:packages/codev/codev— absent,packages/codev/.git— absent → walk uppackages/→resolve('packages', 'codev')matchespackages/codev, the npm package directory, not a codev instance → returns<repo-root>/packagesEvery downstream check then receives
workspaceRoot = <repo-root>/packages.auditStateFileIgnoreruns its probe withcwd=packages/, so the path resolves topackages/codev/state/__doctor-probe__.md, which the root-anchoredcodev/state/*.mdrule correctly does not match → exit ≠ 0 → warning.Evidence
Calling the built
auditStateFileIgnoredirectly with explicit roots (withnode_modulesinstalled, so this is not an artifact of an unbuilt tree):workspaceRootpassed<worktree>[]<worktree>/packages/…/cluesmith/codev(main checkout)[]/…/cluesmith/codev/packagesBlast radius — wider than the one warning
The bogus gitignore warning is just the visible symptom.
workspaceRootfromfindWorkspaceRoot()feeds every project-scoped check indoctor(), all of which then silently auditpackages/instead of the repo:checkCodevStructure(incl.auditStateFileIgnore, git-remote check)hasFrameworkOverrides/auditFrameworkRefs(scaffold: deliver framework files (protocol.md, templates, workflow-ref) via resolver-aware channels (fresh-install class fix) #1011)auditPrGates(codev doctor/update: warn on PR-producing protocol overrides missing a 'pr' gate (post-#927 silent-breakage guardrail) #943) — "All PR-producing protocols are pr-gated" becomes vacuousauditProtocolDrift(codev doctor: detect protocol-file drift — local copies shadowing newer skeleton files, and stale installed skeletons #1210)loadForgeConfig/ forge concept resolutionThese mostly fail quietly — a check that finds nothing to audit reports ✓. So
codev doctorrun frompackages/can print a clean bill of health for a tree it never looked at. That is worse than the false warning, because it is invisible.Fix direction
Don't accept a bare
codevdirectory as the instance marker. Two complementary guards, plus a worktree caveat:1. Require an instance signature. A real codev instance contains at least one of:
Verified against the two directories in question:
codev/packages/codev/specs/,plans/,reviews/,resources/,roles/,consult-types/,protocols/templates/package.json,src/,dist/,tsconfig.jsonTwo traps worth encoding:
templates/as a signature —packages/codev/templates/exists, so it collides and would not fix the bug.protocols/— per the four-tier resolver, a project that customizes no protocol legitimately has nocodev/protocols/on disk (CLAUDE.mdcalls this out explicitly). It belongs in an any-of set, never an all-of.2. Add a negative signal. A candidate
codev/containingpackage.jsonis an npm package, not an instance — reject it outright. Cheap and decisive here.3. Prefer the
.gitroot when both markers match, as suggested — but note.gitis a file, not a directory, inside a git worktree (.builders/<id>/.gitis a 71-byte gitlink). The currentexistsSynchandles that correctly; any refactor tostatSync(...).isDirectory()would silently break every builder worktree, which is most of where Codev runs.Ordering that resolves this case correctly: prefer a directory that has both a signature-passing
codev/and.git; else the nearest signature-passingcodev/; else the.gitroot. Frompackages/codev, step 1 skipspackages/(fails the signature) and lands on the repo root — the right answer.Scope
Deliberately not fixed in PR #1243 (#1238's session-log retention) to keep that PR scoped; filing separately at the architect's direction.