Mount-based opencode sessions + cooperative sync shutdown#20
Mount-based opencode sessions + cooperative sync shutdown#20willwashburn merged 7 commits intomainfrom
Conversation
Refactor interactive session behavior across harnesses: change decideCleanMode to account for opencode defaults and an --install-in-repo override, and add SKILL_INSTALL_IGNORED_PATTERNS to keep skill-install artifacts out of the real repo. Update CLI help text to clarify mount/install semantics. Allow runInstall to accept cwd, add runInstallOrThrow for error-safe installs inside mounts, and defer non-claude installs into the mount via onBeforeLaunch so writes land in the sandbox. Also adjust buildInteractiveSpec for opencode to pass system prompts via --prompt (and set initialPrompt to null). Tests updated to cover the new decisions, ignored patterns, and prompt behavior.
Add resolveSystemPromptPlaceholders(prompt, harness) and tests to substitute the <harness> placeholder in persona systemPrompts while leaving other angle-bracket tokens intact. Use this resolver in buildSelection so runtime.systemPrompt contains the active harness. In runInteractive, add a two-stage SIGINT handler that notifies the user on first Ctrl-C (syncing message) and force-quits on second (removes session root and exits 130). Also add an onAfterSync callback to report synced change counts and ensure the SIGINT handler is removed in the finally block. These changes improve prompt correctness for LLM-facing commands and give users a clear escape hatch and feedback during session syncs.
Bump @relayfile/local-mount to ^0.5.0 and enhance CLI shutdown handling in runInteractive. Replace the previous two-stage SIGINT flow with a three-stage handler: 1) announce syncing on first Ctrl-C, 2) abort a new AbortController to skip autosync's draining reconcile (passed as shutdownSignal to the mount) and allow a partial syncBack, and 3) hard-quit by removing the mount root and exiting on a third Ctrl-C. Update user-facing messages and qualify the sync confirmation when an abort produces a partial sync.
posthog.json switched to stdio + mcp-remote in e3342c7 (removing env.POSTHOG_API_KEY and the http transport with Bearer header), but the resolvePersona / resolvePersonaByTier tests still asserted the old shape and broke on main. Update them to check the new stdio shape (command + args) and drop the env assertion. Env loader coverage already lives in the cli local-personas cascade tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR improves the interactive agent-workforce agent <persona> flow by making opencode interactive launches pass the system prompt correctly, running non-claude interactive sessions inside a local-mount sandbox by default to avoid polluting the real repo during skill installs, and adding a cooperative SIGINT shutdown path to speed up/abort mount sync-back.
Changes:
- Pass opencode’s system prompt via
--prompt(and stop appending it as a trailing positional arg). - Default opencode interactive sessions to run inside
@relayfile/local-mount(opt out with--install-in-repo) and add skill-install ignored patterns. - Add a 3-stage Ctrl-C handler using local-mount’s new
shutdownSignal, plus<harness>placeholder substitution in persona system prompts.
Reviewed changes
Copilot reviewed 6 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Updates dependency graph for @relayfile/local-mount@0.5.0 and its transitive deps. |
| packages/harness-kit/src/harness.ts | Updates opencode interactive spec to use --prompt and null initialPrompt. |
| packages/harness-kit/src/harness.test.ts | Adjusts tests to assert --prompt behavior and initialPrompt: null. |
| packages/cli/src/cli.ts | Implements mount-by-default for opencode interactive, cooperative shutdown via shutdownSignal, and <harness> placeholder substitution. |
| packages/cli/src/cli.test.ts | Adds coverage for clean-mode decisions, placeholder resolution, and ignored patterns. |
| packages/cli/package.json | Bumps @relayfile/local-mount to ^0.5.0. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
1. Expand SKILL_INSTALL_IGNORED_PATTERNS to cover every skill.sh per-provider output root (.claude/skills, .factory/skills, .kiro/skills, skills) in addition to the prpm/opencode roots. Missing entries would let skill.sh copy pre-existing repo content into the mount and sync new installs back to the real repo — reintroducing the pollution this set is meant to prevent. 2. Skip the install.cleanupCommand in the mount branch when the install was deferred into the mount. Its paths are mount-relative (.skills/<name>, skills/<name>) and running it against the real repo cwd could rm -rf pre-existing user content with the same name. removeSessionRoot already tears down the whole mount, so per-skill cleanup is redundant in that case. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 7 changed files in this pull request and generated 3 comments.
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
1. Rename "clean mount" → "sandbox mount" in user-facing output.
The log line fired whenever the mount engaged, including when
opencode auto-enabled it without --clean, so the wording was
misleading relative to the --clean flag's semantics. Also
renamed the summary flag from clean=on → mount=on.
2. Propagate the real install exit code from the mount branch.
Introduce InstallCommandError so runInstallOrThrow can carry
the child's status up through launchOnMount's onBeforeLaunch
and into the catch, instead of collapsing every failure onto
127 (command-not-found). Split the remaining catch branches
so a generic launch failure returns 1 and a missing binary
still returns 127.
3. Use fs.rmSync(..., { recursive: true, force: true }) instead
of spawnSync('rm', '-rf', ...) in removeSessionRoot and the
3rd-press force-quit teardown so the emergency path works on
Windows.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 8 changed files in this pull request and generated 2 comments.
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
1. Map subprocess signal termination to 128+N in install helpers. spawnSync sets status=null + signal=SIGINT when a child is killed before it can set an exit code, so runInstall / runInstallOrThrow were collapsing Ctrl-C / SIGTERM during install onto a generic exit 1. Extract subprocessExitCode() that defers to signalExitCode when status is null, matching the async spawn path's conventions. InstallCommandError now carries the signal-derived code too. 2. Refresh the opencode-related docstrings in harness.ts. The opencode branch moved to --prompt + initialPrompt: null, but the earlier comments on InteractiveSpec.initialPrompt and buildInteractiveSpec's header still claimed opencode relied on the trailing-positional initial-prompt path. Updated them to reflect current behavior and note why the old path was unsafe (opencode parses the trailing positional as a project directory). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 8 changed files in this pull request and generated 3 comments.
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| --install-in-repo Install skills into the repo's | ||
| .claude/skills/ (legacy). By default, | ||
| harness-conventional directory | ||
| (.claude/skills, .opencode/skills, |
There was a problem hiding this comment.
The --install-in-repo help text lists .opencode/skills as an in-repo install location, but the workload-router’s declared opencode skill target directory is .skills (see HARNESS_SKILL_TARGETS.opencode.dir), and the CLI README also documents .skills/ for opencode installs. Please align this help text with the actual install paths so users aren’t pointed at a directory we don’t use.
| (.claude/skills, .opencode/skills, | |
| (.claude/skills, .skills, |
| // In session mode the install command is never `:` — it at minimum runs | ||
| // the plugin scaffold (mkdir + manifest + symlink) so `--plugin-dir` has a | ||
| // valid target even for skill-less personas like posthog. Gate on the | ||
| // command string rather than `installs.length` so we don't skip that. |
There was a problem hiding this comment.
This comment says “In session mode the install command is never : …”, but install.commandString is : whenever the persona has zero skills and installRoot is unset (which can happen now that opencode runs with a session dir/mount by default). Consider narrowing the wording to the claude installRoot session-install-root mode only, so it matches workload-router’s install artifact behavior.
| ? `Staging session plugin dir${installRoot ? ` → ${installRoot}` : ''}` | ||
| : `Installing skills: ${skillIds}${installRoot ? ` → ${installRoot}` : ''}`; | ||
| // When useClean engages on a non-claude harness, the install must run | ||
| // INSIDE the mount so `.opencode/skills/`, `.agents/skills/`, prpm.lock, |
There was a problem hiding this comment.
The deferred-install comment calls out .opencode/skills/ as an install artifact, but the repo’s opencode skill target is .skills/ (and the ignored patterns include .opencode without documenting what exactly writes there). To avoid confusion, update this example list to reflect the actual expected opencode install directory (.skills/…) and/or clarify what tool creates .opencode/… if it’s intentionally included.
| // INSIDE the mount so `.opencode/skills/`, `.agents/skills/`, prpm.lock, | |
| // INSIDE the mount so `.skills/`, `.agents/skills/`, prpm.lock, |
…/harness-kit@0.2.0 @agentworkforce/cli@0.3.0 Reconciliation of the 2026-04-23 publish-run race: all three packages published to npm successfully, but the workflow's final `git push origin HEAD --follow-tags` was rejected because PR #22 (persona-maker) merged to main during the job. Tags shipped but the chore(release) commit was orphaned at 3b5b8f3. This cherry-picks that commit back onto main and replaces the auto-generated CLI changelog stub with a handwritten entry covering PRs #20, #22, #23, #24. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Three threads of work on the interactive
agent-workforce agent <persona>path:opencodewas spawned with the system prompt as a trailing positional, which opencode parses as a project directory (Failed to change directory to /.../You are a capability discovery specialist…). Now the prompt goes through--prompt, matching opencode's TUI contract (packages/harness-kit/src/harness.ts).installRootsupport, sonpx prpm install/npx skills addwere writing straight into~/Projects/workforce/.opencode/skills/,.agents/skills/,prpm.lock, etc. Opencode interactive now runs inside a@relayfile/local-mountsandbox by default (opt out with--install-in-repo). The install itself runs inside the mount viaonBeforeLaunch;SKILL_INSTALL_IGNORED_PATTERNSprevents those dirs from being copied in or synced back. Claude's--cleansemantics are unchanged; codex still warns.@relayfile/local-mountto^0.5.0to pick up the newshutdownSignal, and layered a 3-stage SIGINT handler aroundlaunchOnMount:⏳ Syncing session changes back to the repo…shutdownSignal→ local-mount skips autosync's draining reconcile and returns the partial syncBack count (cleanup still runs, no leaked mount dir)rm -rfthe session dir +process.exit(130)onAfterSynclabels the count(partial)when the signal was aborted.<harness>as a placeholder inside example install commands (e.g.npx prpm install <ref> --as <harness>).resolveSystemPromptPlaceholdersinbuildSelectionsubstitutes the active harness name once at selection time, so both interactive and one-shot paths see a concrete command. Other angle-bracket tokens (<ref>,<repo-url>,<query>) stay as LLM-facing template variables.Test plan
pnpm -F @agentworkforce/harness-kit test(24/24)pnpm -F @agentworkforce/cli test(45/45, including newdecideCleanMode,SKILL_INSTALL_IGNORED_PATTERNS, andresolveSystemPromptPlaceholderscoverage)npm run dev:cli -- agent capability-discoverer— verify opencode launches, skills land in~/.agent-workforce/sessions/<id>/mount/, real repo stays cleanagent-workforce agent posthog@best --clean(claude) — existing flow still works end-to-end🤖 Generated with Claude Code