Skip to content

doctor: repair auto-memory junction + OS-aware env-contract checks (HARNESS-040) - #576

Merged
mlorentedev merged 1 commit into
mainfrom
feat/doctor-fix-drift-repair
Jun 25, 2026
Merged

doctor: repair auto-memory junction + OS-aware env-contract checks (HARNESS-040)#576
mlorentedev merged 1 commit into
mainfrom
feat/doctor-fix-drift-repair

Conversation

@mlorentedev

@mlorentedev mlorentedev commented Jun 25, 2026

Copy link
Copy Markdown
Owner

Wires dotf doctor to detect and repair two classes of per-machine setup drift the no-CI model relies on it to catch. Scoped to parts 1 + 2 of #551; the cross-language Hive-venv repair and a memlink robustness gap are split out (see Deferred).

What changed

Part 1 — auto-memory ↔ vault junction. New checkAutoMemoryLink: verifies the Claude auto-memory dir is a link into the knowledge vault (so /handoff lands the continuity block in the single sink, not in .claude). It consumes the merged memlink primitive, so doctor and the session-start adapter compute an identical target on every OS.

  • Verify always, repair only under --fix, idempotent — mirrors checkVaultHooks.
  • A real, non-empty auto-memory dir (diverged data, the knowledge#120 case) is a WARN to reconcile by hand, never a destructive --fixmemlink refuses to overwrite real data by contract.

Two latent Windows bugs fixed in memlink (the root cause of the never-created junction here):

  • Claude's per-project key maps every separator and the drive colon to - (C:\Users\me\projC--Users-me-proj); the ported encoder mapped only /. Now a single cross-OS encoding (ClaudeProjectKey/ClaudeMemoryTarget) shared by both callers; the local mem.encodeProjectPath is deleted.
  • A mklink /J junction surfaces via os.Lstat as ModeIrregular, not ModeSymlink, on Go 1.26 (verified empirically) — isLink widened accordingly.

Part 2 — OS-aware env-contract checks. checkContractEnvVars/checkContractPath selected the linux dialect unconditionally, so on Windows they checked Linux PATH entries/defaults and emitted false-positive drift every session. They now pick the dialect from System.GOOS; expandHome also resolves $env:USERPROFILE. Linux output is unchanged.

Test plan

  • go test ./internal/doctor/ ./internal/mem/ ./internal/memlink/ → green (run on a Windows host, so the Windows branches are exercised natively).
  • New tests: TestCheckAutoMemoryLink (FAIL/SKIP/WARN/--fix+idempotent), TestContractOS, TestCheckContractPath_Dialects, TestCheckContractEnvVars_WindowsDialect, TestClaudeProjectKey/TestClaudeMemoryTarget/TestStatus.
  • Live smoke on this machine: the false PATH drift is gone ((2 checks, all ok)), and dotf doctor WARNs on the real diverged auto-memory dir without touching its files.
  • The 3 TestEmbeddedTemplatesMatchVault failures in the module are pre-existing (tracked by Re-vendor vault templates into the CLI (TestEmbeddedTemplatesMatchVault drift) #461), confirmed by re-running them with this branch stashed.

Deferred (ticketed)

Spec

specs/HARNESS-040-doctor-fix-drift-repair/ (proposal + tasks + verification + features.json).

Closes #551

Summary by CodeRabbit

  • New Features

    • Added a doctor check for Claude auto-memory links, with clear PASS/FAIL/SKIP/WARN results and optional repair when safe.
    • Improved cross-platform handling so environment and path checks behave correctly on Windows, macOS, and Linux.
    • Updated shared memory path handling to better support vault-backed project memory locations.
  • Bug Fixes

    • Prevented false Windows path warnings during doctor checks.
    • Improved detection of linked memory locations so junctions are recognized correctly.

…cks (HARNESS-040)

Wire `dotf doctor` to detect and repair the per-machine setup drift the no-CI
model relies on it to catch.

- Add `checkAutoMemoryLink`: verify the Claude auto-memory dir is a vault link so
  /handoff lands in the single sink, FAIL/--fix-repair when missing, SKIP when no
  vault source, and WARN (never destroy) when it is a real diverged dir.
- memlink: add read-only `Status`; share the cross-OS Claude project-key encoding
  (`ClaudeProjectKey`/`ClaudeMemoryTarget` map / \ : -> -, fixing the wrong target
  on Windows) and delete the local `encodeProjectPath`; widen `isLink` to detect
  junctions (`ModeIrregular`, not `ModeSymlink`, on Go 1.26).
- Select the env-contract OS dialect from `GOOS` instead of a hardcoded "linux",
  ending the false-positive PATH drift the session-start banner showed on Windows;
  `expandHome` now resolves `$env:USERPROFILE` for the windows dialect.

Defers the Hive venv repair (#574) and createLink cmd-quoting robustness (#575).

Closes #551
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds auto-memory link repair to doctor, centralizes Claude memory path helpers in memlink, makes contract checks OS-aware, and updates the related tests, docs, and harness specs.

Changes

Doctor repair flow

Layer / File(s) Summary
Shared Claude memory helpers
cli/internal/memlink/*, cli/internal/mem/session_start_*, docs/lessons.md
Adds shared Claude project-key and memory-target helpers, updates session-start code to use them, broadens Windows link detection, and records the related Windows notes.
Auto-memory doctor check
cli/internal/doctor/checks_automemory.go, cli/internal/doctor/checks_automemory_test.go, cli/internal/doctor/doctor.go
Adds checkAutoMemoryLink to the doctor sweep and covers skip, fail, fix, idempotency, and the non-destructive real-directory case.
OS-aware contract sweep
cli/internal/doctor/contract.go, cli/internal/doctor/checks_contract.go, cli/internal/doctor/checks_contract_os_test.go, cli/internal/doctor/fs.go
Threads a contract OS dialect through env-var and PATH checks, expands home tokens for PowerShell paths, and verifies Linux/Windows dialect behavior.
Harness specs and verification
specs/HARNESS-040-doctor-fix-drift-repair/*
Adds the harness feature matrix, proposal, task list, and verification notes for the doctor repair work.

Sequence Diagram(s)

sequenceDiagram
  participant "doctor.Run" as doctorRun
  participant "checkAutoMemoryLink" as checkAutoMemoryLink
  participant "memlink.Status" as memlinkStatus
  participant "memlink.Ensure" as memlinkEnsure
  doctorRun->>checkAutoMemoryLink: run in the non-quick sweep
  checkAutoMemoryLink->>memlinkStatus: classify the vault target
  memlinkStatus-->>checkAutoMemoryLink: StateLinked / StateNoSource / StateRealDir / StateRepairable
  alt StateRepairable and --fix
    checkAutoMemoryLink->>memlinkEnsure: create the link
    memlinkEnsure-->>checkAutoMemoryLink: success or failure
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • mlorentedev/dotfiles#557 - Introduces the shared memlink linkage primitive that this PR extends with doctor verification and session-start consumers.
  • mlorentedev/dotfiles#553 - Also wires an additional doctor health check into doctor.Run, so it changes the same orchestration path.
  • mlorentedev/dotfiles#458 - Adjusts dotf doctor for Windows/GOOS-specific behavior, overlapping with this PR’s OS-dialect contract handling.

Poem

I hop to the vault where the memory gleams,
With links and checks and tidy little seams.
🐇 The doctor said, “All paths align!”
So I nibble a carrot and call it divine.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR covers the junction repair and dialect selection, but it does not implement the env-contract repair and Hive venv work required by #551. Add the missing path-seam and Hive venv repair work, or narrow the linked issue scope to the parts this PR actually completes.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately reflects the auto-memory and OS-aware env-contract changes.
Description check ✅ Passed The description covers the main changes, testing, deferred scope, and spec references, though the template formatting is not exact.
Out of Scope Changes check ✅ Passed The added tests, specs, and docs support the doctor/memlink changes, and no unrelated code changes are apparent.
Docstring Coverage ✅ Passed Docstring coverage is 92.86% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/doctor-fix-drift-repair

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
cli/internal/doctor/checks_contract_os_test.go (1)

12-23: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Wrap the dialect cases in t.Run subtests.

TestContractOS iterates a map but doesn't use t.Run, so a failure reports inside one combined test and map iteration order is non-deterministic. Table-driven t.Run subtests give per-case isolation and naming.

♻️ Table-driven with t.Run
-	for goos, want := range map[string]string{
-		"windows": "windows",
-		"linux":   "linux",
-		"darwin":  "linux",
-		"":        "linux",
-	} {
-		if got := contractOS(&System{GOOS: goos}); got != want {
-			t.Errorf("contractOS(GOOS=%q) = %q, want %q", goos, got, want)
-		}
-	}
+	for goos, want := range map[string]string{
+		"windows": "windows",
+		"linux":   "linux",
+		"darwin":  "linux",
+		"":        "linux",
+	} {
+		goos, want := goos, want
+		t.Run(goos, func(t *testing.T) {
+			if got := contractOS(&System{GOOS: goos}); got != want {
+				t.Errorf("contractOS(GOOS=%q) = %q, want %q", goos, got, want)
+			}
+		})
+	}

As per coding guidelines: "Use table-driven tests with t.Run in Go" for **/*_test.go.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/internal/doctor/checks_contract_os_test.go` around lines 12 - 23,
`TestContractOS` currently loops over a map without per-case subtests, so update
it to use table-driven `t.Run` cases for each GOOS value. Keep the existing
assertions in `contractOS` but move each case into a named subtest so failures
are isolated and readable, and use stable case definitions instead of relying on
map iteration order.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cli/internal/memlink/memlink.go`:
- Around line 99-107: ClaudeProjectKey currently normalizes only slashes,
backslashes, and colons, but Claude Code also replaces dots with hyphens, so
update the replacement logic in ClaudeProjectKey to include "." as a mapped
separator as well. Keep the change localized to the strings.NewReplacer call in
memlink.go so paths like .config or v1.2 resolve to the same per-project key
Claude uses.

---

Nitpick comments:
In `@cli/internal/doctor/checks_contract_os_test.go`:
- Around line 12-23: `TestContractOS` currently loops over a map without
per-case subtests, so update it to use table-driven `t.Run` cases for each GOOS
value. Keep the existing assertions in `contractOS` but move each case into a
named subtest so failures are isolated and readable, and use stable case
definitions instead of relying on map iteration order.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c93e2061-1682-4597-96cf-1a0e4f0c3eaf

📥 Commits

Reviewing files that changed from the base of the PR and between 0f9699c and e72e539.

📒 Files selected for processing (18)
  • cli/internal/doctor/checks_automemory.go
  • cli/internal/doctor/checks_automemory_test.go
  • cli/internal/doctor/checks_contract.go
  • cli/internal/doctor/checks_contract_os_test.go
  • cli/internal/doctor/contract.go
  • cli/internal/doctor/doctor.go
  • cli/internal/doctor/fs.go
  • cli/internal/mem/session_start_adapter.go
  • cli/internal/mem/session_start_detect.go
  • cli/internal/mem/session_start_injectors.go
  • cli/internal/mem/session_start_injectors_test.go
  • cli/internal/memlink/memlink.go
  • cli/internal/memlink/memlink_test.go
  • docs/lessons.md
  • specs/HARNESS-040-doctor-fix-drift-repair/features.json
  • specs/HARNESS-040-doctor-fix-drift-repair/proposal.md
  • specs/HARNESS-040-doctor-fix-drift-repair/tasks.md
  • specs/HARNESS-040-doctor-fix-drift-repair/verification.md
💤 Files with no reviewable changes (2)
  • cli/internal/mem/session_start_injectors_test.go
  • cli/internal/mem/session_start_injectors.go

Comment on lines +99 to +107
// ClaudeProjectKey encodes a working directory into Claude Code's per-project key
// — the directory name under ~/.claude/projects. Claude maps every path separator
// AND the Windows drive colon to '-', so `/home/me/proj` and `C:\Users\me\proj`
// become `-home-me-proj` and `C--Users-me-proj`. The retired shell twin mapped
// only '/', silently producing the wrong key — and thus the wrong junction target
// — on Windows (the root cause of the unlinked auto-memory dir, #551).
func ClaudeProjectKey(cwd string) string {
return strings.NewReplacer("/", "-", `\`, "-", ":", "-").Replace(cwd)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Does Claude Code encode ~/.claude/projects directory keys by replacing dots (.) with hyphens in addition to path separators?

💡 Result:

Yes, Claude Code encodes project directory paths stored in ~/.claude/projects/ by replacing dots (.) with hyphens (-), in addition to replacing path separators (such as / and ), underscores, and other special characters [1][2][3][4]. This encoding behavior is part of how Claude Code generates unique storage keys for projects [5][6]. Because the encoding process is lossy—meaning it collapses different characters (like dots, slashes, and dashes) into the same hyphen character—it can create directory name collisions where distinct project paths map to the same folder in ~/.claude/projects/ [1][7][5][6]. Users have reported that this behavior can lead to cross-project session contamination, where different projects inadvertently share memory, conventions, and session history [5][6].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant file and nearby tests/usages.
git ls-files 'cli/internal/memlink/*' 'cli/**/memlink*' | sed 's#^`#FILE` #'
printf '\n---\n'
ast-grep outline cli/internal/memlink/memlink.go --view expanded || true
printf '\n---\n'
sed -n '1,180p' cli/internal/memlink/memlink.go
printf '\n---\n'
rg -n "ClaudeProjectKey|projects/|\\.claude" cli -S

Repository: mlorentedev/dotfiles

Length of output: 14036


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant file and nearby tests/usages.
git ls-files 'cli/internal/memlink/*' 'cli/**/memlink*' | sed 's#^`#FILE` #'
printf '\n---\n'
ast-grep outline cli/internal/memlink/memlink.go --view expanded || true
printf '\n---\n'
sed -n '1,180p' cli/internal/memlink/memlink.go
printf '\n---\n'
rg -n "ClaudeProjectKey|projects/|\\.claude" cli -S

Repository: mlorentedev/dotfiles

Length of output: 14036


Add . to ClaudeProjectKey
Claude Code also turns dots into hyphens, so paths like .config or v1.2 still map to the wrong ~/.claude/projects/<key> directory unless this replacer includes . too.

🛠️ Possible fix
-	return strings.NewReplacer("/", "-", `\`, "-", ":", "-").Replace(cwd)
+	return strings.NewReplacer("/", "-", `\`, "-", ":", "-", ".", "-").Replace(cwd)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// ClaudeProjectKey encodes a working directory into Claude Code's per-project key
// — the directory name under ~/.claude/projects. Claude maps every path separator
// AND the Windows drive colon to '-', so `/home/me/proj` and `C:\Users\me\proj`
// become `-home-me-proj` and `C--Users-me-proj`. The retired shell twin mapped
// only '/', silently producing the wrong key — and thus the wrong junction target
// — on Windows (the root cause of the unlinked auto-memory dir, #551).
func ClaudeProjectKey(cwd string) string {
return strings.NewReplacer("/", "-", `\`, "-", ":", "-").Replace(cwd)
}
// ClaudeProjectKey encodes a working directory into Claude Code's per-project key
// — the directory name under ~/.claude/projects. Claude maps every path separator
// AND the Windows drive colon to '-', so `/home/me/proj` and `C:\Users\me\proj`
// become `-home-me-proj` and `C--Users-me-proj`. The retired shell twin mapped
// only '/', silently producing the wrong key — and thus the wrong junction target
// — on Windows (the root cause of the unlinked auto-memory dir, `#551`).
func ClaudeProjectKey(cwd string) string {
return strings.NewReplacer("/", "-", `\`, "-", ":", "-", ".", "-").Replace(cwd)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/internal/memlink/memlink.go` around lines 99 - 107, ClaudeProjectKey
currently normalizes only slashes, backslashes, and colons, but Claude Code also
replaces dots with hyphens, so update the replacement logic in ClaudeProjectKey
to include "." as a mapped separator as well. Keep the change localized to the
strings.NewReplacer call in memlink.go so paths like .config or v1.2 resolve to
the same per-project key Claude uses.

@mlorentedev
mlorentedev merged commit 6d2627c into main Jun 25, 2026
14 of 15 checks passed
@mlorentedev
mlorentedev deleted the feat/doctor-fix-drift-repair branch June 25, 2026 02:40
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.

HARNESS-040: doctor --fix repairs the auto-memory/vault junction + env-contract drift

1 participant