Skip to content

fix(gitignore): anchor .Codex/ to repo root, report outside worktrees (ADR-1078) - #602

Merged
cdeust merged 5 commits into
mainfrom
fix/codex-ignore-worktree-doctor
Sep 17, 2026
Merged

cdeust merged 5 commits into
mainfrom
fix/codex-ignore-worktree-doctor

Conversation

@cdeust

@cdeust cdeust commented Sep 16, 2026 •

Copy link
Copy Markdown
Owner

PR #602 accepts both repository host worktree roots, preserves Git paths with NUL framing, and reports unavailable inspection as an optional WARN. ADR1079 avoids the allocation used by PR600.

Final author SHA: 28aea2a (rebased onto origin/main 4bd7e7f, plus one added commit fixing the not-a-git-checkout WARN a review round found).

Validation: 51 passed in 8.83s across test_doctor, test_doctor_worktrees, handlers/test_check_setup and shared/test_subprocess_safe (macOS, Python3.13; the added test is test_git_unavailable_in_a_checkout_warns). Full pytest -q: 8829 passed, 8 skipped, 459 subtests passed, 2 failed — both in tests_py/hooks/test_entry.py, unrelated to this PR's files (console-entry-point parity from #605), pre-existing on origin/main. Ruff0.16.6 check and format passed; pyright mcp_server/doctor.py clean; craftsmanship --base origin/main passed; check_doc_claims and check_project_wiki passed. Ignore probe ignored only root .Codex/probe, not nested/.Codex/probe. Newline regression failed before fix; two targeted mutants killed (reject Codex, trim path). No exhaustive mutation score claimed.

Review round two (ZETETIC-REVIEW: REQUEST_CHANGES) found that a project with no Git repository at all produced the same optional WARN as Git being unavailable or timing out, with a fix command that cannot succeed there. Fixed by probing for a .git entry via stdlib walk before ever shelling out; "not a git checkout" is now ok True, the WARN is reserved for Git itself failing to answer. ADR-1079 records the distinction.

Completion ledger. All remote checks completed without failure on final head 5447cbb837649ef26e967b763121d16ec65003c1. Independent final code review: APPROVE; report held locally.

Item Status Evidence or analysis
A1 Done Real Git tests and CLI/MCP nominal consumer assertions.
A2 Done Empty, one, many, duplicate, exact-root, prefix, symlink, Unicode, newline, trailing-space, bare, prunable and orphan-marker cases mapped below. Git guarantees main-first ordering; no configured maximum or scalability claim.
A3 Done Caller None/empty tests, shared helper failure/timeout tests, rendered WARN assertion.
A4 Done Fixed argv through existing helper; NUL framing and orphan-marker tests; internal entries follow Git porcelain.
A5 Done Function contracts documented; no repository state written by diagnostic.
A6 Done Fresh invocation-local observations, no persistence/retry; repository changes can change later results.
B1 Done No locks/threads added; helper bounds external wait; timeout tests run.
B2 Done No shared read-modify-write. Listing is an advisory snapshot.
B3 Done No persistent mutation; helper lifecycle reused, deterministic kill/reap tests run. General KeyboardInterrupt behavior unchanged and not separately claimed.
C1 Done One Git invocation, linear token/entry passes with two roots; filesystem resolution per entry, no pairwise scan.
C2 Done Helper owns process/pipes, cleanup tests run; no new database connections/files; unique tmp_path fixtures.
C3 N/A Optional doctor command, no retrieval/memory hot path touched.
D1 Done Constant argv, no shell; newline-path regression exercises delimiter content.
D2 Done NUL framing, resolved containment, no path execution; orphan/empty/unrecognized output tests.
D3 Done No authentication material or privilege changes; local paths are intended diagnostic output.
E1 Done Additive optional Check; existing schema and both registries pinned.
E2 Done Actual check tested through doctor.run and check_setup.handler for nominal/outside/unavailable; preflight uses doctor.
E3 N/A No application persisted format change; wiki mirror/manifest check passes.
E4 Done Windows SQLite CI passed on final head. The POSIX newline-filename case is intentionally skipped on Windows, where that filename is invalid.
F1 Done WARN/no FAIL, path, fix text, nominal empty fix and MCP serialized output asserted.
F2 Done Unavailable optional WARN distinct from empty successful listing; both asserted.
G1 Done Introduced paths mapped below.
G2 Done Newline pre-fix failure and two killed mutants; actual root-ignore probe; not-a-git-checkout now short-circuits before the subprocess call instead of relying on its response.
G3 Done Unique repositories/identity, restored monkeypatches, no new timing tests; expanded49 then final51 pass. Existing helper timing tests retained.
G4 Done Allowed path absent from outside detail, no FAIL optional/no WARN nominal, empty nominal fix and zero required MCP fixes.
G5 Done 51 focused tests and pinned lint/format passed locally on the rebased head. Remote CI at the pushed head is the coordinator's acceptance step.
H1 Done Staged craftsmanship passes against origin/main; existing helper and timeout precedent used.
H2 Done Parser/classifier/check separated, contracts documented, staged diff inspected.
H3 Done Existing naming/schema, English prose, Ruff passes.
H4 Done CHANGELOG/guidance/ADR1079 canonical/mirror/manifest aligned; wiki gate passes.
H5 Done Conventional dedicated commits; rebased onto origin/main 4bd7e7f and force-pushed with lease after the ADR numbering collision with #600/#606 was resolved.
H6 Pending GitHub head is 28aea2a; remote CI at this head is the coordinator's acceptance step.
H7 Done locally Host mismatch, path truncation and false noncheckout WARN all fixed; independent review/CI remain acceptance steps.

Path ledger prefixes: D = tests_py/test_doctor.py::TestWorktreeLocations; W = tests_py/test_doctor_worktrees.py; S = tests_py/shared/test_subprocess_safe.py.

Introduced path Named test or external check
None listing early return W test_unavailable_or_empty_listing_contract[None]; D test_git_unavailable_in_a_checkout_warns
Not a git checkout (no .git in the ancestor walk) D test_not_a_git_checkout_passes
Missing executable/nonzero S test_returns_none_when_binary_does_not_exist; test_returns_none_on_nonzero_exit
Timeout/cleanup S test_returns_none_on_real_timeout_without_hanging; test_timeout_kills_and_never_recalls_communicate; test_timeout_survives_a_reap_that_also_times_out
Empty/unrecognized output, no current W test_unavailable_or_empty_listing_contract empty/unrecognized cases
First record initializes current D test_main_checkout_only_passes; W test_nul_parser_preserves_records_markers_and_unicode
Next record flushes previous W test_nul_parser_preserves_records_markers_and_unicode
Bare marker with current W test_nul_parser_preserves_records_markers_and_unicode
Prunable marker with current W test_nul_parser_preserves_records_markers_and_unicode
Orphan bare/prunable ignored W test_orphan_markers_are_ignored
HEAD/branch/detached/empty ignored D real Git tests; W test_orphan_markers_are_ignored
Last current appended D test_main_checkout_only_passes; W test_nul_parser_preserves_records_markers_and_unicode
Unicode path W test_nul_parser_preserves_records_markers_and_unicode
Newline/trailing-space path D test_reports_exact_path_with_newline
Empty classification/detail W test_unavailable_or_empty_listing_contract empty/unrecognized cases
Bare classification/detail W test_bare_and_exact_root_details
Both roots/main listing D test_host_worktree_passes_from_linked_checkout both parameters; test_both_hosts_and_prunable_entries
Prunable excluded D test_both_hosts_and_prunable_entries
Outside rejected D test_flags_worktree_outside_claude_worktrees
Prefix/symlink escape D test_rejects_prefix_sibling_and_symlink_escape
No outside success/detail W test_bare_and_exact_root_details; test_real_worktree_check_cli_and_mcp_contract[False]
Outside failure/detail D test_flags_worktree_outside_claude_worktrees; W test_real_worktree_check_cli_and_mcp_contract[True]
_under descendant true D test_host_worktree_passes_from_linked_checkout
_under exact-root true/duplicates W test_bare_and_exact_root_details
_under ValueError false D test_rejects_prefix_sibling_and_symlink_escape
Unavailable WARN/fix W test_unavailable_listing_warns_without_blocking_consumers
Nominal empty fix W test_real_worktree_check_cli_and_mcp_contract[False]
Outside correction text W test_real_worktree_check_cli_and_mcp_contract[True]
Both registries W test_registries_include_optional_worktree_check
CLI warning exit0/MCP ready/serialization W test_real_worktree_check_cli_and_mcp_contract[True]; test_unavailable_listing_warns_without_blocking_consumers
CLI nominal no WARN/MCP zero required fixes W test_real_worktree_check_cli_and_mcp_contract[False]
Ignore root-only Actual git check-ignore probe returns only .Codex/probe
ADR canonical/mirror/manifest check_project_wiki.py reports matching canonical sources

No remote publication of this draft by author. CI-dependent rows require final-SHA verification before merge.

@cdeust

cdeust commented Sep 16, 2026

Copy link
Copy Markdown
Owner Author

ZETETIC-REVIEW: APPROVE

Reviewed final head 5447cbb. Whole PR inspected at 7fcb1cb against origin/main, including delta since 6714ed9, then all six changed paths through final head. No blocking findings. Remote CI at this exact head remains required and is owned by the coordinator.

Independently verified: clean exact head; diff whitespace check; both backend check lists; optional warning exit behavior; main-checkout anchoring; resolved ancestry and both host roots; NUL parser preserving newline and trailing space; real-Git regression coverage; ADR-1079 manifest and exact mirror agreement; no stale ADR-1078 pointers in changed code or documentation; timeout precedent; function size split.

The proposed PR body identifies the behavior, validation versions, targeted mutation limits and Windows filename-test limitation. Its 19-test count described the earlier head and should be refreshed before publication with the final validation result. Reported validation for the earlier head: 19 doctor tests, pinned Ruff 0.16.6 check and format, craftsmanship, wiki gate, and two killed mutants. These executions belong to the implementer/coordinator; reviewer did not rerun them or assert that count for final head.

Final delta inspection verifies that unavailable Git results now produce an honest optional warning with possible causes and a retry command. Empty successful output remains distinct. New tests assert parser markers, Unicode, empty/None results, orphan markers, both registries, CLI warning and exit status, repair text, and MCP readiness/check payload. A separate consumer test covers unavailable Git without blocking CLI or MCP readiness. ADR and changelog document this behavior; the final mirror exactly matches the canonical ADR. The wording observation about the parser docstring has been corrected to inspection unavailable.

Final HEAD and clean status independently checked; final whitespace check passed. Repository remained read-only. No GitHub comment or merge performed.

@cdeust

cdeust commented Sep 17, 2026

Copy link
Copy Markdown
Owner Author

ZETETIC-REVIEW: REQUEST_CHANGES

Reviewed 5447cbb in a detached worktree at that sha, gates run with the repository's uv.lock environment.

Measured on the head:

pytest tests_py/test_doctor.py tests_py/test_doctor_worktrees.py -q -p no:cacheprovider
29 passed in 3.21s
python scripts/check_craftsmanship.py --base origin/main
Craftsmanship gate: OK
ruff check; ruff format --check            clean
python scripts/check_project_wiki.py       Project wiki mirrors match their canonical sources.
python scripts/check_doc_claims.py         OK (one pre-existing declared exemption)
gh pr checks 602                           every check pass or skipping, none failed
ADR number                                 1079; #600 merged as 9656fc4d with 1078, no collision

Blocking finding, measured by running the PR's mcp_server from a directory that is not a git checkout:

cd /private/tmp/claude-501 && PYTHONPATH=<worktree> python -m mcp_server.doctor
  [WARN] worktree locations (optional)    Unable to inspect worktree locations: not a Git checkout, Git unavailable, or command failed.

check_setup returns the same check with ok: false. The plan this PR implements had "not a git checkout" as ok True, and the earlier head 6714ed9 behaved that way (test_not_a_git_checkout_passes); 7fcb1cb inverted it into test_not_a_git_checkout_warns. Cortex is an MCP server whose cwd is whatever project the host opened, and a project that is not a git repository is an ordinary condition, not a failure to inspect. With this head every preflight in such a project shows a WARN and a fix command (git worktree list --porcelain -z) that cannot succeed there. Git already distinguishes the cases: "not a git repository" exits 128 with that message, while a missing binary raises FileNotFoundError and a hang raises TimeoutExpired. run_with_hard_timeout collapses all of them to None, so the check needs either a stdlib probe before the call (walk Path.cwd() and its parents for a .git entry, which is the discovery git itself performs) or a variant of the helper that exposes the return code. The expected shape: ok True, detail "not a git checkout" on a plain tmp_path; the WARN kept for git missing or timing out (a test with a .git present and run_with_hard_timeout patched to None). ADR-1079's last paragraph should say the same.

Non-blocking: docs/agent-guidance.md and ADR-1079 now name .Codex/worktrees/<name>/ as a second allowed location, following the #601 decision. CLAUDE.md:27 still reads "Every git worktree lives at .claude/worktrees/<name>/", so the non-negotiable and the guidance disagree on the same page set; one line in CLAUDE.md naming the Codex directory closes that.

The rest of the delta since 6714ed9 holds up: the -z parser keeps newlines and trailing spaces in paths (test_reports_exact_path_with_newline creates a real worktree with both), orphan markers are ignored, the symlink escape and the prefix sibling .Codex/worktrees-other are rejected, and _worktree_locations is under the 40-line limit after the split.

cdeust added a commit that referenced this pull request Sep 17, 2026
cdeust added a commit that referenced this pull request Sep 17, 2026
…and Codex (#603)

* docs(readme): predictions and calibration, one store for Claude Code and Codex

The README described what Cortex remembers and never that it can record
what it expected (#597, ADR-1076): a section under "Keep context useful"
shows predict, resolve_prediction and calibration with their real return
shapes and says what the Brier score means against the 0.25 of a constant
0.5 forecast. The host table gains the row, and the Codex paragraph says
the plugin reads the same saved backend selection as the Claude launcher
(#600). The unreleased changelog had two "### Added" headings; one now.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cz7A6goxVzfKL9NT7k4zDH

* docs(readme): one column per host in the capability table

Codex was folded into "local stdio hosts" although its plugin starts the
server with --profile lean (ten named tools, no hooks) while a direct
codex mcp add gets the full surface. The table now has a column for each,
says which tools lean lacks (predictions, wiki writes, ingestion), where the
shared backend selection lives (#600), and which worktree directory doctor
checks per host.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cz7A6goxVzfKL9NT7k4zDH

* docs(readme): the calibration band the code can return, and the release hedge for Codex

BAND_EDGES steps by 0.2, so the illustrative reliability row shows
[0.6, 0.8]. The Codex paragraph names CORTEX_BACKEND between the two
variables it already named and says the shared backend selection reaches
the plugin with the release after 4.22.0, since the plugin launches the
published package.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cz7A6goxVzfKL9NT7k4zDH

* docs(readme): the worktree row states the convention, not a doctor check

The doctor check for worktree locations is PR #602, not merged.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cz7A6goxVzfKL9NT7k4zDH

* docs(readme): the lean profile's real reason, and each worktree cell sourced to what names it

ADR-0693 records why the Codex plugin starts lean: the full surface is the
largest fixed token cost a session pays, not a startup-time limit. The
worktree row cites docs/agent-guidance.md for the Claude Code directory
only; the Codex directory is observed behaviour that #601 gitignored.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cz7A6goxVzfKL9NT7k4zDH

---------

Co-authored-by: Claude Fable 5.1 <[email protected]>
cdeust and others added 5 commits September 17, 2026 14:19
… (ADR-1078)

.gitignore's .Codex/ rule (#601) was unanchored, matching a .Codex
directory at any depth instead of just the repo root. Anchor to
/.Codex/.

The condition that rule hid: a git worktree living outside
.claude/worktrees/, the only location docs/agent-guidance.md:160-166
names, stays where the owner decided it stays, but is now reported
rather than silenced. mcp_server/doctor.py gains an optional,
non-blocking "worktree locations" check (surfaced through
python -m mcp_server.doctor, check_setup, and /preflight) that lists
any git worktree list --porcelain entry outside
<main-worktree>/.claude/worktrees/. The allowed root is derived from
the main worktree's own listing (always first, per git-worktree(1)
section list) rather than git rev-parse --show-toplevel, which
resolves to a linked worktree's own path and would false-positive on
every sibling worktree run from inside one.

Decision recorded as ADR-1078.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cz7A6goxVzfKL9NT7k4zDH
…hod-size gate

CI's craftsmanship gate job was red on 217ec1f: _worktree_locations
spanned 51 lines (end_lineno - lineno), over the 40-line method cap
(scripts/craftsmanship_rules.py METHOD_LINE_LIMIT). Split the
classification logic into _worktree_classification(entries) -> (ok,
detail), leaving _worktree_locations as a short orchestrator that
turns the tuple into a Check. Same behavior, same tests.

python scripts/check_craftsmanship.py --base origin/main -> Craftsmanship gate: OK

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cz7A6goxVzfKL9NT7k4zDH
_worktree_locations() treated every failure mode of `git worktree list`
(no repository, git missing, timeout) as the same "inspection unavailable"
WARN. Cortex is an MCP server whose cwd is whatever project the host
opened; a project with no Git repository is ordinary, not a failure to
inspect, so every such session's preflight showed a WARN whose fix
command (`git worktree list --porcelain -z`) can never succeed there.

`run_with_hard_timeout` collapses every subprocess failure mode to
`None`, so the two cases can't be told apart after the call. A new
`_is_git_checkout()` stdlib probe runs first instead — the same upward
walk for a `.git` entry that Git itself performs to discover a
repository, no subprocess involved. No `.git` anywhere up the tree now
reports `ok True, detail "not a git checkout"`; a `.git` present but Git
missing from PATH or the listing timing out keeps the optional WARN.

Renamed test_not_a_git_checkout_warns to test_not_a_git_checkout_passes
(now asserts the ok True case) and added
test_git_unavailable_in_a_checkout_warns for the WARN case with a real
.git present. Updated the parametrised None case in
test_unavailable_or_empty_listing_contract, whose message no longer
claims "not a Git checkout" as a possible cause since that case now
short-circuits before the subprocess call. ADR-1079's Consequences
section records the distinction.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cz7A6goxVzfKL9NT7k4zDH
@cdeust
cdeust force-pushed the fix/codex-ignore-worktree-doctor branch from 5447cbb to 28aea2a Compare September 17, 2026 12:39
@cdeust

cdeust commented Sep 17, 2026

Copy link
Copy Markdown
Owner Author

ZETETIC-REVIEW: APPROVE

This supersedes the REQUEST_CHANGES of round two (#602 (comment)). Both findings from that round are fixed and verified at head 28aea2af, rebased onto origin/main 4bd7e7f0. Everything below was measured in a read-only worktree at that head, with the repository interpreter (.venv/bin/python, Python 3.13.7) reached through a symlink to the main checkout's venv, so the executor's replaced-venv artifact does not enter the picture.

Base and scope

$ git merge-base HEAD origin/main
4bd7e7f05a40ddd66aa59ec68ce19f7f17ea9a20

$ git diff origin/main --stat
 .gitignore                                         |   2 +-
 CHANGELOG.md                                       |  13 ++
 CLAUDE.md                                          |   3 +-
 ...ion-git-worktrees-as-an-optional-doctor-warn.md |  35 +++++
 docs/agent-guidance.md                             |   7 +-
 mcp_server/doctor.py                               | 154 +++++++++++++++++++++
 tests_py/test_doctor.py                            | 129 +++++++++++++++++
 tests_py/test_doctor_worktrees.py                  | 140 +++++++++++++++++++
 ...ion-git-worktrees-as-an-optional-doctor-warn.md |  33 +++++
 wiki/manifest.json                                 |   4 +
 10 files changed, 515 insertions(+), 5 deletions(-)

The round-two blocker: a project without git

Fixed. From a directory with no .git anywhere up the tree:

$ cd /private/tmp/claude-501
$ PYTHONPATH=<worktree> <repo>/.venv/bin/python -m mcp_server.doctor
  [OK  ] worktree locations (optional)    not a git checkout

From the checkout root, the check names both allowed roots:

  [OK  ] worktree locations (optional)    all worktrees under /Users/cdeust/Developments/anthropic-partnership/Cortex/.claude/worktrees or /Users/cdeust/Developments/anthropic-partnership/Cortex/.Codex/worktrees

And in a real checkout where git cannot answer, with a stub git on PATH that exits 127:

$ env PATH=<stub-dir> <repo>/.venv/bin/python -m mcp_server.doctor
  [WARN] worktree locations (optional)    Unable to inspect worktree locations: Git unavailable or command failed.
  2. worktree locations (optional):
     → Install Git and ensure it's on PATH; retry git worktree list --porcelain -z.

No [FAIL], exit code unchanged. The distinction the review asked for is the one now implemented, and ADR-1079's closing paragraph states it in the same terms as the code.

_is_git_checkout semantics, probed

linked worktree root (.git is a file?): True  -> True
subdir of repo:                               -> True
root /:                                       -> False
tmp non-repo:                                 -> False
from a subdir, the check still resolves the main worktree:
  True  all worktrees under .../.claude/worktrees or .../.Codex/worktrees

The upward walk matches git's own discovery on every case I could construct, with one gap and one residue, both recorded below as non-blocking.

The gitignore anchor

$ git check-ignore -q sub/.Codex/x ; echo $?
1
$ git check-ignore -q .Codex/x ; echo $?
0

Gates

$ python scripts/check_craftsmanship.py --base origin/main
Craftsmanship gate: OK
$ uvx [email protected] check .      -> All checks passed!
$ uvx [email protected] format --check . -> 3377 files already formatted
$ python -m pyright mcp_server/doctor.py -> 0 errors, 0 warnings, 0 informations
$ python scripts/check_doc_claims.py -> doc claims OK (1 declared not-a-claim exemption)
$ python scripts/check_project_wiki.py -> Project wiki mirrors match their canonical sources.
$ python scripts/craftsmanship_decisions.py -> exit 0

Longest consecutive comment block in the touched sources: 6 lines in tests_py/test_doctor_worktrees.py, 3 in mcp_server/doctor.py, under the 8-line decision threshold. ADR-1079 occupies one canonical file and one mirror, and sits between 1078 and 1080 in wiki/manifest.json without collision. The CHANGELOG entry is in [Unreleased].

Tests

$ pytest tests_py/test_doctor.py tests_py/test_doctor_worktrees.py tests_py/handlers/test_check_setup.py -q
40 passed in 5.86s

$ pytest <those three> tests_py/shared/test_subprocess_safe.py -q
51 passed in 13.86s

$ pytest -q -p no:randomly
8831 passed, 8 skipped, 2 warnings, 459 subtests passed in 674.85s (0:11:14)

Zero failures on the full suite at this head. The two tests_py/hooks/test_entry.py::test_entry_matches_launcher_on_benign_event failures the author reported do not reproduce here. I ran the controlled comparison rather than taking the "pre-existing, unrelated" reading on trust:

worktree at 28aea2af (PR head):        2 passed in 216.95s
worktree at 4bd7e7f0 (origin/main):   11 passed in 293.66s
primary checkout at 4bd7e7f0:          2 failed in 171.70s
    AssertionError: assert '## Cortex — ... as you go.\n' == ''
    + ## Cortex — First Run

Same interpreter in all three. The variable is the checkout, not the diff: the failure reproduces in the primary working copy at main and vanishes in a clean worktree at main. Blast radius checked directly, tests_py/hooks/test_entry.py and mcp_server/hooks/entry.py contain no reference to doctor. The deferral is therefore verified rather than asserted, but it wants an issue of its own: the parity test is sensitive to something in the primary checkout's local state, which is a latent false alarm for the next person who runs pytest there.

CI at this head

$ gh pr checks 602 --repo cdeust/Cortex --watch --fail-fast
Craftsmanship Gate  pass  16s
Lint                pass  18s
Type Check          pass  1m38s
Test (Python 3.10)  pass  5m43s
Test (Python 3.11)  pass  8m14s
Test (Python 3.12)  pass  8m32s
Test (Python 3.13)  pass  7m2s
Test (SQLite backend)          pass  6m44s
Test (Windows, SQLite backend) pass  5m51s
CI Green            pass  6s
[exited with code 0]

Every required job passes, the two Docker Build jobs and the scheduled fuzz batch skip as designed.

Non-blocking

A malformed .git entry still lands in the WARN branch with a repair instruction that cannot help. Measured in a directory holding an empty .git directory, git present on PATH:

$ git worktree list --porcelain -z
fatal: not a git repository (or any of the parent directories): .git
  [WARN] worktree locations (optional)    Unable to inspect worktree locations: Git unavailable or command failed.
     → Install Git and ensure it's on PATH; retry git worktree list --porcelain -z.

An empty .git file gives fatal: invalid gitfile format. The detail line's "or command failed" covers the case honestly and the check stays optional, so nothing is broken; it is the fix text that misleads, telling an owner who has git installed to install git. Conditioning that first clause on shutil.which("git") would keep the useful half of the message and drop the wrong half.

The probe reads the filesystem only, so it does not see GIT_DIR. A session started with GIT_DIR exported and no .git in the cwd's ancestry reports "not a git checkout" where git itself would have listed worktrees. That is a silent under-report on an advisory check, cheap to leave as is, worth knowing.

mcp_server/doctor.py goes from 394 to 548 lines against the project's 300-line cap. The gate is right to pass: .craftsmanship-baseline.json already carries a file-size entry for this file, and that entry records presence, not magnitude, so a file already over the cap can absorb any amount of new code invisibly. The four added functions form one cohesive unit and the tests are already split along exactly that seam as tests_py/test_doctor_worktrees.py; moving _worktree_list, _worktree_classification, _is_git_checkout, _under and _worktree_locations into mcp_server/doctor_worktrees.py would return doctor.py to its pre-PR size and cost nothing but an import. Worth a follow-up, not a reason to hold this merge.

Verdict

APPROVE at 28aea2afa76085d16cc6d7b01765dc69a4fe0814. The completion ledger's paths reconcile against the diff, every branch I enumerated maps to a named test, and the one deferral in the report is verified by measurement rather than by phrasing. File the parity-test issue against tests_py/hooks/test_entry.py, not against this branch.

@cdeust
cdeust merged commit 19b78df into main Sep 17, 2026
28 checks passed
@cdeust
cdeust deleted the fix/codex-ignore-worktree-doctor branch September 17, 2026 13:11
@cdeust cdeust mentioned this pull request Sep 17, 2026
cdeust added a commit that referenced this pull request Sep 17, 2026
Promotes the Unreleased section to 4.23.0 and moves the seventeen version
surfaces (pyproject, manifest, plugin and marketplace manifests, Codex and
Gemini manifests, package.json and lock, server.json, badge, uv.lock).
Since 4.22.0: session hooks inject only the session's project and global
memories (#604), one saved backend for Claude Code and Codex (#600),
prediction records and calibration (#597), the wiki draft tools over MCP
(#579), procedural skills on SQLite (#596) with a transcript-fed tool
sequence (#591), the console entry point and Codex event adapter for the
lifecycle hooks (#605, #608), the doctor worktree check (#602), and the
launcher fixes (#573, #575, #582). The marketplace pin for
zetetic-team-subagents moves to its released 2.42.0.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cz7A6goxVzfKL9NT7k4zDH
cdeust added a commit that referenced this pull request Sep 17, 2026
* release: 4.23.0

Promotes the Unreleased section to 4.23.0 and moves the seventeen version
surfaces (pyproject, manifest, plugin and marketplace manifests, Codex and
Gemini manifests, package.json and lock, server.json, badge, uv.lock).
Since 4.22.0: session hooks inject only the session's project and global
memories (#604), one saved backend for Claude Code and Codex (#600),
prediction records and calibration (#597), the wiki draft tools over MCP
(#579), procedural skills on SQLite (#596) with a transcript-fed tool
sequence (#591), the console entry point and Codex event adapter for the
lifecycle hooks (#605, #608), the doctor worktree check (#602), and the
launcher fixes (#573, #575, #582). The marketplace pin for
zetetic-team-subagents moves to its released 2.42.0.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cz7A6goxVzfKL9NT7k4zDH

* release: the zetetic-team-subagents pin fetches the 2.42.0 tree it advertises

The review of this cut found version 2.42.0 beside ref v2.41.0 and the
2.41.0 sha; installs would have checked out 77b4f785 under a 2.42.0 label.
ref and sha now name the v2.42.0 tag commit 96454db1, as the earlier
deliveries (#553, #554) moved all three fields together.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cz7A6goxVzfKL9NT7k4zDH

* docs(changelog): name the consolidation launcher that shipped

The ADR-1082 entry cited a consolidate_launch module the craftsmanship
gate refused; the shipped design relaunches session_lifecycle with
--consolidate <mode>, as ADR-1082 and the code say.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cz7A6goxVzfKL9NT7k4zDH

---------

Co-authored-by: Claude Fable 5.1 <[email protected]>
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.

1 participant