Skip to content

feat(memlink): OS-agnostic vault->memory link primitive (CLI-025) - #557

Merged
mlorentedev merged 4 commits into
mainfrom
feat/CLI-025-memlink-primitive
Jun 24, 2026
Merged

feat(memlink): OS-agnostic vault->memory link primitive (CLI-025)#557
mlorentedev merged 4 commits into
mainfrom
feat/CLI-025-memlink-primitive

Conversation

@mlorentedev

@mlorentedev mlorentedev commented Jun 24, 2026

Copy link
Copy Markdown
Owner

Summary

Adds cli/internal/memlink — one Go primitive that links an agent's per-project memory
directory to its source in the knowledge vault, converging two drifting OS twins into one
agnostic noun
: the POSIX ensure-memory-symlink.sh (symlink) and the Windows-only PowerShell
junction hook (the MEMORY-002 R4 gap the shell deferred). CLI-025 PR2b-1.

  • resolveVaultMemory — OS-agnostic source resolution via the three conventions
    (10_projects/<project>/memory, in-vault <cwd>/memory, work-SDK family/component slug match),
    faithfully reproducing the shell's break-on-first-match.
  • createLinkos.Symlink on POSIX, a directory junction on Windows (no privilege needed,
    confirmed end-to-end). Idempotent + best-effort: a failed link is a silent no-op so it can never
    crash a session start.
  • No new dependency — junction via cmd /c mklink /J keeps go.mod at stdlib + cobra.

Consumed next by the Claude session-start adapter (PR2b-2) and dotf doctor --fix (#551).

SDD checklist

  • Backlog entry exists — tracked as CLI-025 (CLI-025: dotf mem — heal + session-start/end, thin the hooks, delete 4+ scripts #494) on the bitácora Project (ADR-018 moved task state off 11-tasks.md)
  • Spec folder specs/CLI-025-dotf-mem-heal-and-session-start/ is included in this PR (feature f6 + tasks PR2b-1 split)
  • proposal.md has Why / What / Acceptance criteria
  • tasks.md is in TDD order
  • verification.md is filled at spec close (multi-PR spec); per-PR evidence is the green CI below

Test plan

  • go test ./internal/memlink/ green (9 subtests, incl. real junction creation on Windows and symlink on POSIX)
  • go build ./... · go vet · golangci-lint clean
  • Manual verification: mklink /J junction creation confirmed without admin (Go + CI test (windows-latest)); symlink read-through on POSIX (test (ubuntu-latest))
  • n/a bats / shellcheck — no .sh or .bats files changed

Refs #494. Unblocks #551.

Add cli/internal/memlink: one Go primitive that links an agent's per-project
memory directory to its source in the knowledge vault, converging the POSIX
ensure-memory-symlink.sh and the Windows-only PowerShell junction hook into a
single agent- and OS-agnostic noun.

- resolveVaultMemory: shared source resolution via the three conventions
  (10_projects, in-vault CWD, work-SDK family/component slug match), faithfully
  reproducing the shell's break-on-first-match.
- createLink branches symlink (POSIX) / directory junction (Windows). The
  junction needs no special privilege, closing the MEMORY-002 R4 gap the shell
  script deferred.
- Idempotent and best-effort: a failed link is a silent no-op so it can never
  crash a session start.

Consumed next by the Claude session-start adapter and by dotf doctor --fix (#551).

Refs #494.
@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a new cli/internal/memlink package that resolves a vault memory source and creates an OS-specific directory link from an agent target. It includes tests for resolution and idempotent link creation, plus spec updates for the new memlink task split.

Changes

memlink: vault→memory link primitive

Layer / File(s) Summary
Vault memory source resolution
cli/internal/memlink/memlink.go
Adds resolveVaultMemory, resolveWorkSDK, slugify, and isUnder for ordered vault-memory source lookup and path matching.
OS-specific link creation and filesystem helpers
cli/internal/memlink/memlink.go
Adds createLink, linkNoun, isDir, isLink, and dirNotEmpty for link creation and target-state checks.
Ensure orchestrator, tests, and spec updates
cli/internal/memlink/memlink.go, cli/internal/memlink/memlink_test.go, specs/CLI-025-dotf-mem-heal-and-session-start/*
Adds Ensure for source resolution, placeholder cleanup, and best-effort link creation, with tests for resolution and idempotency and spec entries that record the new feature and task split.

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant Ensure
    participant resolveVaultMemory
    participant createLink

    Caller->>Ensure: Ensure(cwd, target, project, vault)
    Ensure->>Ensure: default project from cwd if empty
    Ensure->>Ensure: check isLink(target) or dirNotEmpty(target)
    alt already linked or non-empty dir
        Ensure-->>Caller: "", nil
    else
        Ensure->>resolveVaultMemory: resolveVaultMemory(cwd, project, vault)
        resolveVaultMemory-->>Ensure: source path or ""
        alt source empty
            Ensure-->>Caller: "", nil
        else
            Ensure->>Ensure: MkdirAll parent, remove empty placeholder
            Ensure->>createLink: createLink(source, target)
            createLink-->>Ensure: error or nil
            alt link error
                Ensure-->>Caller: "", nil
            else
                Ensure-->>Caller: "[auto-memory] Created symlink/junction …", nil
            end
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • mlorentedev/dotfiles#404: Extracts the same vault-to-memory source resolution and idempotent link creation flow into scripts/ensure-memory-symlink.sh and claude-session-start.sh.
  • mlorentedev/dotfiles#536: Lays out the CLI-025 dotf mem strangler plan that this memlink primitive implements for session-start wiring.

Poem

🐇 I hopped through vaults and found the way,
A link to memory, night or day.
On Windows junctions, POSIX ties,
The rabbit nods and watches it arise.
No crashes, just a quiet little cheer,
A memory trail that now stays near.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: an OS-agnostic memlink primitive for vault-to-memory linking.
Docstring Coverage ✅ Passed Docstring coverage is 85.71% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description follows the required template and includes a clear summary, completed SDD checklist, and test plan details.

✏️ 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/CLI-025-memlink-primitive

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 (2)
cli/internal/memlink/memlink_test.go (1)

27-73: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Consider a table-driven form for TestResolveVaultMemory.

These subtests share the same shape (set up dirs, call resolveVaultMemory, assert the resolved path), which maps cleanly onto a []struct{name, cwd, project string; setup func; want string} driven by t.Run.

As per coding guidelines: "Use table-driven tests with t.Run in 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/memlink/memlink_test.go` around lines 27 - 73, Refactor the
TestResolveVaultMemory function from individual t.Run subtests to a table-driven
test format. Define a table as a slice of structs with fields for the test name,
cwd parameter, project parameter, a setup function to create the necessary vault
directory structure, and the expected want result. Then replace the five
separate t.Run blocks with a single loop that iterates through the table
entries, calling resolveVaultMemory with the corresponding test parameters and
asserting the result matches the want value. This maintains the same test logic
while improving readability and making it easier to add new test cases.

Source: Coding guidelines

cli/internal/memlink/memlink.go (1)

129-138: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add a timeout/context to the Windows mklink invocation.

exec.Command(...).Run() blocks with no deadline. In a session-start hook a hung cmd /c mklink would stall startup indefinitely — arguably worse than a crash. Prefer exec.CommandContext with a bounded timeout (threading a context.Context from Ensure).

Sketch
-	if runtime.GOOS == "windows" {
-		return exec.Command("cmd", "/c", "mklink", "/J", target, src).Run()
-	}
+	if runtime.GOOS == "windows" {
+		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+		defer cancel()
+		return exec.CommandContext(ctx, "cmd", "/c", "mklink", "/J", target, src).Run()
+	}

As per coding guidelines: "Propagate context.Context in all I/O operations in 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/memlink/memlink.go` around lines 129 - 138, The createLink
function uses exec.Command(...).Run() on Windows without a timeout or context,
which can block indefinitely during startup. Modify the createLink function
signature to accept a context.Context parameter, then replace the Windows
branch's exec.Command call with exec.CommandContext, passing the context as the
first argument to enable bounded timeout control. This ensures the mklink
operation respects the deadline set by the caller (from Ensure) and prevents
indefinite blocking.

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 49-58: The error handling for os.MkdirAll is inconsistent with the
documented "never crash" contract and the best-effort approach used for
createLink. Either swallow the os.MkdirAll error with a similar comment pattern
as createLink (line 56-58) to maintain the best-effort, never-crash behavior, or
if returning the error is intentional, wrap it with context information before
returning using a wrapping pattern that provides diagnostic details about the
failure.

---

Nitpick comments:
In `@cli/internal/memlink/memlink_test.go`:
- Around line 27-73: Refactor the TestResolveVaultMemory function from
individual t.Run subtests to a table-driven test format. Define a table as a
slice of structs with fields for the test name, cwd parameter, project
parameter, a setup function to create the necessary vault directory structure,
and the expected want result. Then replace the five separate t.Run blocks with a
single loop that iterates through the table entries, calling resolveVaultMemory
with the corresponding test parameters and asserting the result matches the want
value. This maintains the same test logic while improving readability and making
it easier to add new test cases.

In `@cli/internal/memlink/memlink.go`:
- Around line 129-138: The createLink function uses exec.Command(...).Run() on
Windows without a timeout or context, which can block indefinitely during
startup. Modify the createLink function signature to accept a context.Context
parameter, then replace the Windows branch's exec.Command call with
exec.CommandContext, passing the context as the first argument to enable bounded
timeout control. This ensures the mklink operation respects the deadline set by
the caller (from Ensure) and prevents indefinite blocking.
🪄 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: b63116af-fb30-4afa-a4ed-58f9175d6e62

📥 Commits

Reviewing files that changed from the base of the PR and between 6cfe039 and 69820c8.

📒 Files selected for processing (2)
  • cli/internal/memlink/memlink.go
  • cli/internal/memlink/memlink_test.go

Comment on lines +49 to +58
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
return "", err
}
// Remove an empty placeholder target dir so the link can be created.
if isDir(target) {
_ = os.Remove(target)
}
if err := createLink(src, target); err != nil {
return "", nil // best-effort: a failed link must never fail the session
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

MkdirAll error path breaks the documented "never crash" contract and is unwrapped.

The doc (Lines 24-30) states a session-start hook "must never crash" and that failures are silent no-ops, yet a failed os.MkdirAll propagates an error to the caller while a failed createLink is swallowed. This is inconsistent, and the returned error is also not wrapped with context.

If the intent is best-effort, swallow this too; otherwise wrap it.

Proposed change
-	if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
-		return "", err
-	}
+	if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
+		return "", nil // best-effort: never fail the session
+	}

As per coding guidelines: "Use if err != nil with context wrapping for error handling in Go".

📝 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
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
return "", err
}
// Remove an empty placeholder target dir so the link can be created.
if isDir(target) {
_ = os.Remove(target)
}
if err := createLink(src, target); err != nil {
return "", nil // best-effort: a failed link must never fail the session
}
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
return "", nil // best-effort: never fail the session
}
// Remove an empty placeholder target dir so the link can be created.
if isDir(target) {
_ = os.Remove(target)
}
if err := createLink(src, target); err != nil {
return "", nil // best-effort: a failed link must never fail the session
}
🤖 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 49 - 58, The error handling for
os.MkdirAll is inconsistent with the documented "never crash" contract and the
best-effort approach used for createLink. Either swallow the os.MkdirAll error
with a similar comment pattern as createLink (line 56-58) to maintain the
best-effort, never-crash behavior, or if returning the error is intentional,
wrap it with context information before returning using a wrapping pattern that
provides diagnostic details about the failure.

Source: Coding guidelines

@mlorentedev mlorentedev self-assigned this Jun 24, 2026
Touch the CLI-025 spec for the memlink primitive PR: add feature f6 (the
OS-agnostic vault->memory link) and split the tasks PR2b section into PR2b-1
(memlink, this PR) and PR2b-2 (the Claude adapter). Satisfies the SDD
Discipline Gate, which requires a >=50 LOC PR to touch its spec folder.
@mlorentedev
mlorentedev merged commit edec57c into main Jun 24, 2026
15 checks passed
@mlorentedev
mlorentedev deleted the feat/CLI-025-memlink-primitive branch June 24, 2026 05:09
mlorentedev added a commit that referenced this pull request Jun 24, 2026
…e spec (#571)

The session-start convergence (PR2a → PR3 cutover, #554/#557/#566/#569/#570) is
complete on main; the session hook cluster is one Go noun with zero shell twins.

- docs/lessons.md: two lessons from the migration — (1) a Go-vs-shell
  byte-equivalence gate is POSIX-only (jq CRLF + MSYS /tmp) and retires at cutover;
  (2) CI golangci-lint enforces staticcheck QF* quickfixes a stale local version skips.
- Move specs/CLI-025-dotf-mem-heal-and-session-start/ -> specs/archive/ and set its
  proposal status to archived.

Refs #494.
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