Skip to content

Make bundled helpers self-anchor repo-relative paths at the git root #661

Description

@williamthorsen

Problem

Several agents helpers read or write repo-relative paths (notably .agents/) using the ambient process.cwd() as their base. Agents routinely run from a subdirectory of the repo rather than the directory where the session started, so the base is wrong whenever that happens. The consequences are silent and corrosive: manifests get written to {subdir}/.agents/ but read from {repo_root}/.agents/, so every call re-derives (a cache-miss loop), stale files accumulate in unintended directories, and preference lookups silently miss.

The #653 review caught one instance of this in resolve-frontmatter.sh and fixed that single site. The underlying defect is a class, not a site: any helper that anchors a repo-relative path at the ambient working directory carries the same bug, and any new helper or invocation reintroduces it.

Context

This session audited every bundled-helper invocation site under packages/agents/content/. The findings:

  • derive-session-context (writes .agents/{branch}.branch-manifest.json) defaults its base to process.cwd(). It is correct when invoked through resolve-frontmatter.sh (which passes an explicit base) but vulnerable in the roughly twenty skill-markdown invocations that call the bundled .mjs directly without one.
  • describe-change.sh and get-ticket-id.sh read .agents/preferences.yaml as a bare relative path, so a subdirectory invocation finds the wrong file or none.
  • The KB helpers (kb-add, kb-retrieve, kb-edit, kb-curate) start their .kb/ discovery and registry selection at process.cwd(). This is a different anchor concept (see Out of scope).

Three directory concepts are at play, and the bug comes from conflating them: the invocation directory (where the session started, stable but unrecoverable once the agent moves), the repo root (git rev-parse --show-toplevel, derived from the current directory and worktree-aware), and the ambient working directory (process.cwd(), which agents mutate). For anything anchored at the repo root, git rev-parse --show-toplevel is the correct, platform-agnostic base: it resolves correctly from any subdirectory of a worktree and requires no harness cooperation. Recovering the true invocation directory would need per-platform hooks and is deferred.

Proposed solution

Make the helpers self-anchoring. Move the base-directory decision out of the callers and into the helpers themselves, so a caller cannot supply the wrong base by omission.

For the TypeScript helpers, introduce one shared resolution utility that encapsulates the precedence: an explicit override wins, otherwise the git repo root, otherwise the ambient directory as a last resort with a stderr diagnostic. derive-session-context adopts it in place of its process.cwd() default; the explicit-override path preserves the behavior that resolve-frontmatter.sh and the test suite already rely on.

For the bash scripts, anchor the .agents/ reads at the git repo root inline. Their entire resolver is a single git command, so a shared file would cost more than it saves; resolve-frontmatter.sh already follows this inline pattern and is left unchanged.

Skill-markdown invocations stay as they are. Self-anchoring is what makes that safe: the roughly twenty direct .mjs calls become correct without any change to the markdown.

Document the working-directory contract on the derive-session-context CLI so a future maintainer can see which inputs are caller-supplied and which are resolved, and against what base.

Out of scope

Tracked in follow-up #699, because their correct anchor is the invocation directory rather than the repo root:

  • The invocation-directory capture layer: a session-start hook (per platform) that records the starting directory into a breadcrumb the resolver can prefer above the git root, closing the residual gap where an agent leaves the worktree entirely.
  • The KB helpers. Their .kb/ discovery walks upward, so it already survives subdirectory wandering; forcing the git root could skip a legitimate nested .kb/ and shift which registry is selected. Their real fix is the captured invocation directory, so they move with the capture layer.

Acceptance criteria

Must have

  • Helpers and scripts that read or write .agents/ resolve their base at the git repo root, independent of the caller's working directory.
  • A single shared utility encapsulates the resolution precedence (explicit override, then git root, then ambient directory) for the TypeScript helpers.
  • derive-session-context, describe-change.sh, and get-ticket-id.sh produce correct results when invoked from any subdirectory of the repo.
  • The working-directory contract is documented on the derive-session-context CLI: which inputs are caller-supplied, which are resolved, and against what base.
  • The site fixed in Replace get-session-context skill with a bundled TS deriver #653 (resolve-frontmatter.sh) continues to behave correctly.
  • New and modified behavior in this change is covered by tests, including at least one invocation from a subdirectory.

Should have

  • The last-resort fallback (not inside a git repository) emits a stderr diagnostic so a misanchored run is debuggable rather than silent.
  • Skill-markdown invocations of the bundled helpers remain unchanged.

Nice to have

  • The resolver's precedence is structured so a future captured-invocation-directory tier can be inserted above the git root without changing helper logic.

Metadata

Metadata

Labels

refactoringImprovement to code without change in functionalityscope:agents

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions