Skip to content

feat: add per-repo session strategy#32

Open
sasha-id wants to merge 1 commit into
plastic-labs:mainfrom
sasha-id:feat/per-repo-strategy
Open

feat: add per-repo session strategy#32
sasha-id wants to merge 1 commit into
plastic-labs:mainfrom
sasha-id:feat/per-repo-strategy

Conversation

@sasha-id
Copy link
Copy Markdown

@sasha-id sasha-id commented Apr 25, 2026

Summary

Adds a per-repo session strategy that derives the session name from basename(git rev-parse --show-toplevel) instead of basename(cwd).

Why

Multi-tool setups where claude-honcho and a server-side memory writer (e.g., a Python or other-language Honcho client) target the same workspace need a session-naming scheme that survives cwd variation within a single repo. Current strategies all derive from cwd or branch:

  • per-directory resolves differently from repo/ vs repo/src/.
  • git-branch fragments memory across branches.
  • chat-instance doesn't persist across restarts.

This per-repo strategy mirrors the existing per-repo logic in Hermes (NousResearch/hermes-agent's Python Honcho client at plugins/memory/honcho/client.py:509-523, 578-582). Two clients pointing at the same workspace can now share a session name without per-cwd config translation.

Implementation

Two source files:

plugins/honcho/src/config.ts — four small edits:

  1. SessionStrategy type extended with "per-repo".
  2. JSDoc on HonchoCLAUDEConfig.sessionStrategy updated to list the new value.
  3. Manual override gate (getSessionForPath) extended to fire for both per-directory and per-repo (matches Hermes behavior — client.py:548-551).
  4. New case "per-repo" in the strategy switch:
    • Bun.spawnSync(["git", "rev-parse", "--show-toplevel"], { cwd, timeout: 5000, ... })
    • On exit code 0 with non-empty stdout: basename(root) then sanitizeForSessionName.
    • Otherwise (non-git, timeout, non-zero exit, missing stdout, thrown error): falls back to sanitizeForSessionName(basename(cwd)).
    • Peer prefix honored when configured.

plugins/honcho/src/mcp/server.ts — one cosmetic edit:

  1. Register "per-repo": "per repo" in the strategyLabels map so /honcho:status and the get_config MCP tool render mapping per repo rather than the raw hyphenated strategy name.

No new imports. Bun.spawnSync is the native runtime API.

README.md Session Strategies table gains a per-repo row. CHANGELOG.md updated under a new ## [Unreleased] block per CONTRIBUTING.md.

Note on session caching: src/hooks/session-start.ts:121 only auto-caches resolved session names for per-directory (and unset) strategies. With per-repo, the resolved name is recomputed on every session start from git rev-parse --show-toplevel — stable across cwd movement within the repo, no cache needed. This matches the Python reference (Hermes also recomputes per session). Intentionally not extending the cache gate to per-repo.

Behavior parity matrix

For cwd ~/projects/<group>/<project>/ (project root, git toplevel coincides), with peerName=alice, sessionPeerPrefix=false:

Strategy Before this PR After this PR Hermes-agent
per-directory <project> <project> (unchanged) <project>
per-repo (unsupported) <project> <project>

For cwd ~/projects/<group>/<project>/repo/ (a subdir that is also its own git repo):

Strategy Before this PR After this PR Hermes-agent
per-directory repo repo (unchanged) repo
per-repo (unsupported) repo repo

Strict parity confirmed.

Known divergences from Hermes (intentional, called out for reviewers)

  • Sanitization: Hermes returns raw basename (Path(...).name); this patch applies the existing sanitizeForSessionName (toLowerCase + [^a-z0-9-_]-). For ASCII-lowercase repo names the outputs are identical; mixed-case or unicode repo names will diverge. Closing the gap requires either dropping sanitization here (breaks URL/header safety) or adding it in Hermes. Out of scope for this PR.
  • Worktrees: git rev-parse --show-toplevel from inside a worktree returns the worktree path, not the main repo path. Two worktrees of the same repo resolve to different session names. Hermes has identical behavior.
  • Override map keying: Keyed by literal cwd (not by resolved repo root). Override fires only when launching from the exact configured cwd. Hermes has identical behavior (client.py:548-551). A future "key by repo root" improvement is a separate, paired change in both codebases.

Tests

Repo doesn't ship a test suite. Verification was by inspection of ~/.honcho/cache.json after switching the strategy — sessions map shows the resolved name matches the parity matrix above.

Checklist

  • Type-checks with bunx tsc --noEmit -p plugins/honcho/tsconfig.json
  • No new imports
  • README Session Strategies table updated
  • CHANGELOG.md [Unreleased] block added per CONTRIBUTING.md
  • Behavior parity verified against the reference implementation
  • No breaking changes to existing strategies

Summary by CodeRabbit

  • New Features
    • Added a new per-repo session strategy that derives session names from the git repository root, with fallback to the current working directory if git resolution fails.
    • Manual session overrides continue to take precedence with the new strategy.
    • Enhanced display labeling to show "per repo" for improved readability of session strategy configuration.

Adds a per-repo session strategy that derives the session name from
`basename(git rev-parse --show-toplevel)` instead of `basename(cwd)`.
Stable across cwd movement within a repo. Useful for multi-tool setups
where a separate client (e.g., a server-side memory writer) needs to
resolve the same session name from a different cwd within the repo.

Implementation matches the per-repo logic shipped in
NousResearch/hermes-agent's Honcho client (Python) bit-for-bit:
- basename of git toplevel, falling back to cwd basename on any failure
- 5s git timeout
- manual override map honored for both per-directory and per-repo
- sanitization, sessionPeerPrefix, and override-keying behavior
  unchanged

Uses Bun.spawnSync (no Node child_process import) for runtime idiom.
Guards against missing stdout on exit-zero and falls through to cwd
basename on any error.

Plus a small UX fix in `mcp/server.ts`: register `per-repo` in the
status-card label map so `/honcho:status` renders `mapping per repo`
instead of the raw hyphenated strategy name.

README updated with new strategy row.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 25, 2026

Walkthrough

A new per-repo session strategy is introduced that derives session names from git repository root directories, with fallback to current working directory. Documentation is updated to describe the strategy, and UI labeling is added to display "per repo" instead of the raw identifier.

Changes

Cohort / File(s) Summary
Documentation & Changelog
CHANGELOG.md, README.md
Added documentation for the new per-repo session strategy and its behavior including git root resolution and fallback handling.
Core Strategy Implementation
plugins/honcho/src/config.ts
Implemented per-repo strategy by adding it to the SessionStrategy type, updating getSessionName to respect manual overrides for this strategy, and implementing git-based session naming with 5-second timeout and fallback to working directory basename. Includes path sanitization for the session component.
UI Display Mapping
plugins/honcho/src/mcp/server.ts
Added strategyLabels entry to display "per repo" as the user-visible label for the per-repo strategy in MCP status output.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • VVoruganti

Poem

🐰 A repo-rooted session blooms so bright,
Git's toplevel guides us to the right!
When git grows quiet, we're calm and steady,
Fallback paths keep sessions ready—
Per-repo naming shines its light! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add per-repo session strategy' directly and concisely describes the main change in the PR: introduction of a new 'per-repo' session strategy. It matches the primary implementation across config.ts, mcp/server.ts, and documentation files.
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.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
README.md (1)

216-216: ⚠️ Potential issue | 🟡 Minor

Stale union in the config example: include per-repo.

The strategies table further down lists per-repo, but the inline comment on the JSONC example here still enumerates only the three legacy values. Users copy-pasting this example as a reference for valid values will think per-repo is unsupported.

📝 Proposed fix
-  "sessionStrategy": "per-directory", // "per-directory" | "git-branch" | "chat-instance"
+  "sessionStrategy": "per-directory", // "per-directory" | "per-repo" | "git-branch" | "chat-instance"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 216, Update the inline comment for the "sessionStrategy"
example to include the new "per-repo" option so the JSONC sample matches the
strategies table; locate the "sessionStrategy" line in the README example and
append "per-repo" to the comment enum (e.g., "// \"per-directory\" |
\"git-branch\" | \"chat-instance\" | \"per-repo\"") so copy-paste users see the
current valid values.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@plugins/honcho/src/config.ts`:
- Around line 571-600: The code uses Bun.spawnSync(..., { timeout }) in the
"per-repo" case, so add a minimum Bun engine requirement to package.json to
guarantee the timeout option exists (set "engines": { "bun":
"<minimum-tested-version-or-the-sep-2024-release-that-introduced-timeout>" } or
at least ">= 1.0.0" if you prefer conservative); also optionally deduplicate the
cwd-basename fallback inside the same "per-repo" case by extracting const
fallback = sanitizeForSessionName(basename(cwd)) and using it in the three
branches (references: Bun.spawnSync, sanitizeForSessionName, basename, the
"per-repo" case).

---

Outside diff comments:
In `@README.md`:
- Line 216: Update the inline comment for the "sessionStrategy" example to
include the new "per-repo" option so the JSONC sample matches the strategies
table; locate the "sessionStrategy" line in the README example and append
"per-repo" to the comment enum (e.g., "// \"per-directory\" | \"git-branch\" |
\"chat-instance\" | \"per-repo\"") so copy-paste users see the current valid
values.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e33fbcb6-a3a7-4b07-bccc-42efde396494

📥 Commits

Reviewing files that changed from the base of the PR and between 0813ebd and a3950a5.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • README.md
  • plugins/honcho/src/config.ts
  • plugins/honcho/src/mcp/server.ts

Comment on lines +571 to +600
case "per-repo": {
let repoBase: string;
try {
// git rev-parse --show-toplevel with 5s timeout. Falls back to cwd
// basename on any failure (non-git, timeout, non-zero exit, empty
// stdout, thrown error). Mirrors hermes-agent's per-repo derivation.
const result = Bun.spawnSync(["git", "rev-parse", "--show-toplevel"], {
cwd,
stdout: "pipe",
stderr: "ignore",
stdin: "ignore",
timeout: 5000,
});
// SyncSubprocess.stdout is `Buffer | undefined`; guard against the
// exit-0-but-empty-stdout case so we never produce an empty session.
if (result.exitCode === 0 && result.stdout) {
const root = result.stdout.toString().trim();
if (root) {
repoBase = sanitizeForSessionName(basename(root));
} else {
repoBase = sanitizeForSessionName(basename(cwd));
}
} else {
repoBase = sanitizeForSessionName(basename(cwd));
}
} catch {
repoBase = sanitizeForSessionName(basename(cwd));
}
return usePrefix ? `${peerPart}-${repoBase}` : repoBase;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find any declared Bun version floor (engines/bun, packageManager, README, CI).
fd -t f -e json -e md -e yml -e yaml --exec rg -nH -C2 -i 'bun.*[0-9]+\.[0-9]+|engines|packageManager' {} \;

Repository: plastic-labs/claude-honcho

Length of output: 36709


🏁 Script executed:

cat plugins/honcho/package.json

Repository: plastic-labs/claude-honcho

Length of output: 582


🌐 Web query:

Bun spawnSync timeout option version added changelog

💡 Result:

The timeout option for Bun.spawnSync (and Bun.spawn) was added in late September 2024. The feature request was opened in January 2023 (Issue #1788) and closed as completed on September 26, 2024, when the implementing pull request #14180 (for AbortSignal support, which was part of the same feature) was merged. Official Bun documentation now includes the timeout?: number option in SpawnSyncOptions, specifying it kills the process with killSignal (default SIGTERM) after the specified milliseconds. If timeout occurs, SyncSubprocess.exitedDueToTimeout is true.

Citations:


Verify the minimum Bun version requirement and add it to package.json.

The implementation is correct, but the plugin has no explicit Bun version constraint. The timeout option in Bun.spawnSync was added in September 2024 (not v1.2.6 as mentioned earlier). Without a declared minimum, users on earlier Bun versions will silently skip the timeout, and a hung git process could block session initialization indefinitely. Add "bun": ">= 1.0.0" (or more conservatively, the current tested Bun version) to the engines field in package.json to enforce this requirement.

♻️ Optional readability nit: deduplicate the fallback

The cwd-basename fallback is constructed in three branches. A small helper makes intent clearer and shrinks the case body:

     case "per-repo": {
+      const cwdFallback = () => sanitizeForSessionName(basename(cwd));
       let repoBase: string;
       try {
         // git rev-parse --show-toplevel with 5s timeout. Falls back to cwd
         // basename on any failure (non-git, timeout, non-zero exit, empty
         // stdout, thrown error). Mirrors hermes-agent's per-repo derivation.
         const result = Bun.spawnSync(["git", "rev-parse", "--show-toplevel"], {
           cwd,
           stdout: "pipe",
           stderr: "ignore",
           stdin: "ignore",
           timeout: 5000,
         });
-        // SyncSubprocess.stdout is `Buffer | undefined`; guard against the
-        // exit-0-but-empty-stdout case so we never produce an empty session.
-        if (result.exitCode === 0 && result.stdout) {
-          const root = result.stdout.toString().trim();
-          if (root) {
-            repoBase = sanitizeForSessionName(basename(root));
-          } else {
-            repoBase = sanitizeForSessionName(basename(cwd));
-          }
-        } else {
-          repoBase = sanitizeForSessionName(basename(cwd));
-        }
+        const root = result.exitCode === 0 && result.stdout
+          ? result.stdout.toString().trim()
+          : "";
+        repoBase = root ? sanitizeForSessionName(basename(root)) : cwdFallback();
       } catch {
-        repoBase = sanitizeForSessionName(basename(cwd));
+        repoBase = cwdFallback();
       }
       return usePrefix ? `${peerPart}-${repoBase}` : repoBase;
     }
📝 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
case "per-repo": {
let repoBase: string;
try {
// git rev-parse --show-toplevel with 5s timeout. Falls back to cwd
// basename on any failure (non-git, timeout, non-zero exit, empty
// stdout, thrown error). Mirrors hermes-agent's per-repo derivation.
const result = Bun.spawnSync(["git", "rev-parse", "--show-toplevel"], {
cwd,
stdout: "pipe",
stderr: "ignore",
stdin: "ignore",
timeout: 5000,
});
// SyncSubprocess.stdout is `Buffer | undefined`; guard against the
// exit-0-but-empty-stdout case so we never produce an empty session.
if (result.exitCode === 0 && result.stdout) {
const root = result.stdout.toString().trim();
if (root) {
repoBase = sanitizeForSessionName(basename(root));
} else {
repoBase = sanitizeForSessionName(basename(cwd));
}
} else {
repoBase = sanitizeForSessionName(basename(cwd));
}
} catch {
repoBase = sanitizeForSessionName(basename(cwd));
}
return usePrefix ? `${peerPart}-${repoBase}` : repoBase;
}
case "per-repo": {
const cwdFallback = () => sanitizeForSessionName(basename(cwd));
let repoBase: string;
try {
// git rev-parse --show-toplevel with 5s timeout. Falls back to cwd
// basename on any failure (non-git, timeout, non-zero exit, empty
// stdout, thrown error). Mirrors hermes-agent's per-repo derivation.
const result = Bun.spawnSync(["git", "rev-parse", "--show-toplevel"], {
cwd,
stdout: "pipe",
stderr: "ignore",
stdin: "ignore",
timeout: 5000,
});
const root = result.exitCode === 0 && result.stdout
? result.stdout.toString().trim()
: "";
repoBase = root ? sanitizeForSessionName(basename(root)) : cwdFallback();
} catch {
repoBase = cwdFallback();
}
return usePrefix ? `${peerPart}-${repoBase}` : repoBase;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/honcho/src/config.ts` around lines 571 - 600, The code uses
Bun.spawnSync(..., { timeout }) in the "per-repo" case, so add a minimum Bun
engine requirement to package.json to guarantee the timeout option exists (set
"engines": { "bun":
"<minimum-tested-version-or-the-sep-2024-release-that-introduced-timeout>" } or
at least ">= 1.0.0" if you prefer conservative); also optionally deduplicate the
cwd-basename fallback inside the same "per-repo" case by extracting const
fallback = sanitizeForSessionName(basename(cwd)) and using it in the three
branches (references: Bun.spawnSync, sanitizeForSessionName, basename, the
"per-repo" case).

sasha-id added a commit to sasha-id/claude-honcho that referenced this pull request Apr 25, 2026
Brings the per-repo session strategy patch (PR plastic-labs#32 at upstream) into
fork main so /plugin install honcho@honcho from sasha-id/claude-honcho
includes the patch. Upstream PR is unaffected — PR diff is computed
against plastic-labs/main, not fork main.
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