Skip to content

Mount-based opencode sessions + cooperative sync shutdown#20

Merged
willwashburn merged 7 commits intomainfrom
opencode-parity
Apr 21, 2026
Merged

Mount-based opencode sessions + cooperative sync shutdown#20
willwashburn merged 7 commits intomainfrom
opencode-parity

Conversation

@willwashburn
Copy link
Copy Markdown
Member

Summary

Three threads of work on the interactive agent-workforce agent <persona> path:

  • Opencode interactive is usable. Previously opencode was 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).
  • Skills stay out of the real repo. For non-claude harnesses the SDK has no installRoot support, so npx prpm install / npx skills add were writing straight into ~/Projects/workforce/.opencode/skills/, .agents/skills/, prpm.lock, etc. Opencode interactive now runs inside a @relayfile/local-mount sandbox by default (opt out with --install-in-repo). The install itself runs inside the mount via onBeforeLaunch; SKILL_INSTALL_IGNORED_PATTERNS prevents those dirs from being copied in or synced back. Claude's --clean semantics are unchanged; codex still warns.
  • Cooperative sync shutdown. Bumped @relayfile/local-mount to ^0.5.0 to pick up the new shutdownSignal, and layered a 3-stage SIGINT handler around launchOnMount:
    1. announce ⏳ Syncing session changes back to the repo…
    2. abort shutdownSignal → local-mount skips autosync's draining reconcile and returns the partial syncBack count (cleanup still runs, no leaked mount dir)
    3. hard escape: sync rm -rf the session dir + process.exit(130)
      onAfterSync labels the count (partial) when the signal was aborted.
  • Bonus fix. Persona systemPrompts use <harness> as a placeholder inside example install commands (e.g. npx prpm install <ref> --as <harness>). resolveSystemPromptPlaceholders in buildSelection substitutes 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 new decideCleanMode, SKILL_INSTALL_IGNORED_PATTERNS, and resolveSystemPromptPlaceholders coverage)
  • Manual: npm run dev:cli -- agent capability-discoverer — verify opencode launches, skills land in ~/.agent-workforce/sessions/<id>/mount/, real repo stays clean
  • Manual: Ctrl-C once mid-session → syncing message; Ctrl-C twice → abort with partial count; Ctrl-C three times → hard quit
  • Manual: agent-workforce agent posthog@best --clean (claude) — existing flow still works end-to-end

🤖 Generated with Claude Code

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>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.

Comment thread packages/cli/src/cli.ts Outdated
Comment thread packages/cli/src/cli.ts Outdated
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>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.

Comment thread packages/cli/src/cli.ts Outdated
Comment thread packages/cli/src/cli.ts Outdated
Comment thread packages/cli/src/cli.ts
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>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.

Comment thread packages/cli/src/cli.ts
Comment thread packages/harness-kit/src/harness.ts
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>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.

Comment thread packages/cli/src/cli.ts
--install-in-repo Install skills into the repo's
.claude/skills/ (legacy). By default,
harness-conventional directory
(.claude/skills, .opencode/skills,
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
(.claude/skills, .opencode/skills,
(.claude/skills, .skills,

Copilot uses AI. Check for mistakes.
Comment thread packages/cli/src/cli.ts
Comment on lines 467 to 470
// 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.
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment thread packages/cli/src/cli.ts
? `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,
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
// INSIDE the mount so `.opencode/skills/`, `.agents/skills/`, prpm.lock,
// INSIDE the mount so `.skills/`, `.agents/skills/`, prpm.lock,

Copilot uses AI. Check for mistakes.
@willwashburn willwashburn merged commit 0464ee6 into main Apr 21, 2026
5 checks passed
@willwashburn willwashburn deleted the opencode-parity branch April 21, 2026 22:55
willwashburn added a commit that referenced this pull request Apr 23, 2026
…/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>
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.

2 participants