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
Should have
Nice to have
Problem
Several agents helpers read or write repo-relative paths (notably
.agents/) using the ambientprocess.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.shand 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 toprocess.cwd(). It is correct when invoked throughresolve-frontmatter.sh(which passes an explicit base) but vulnerable in the roughly twenty skill-markdown invocations that call the bundled.mjsdirectly without one.describe-change.shandget-ticket-id.shread.agents/preferences.yamlas a bare relative path, so a subdirectory invocation finds the wrong file or none.kb-add,kb-retrieve,kb-edit,kb-curate) start their.kb/discovery and registry selection atprocess.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-toplevelis 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-contextadopts it in place of itsprocess.cwd()default; the explicit-override path preserves the behavior thatresolve-frontmatter.shand 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.shalready 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
.mjscalls become correct without any change to the markdown.Document the working-directory contract on the
derive-session-contextCLI 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:
.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
.agents/resolve their base at the git repo root, independent of the caller's working directory.derive-session-context,describe-change.sh, andget-ticket-id.shproduce correct results when invoked from any subdirectory of the repo.derive-session-contextCLI: which inputs are caller-supplied, which are resolved, and against what base.get-session-contextskill with a bundled TS deriver #653 (resolve-frontmatter.sh) continues to behave correctly.Should have
Nice to have