Show spinners during persona session install and exit sync - #59

Merged
willwashburn merged 2 commits into
mainfrom
fix-cli-launch-spinners
May 8, 2026
Merged

Show spinners during persona session install and exit sync#59
willwashburn merged 2 commits into
mainfrom
fix-cli-launch-spinners

Conversation

@willwashburn

Copy link
Copy Markdown
Member

Summary

  • Wrap skill install (npx prpm install / npx skills add) in an "Installing skills…" ora spinner with stdio captured. Successful runs collapse to a single ✓ line; failures dump the buffered output after spinner.fail. Replaces the prior raw output that looked broken at session start.
  • Inline the mount lifecycle (formerly launchOnMount) so the sync-back spinner appears the moment the persona child exits — not just when the user presses Ctrl-C. Exiting via /exit no longer looks like a silent hang. Abort reduced from 3 presses to 2 (1st press during sync aborts immediately; 2nd force-quits and rms the session dir).
  • Bump @relayfile/local-mount from ^0.6.14 to ^0.6.15.
  • Isolate AGENT_WORKFORCE_HOME in the --version test so the load-time local-persona scan can't surface warnings from developer-local state.

Test plan

  • pnpm typecheck (after a fresh workspace pnpm build — workload-router's generated types must be up to date)
  • pnpm test from packages/cli — 126/126 pass
  • Manual: launch a persona, watch the "Installing skills…" spinner during start
  • Manual: /exit a persona session, confirm the "Syncing session changes back…" spinner appears immediately and Ctrl-C aborts on first press

🤖 Generated with Claude Code

Two visible UX fixes for the CLI persona launch lifecycle:
- Skill install now runs behind an "Installing skills…" ora spinner with
stdio captured. On success it collapses to a single line; on failure the
buffered output is dumped after spinner.fail so the user still sees what
broke. Replaces the prior raw `npx prpm install` / `npx skills add`
output that looked broken to the eye.
- Exiting a persona session via /exit no longer hangs silently while
relayfile syncs back. The mount lifecycle (createMount → onBeforeLaunch →
startAutoSync → spawn → autoSync.stop → syncBack → cleanup) is now
inlined so we own the gap between child-exit and finalize, and the sync
spinner now starts the moment the child closes — not just on Ctrl-C.
Reduced abort to 2 presses (was 3): 1st press during sync = abort
immediately; 2nd press = force-quit and rm session dir.
Also bumps @relayfile/local-mount from ^0.6.14 to ^0.6.15, and isolates
AGENT_WORKFORCE_HOME in the --version test so the load-time persona scan
can't surface unrelated warnings from the developer's own ~/.agentworkforce.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented May 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 79393cb4-a520-400a-87ce-3036a485ac8a

📥 Commits

Reviewing files that changed from the base of the PR and between 6b651cc and a2bcee3.

📒 Files selected for processing (1)
  • packages/cli/src/cli.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/cli/src/cli.ts

📝 Walkthrough

Walkthrough

The CLI inlines the @relayfile/local-mount lifecycle (createMount replaces launchOnMount), reworks SIGINT and teardown behavior, introduces spinner-backed install helpers, isolates the --version test environment, and bumps the dependency to ^0.6.15.

Changes

Mount-mode execution and install UI refactor

Layer / File(s)Summary
Dependency update and import refactor
packages/cli/package.json, packages/cli/src/cli.ts
@relayfile/local-mount bumped ^0.6.14^0.6.15; import switched from launchOnMount to createMount with AutoSyncHandle.
Install-time UI spinner helpers
packages/cli/src/cli.ts
Added runInstallWithSpinner capturing subprocess stdout/stderr and showing an ora spinner; runInstall and runInstallOrThrow updated to use the spinner helper.
Mount-mode execution refactor with createMount
packages/cli/src/cli.ts
Mount-mode inlines createMount: configures git skip-worktree, optionally runs deferred installs inside the mount, materializes config/sidecar files, records launch metadata, starts autosync, spawns the harness in the mount, then stops autosync and performs explicit syncBack with a spinner; SIGINT forwards to child, aborts on first Ctrl-C during syncing, and forces exit on subsequent Ctrl-C.
Teardown updates
packages/cli/src/cli.ts
Teardown stops the syncing spinner, best-effort stops autosync on error, calls handle.cleanup(), stops launch-metadata, removes SIGINT listener, and avoids duplicate cleanup when installs ran inside mount.
Test isolation
packages/cli/src/cli.test.ts
--version test creates temporary isolated AGENT_WORKFORCE_HOME with personas/, passes it into runCliCapturingStderr, and removes the directory in a finally block.

Sequence Diagram(s)

sequenceDiagram
participant User
participant CLI
participant createMount
participant Installer
participant Harness
participant AutoSync
participant SyncBack
User->>CLI: run CLI in mount-mode
CLI->>createMount: create mount
createMount-->>CLI: mount handle
alt deferred installs
CLI->>Installer: run installs inside mount
Installer-->>CLI: install results
end
CLI->>Harness: spawn child inside mount
Harness-->>CLI: child exits
CLI->>AutoSync: stop autosync (if present)
CLI->>SyncBack: run syncBack with spinner
SyncBack-->>CLI: sync results
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I hopped into mount-mode, bright and spry,
Spinners spinning as installs flew by,
Signals forwarded, syncs that mend,
Tests tucked safe until the end,
A tiny leap — the CLI says hi!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 60.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 nameStatusExplanation
Title check✅ PassedThe title directly matches the main changes: adding spinners during persona session install and exit sync, which are the primary UX improvements in this PR.
Description check✅ PassedThe description comprehensively covers all major changes in the changeset, including skill install spinners, mount lifecycle inlining, dependency bumps, and test isolation, with clear context and rationale.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck 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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-cli-launch-spinners

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

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:6b651cc321

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadpackages/cli/src/cli.ts Outdated
Comment on lines +1250 to +1253
if (activeChild && !activeChild.killed && activeChild.exitCode === null) {
try {
activeChild.kill('SIGINT');
} catch {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid sending SIGINT to child twice during interactive runs

In the non-syncing path this handler always calls activeChild.kill('SIGINT'), but with spawn(..., { stdio: 'inherit' }) the child is already in the foreground process group and receives the terminal Ctrl-C directly. That means each Ctrl-C can deliver two SIGINTs to the harness, which can skip graceful first-interrupt behavior and force premature termination for CLIs that escalate on repeated SIGINT.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good catch — fixed in a2bcee3. The handler is now a true no-op while the child runs (its only purpose is suppressing Node's default exit-on-SIGINT). Removed the activeChild.kill('SIGINT') forward and the activeChild ref. Updated the surrounding comment to call out why the no-op is required.

Comment on lines +419 to +423
const res = spawnSync(bin, args, {
stdio: ['ignore', 'pipe', 'pipe'],
shell: false,
encoding: 'utf8',
...(cwd ? { cwd } : {})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Raise spawnSync maxBuffer when capturing installer output

This install path now captures stdout/stderr via spawnSync pipes but does not set maxBuffer, so Node applies its default per-stream buffer limit (about 1 MiB). Verbose install commands can exceed that limit, causing spawnSync to terminate the subprocess (ENOBUFS/SIGTERM) and report a non-zero exit even when the install would otherwise complete, which is a regression from the previous inherited-stdio behavior.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed in a2bcee3spawnSync now sets maxBuffer: 100 * 1024 * 1024 (100 MiB), well past anything these installers print in practice. Comment in-place explains why.

@devin-ai-integrationdevin-ai-integrationBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

View 4 additional findings in Devin Review.

Open in Devin Review

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Duplicate failure message on install error in runInstallOrThrow path

Same pattern as runInstall: runInstallWithSpinner already outputs spinner.fail(...) with the failure message at packages/cli/src/cli.ts:430. When runInstallOrThrow throws InstallCommandError, the catch block at line 1379 writes ${err.message}. Aborting.\n — and err.message is "${label} failed (exit ${exitCode})" (set at packages/cli/src/cli.ts:454), producing a second nearly identical error line on stderr.

(Refers to lines 1378-1380)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed in a2bcee3 — the InstallCommandError catch no longer re-prints err.message. The failure line is shown once via spinner.fail inside runInstallWithSpinner.

Comment threadpackages/cli/src/cli.ts Outdated
Comment on lines 440 to 442
if (code !== 0) {
process.stderr.write(`${label} failed (exit ${code}). Aborting.\n`);
process.exit(code);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Duplicate failure message on install error in runInstall

runInstallWithSpinner already writes a failure message via spinner.fail(${label} failed (exit ${code})) at packages/cli/src/cli.ts:430, and dumps captured output at line 431. Then runInstall at line 441 writes a nearly identical message "${label} failed (exit ${code}). Aborting.\n" to stderr before exiting. On a failed install the user sees the same failure reported twice:

Example duplicated output

✗ Installing skills: foo → /path failed (exit 1)
<captured subprocess output>
Installing skills: foo → /path failed (exit 1). Aborting.

Suggested change
if(code!==0){
process.stderr.write(`${label} failed (exit${code}). Aborting.\n`);
process.exit(code);
if(code!==0){
process.exit(code);
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed in a2bcee3runInstall no longer writes the duplicate "Aborting" line; the failure message already comes from spinner.fail inside runInstallWithSpinner.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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)
packages/cli/src/cli.ts (1)

1246-1283: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add SIGTERM handler to the inline mount path for consistent signal cleanup.

The inline mount lifecycle registers only SIGINT, while the other execution path handles both SIGINT and SIGTERM. Without a SIGTERM listener, Node uses its default behavior (immediate exit), bypassing the sync and cleanup logic entirely. A SIGTERM signal during a session would skip syncBack() and leave the session directory behind.

Suggested refactor
- const sigintHandler = () => {+ const handleShutdownSignal = (signal: NodeJS.Signals) => {
if (!isSyncing) {
// Defensive: forward to child if it somehow didn't get the TTY signal.
// No spinner side effects while the harness owns the screen.
if (activeChild && !activeChild.killed && activeChild.exitCode === null) {
try {
- activeChild.kill('SIGINT');+ activeChild.kill(signal);
} catch {
/* ignore — child likely exited between checks */
}
}
return;
}
abortPresses += 1;
if (abortPresses === 1) {
if (syncSpinner) {
syncSpinner.text =
'Aborting sync — partial changes will be propagated. (Ctrl-C again to force quit)';
}
shutdownController.abort();
return;
}
...
};
- process.on('SIGINT', sigintHandler);+ const sigintHandler = () => handleShutdownSignal('SIGINT');+ const sigtermHandler = () => handleShutdownSignal('SIGTERM');+ process.on('SIGINT', sigintHandler);+ process.on('SIGTERM', sigtermHandler);
...
process.removeListener('SIGINT', sigintHandler);
+ process.removeListener('SIGTERM', sigtermHandler);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/cli/src/cli.ts` around lines 1246 - 1283, The inline mount path only
registers sigintHandler for SIGINT so SIGTERM bypasses cleanup; add a SIGTERM
listener that invokes the same sigintHandler to ensure syncSpinner,
shutdownController.abort(), rmSync(sessionRoot, ...) and process.exit handling
run for both signals. Locate the sigintHandler function and replace or augment
the process.on('SIGINT', sigintHandler) registration to also register
process.on('SIGTERM', sigintHandler) (or otherwise wire SIGTERM to call
sigintHandler) so both signals trigger the same teardown logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/cli/src/cli.ts`:
- Around line 411-433: In runInstallWithSpinner, increase spawnSync's
stdout/stderr buffer by adding maxBuffer: 16 * 1024 * 1024 to the options and
include any spawn error text by appending res.error?.message (if present) to the
computed output so buffer exhaustion or spawn errors are visible; keep using
subprocessExitCode(res) but ensure the final output string includes
res.error?.message (and is written to stderr on failure) so ENOBUFS or other
spawn errors are distinguishable from normal command exit codes.
---
Outside diff comments:
In `@packages/cli/src/cli.ts`:
- Around line 1246-1283: The inline mount path only registers sigintHandler for
SIGINT so SIGTERM bypasses cleanup; add a SIGTERM listener that invokes the same
sigintHandler to ensure syncSpinner, shutdownController.abort(),
rmSync(sessionRoot, ...) and process.exit handling run for both signals. Locate
the sigintHandler function and replace or augment the process.on('SIGINT',
sigintHandler) registration to also register process.on('SIGTERM',
sigintHandler) (or otherwise wire SIGTERM to call sigintHandler) so both signals
trigger the same teardown logic.
🪄 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 Plus

Run ID: 22410145-6940-4768-9210-d880b3f5908b

📥 Commits

Reviewing files that changed from the base of the PR and between 3fee405 and 6b651cc.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (3)
  • packages/cli/package.json
  • packages/cli/src/cli.test.ts
  • packages/cli/src/cli.ts

Comment threadpackages/cli/src/cli.ts
- Drop duplicate failure messages on install error: spinner.fail in
runInstallWithSpinner already prints the failure line, so both runInstall
and the InstallCommandError catch were re-printing nearly the same text.
- Stop double-delivering SIGINT to the harness: with stdio: 'inherit' the
child shares the parent's process group, so Ctrl-C at the TTY already
reaches it. Forwarding via child.kill('SIGINT') from our handler
delivered a second SIGINT, which can break harnesses that escalate on
repeated interrupts (e.g. claude treats 1st = cancel, 2nd = quit). The
handler now stays a true no-op while the child runs, only there to
suppress Node's default exit-on-SIGINT.
- Raise spawnSync maxBuffer to 100 MiB for the captured installer output.
Default is 1 MiB; verbose `npx prpm install` / `npx skills add` runs
can blow past it and have spawnSync kill the child with ENOBUFS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for freeto 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

@willwashburn
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Show spinners during persona session install and exit sync - #59

Merged
willwashburn merged 2 commits into
mainfrom
fix-cli-launch-spinners
May 8, 2026
Merged

Show spinners during persona session install and exit sync#59
willwashburn merged 2 commits into
mainfrom
fix-cli-launch-spinners

Conversation

@willwashburn

Copy link
Copy Markdown
Member

Summary

  • Wrap skill install (npx prpm install / npx skills add) in an "Installing skills…" ora spinner with stdio captured. Successful runs collapse to a single ✓ line; failures dump the buffered output after spinner.fail. Replaces the prior raw output that looked broken at session start.
  • Inline the mount lifecycle (formerly launchOnMount) so the sync-back spinner appears the moment the persona child exits — not just when the user presses Ctrl-C. Exiting via /exit no longer looks like a silent hang. Abort reduced from 3 presses to 2 (1st press during sync aborts immediately; 2nd force-quits and rms the session dir).
  • Bump @relayfile/local-mount from ^0.6.14 to ^0.6.15.
  • Isolate AGENT_WORKFORCE_HOME in the --version test so the load-time local-persona scan can't surface warnings from developer-local state.

Test plan

  • pnpm typecheck (after a fresh workspace pnpm build — workload-router's generated types must be up to date)
  • pnpm test from packages/cli — 126/126 pass
  • Manual: launch a persona, watch the "Installing skills…" spinner during start
  • Manual: /exit a persona session, confirm the "Syncing session changes back…" spinner appears immediately and Ctrl-C aborts on first press

🤖 Generated with Claude Code

Two visible UX fixes for the CLI persona launch lifecycle:
- Skill install now runs behind an "Installing skills…" ora spinner with
stdio captured. On success it collapses to a single line; on failure the
buffered output is dumped after spinner.fail so the user still sees what
broke. Replaces the prior raw `npx prpm install` / `npx skills add`
output that looked broken to the eye.
- Exiting a persona session via /exit no longer hangs silently while
relayfile syncs back. The mount lifecycle (createMount → onBeforeLaunch →
startAutoSync → spawn → autoSync.stop → syncBack → cleanup) is now
inlined so we own the gap between child-exit and finalize, and the sync
spinner now starts the moment the child closes — not just on Ctrl-C.
Reduced abort to 2 presses (was 3): 1st press during sync = abort
immediately; 2nd press = force-quit and rm session dir.
Also bumps @relayfile/local-mount from ^0.6.14 to ^0.6.15, and isolates
AGENT_WORKFORCE_HOME in the --version test so the load-time persona scan
can't surface unrelated warnings from the developer's own ~/.agentworkforce.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented May 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 79393cb4-a520-400a-87ce-3036a485ac8a

📥 Commits

Reviewing files that changed from the base of the PR and between 6b651cc and a2bcee3.

📒 Files selected for processing (1)
  • packages/cli/src/cli.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/cli/src/cli.ts

📝 Walkthrough

Walkthrough

The CLI inlines the @relayfile/local-mount lifecycle (createMount replaces launchOnMount), reworks SIGINT and teardown behavior, introduces spinner-backed install helpers, isolates the --version test environment, and bumps the dependency to ^0.6.15.

Changes

Mount-mode execution and install UI refactor

Layer / File(s)Summary
Dependency update and import refactor
packages/cli/package.json, packages/cli/src/cli.ts
@relayfile/local-mount bumped ^0.6.14^0.6.15; import switched from launchOnMount to createMount with AutoSyncHandle.
Install-time UI spinner helpers
packages/cli/src/cli.ts
Added runInstallWithSpinner capturing subprocess stdout/stderr and showing an ora spinner; runInstall and runInstallOrThrow updated to use the spinner helper.
Mount-mode execution refactor with createMount
packages/cli/src/cli.ts
Mount-mode inlines createMount: configures git skip-worktree, optionally runs deferred installs inside the mount, materializes config/sidecar files, records launch metadata, starts autosync, spawns the harness in the mount, then stops autosync and performs explicit syncBack with a spinner; SIGINT forwards to child, aborts on first Ctrl-C during syncing, and forces exit on subsequent Ctrl-C.
Teardown updates
packages/cli/src/cli.ts
Teardown stops the syncing spinner, best-effort stops autosync on error, calls handle.cleanup(), stops launch-metadata, removes SIGINT listener, and avoids duplicate cleanup when installs ran inside mount.
Test isolation
packages/cli/src/cli.test.ts
--version test creates temporary isolated AGENT_WORKFORCE_HOME with personas/, passes it into runCliCapturingStderr, and removes the directory in a finally block.

Sequence Diagram(s)

sequenceDiagram
participant User
participant CLI
participant createMount
participant Installer
participant Harness
participant AutoSync
participant SyncBack
User->>CLI: run CLI in mount-mode
CLI->>createMount: create mount
createMount-->>CLI: mount handle
alt deferred installs
CLI->>Installer: run installs inside mount
Installer-->>CLI: install results
end
CLI->>Harness: spawn child inside mount
Harness-->>CLI: child exits
CLI->>AutoSync: stop autosync (if present)
CLI->>SyncBack: run syncBack with spinner
SyncBack-->>CLI: sync results
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I hopped into mount-mode, bright and spry,
Spinners spinning as installs flew by,
Signals forwarded, syncs that mend,
Tests tucked safe until the end,
A tiny leap — the CLI says hi!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 60.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 nameStatusExplanation
Title check✅ PassedThe title directly matches the main changes: adding spinners during persona session install and exit sync, which are the primary UX improvements in this PR.
Description check✅ PassedThe description comprehensively covers all major changes in the changeset, including skill install spinners, mount lifecycle inlining, dependency bumps, and test isolation, with clear context and rationale.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck 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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-cli-launch-spinners

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

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:6b651cc321

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadpackages/cli/src/cli.ts Outdated
Comment on lines +1250 to +1253
if (activeChild && !activeChild.killed && activeChild.exitCode === null) {
try {
activeChild.kill('SIGINT');
} catch {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid sending SIGINT to child twice during interactive runs

In the non-syncing path this handler always calls activeChild.kill('SIGINT'), but with spawn(..., { stdio: 'inherit' }) the child is already in the foreground process group and receives the terminal Ctrl-C directly. That means each Ctrl-C can deliver two SIGINTs to the harness, which can skip graceful first-interrupt behavior and force premature termination for CLIs that escalate on repeated SIGINT.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good catch — fixed in a2bcee3. The handler is now a true no-op while the child runs (its only purpose is suppressing Node's default exit-on-SIGINT). Removed the activeChild.kill('SIGINT') forward and the activeChild ref. Updated the surrounding comment to call out why the no-op is required.

Comment on lines +419 to +423
const res = spawnSync(bin, args, {
stdio: ['ignore', 'pipe', 'pipe'],
shell: false,
encoding: 'utf8',
...(cwd ? { cwd } : {})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Raise spawnSync maxBuffer when capturing installer output

This install path now captures stdout/stderr via spawnSync pipes but does not set maxBuffer, so Node applies its default per-stream buffer limit (about 1 MiB). Verbose install commands can exceed that limit, causing spawnSync to terminate the subprocess (ENOBUFS/SIGTERM) and report a non-zero exit even when the install would otherwise complete, which is a regression from the previous inherited-stdio behavior.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed in a2bcee3spawnSync now sets maxBuffer: 100 * 1024 * 1024 (100 MiB), well past anything these installers print in practice. Comment in-place explains why.

@devin-ai-integrationdevin-ai-integrationBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

View 4 additional findings in Devin Review.

Open in Devin Review

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Duplicate failure message on install error in runInstallOrThrow path

Same pattern as runInstall: runInstallWithSpinner already outputs spinner.fail(...) with the failure message at packages/cli/src/cli.ts:430. When runInstallOrThrow throws InstallCommandError, the catch block at line 1379 writes ${err.message}. Aborting.\n — and err.message is "${label} failed (exit ${exitCode})" (set at packages/cli/src/cli.ts:454), producing a second nearly identical error line on stderr.

(Refers to lines 1378-1380)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed in a2bcee3 — the InstallCommandError catch no longer re-prints err.message. The failure line is shown once via spinner.fail inside runInstallWithSpinner.

Comment threadpackages/cli/src/cli.ts Outdated
Comment on lines 440 to 442
if (code !== 0) {
process.stderr.write(`${label} failed (exit ${code}). Aborting.\n`);
process.exit(code);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Duplicate failure message on install error in runInstall

runInstallWithSpinner already writes a failure message via spinner.fail(${label} failed (exit ${code})) at packages/cli/src/cli.ts:430, and dumps captured output at line 431. Then runInstall at line 441 writes a nearly identical message "${label} failed (exit ${code}). Aborting.\n" to stderr before exiting. On a failed install the user sees the same failure reported twice:

Example duplicated output

✗ Installing skills: foo → /path failed (exit 1)
<captured subprocess output>
Installing skills: foo → /path failed (exit 1). Aborting.

Suggested change
if(code!==0){
process.stderr.write(`${label} failed (exit${code}). Aborting.\n`);
process.exit(code);
if(code!==0){
process.exit(code);
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed in a2bcee3runInstall no longer writes the duplicate "Aborting" line; the failure message already comes from spinner.fail inside runInstallWithSpinner.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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)
packages/cli/src/cli.ts (1)

1246-1283: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add SIGTERM handler to the inline mount path for consistent signal cleanup.

The inline mount lifecycle registers only SIGINT, while the other execution path handles both SIGINT and SIGTERM. Without a SIGTERM listener, Node uses its default behavior (immediate exit), bypassing the sync and cleanup logic entirely. A SIGTERM signal during a session would skip syncBack() and leave the session directory behind.

Suggested refactor
- const sigintHandler = () => {+ const handleShutdownSignal = (signal: NodeJS.Signals) => {
if (!isSyncing) {
// Defensive: forward to child if it somehow didn't get the TTY signal.
// No spinner side effects while the harness owns the screen.
if (activeChild && !activeChild.killed && activeChild.exitCode === null) {
try {
- activeChild.kill('SIGINT');+ activeChild.kill(signal);
} catch {
/* ignore — child likely exited between checks */
}
}
return;
}
abortPresses += 1;
if (abortPresses === 1) {
if (syncSpinner) {
syncSpinner.text =
'Aborting sync — partial changes will be propagated. (Ctrl-C again to force quit)';
}
shutdownController.abort();
return;
}
...
};
- process.on('SIGINT', sigintHandler);+ const sigintHandler = () => handleShutdownSignal('SIGINT');+ const sigtermHandler = () => handleShutdownSignal('SIGTERM');+ process.on('SIGINT', sigintHandler);+ process.on('SIGTERM', sigtermHandler);
...
process.removeListener('SIGINT', sigintHandler);
+ process.removeListener('SIGTERM', sigtermHandler);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/cli/src/cli.ts` around lines 1246 - 1283, The inline mount path only
registers sigintHandler for SIGINT so SIGTERM bypasses cleanup; add a SIGTERM
listener that invokes the same sigintHandler to ensure syncSpinner,
shutdownController.abort(), rmSync(sessionRoot, ...) and process.exit handling
run for both signals. Locate the sigintHandler function and replace or augment
the process.on('SIGINT', sigintHandler) registration to also register
process.on('SIGTERM', sigintHandler) (or otherwise wire SIGTERM to call
sigintHandler) so both signals trigger the same teardown logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/cli/src/cli.ts`:
- Around line 411-433: In runInstallWithSpinner, increase spawnSync's
stdout/stderr buffer by adding maxBuffer: 16 * 1024 * 1024 to the options and
include any spawn error text by appending res.error?.message (if present) to the
computed output so buffer exhaustion or spawn errors are visible; keep using
subprocessExitCode(res) but ensure the final output string includes
res.error?.message (and is written to stderr on failure) so ENOBUFS or other
spawn errors are distinguishable from normal command exit codes.
---
Outside diff comments:
In `@packages/cli/src/cli.ts`:
- Around line 1246-1283: The inline mount path only registers sigintHandler for
SIGINT so SIGTERM bypasses cleanup; add a SIGTERM listener that invokes the same
sigintHandler to ensure syncSpinner, shutdownController.abort(),
rmSync(sessionRoot, ...) and process.exit handling run for both signals. Locate
the sigintHandler function and replace or augment the process.on('SIGINT',
sigintHandler) registration to also register process.on('SIGTERM',
sigintHandler) (or otherwise wire SIGTERM to call sigintHandler) so both signals
trigger the same teardown logic.
🪄 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 Plus

Run ID: 22410145-6940-4768-9210-d880b3f5908b

📥 Commits

Reviewing files that changed from the base of the PR and between 3fee405 and 6b651cc.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (3)
  • packages/cli/package.json
  • packages/cli/src/cli.test.ts
  • packages/cli/src/cli.ts

Comment threadpackages/cli/src/cli.ts
- Drop duplicate failure messages on install error: spinner.fail in
runInstallWithSpinner already prints the failure line, so both runInstall
and the InstallCommandError catch were re-printing nearly the same text.
- Stop double-delivering SIGINT to the harness: with stdio: 'inherit' the
child shares the parent's process group, so Ctrl-C at the TTY already
reaches it. Forwarding via child.kill('SIGINT') from our handler
delivered a second SIGINT, which can break harnesses that escalate on
repeated interrupts (e.g. claude treats 1st = cancel, 2nd = quit). The
handler now stays a true no-op while the child runs, only there to
suppress Node's default exit-on-SIGINT.
- Raise spawnSync maxBuffer to 100 MiB for the captured installer output.
Default is 1 MiB; verbose `npx prpm install` / `npx skills add` runs
can blow past it and have spawnSync kill the child with ENOBUFS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for freeto 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

@willwashburn
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Show spinners during persona session install and exit sync - #59

Merged
willwashburn merged 2 commits into
mainfrom
fix-cli-launch-spinners
May 8, 2026
Merged

Show spinners during persona session install and exit sync#59
willwashburn merged 2 commits into
mainfrom
fix-cli-launch-spinners

Conversation

@willwashburn

Copy link
Copy Markdown
Member

Summary

  • Wrap skill install (npx prpm install / npx skills add) in an "Installing skills…" ora spinner with stdio captured. Successful runs collapse to a single ✓ line; failures dump the buffered output after spinner.fail. Replaces the prior raw output that looked broken at session start.
  • Inline the mount lifecycle (formerly launchOnMount) so the sync-back spinner appears the moment the persona child exits — not just when the user presses Ctrl-C. Exiting via /exit no longer looks like a silent hang. Abort reduced from 3 presses to 2 (1st press during sync aborts immediately; 2nd force-quits and rms the session dir).
  • Bump @relayfile/local-mount from ^0.6.14 to ^0.6.15.
  • Isolate AGENT_WORKFORCE_HOME in the --version test so the load-time local-persona scan can't surface warnings from developer-local state.

Test plan

  • pnpm typecheck (after a fresh workspace pnpm build — workload-router's generated types must be up to date)
  • pnpm test from packages/cli — 126/126 pass
  • Manual: launch a persona, watch the "Installing skills…" spinner during start
  • Manual: /exit a persona session, confirm the "Syncing session changes back…" spinner appears immediately and Ctrl-C aborts on first press

🤖 Generated with Claude Code

Two visible UX fixes for the CLI persona launch lifecycle:
- Skill install now runs behind an "Installing skills…" ora spinner with
stdio captured. On success it collapses to a single line; on failure the
buffered output is dumped after spinner.fail so the user still sees what
broke. Replaces the prior raw `npx prpm install` / `npx skills add`
output that looked broken to the eye.
- Exiting a persona session via /exit no longer hangs silently while
relayfile syncs back. The mount lifecycle (createMount → onBeforeLaunch →
startAutoSync → spawn → autoSync.stop → syncBack → cleanup) is now
inlined so we own the gap between child-exit and finalize, and the sync
spinner now starts the moment the child closes — not just on Ctrl-C.
Reduced abort to 2 presses (was 3): 1st press during sync = abort
immediately; 2nd press = force-quit and rm session dir.
Also bumps @relayfile/local-mount from ^0.6.14 to ^0.6.15, and isolates
AGENT_WORKFORCE_HOME in the --version test so the load-time persona scan
can't surface unrelated warnings from the developer's own ~/.agentworkforce.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented May 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 79393cb4-a520-400a-87ce-3036a485ac8a

📥 Commits

Reviewing files that changed from the base of the PR and between 6b651cc and a2bcee3.

📒 Files selected for processing (1)
  • packages/cli/src/cli.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/cli/src/cli.ts

📝 Walkthrough

Walkthrough

The CLI inlines the @relayfile/local-mount lifecycle (createMount replaces launchOnMount), reworks SIGINT and teardown behavior, introduces spinner-backed install helpers, isolates the --version test environment, and bumps the dependency to ^0.6.15.

Changes

Mount-mode execution and install UI refactor

Layer / File(s)Summary
Dependency update and import refactor
packages/cli/package.json, packages/cli/src/cli.ts
@relayfile/local-mount bumped ^0.6.14^0.6.15; import switched from launchOnMount to createMount with AutoSyncHandle.
Install-time UI spinner helpers
packages/cli/src/cli.ts
Added runInstallWithSpinner capturing subprocess stdout/stderr and showing an ora spinner; runInstall and runInstallOrThrow updated to use the spinner helper.
Mount-mode execution refactor with createMount
packages/cli/src/cli.ts
Mount-mode inlines createMount: configures git skip-worktree, optionally runs deferred installs inside the mount, materializes config/sidecar files, records launch metadata, starts autosync, spawns the harness in the mount, then stops autosync and performs explicit syncBack with a spinner; SIGINT forwards to child, aborts on first Ctrl-C during syncing, and forces exit on subsequent Ctrl-C.
Teardown updates
packages/cli/src/cli.ts
Teardown stops the syncing spinner, best-effort stops autosync on error, calls handle.cleanup(), stops launch-metadata, removes SIGINT listener, and avoids duplicate cleanup when installs ran inside mount.
Test isolation
packages/cli/src/cli.test.ts
--version test creates temporary isolated AGENT_WORKFORCE_HOME with personas/, passes it into runCliCapturingStderr, and removes the directory in a finally block.

Sequence Diagram(s)

sequenceDiagram
participant User
participant CLI
participant createMount
participant Installer
participant Harness
participant AutoSync
participant SyncBack
User->>CLI: run CLI in mount-mode
CLI->>createMount: create mount
createMount-->>CLI: mount handle
alt deferred installs
CLI->>Installer: run installs inside mount
Installer-->>CLI: install results
end
CLI->>Harness: spawn child inside mount
Harness-->>CLI: child exits
CLI->>AutoSync: stop autosync (if present)
CLI->>SyncBack: run syncBack with spinner
SyncBack-->>CLI: sync results
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I hopped into mount-mode, bright and spry,
Spinners spinning as installs flew by,
Signals forwarded, syncs that mend,
Tests tucked safe until the end,
A tiny leap — the CLI says hi!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 60.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 nameStatusExplanation
Title check✅ PassedThe title directly matches the main changes: adding spinners during persona session install and exit sync, which are the primary UX improvements in this PR.
Description check✅ PassedThe description comprehensively covers all major changes in the changeset, including skill install spinners, mount lifecycle inlining, dependency bumps, and test isolation, with clear context and rationale.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck 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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-cli-launch-spinners

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

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:6b651cc321

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadpackages/cli/src/cli.ts Outdated
Comment on lines +1250 to +1253
if (activeChild && !activeChild.killed && activeChild.exitCode === null) {
try {
activeChild.kill('SIGINT');
} catch {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid sending SIGINT to child twice during interactive runs

In the non-syncing path this handler always calls activeChild.kill('SIGINT'), but with spawn(..., { stdio: 'inherit' }) the child is already in the foreground process group and receives the terminal Ctrl-C directly. That means each Ctrl-C can deliver two SIGINTs to the harness, which can skip graceful first-interrupt behavior and force premature termination for CLIs that escalate on repeated SIGINT.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good catch — fixed in a2bcee3. The handler is now a true no-op while the child runs (its only purpose is suppressing Node's default exit-on-SIGINT). Removed the activeChild.kill('SIGINT') forward and the activeChild ref. Updated the surrounding comment to call out why the no-op is required.

Comment on lines +419 to +423
const res = spawnSync(bin, args, {
stdio: ['ignore', 'pipe', 'pipe'],
shell: false,
encoding: 'utf8',
...(cwd ? { cwd } : {})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Raise spawnSync maxBuffer when capturing installer output

This install path now captures stdout/stderr via spawnSync pipes but does not set maxBuffer, so Node applies its default per-stream buffer limit (about 1 MiB). Verbose install commands can exceed that limit, causing spawnSync to terminate the subprocess (ENOBUFS/SIGTERM) and report a non-zero exit even when the install would otherwise complete, which is a regression from the previous inherited-stdio behavior.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed in a2bcee3spawnSync now sets maxBuffer: 100 * 1024 * 1024 (100 MiB), well past anything these installers print in practice. Comment in-place explains why.

@devin-ai-integrationdevin-ai-integrationBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

View 4 additional findings in Devin Review.

Open in Devin Review

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Duplicate failure message on install error in runInstallOrThrow path

Same pattern as runInstall: runInstallWithSpinner already outputs spinner.fail(...) with the failure message at packages/cli/src/cli.ts:430. When runInstallOrThrow throws InstallCommandError, the catch block at line 1379 writes ${err.message}. Aborting.\n — and err.message is "${label} failed (exit ${exitCode})" (set at packages/cli/src/cli.ts:454), producing a second nearly identical error line on stderr.

(Refers to lines 1378-1380)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed in a2bcee3 — the InstallCommandError catch no longer re-prints err.message. The failure line is shown once via spinner.fail inside runInstallWithSpinner.

Comment threadpackages/cli/src/cli.ts Outdated
Comment on lines 440 to 442
if (code !== 0) {
process.stderr.write(`${label} failed (exit ${code}). Aborting.\n`);
process.exit(code);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Duplicate failure message on install error in runInstall

runInstallWithSpinner already writes a failure message via spinner.fail(${label} failed (exit ${code})) at packages/cli/src/cli.ts:430, and dumps captured output at line 431. Then runInstall at line 441 writes a nearly identical message "${label} failed (exit ${code}). Aborting.\n" to stderr before exiting. On a failed install the user sees the same failure reported twice:

Example duplicated output

✗ Installing skills: foo → /path failed (exit 1)
<captured subprocess output>
Installing skills: foo → /path failed (exit 1). Aborting.

Suggested change
if(code!==0){
process.stderr.write(`${label} failed (exit${code}). Aborting.\n`);
process.exit(code);
if(code!==0){
process.exit(code);
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed in a2bcee3runInstall no longer writes the duplicate "Aborting" line; the failure message already comes from spinner.fail inside runInstallWithSpinner.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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)
packages/cli/src/cli.ts (1)

1246-1283: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add SIGTERM handler to the inline mount path for consistent signal cleanup.

The inline mount lifecycle registers only SIGINT, while the other execution path handles both SIGINT and SIGTERM. Without a SIGTERM listener, Node uses its default behavior (immediate exit), bypassing the sync and cleanup logic entirely. A SIGTERM signal during a session would skip syncBack() and leave the session directory behind.

Suggested refactor
- const sigintHandler = () => {+ const handleShutdownSignal = (signal: NodeJS.Signals) => {
if (!isSyncing) {
// Defensive: forward to child if it somehow didn't get the TTY signal.
// No spinner side effects while the harness owns the screen.
if (activeChild && !activeChild.killed && activeChild.exitCode === null) {
try {
- activeChild.kill('SIGINT');+ activeChild.kill(signal);
} catch {
/* ignore — child likely exited between checks */
}
}
return;
}
abortPresses += 1;
if (abortPresses === 1) {
if (syncSpinner) {
syncSpinner.text =
'Aborting sync — partial changes will be propagated. (Ctrl-C again to force quit)';
}
shutdownController.abort();
return;
}
...
};
- process.on('SIGINT', sigintHandler);+ const sigintHandler = () => handleShutdownSignal('SIGINT');+ const sigtermHandler = () => handleShutdownSignal('SIGTERM');+ process.on('SIGINT', sigintHandler);+ process.on('SIGTERM', sigtermHandler);
...
process.removeListener('SIGINT', sigintHandler);
+ process.removeListener('SIGTERM', sigtermHandler);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/cli/src/cli.ts` around lines 1246 - 1283, The inline mount path only
registers sigintHandler for SIGINT so SIGTERM bypasses cleanup; add a SIGTERM
listener that invokes the same sigintHandler to ensure syncSpinner,
shutdownController.abort(), rmSync(sessionRoot, ...) and process.exit handling
run for both signals. Locate the sigintHandler function and replace or augment
the process.on('SIGINT', sigintHandler) registration to also register
process.on('SIGTERM', sigintHandler) (or otherwise wire SIGTERM to call
sigintHandler) so both signals trigger the same teardown logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/cli/src/cli.ts`:
- Around line 411-433: In runInstallWithSpinner, increase spawnSync's
stdout/stderr buffer by adding maxBuffer: 16 * 1024 * 1024 to the options and
include any spawn error text by appending res.error?.message (if present) to the
computed output so buffer exhaustion or spawn errors are visible; keep using
subprocessExitCode(res) but ensure the final output string includes
res.error?.message (and is written to stderr on failure) so ENOBUFS or other
spawn errors are distinguishable from normal command exit codes.
---
Outside diff comments:
In `@packages/cli/src/cli.ts`:
- Around line 1246-1283: The inline mount path only registers sigintHandler for
SIGINT so SIGTERM bypasses cleanup; add a SIGTERM listener that invokes the same
sigintHandler to ensure syncSpinner, shutdownController.abort(),
rmSync(sessionRoot, ...) and process.exit handling run for both signals. Locate
the sigintHandler function and replace or augment the process.on('SIGINT',
sigintHandler) registration to also register process.on('SIGTERM',
sigintHandler) (or otherwise wire SIGTERM to call sigintHandler) so both signals
trigger the same teardown logic.
🪄 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 Plus

Run ID: 22410145-6940-4768-9210-d880b3f5908b

📥 Commits

Reviewing files that changed from the base of the PR and between 3fee405 and 6b651cc.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (3)
  • packages/cli/package.json
  • packages/cli/src/cli.test.ts
  • packages/cli/src/cli.ts

Comment threadpackages/cli/src/cli.ts
- Drop duplicate failure messages on install error: spinner.fail in
runInstallWithSpinner already prints the failure line, so both runInstall
and the InstallCommandError catch were re-printing nearly the same text.
- Stop double-delivering SIGINT to the harness: with stdio: 'inherit' the
child shares the parent's process group, so Ctrl-C at the TTY already
reaches it. Forwarding via child.kill('SIGINT') from our handler
delivered a second SIGINT, which can break harnesses that escalate on
repeated interrupts (e.g. claude treats 1st = cancel, 2nd = quit). The
handler now stays a true no-op while the child runs, only there to
suppress Node's default exit-on-SIGINT.
- Raise spawnSync maxBuffer to 100 MiB for the captured installer output.
Default is 1 MiB; verbose `npx prpm install` / `npx skills add` runs
can blow past it and have spawnSync kill the child with ENOBUFS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for freeto 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

@willwashburn
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Show spinners during persona session install and exit sync - #59

Merged
willwashburn merged 2 commits into
mainfrom
fix-cli-launch-spinners
May 8, 2026
Merged

Show spinners during persona session install and exit sync#59
willwashburn merged 2 commits into
mainfrom
fix-cli-launch-spinners

Conversation

@willwashburn

Copy link
Copy Markdown
Member

Summary

  • Wrap skill install (npx prpm install / npx skills add) in an "Installing skills…" ora spinner with stdio captured. Successful runs collapse to a single ✓ line; failures dump the buffered output after spinner.fail. Replaces the prior raw output that looked broken at session start.
  • Inline the mount lifecycle (formerly launchOnMount) so the sync-back spinner appears the moment the persona child exits — not just when the user presses Ctrl-C. Exiting via /exit no longer looks like a silent hang. Abort reduced from 3 presses to 2 (1st press during sync aborts immediately; 2nd force-quits and rms the session dir).
  • Bump @relayfile/local-mount from ^0.6.14 to ^0.6.15.
  • Isolate AGENT_WORKFORCE_HOME in the --version test so the load-time local-persona scan can't surface warnings from developer-local state.

Test plan

  • pnpm typecheck (after a fresh workspace pnpm build — workload-router's generated types must be up to date)
  • pnpm test from packages/cli — 126/126 pass
  • Manual: launch a persona, watch the "Installing skills…" spinner during start
  • Manual: /exit a persona session, confirm the "Syncing session changes back…" spinner appears immediately and Ctrl-C aborts on first press

🤖 Generated with Claude Code

Two visible UX fixes for the CLI persona launch lifecycle:
- Skill install now runs behind an "Installing skills…" ora spinner with
stdio captured. On success it collapses to a single line; on failure the
buffered output is dumped after spinner.fail so the user still sees what
broke. Replaces the prior raw `npx prpm install` / `npx skills add`
output that looked broken to the eye.
- Exiting a persona session via /exit no longer hangs silently while
relayfile syncs back. The mount lifecycle (createMount → onBeforeLaunch →
startAutoSync → spawn → autoSync.stop → syncBack → cleanup) is now
inlined so we own the gap between child-exit and finalize, and the sync
spinner now starts the moment the child closes — not just on Ctrl-C.
Reduced abort to 2 presses (was 3): 1st press during sync = abort
immediately; 2nd press = force-quit and rm session dir.
Also bumps @relayfile/local-mount from ^0.6.14 to ^0.6.15, and isolates
AGENT_WORKFORCE_HOME in the --version test so the load-time persona scan
can't surface unrelated warnings from the developer's own ~/.agentworkforce.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented May 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 79393cb4-a520-400a-87ce-3036a485ac8a

📥 Commits

Reviewing files that changed from the base of the PR and between 6b651cc and a2bcee3.

📒 Files selected for processing (1)
  • packages/cli/src/cli.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/cli/src/cli.ts

📝 Walkthrough

Walkthrough

The CLI inlines the @relayfile/local-mount lifecycle (createMount replaces launchOnMount), reworks SIGINT and teardown behavior, introduces spinner-backed install helpers, isolates the --version test environment, and bumps the dependency to ^0.6.15.

Changes

Mount-mode execution and install UI refactor

Layer / File(s)Summary
Dependency update and import refactor
packages/cli/package.json, packages/cli/src/cli.ts
@relayfile/local-mount bumped ^0.6.14^0.6.15; import switched from launchOnMount to createMount with AutoSyncHandle.
Install-time UI spinner helpers
packages/cli/src/cli.ts
Added runInstallWithSpinner capturing subprocess stdout/stderr and showing an ora spinner; runInstall and runInstallOrThrow updated to use the spinner helper.
Mount-mode execution refactor with createMount
packages/cli/src/cli.ts
Mount-mode inlines createMount: configures git skip-worktree, optionally runs deferred installs inside the mount, materializes config/sidecar files, records launch metadata, starts autosync, spawns the harness in the mount, then stops autosync and performs explicit syncBack with a spinner; SIGINT forwards to child, aborts on first Ctrl-C during syncing, and forces exit on subsequent Ctrl-C.
Teardown updates
packages/cli/src/cli.ts
Teardown stops the syncing spinner, best-effort stops autosync on error, calls handle.cleanup(), stops launch-metadata, removes SIGINT listener, and avoids duplicate cleanup when installs ran inside mount.
Test isolation
packages/cli/src/cli.test.ts
--version test creates temporary isolated AGENT_WORKFORCE_HOME with personas/, passes it into runCliCapturingStderr, and removes the directory in a finally block.

Sequence Diagram(s)

sequenceDiagram
participant User
participant CLI
participant createMount
participant Installer
participant Harness
participant AutoSync
participant SyncBack
User->>CLI: run CLI in mount-mode
CLI->>createMount: create mount
createMount-->>CLI: mount handle
alt deferred installs
CLI->>Installer: run installs inside mount
Installer-->>CLI: install results
end
CLI->>Harness: spawn child inside mount
Harness-->>CLI: child exits
CLI->>AutoSync: stop autosync (if present)
CLI->>SyncBack: run syncBack with spinner
SyncBack-->>CLI: sync results
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I hopped into mount-mode, bright and spry,
Spinners spinning as installs flew by,
Signals forwarded, syncs that mend,
Tests tucked safe until the end,
A tiny leap — the CLI says hi!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 60.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 nameStatusExplanation
Title check✅ PassedThe title directly matches the main changes: adding spinners during persona session install and exit sync, which are the primary UX improvements in this PR.
Description check✅ PassedThe description comprehensively covers all major changes in the changeset, including skill install spinners, mount lifecycle inlining, dependency bumps, and test isolation, with clear context and rationale.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck 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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-cli-launch-spinners

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

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:6b651cc321

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadpackages/cli/src/cli.ts Outdated
Comment on lines +1250 to +1253
if (activeChild && !activeChild.killed && activeChild.exitCode === null) {
try {
activeChild.kill('SIGINT');
} catch {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid sending SIGINT to child twice during interactive runs

In the non-syncing path this handler always calls activeChild.kill('SIGINT'), but with spawn(..., { stdio: 'inherit' }) the child is already in the foreground process group and receives the terminal Ctrl-C directly. That means each Ctrl-C can deliver two SIGINTs to the harness, which can skip graceful first-interrupt behavior and force premature termination for CLIs that escalate on repeated SIGINT.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good catch — fixed in a2bcee3. The handler is now a true no-op while the child runs (its only purpose is suppressing Node's default exit-on-SIGINT). Removed the activeChild.kill('SIGINT') forward and the activeChild ref. Updated the surrounding comment to call out why the no-op is required.

Comment on lines +419 to +423
const res = spawnSync(bin, args, {
stdio: ['ignore', 'pipe', 'pipe'],
shell: false,
encoding: 'utf8',
...(cwd ? { cwd } : {})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Raise spawnSync maxBuffer when capturing installer output

This install path now captures stdout/stderr via spawnSync pipes but does not set maxBuffer, so Node applies its default per-stream buffer limit (about 1 MiB). Verbose install commands can exceed that limit, causing spawnSync to terminate the subprocess (ENOBUFS/SIGTERM) and report a non-zero exit even when the install would otherwise complete, which is a regression from the previous inherited-stdio behavior.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed in a2bcee3spawnSync now sets maxBuffer: 100 * 1024 * 1024 (100 MiB), well past anything these installers print in practice. Comment in-place explains why.

@devin-ai-integrationdevin-ai-integrationBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

View 4 additional findings in Devin Review.

Open in Devin Review

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Duplicate failure message on install error in runInstallOrThrow path

Same pattern as runInstall: runInstallWithSpinner already outputs spinner.fail(...) with the failure message at packages/cli/src/cli.ts:430. When runInstallOrThrow throws InstallCommandError, the catch block at line 1379 writes ${err.message}. Aborting.\n — and err.message is "${label} failed (exit ${exitCode})" (set at packages/cli/src/cli.ts:454), producing a second nearly identical error line on stderr.

(Refers to lines 1378-1380)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed in a2bcee3 — the InstallCommandError catch no longer re-prints err.message. The failure line is shown once via spinner.fail inside runInstallWithSpinner.

Comment threadpackages/cli/src/cli.ts Outdated
Comment on lines 440 to 442
if (code !== 0) {
process.stderr.write(`${label} failed (exit ${code}). Aborting.\n`);
process.exit(code);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Duplicate failure message on install error in runInstall

runInstallWithSpinner already writes a failure message via spinner.fail(${label} failed (exit ${code})) at packages/cli/src/cli.ts:430, and dumps captured output at line 431. Then runInstall at line 441 writes a nearly identical message "${label} failed (exit ${code}). Aborting.\n" to stderr before exiting. On a failed install the user sees the same failure reported twice:

Example duplicated output

✗ Installing skills: foo → /path failed (exit 1)
<captured subprocess output>
Installing skills: foo → /path failed (exit 1). Aborting.

Suggested change
if(code!==0){
process.stderr.write(`${label} failed (exit${code}). Aborting.\n`);
process.exit(code);
if(code!==0){
process.exit(code);
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed in a2bcee3runInstall no longer writes the duplicate "Aborting" line; the failure message already comes from spinner.fail inside runInstallWithSpinner.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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)
packages/cli/src/cli.ts (1)

1246-1283: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add SIGTERM handler to the inline mount path for consistent signal cleanup.

The inline mount lifecycle registers only SIGINT, while the other execution path handles both SIGINT and SIGTERM. Without a SIGTERM listener, Node uses its default behavior (immediate exit), bypassing the sync and cleanup logic entirely. A SIGTERM signal during a session would skip syncBack() and leave the session directory behind.

Suggested refactor
- const sigintHandler = () => {+ const handleShutdownSignal = (signal: NodeJS.Signals) => {
if (!isSyncing) {
// Defensive: forward to child if it somehow didn't get the TTY signal.
// No spinner side effects while the harness owns the screen.
if (activeChild && !activeChild.killed && activeChild.exitCode === null) {
try {
- activeChild.kill('SIGINT');+ activeChild.kill(signal);
} catch {
/* ignore — child likely exited between checks */
}
}
return;
}
abortPresses += 1;
if (abortPresses === 1) {
if (syncSpinner) {
syncSpinner.text =
'Aborting sync — partial changes will be propagated. (Ctrl-C again to force quit)';
}
shutdownController.abort();
return;
}
...
};
- process.on('SIGINT', sigintHandler);+ const sigintHandler = () => handleShutdownSignal('SIGINT');+ const sigtermHandler = () => handleShutdownSignal('SIGTERM');+ process.on('SIGINT', sigintHandler);+ process.on('SIGTERM', sigtermHandler);
...
process.removeListener('SIGINT', sigintHandler);
+ process.removeListener('SIGTERM', sigtermHandler);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/cli/src/cli.ts` around lines 1246 - 1283, The inline mount path only
registers sigintHandler for SIGINT so SIGTERM bypasses cleanup; add a SIGTERM
listener that invokes the same sigintHandler to ensure syncSpinner,
shutdownController.abort(), rmSync(sessionRoot, ...) and process.exit handling
run for both signals. Locate the sigintHandler function and replace or augment
the process.on('SIGINT', sigintHandler) registration to also register
process.on('SIGTERM', sigintHandler) (or otherwise wire SIGTERM to call
sigintHandler) so both signals trigger the same teardown logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/cli/src/cli.ts`:
- Around line 411-433: In runInstallWithSpinner, increase spawnSync's
stdout/stderr buffer by adding maxBuffer: 16 * 1024 * 1024 to the options and
include any spawn error text by appending res.error?.message (if present) to the
computed output so buffer exhaustion or spawn errors are visible; keep using
subprocessExitCode(res) but ensure the final output string includes
res.error?.message (and is written to stderr on failure) so ENOBUFS or other
spawn errors are distinguishable from normal command exit codes.
---
Outside diff comments:
In `@packages/cli/src/cli.ts`:
- Around line 1246-1283: The inline mount path only registers sigintHandler for
SIGINT so SIGTERM bypasses cleanup; add a SIGTERM listener that invokes the same
sigintHandler to ensure syncSpinner, shutdownController.abort(),
rmSync(sessionRoot, ...) and process.exit handling run for both signals. Locate
the sigintHandler function and replace or augment the process.on('SIGINT',
sigintHandler) registration to also register process.on('SIGTERM',
sigintHandler) (or otherwise wire SIGTERM to call sigintHandler) so both signals
trigger the same teardown logic.
🪄 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 Plus

Run ID: 22410145-6940-4768-9210-d880b3f5908b

📥 Commits

Reviewing files that changed from the base of the PR and between 3fee405 and 6b651cc.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (3)
  • packages/cli/package.json
  • packages/cli/src/cli.test.ts
  • packages/cli/src/cli.ts

Comment threadpackages/cli/src/cli.ts
- Drop duplicate failure messages on install error: spinner.fail in
runInstallWithSpinner already prints the failure line, so both runInstall
and the InstallCommandError catch were re-printing nearly the same text.
- Stop double-delivering SIGINT to the harness: with stdio: 'inherit' the
child shares the parent's process group, so Ctrl-C at the TTY already
reaches it. Forwarding via child.kill('SIGINT') from our handler
delivered a second SIGINT, which can break harnesses that escalate on
repeated interrupts (e.g. claude treats 1st = cancel, 2nd = quit). The
handler now stays a true no-op while the child runs, only there to
suppress Node's default exit-on-SIGINT.
- Raise spawnSync maxBuffer to 100 MiB for the captured installer output.
Default is 1 MiB; verbose `npx prpm install` / `npx skills add` runs
can blow past it and have spawnSync kill the child with ENOBUFS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for freeto 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

@willwashburn
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Show spinners during persona session install and exit sync - #59

Merged
willwashburn merged 2 commits into
mainfrom
fix-cli-launch-spinners
May 8, 2026
Merged

Show spinners during persona session install and exit sync#59
willwashburn merged 2 commits into
mainfrom
fix-cli-launch-spinners

Conversation

@willwashburn

Copy link
Copy Markdown
Member

Summary

  • Wrap skill install (npx prpm install / npx skills add) in an "Installing skills…" ora spinner with stdio captured. Successful runs collapse to a single ✓ line; failures dump the buffered output after spinner.fail. Replaces the prior raw output that looked broken at session start.
  • Inline the mount lifecycle (formerly launchOnMount) so the sync-back spinner appears the moment the persona child exits — not just when the user presses Ctrl-C. Exiting via /exit no longer looks like a silent hang. Abort reduced from 3 presses to 2 (1st press during sync aborts immediately; 2nd force-quits and rms the session dir).
  • Bump @relayfile/local-mount from ^0.6.14 to ^0.6.15.
  • Isolate AGENT_WORKFORCE_HOME in the --version test so the load-time local-persona scan can't surface warnings from developer-local state.

Test plan

  • pnpm typecheck (after a fresh workspace pnpm build — workload-router's generated types must be up to date)
  • pnpm test from packages/cli — 126/126 pass
  • Manual: launch a persona, watch the "Installing skills…" spinner during start
  • Manual: /exit a persona session, confirm the "Syncing session changes back…" spinner appears immediately and Ctrl-C aborts on first press

🤖 Generated with Claude Code

Two visible UX fixes for the CLI persona launch lifecycle:
- Skill install now runs behind an "Installing skills…" ora spinner with
stdio captured. On success it collapses to a single line; on failure the
buffered output is dumped after spinner.fail so the user still sees what
broke. Replaces the prior raw `npx prpm install` / `npx skills add`
output that looked broken to the eye.
- Exiting a persona session via /exit no longer hangs silently while
relayfile syncs back. The mount lifecycle (createMount → onBeforeLaunch →
startAutoSync → spawn → autoSync.stop → syncBack → cleanup) is now
inlined so we own the gap between child-exit and finalize, and the sync
spinner now starts the moment the child closes — not just on Ctrl-C.
Reduced abort to 2 presses (was 3): 1st press during sync = abort
immediately; 2nd press = force-quit and rm session dir.
Also bumps @relayfile/local-mount from ^0.6.14 to ^0.6.15, and isolates
AGENT_WORKFORCE_HOME in the --version test so the load-time persona scan
can't surface unrelated warnings from the developer's own ~/.agentworkforce.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented May 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 79393cb4-a520-400a-87ce-3036a485ac8a

📥 Commits

Reviewing files that changed from the base of the PR and between 6b651cc and a2bcee3.

📒 Files selected for processing (1)
  • packages/cli/src/cli.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/cli/src/cli.ts

📝 Walkthrough

Walkthrough

The CLI inlines the @relayfile/local-mount lifecycle (createMount replaces launchOnMount), reworks SIGINT and teardown behavior, introduces spinner-backed install helpers, isolates the --version test environment, and bumps the dependency to ^0.6.15.

Changes

Mount-mode execution and install UI refactor

Layer / File(s)Summary
Dependency update and import refactor
packages/cli/package.json, packages/cli/src/cli.ts
@relayfile/local-mount bumped ^0.6.14^0.6.15; import switched from launchOnMount to createMount with AutoSyncHandle.
Install-time UI spinner helpers
packages/cli/src/cli.ts
Added runInstallWithSpinner capturing subprocess stdout/stderr and showing an ora spinner; runInstall and runInstallOrThrow updated to use the spinner helper.
Mount-mode execution refactor with createMount
packages/cli/src/cli.ts
Mount-mode inlines createMount: configures git skip-worktree, optionally runs deferred installs inside the mount, materializes config/sidecar files, records launch metadata, starts autosync, spawns the harness in the mount, then stops autosync and performs explicit syncBack with a spinner; SIGINT forwards to child, aborts on first Ctrl-C during syncing, and forces exit on subsequent Ctrl-C.
Teardown updates
packages/cli/src/cli.ts
Teardown stops the syncing spinner, best-effort stops autosync on error, calls handle.cleanup(), stops launch-metadata, removes SIGINT listener, and avoids duplicate cleanup when installs ran inside mount.
Test isolation
packages/cli/src/cli.test.ts
--version test creates temporary isolated AGENT_WORKFORCE_HOME with personas/, passes it into runCliCapturingStderr, and removes the directory in a finally block.

Sequence Diagram(s)

sequenceDiagram
participant User
participant CLI
participant createMount
participant Installer
participant Harness
participant AutoSync
participant SyncBack
User->>CLI: run CLI in mount-mode
CLI->>createMount: create mount
createMount-->>CLI: mount handle
alt deferred installs
CLI->>Installer: run installs inside mount
Installer-->>CLI: install results
end
CLI->>Harness: spawn child inside mount
Harness-->>CLI: child exits
CLI->>AutoSync: stop autosync (if present)
CLI->>SyncBack: run syncBack with spinner
SyncBack-->>CLI: sync results
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I hopped into mount-mode, bright and spry,
Spinners spinning as installs flew by,
Signals forwarded, syncs that mend,
Tests tucked safe until the end,
A tiny leap — the CLI says hi!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 60.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 nameStatusExplanation
Title check✅ PassedThe title directly matches the main changes: adding spinners during persona session install and exit sync, which are the primary UX improvements in this PR.
Description check✅ PassedThe description comprehensively covers all major changes in the changeset, including skill install spinners, mount lifecycle inlining, dependency bumps, and test isolation, with clear context and rationale.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck 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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-cli-launch-spinners

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

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:6b651cc321

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadpackages/cli/src/cli.ts Outdated
Comment on lines +1250 to +1253
if (activeChild && !activeChild.killed && activeChild.exitCode === null) {
try {
activeChild.kill('SIGINT');
} catch {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid sending SIGINT to child twice during interactive runs

In the non-syncing path this handler always calls activeChild.kill('SIGINT'), but with spawn(..., { stdio: 'inherit' }) the child is already in the foreground process group and receives the terminal Ctrl-C directly. That means each Ctrl-C can deliver two SIGINTs to the harness, which can skip graceful first-interrupt behavior and force premature termination for CLIs that escalate on repeated SIGINT.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good catch — fixed in a2bcee3. The handler is now a true no-op while the child runs (its only purpose is suppressing Node's default exit-on-SIGINT). Removed the activeChild.kill('SIGINT') forward and the activeChild ref. Updated the surrounding comment to call out why the no-op is required.

Comment on lines +419 to +423
const res = spawnSync(bin, args, {
stdio: ['ignore', 'pipe', 'pipe'],
shell: false,
encoding: 'utf8',
...(cwd ? { cwd } : {})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Raise spawnSync maxBuffer when capturing installer output

This install path now captures stdout/stderr via spawnSync pipes but does not set maxBuffer, so Node applies its default per-stream buffer limit (about 1 MiB). Verbose install commands can exceed that limit, causing spawnSync to terminate the subprocess (ENOBUFS/SIGTERM) and report a non-zero exit even when the install would otherwise complete, which is a regression from the previous inherited-stdio behavior.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed in a2bcee3spawnSync now sets maxBuffer: 100 * 1024 * 1024 (100 MiB), well past anything these installers print in practice. Comment in-place explains why.

@devin-ai-integrationdevin-ai-integrationBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

View 4 additional findings in Devin Review.

Open in Devin Review

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Duplicate failure message on install error in runInstallOrThrow path

Same pattern as runInstall: runInstallWithSpinner already outputs spinner.fail(...) with the failure message at packages/cli/src/cli.ts:430. When runInstallOrThrow throws InstallCommandError, the catch block at line 1379 writes ${err.message}. Aborting.\n — and err.message is "${label} failed (exit ${exitCode})" (set at packages/cli/src/cli.ts:454), producing a second nearly identical error line on stderr.

(Refers to lines 1378-1380)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed in a2bcee3 — the InstallCommandError catch no longer re-prints err.message. The failure line is shown once via spinner.fail inside runInstallWithSpinner.

Comment threadpackages/cli/src/cli.ts Outdated
Comment on lines 440 to 442
if (code !== 0) {
process.stderr.write(`${label} failed (exit ${code}). Aborting.\n`);
process.exit(code);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Duplicate failure message on install error in runInstall

runInstallWithSpinner already writes a failure message via spinner.fail(${label} failed (exit ${code})) at packages/cli/src/cli.ts:430, and dumps captured output at line 431. Then runInstall at line 441 writes a nearly identical message "${label} failed (exit ${code}). Aborting.\n" to stderr before exiting. On a failed install the user sees the same failure reported twice:

Example duplicated output

✗ Installing skills: foo → /path failed (exit 1)
<captured subprocess output>
Installing skills: foo → /path failed (exit 1). Aborting.

Suggested change
if(code!==0){
process.stderr.write(`${label} failed (exit${code}). Aborting.\n`);
process.exit(code);
if(code!==0){
process.exit(code);
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed in a2bcee3runInstall no longer writes the duplicate "Aborting" line; the failure message already comes from spinner.fail inside runInstallWithSpinner.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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)
packages/cli/src/cli.ts (1)

1246-1283: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add SIGTERM handler to the inline mount path for consistent signal cleanup.

The inline mount lifecycle registers only SIGINT, while the other execution path handles both SIGINT and SIGTERM. Without a SIGTERM listener, Node uses its default behavior (immediate exit), bypassing the sync and cleanup logic entirely. A SIGTERM signal during a session would skip syncBack() and leave the session directory behind.

Suggested refactor
- const sigintHandler = () => {+ const handleShutdownSignal = (signal: NodeJS.Signals) => {
if (!isSyncing) {
// Defensive: forward to child if it somehow didn't get the TTY signal.
// No spinner side effects while the harness owns the screen.
if (activeChild && !activeChild.killed && activeChild.exitCode === null) {
try {
- activeChild.kill('SIGINT');+ activeChild.kill(signal);
} catch {
/* ignore — child likely exited between checks */
}
}
return;
}
abortPresses += 1;
if (abortPresses === 1) {
if (syncSpinner) {
syncSpinner.text =
'Aborting sync — partial changes will be propagated. (Ctrl-C again to force quit)';
}
shutdownController.abort();
return;
}
...
};
- process.on('SIGINT', sigintHandler);+ const sigintHandler = () => handleShutdownSignal('SIGINT');+ const sigtermHandler = () => handleShutdownSignal('SIGTERM');+ process.on('SIGINT', sigintHandler);+ process.on('SIGTERM', sigtermHandler);
...
process.removeListener('SIGINT', sigintHandler);
+ process.removeListener('SIGTERM', sigtermHandler);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/cli/src/cli.ts` around lines 1246 - 1283, The inline mount path only
registers sigintHandler for SIGINT so SIGTERM bypasses cleanup; add a SIGTERM
listener that invokes the same sigintHandler to ensure syncSpinner,
shutdownController.abort(), rmSync(sessionRoot, ...) and process.exit handling
run for both signals. Locate the sigintHandler function and replace or augment
the process.on('SIGINT', sigintHandler) registration to also register
process.on('SIGTERM', sigintHandler) (or otherwise wire SIGTERM to call
sigintHandler) so both signals trigger the same teardown logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/cli/src/cli.ts`:
- Around line 411-433: In runInstallWithSpinner, increase spawnSync's
stdout/stderr buffer by adding maxBuffer: 16 * 1024 * 1024 to the options and
include any spawn error text by appending res.error?.message (if present) to the
computed output so buffer exhaustion or spawn errors are visible; keep using
subprocessExitCode(res) but ensure the final output string includes
res.error?.message (and is written to stderr on failure) so ENOBUFS or other
spawn errors are distinguishable from normal command exit codes.
---
Outside diff comments:
In `@packages/cli/src/cli.ts`:
- Around line 1246-1283: The inline mount path only registers sigintHandler for
SIGINT so SIGTERM bypasses cleanup; add a SIGTERM listener that invokes the same
sigintHandler to ensure syncSpinner, shutdownController.abort(),
rmSync(sessionRoot, ...) and process.exit handling run for both signals. Locate
the sigintHandler function and replace or augment the process.on('SIGINT',
sigintHandler) registration to also register process.on('SIGTERM',
sigintHandler) (or otherwise wire SIGTERM to call sigintHandler) so both signals
trigger the same teardown logic.
🪄 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 Plus

Run ID: 22410145-6940-4768-9210-d880b3f5908b

📥 Commits

Reviewing files that changed from the base of the PR and between 3fee405 and 6b651cc.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (3)
  • packages/cli/package.json
  • packages/cli/src/cli.test.ts
  • packages/cli/src/cli.ts

Comment threadpackages/cli/src/cli.ts
- Drop duplicate failure messages on install error: spinner.fail in
runInstallWithSpinner already prints the failure line, so both runInstall
and the InstallCommandError catch were re-printing nearly the same text.
- Stop double-delivering SIGINT to the harness: with stdio: 'inherit' the
child shares the parent's process group, so Ctrl-C at the TTY already
reaches it. Forwarding via child.kill('SIGINT') from our handler
delivered a second SIGINT, which can break harnesses that escalate on
repeated interrupts (e.g. claude treats 1st = cancel, 2nd = quit). The
handler now stays a true no-op while the child runs, only there to
suppress Node's default exit-on-SIGINT.
- Raise spawnSync maxBuffer to 100 MiB for the captured installer output.
Default is 1 MiB; verbose `npx prpm install` / `npx skills add` runs
can blow past it and have spawnSync kill the child with ENOBUFS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for freeto 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

@willwashburn
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Show spinners during persona session install and exit sync - #59

Merged
willwashburn merged 2 commits into
mainfrom
fix-cli-launch-spinners
May 8, 2026
Merged

Show spinners during persona session install and exit sync#59
willwashburn merged 2 commits into
mainfrom
fix-cli-launch-spinners

Conversation

@willwashburn

Copy link
Copy Markdown
Member

Summary

  • Wrap skill install (npx prpm install / npx skills add) in an "Installing skills…" ora spinner with stdio captured. Successful runs collapse to a single ✓ line; failures dump the buffered output after spinner.fail. Replaces the prior raw output that looked broken at session start.
  • Inline the mount lifecycle (formerly launchOnMount) so the sync-back spinner appears the moment the persona child exits — not just when the user presses Ctrl-C. Exiting via /exit no longer looks like a silent hang. Abort reduced from 3 presses to 2 (1st press during sync aborts immediately; 2nd force-quits and rms the session dir).
  • Bump @relayfile/local-mount from ^0.6.14 to ^0.6.15.
  • Isolate AGENT_WORKFORCE_HOME in the --version test so the load-time local-persona scan can't surface warnings from developer-local state.

Test plan

  • pnpm typecheck (after a fresh workspace pnpm build — workload-router's generated types must be up to date)
  • pnpm test from packages/cli — 126/126 pass
  • Manual: launch a persona, watch the "Installing skills…" spinner during start
  • Manual: /exit a persona session, confirm the "Syncing session changes back…" spinner appears immediately and Ctrl-C aborts on first press

🤖 Generated with Claude Code

Two visible UX fixes for the CLI persona launch lifecycle:
- Skill install now runs behind an "Installing skills…" ora spinner with
stdio captured. On success it collapses to a single line; on failure the
buffered output is dumped after spinner.fail so the user still sees what
broke. Replaces the prior raw `npx prpm install` / `npx skills add`
output that looked broken to the eye.
- Exiting a persona session via /exit no longer hangs silently while
relayfile syncs back. The mount lifecycle (createMount → onBeforeLaunch →
startAutoSync → spawn → autoSync.stop → syncBack → cleanup) is now
inlined so we own the gap between child-exit and finalize, and the sync
spinner now starts the moment the child closes — not just on Ctrl-C.
Reduced abort to 2 presses (was 3): 1st press during sync = abort
immediately; 2nd press = force-quit and rm session dir.
Also bumps @relayfile/local-mount from ^0.6.14 to ^0.6.15, and isolates
AGENT_WORKFORCE_HOME in the --version test so the load-time persona scan
can't surface unrelated warnings from the developer's own ~/.agentworkforce.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented May 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 79393cb4-a520-400a-87ce-3036a485ac8a

📥 Commits

Reviewing files that changed from the base of the PR and between 6b651cc and a2bcee3.

📒 Files selected for processing (1)
  • packages/cli/src/cli.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/cli/src/cli.ts

📝 Walkthrough

Walkthrough

The CLI inlines the @relayfile/local-mount lifecycle (createMount replaces launchOnMount), reworks SIGINT and teardown behavior, introduces spinner-backed install helpers, isolates the --version test environment, and bumps the dependency to ^0.6.15.

Changes

Mount-mode execution and install UI refactor

Layer / File(s)Summary
Dependency update and import refactor
packages/cli/package.json, packages/cli/src/cli.ts
@relayfile/local-mount bumped ^0.6.14^0.6.15; import switched from launchOnMount to createMount with AutoSyncHandle.
Install-time UI spinner helpers
packages/cli/src/cli.ts
Added runInstallWithSpinner capturing subprocess stdout/stderr and showing an ora spinner; runInstall and runInstallOrThrow updated to use the spinner helper.
Mount-mode execution refactor with createMount
packages/cli/src/cli.ts
Mount-mode inlines createMount: configures git skip-worktree, optionally runs deferred installs inside the mount, materializes config/sidecar files, records launch metadata, starts autosync, spawns the harness in the mount, then stops autosync and performs explicit syncBack with a spinner; SIGINT forwards to child, aborts on first Ctrl-C during syncing, and forces exit on subsequent Ctrl-C.
Teardown updates
packages/cli/src/cli.ts
Teardown stops the syncing spinner, best-effort stops autosync on error, calls handle.cleanup(), stops launch-metadata, removes SIGINT listener, and avoids duplicate cleanup when installs ran inside mount.
Test isolation
packages/cli/src/cli.test.ts
--version test creates temporary isolated AGENT_WORKFORCE_HOME with personas/, passes it into runCliCapturingStderr, and removes the directory in a finally block.

Sequence Diagram(s)

sequenceDiagram
participant User
participant CLI
participant createMount
participant Installer
participant Harness
participant AutoSync
participant SyncBack
User->>CLI: run CLI in mount-mode
CLI->>createMount: create mount
createMount-->>CLI: mount handle
alt deferred installs
CLI->>Installer: run installs inside mount
Installer-->>CLI: install results
end
CLI->>Harness: spawn child inside mount
Harness-->>CLI: child exits
CLI->>AutoSync: stop autosync (if present)
CLI->>SyncBack: run syncBack with spinner
SyncBack-->>CLI: sync results
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I hopped into mount-mode, bright and spry,
Spinners spinning as installs flew by,
Signals forwarded, syncs that mend,
Tests tucked safe until the end,
A tiny leap — the CLI says hi!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 60.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 nameStatusExplanation
Title check✅ PassedThe title directly matches the main changes: adding spinners during persona session install and exit sync, which are the primary UX improvements in this PR.
Description check✅ PassedThe description comprehensively covers all major changes in the changeset, including skill install spinners, mount lifecycle inlining, dependency bumps, and test isolation, with clear context and rationale.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck 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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-cli-launch-spinners

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

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:6b651cc321

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadpackages/cli/src/cli.ts Outdated
Comment on lines +1250 to +1253
if (activeChild && !activeChild.killed && activeChild.exitCode === null) {
try {
activeChild.kill('SIGINT');
} catch {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid sending SIGINT to child twice during interactive runs

In the non-syncing path this handler always calls activeChild.kill('SIGINT'), but with spawn(..., { stdio: 'inherit' }) the child is already in the foreground process group and receives the terminal Ctrl-C directly. That means each Ctrl-C can deliver two SIGINTs to the harness, which can skip graceful first-interrupt behavior and force premature termination for CLIs that escalate on repeated SIGINT.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good catch — fixed in a2bcee3. The handler is now a true no-op while the child runs (its only purpose is suppressing Node's default exit-on-SIGINT). Removed the activeChild.kill('SIGINT') forward and the activeChild ref. Updated the surrounding comment to call out why the no-op is required.

Comment on lines +419 to +423
const res = spawnSync(bin, args, {
stdio: ['ignore', 'pipe', 'pipe'],
shell: false,
encoding: 'utf8',
...(cwd ? { cwd } : {})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Raise spawnSync maxBuffer when capturing installer output

This install path now captures stdout/stderr via spawnSync pipes but does not set maxBuffer, so Node applies its default per-stream buffer limit (about 1 MiB). Verbose install commands can exceed that limit, causing spawnSync to terminate the subprocess (ENOBUFS/SIGTERM) and report a non-zero exit even when the install would otherwise complete, which is a regression from the previous inherited-stdio behavior.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed in a2bcee3spawnSync now sets maxBuffer: 100 * 1024 * 1024 (100 MiB), well past anything these installers print in practice. Comment in-place explains why.

@devin-ai-integrationdevin-ai-integrationBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

View 4 additional findings in Devin Review.

Open in Devin Review

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Duplicate failure message on install error in runInstallOrThrow path

Same pattern as runInstall: runInstallWithSpinner already outputs spinner.fail(...) with the failure message at packages/cli/src/cli.ts:430. When runInstallOrThrow throws InstallCommandError, the catch block at line 1379 writes ${err.message}. Aborting.\n — and err.message is "${label} failed (exit ${exitCode})" (set at packages/cli/src/cli.ts:454), producing a second nearly identical error line on stderr.

(Refers to lines 1378-1380)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed in a2bcee3 — the InstallCommandError catch no longer re-prints err.message. The failure line is shown once via spinner.fail inside runInstallWithSpinner.

Comment threadpackages/cli/src/cli.ts Outdated
Comment on lines 440 to 442
if (code !== 0) {
process.stderr.write(`${label} failed (exit ${code}). Aborting.\n`);
process.exit(code);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Duplicate failure message on install error in runInstall

runInstallWithSpinner already writes a failure message via spinner.fail(${label} failed (exit ${code})) at packages/cli/src/cli.ts:430, and dumps captured output at line 431. Then runInstall at line 441 writes a nearly identical message "${label} failed (exit ${code}). Aborting.\n" to stderr before exiting. On a failed install the user sees the same failure reported twice:

Example duplicated output

✗ Installing skills: foo → /path failed (exit 1)
<captured subprocess output>
Installing skills: foo → /path failed (exit 1). Aborting.

Suggested change
if(code!==0){
process.stderr.write(`${label} failed (exit${code}). Aborting.\n`);
process.exit(code);
if(code!==0){
process.exit(code);
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed in a2bcee3runInstall no longer writes the duplicate "Aborting" line; the failure message already comes from spinner.fail inside runInstallWithSpinner.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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)
packages/cli/src/cli.ts (1)

1246-1283: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add SIGTERM handler to the inline mount path for consistent signal cleanup.

The inline mount lifecycle registers only SIGINT, while the other execution path handles both SIGINT and SIGTERM. Without a SIGTERM listener, Node uses its default behavior (immediate exit), bypassing the sync and cleanup logic entirely. A SIGTERM signal during a session would skip syncBack() and leave the session directory behind.

Suggested refactor
- const sigintHandler = () => {+ const handleShutdownSignal = (signal: NodeJS.Signals) => {
if (!isSyncing) {
// Defensive: forward to child if it somehow didn't get the TTY signal.
// No spinner side effects while the harness owns the screen.
if (activeChild && !activeChild.killed && activeChild.exitCode === null) {
try {
- activeChild.kill('SIGINT');+ activeChild.kill(signal);
} catch {
/* ignore — child likely exited between checks */
}
}
return;
}
abortPresses += 1;
if (abortPresses === 1) {
if (syncSpinner) {
syncSpinner.text =
'Aborting sync — partial changes will be propagated. (Ctrl-C again to force quit)';
}
shutdownController.abort();
return;
}
...
};
- process.on('SIGINT', sigintHandler);+ const sigintHandler = () => handleShutdownSignal('SIGINT');+ const sigtermHandler = () => handleShutdownSignal('SIGTERM');+ process.on('SIGINT', sigintHandler);+ process.on('SIGTERM', sigtermHandler);
...
process.removeListener('SIGINT', sigintHandler);
+ process.removeListener('SIGTERM', sigtermHandler);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/cli/src/cli.ts` around lines 1246 - 1283, The inline mount path only
registers sigintHandler for SIGINT so SIGTERM bypasses cleanup; add a SIGTERM
listener that invokes the same sigintHandler to ensure syncSpinner,
shutdownController.abort(), rmSync(sessionRoot, ...) and process.exit handling
run for both signals. Locate the sigintHandler function and replace or augment
the process.on('SIGINT', sigintHandler) registration to also register
process.on('SIGTERM', sigintHandler) (or otherwise wire SIGTERM to call
sigintHandler) so both signals trigger the same teardown logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/cli/src/cli.ts`:
- Around line 411-433: In runInstallWithSpinner, increase spawnSync's
stdout/stderr buffer by adding maxBuffer: 16 * 1024 * 1024 to the options and
include any spawn error text by appending res.error?.message (if present) to the
computed output so buffer exhaustion or spawn errors are visible; keep using
subprocessExitCode(res) but ensure the final output string includes
res.error?.message (and is written to stderr on failure) so ENOBUFS or other
spawn errors are distinguishable from normal command exit codes.
---
Outside diff comments:
In `@packages/cli/src/cli.ts`:
- Around line 1246-1283: The inline mount path only registers sigintHandler for
SIGINT so SIGTERM bypasses cleanup; add a SIGTERM listener that invokes the same
sigintHandler to ensure syncSpinner, shutdownController.abort(),
rmSync(sessionRoot, ...) and process.exit handling run for both signals. Locate
the sigintHandler function and replace or augment the process.on('SIGINT',
sigintHandler) registration to also register process.on('SIGTERM',
sigintHandler) (or otherwise wire SIGTERM to call sigintHandler) so both signals
trigger the same teardown logic.
🪄 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 Plus

Run ID: 22410145-6940-4768-9210-d880b3f5908b

📥 Commits

Reviewing files that changed from the base of the PR and between 3fee405 and 6b651cc.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (3)
  • packages/cli/package.json
  • packages/cli/src/cli.test.ts
  • packages/cli/src/cli.ts

Comment threadpackages/cli/src/cli.ts
- Drop duplicate failure messages on install error: spinner.fail in
runInstallWithSpinner already prints the failure line, so both runInstall
and the InstallCommandError catch were re-printing nearly the same text.
- Stop double-delivering SIGINT to the harness: with stdio: 'inherit' the
child shares the parent's process group, so Ctrl-C at the TTY already
reaches it. Forwarding via child.kill('SIGINT') from our handler
delivered a second SIGINT, which can break harnesses that escalate on
repeated interrupts (e.g. claude treats 1st = cancel, 2nd = quit). The
handler now stays a true no-op while the child runs, only there to
suppress Node's default exit-on-SIGINT.
- Raise spawnSync maxBuffer to 100 MiB for the captured installer output.
Default is 1 MiB; verbose `npx prpm install` / `npx skills add` runs
can blow past it and have spawnSync kill the child with ENOBUFS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for freeto 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

@willwashburn
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Show spinners during persona session install and exit sync - #59

Merged
willwashburn merged 2 commits into
mainfrom
fix-cli-launch-spinners
May 8, 2026
Merged

Show spinners during persona session install and exit sync#59
willwashburn merged 2 commits into
mainfrom
fix-cli-launch-spinners

Conversation

@willwashburn

Copy link
Copy Markdown
Member

Summary

  • Wrap skill install (npx prpm install / npx skills add) in an "Installing skills…" ora spinner with stdio captured. Successful runs collapse to a single ✓ line; failures dump the buffered output after spinner.fail. Replaces the prior raw output that looked broken at session start.
  • Inline the mount lifecycle (formerly launchOnMount) so the sync-back spinner appears the moment the persona child exits — not just when the user presses Ctrl-C. Exiting via /exit no longer looks like a silent hang. Abort reduced from 3 presses to 2 (1st press during sync aborts immediately; 2nd force-quits and rms the session dir).
  • Bump @relayfile/local-mount from ^0.6.14 to ^0.6.15.
  • Isolate AGENT_WORKFORCE_HOME in the --version test so the load-time local-persona scan can't surface warnings from developer-local state.

Test plan

  • pnpm typecheck (after a fresh workspace pnpm build — workload-router's generated types must be up to date)
  • pnpm test from packages/cli — 126/126 pass
  • Manual: launch a persona, watch the "Installing skills…" spinner during start
  • Manual: /exit a persona session, confirm the "Syncing session changes back…" spinner appears immediately and Ctrl-C aborts on first press

🤖 Generated with Claude Code

Two visible UX fixes for the CLI persona launch lifecycle:
- Skill install now runs behind an "Installing skills…" ora spinner with
stdio captured. On success it collapses to a single line; on failure the
buffered output is dumped after spinner.fail so the user still sees what
broke. Replaces the prior raw `npx prpm install` / `npx skills add`
output that looked broken to the eye.
- Exiting a persona session via /exit no longer hangs silently while
relayfile syncs back. The mount lifecycle (createMount → onBeforeLaunch →
startAutoSync → spawn → autoSync.stop → syncBack → cleanup) is now
inlined so we own the gap between child-exit and finalize, and the sync
spinner now starts the moment the child closes — not just on Ctrl-C.
Reduced abort to 2 presses (was 3): 1st press during sync = abort
immediately; 2nd press = force-quit and rm session dir.
Also bumps @relayfile/local-mount from ^0.6.14 to ^0.6.15, and isolates
AGENT_WORKFORCE_HOME in the --version test so the load-time persona scan
can't surface unrelated warnings from the developer's own ~/.agentworkforce.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented May 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 79393cb4-a520-400a-87ce-3036a485ac8a

📥 Commits

Reviewing files that changed from the base of the PR and between 6b651cc and a2bcee3.

📒 Files selected for processing (1)
  • packages/cli/src/cli.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/cli/src/cli.ts

📝 Walkthrough

Walkthrough

The CLI inlines the @relayfile/local-mount lifecycle (createMount replaces launchOnMount), reworks SIGINT and teardown behavior, introduces spinner-backed install helpers, isolates the --version test environment, and bumps the dependency to ^0.6.15.

Changes

Mount-mode execution and install UI refactor

Layer / File(s)Summary
Dependency update and import refactor
packages/cli/package.json, packages/cli/src/cli.ts
@relayfile/local-mount bumped ^0.6.14^0.6.15; import switched from launchOnMount to createMount with AutoSyncHandle.
Install-time UI spinner helpers
packages/cli/src/cli.ts
Added runInstallWithSpinner capturing subprocess stdout/stderr and showing an ora spinner; runInstall and runInstallOrThrow updated to use the spinner helper.
Mount-mode execution refactor with createMount
packages/cli/src/cli.ts
Mount-mode inlines createMount: configures git skip-worktree, optionally runs deferred installs inside the mount, materializes config/sidecar files, records launch metadata, starts autosync, spawns the harness in the mount, then stops autosync and performs explicit syncBack with a spinner; SIGINT forwards to child, aborts on first Ctrl-C during syncing, and forces exit on subsequent Ctrl-C.
Teardown updates
packages/cli/src/cli.ts
Teardown stops the syncing spinner, best-effort stops autosync on error, calls handle.cleanup(), stops launch-metadata, removes SIGINT listener, and avoids duplicate cleanup when installs ran inside mount.
Test isolation
packages/cli/src/cli.test.ts
--version test creates temporary isolated AGENT_WORKFORCE_HOME with personas/, passes it into runCliCapturingStderr, and removes the directory in a finally block.

Sequence Diagram(s)

sequenceDiagram
participant User
participant CLI
participant createMount
participant Installer
participant Harness
participant AutoSync
participant SyncBack
User->>CLI: run CLI in mount-mode
CLI->>createMount: create mount
createMount-->>CLI: mount handle
alt deferred installs
CLI->>Installer: run installs inside mount
Installer-->>CLI: install results
end
CLI->>Harness: spawn child inside mount
Harness-->>CLI: child exits
CLI->>AutoSync: stop autosync (if present)
CLI->>SyncBack: run syncBack with spinner
SyncBack-->>CLI: sync results
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I hopped into mount-mode, bright and spry,
Spinners spinning as installs flew by,
Signals forwarded, syncs that mend,
Tests tucked safe until the end,
A tiny leap — the CLI says hi!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 60.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 nameStatusExplanation
Title check✅ PassedThe title directly matches the main changes: adding spinners during persona session install and exit sync, which are the primary UX improvements in this PR.
Description check✅ PassedThe description comprehensively covers all major changes in the changeset, including skill install spinners, mount lifecycle inlining, dependency bumps, and test isolation, with clear context and rationale.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck 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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-cli-launch-spinners

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

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:6b651cc321

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadpackages/cli/src/cli.ts Outdated
Comment on lines +1250 to +1253
if (activeChild && !activeChild.killed && activeChild.exitCode === null) {
try {
activeChild.kill('SIGINT');
} catch {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid sending SIGINT to child twice during interactive runs

In the non-syncing path this handler always calls activeChild.kill('SIGINT'), but with spawn(..., { stdio: 'inherit' }) the child is already in the foreground process group and receives the terminal Ctrl-C directly. That means each Ctrl-C can deliver two SIGINTs to the harness, which can skip graceful first-interrupt behavior and force premature termination for CLIs that escalate on repeated SIGINT.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good catch — fixed in a2bcee3. The handler is now a true no-op while the child runs (its only purpose is suppressing Node's default exit-on-SIGINT). Removed the activeChild.kill('SIGINT') forward and the activeChild ref. Updated the surrounding comment to call out why the no-op is required.

Comment on lines +419 to +423
const res = spawnSync(bin, args, {
stdio: ['ignore', 'pipe', 'pipe'],
shell: false,
encoding: 'utf8',
...(cwd ? { cwd } : {})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Raise spawnSync maxBuffer when capturing installer output

This install path now captures stdout/stderr via spawnSync pipes but does not set maxBuffer, so Node applies its default per-stream buffer limit (about 1 MiB). Verbose install commands can exceed that limit, causing spawnSync to terminate the subprocess (ENOBUFS/SIGTERM) and report a non-zero exit even when the install would otherwise complete, which is a regression from the previous inherited-stdio behavior.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed in a2bcee3spawnSync now sets maxBuffer: 100 * 1024 * 1024 (100 MiB), well past anything these installers print in practice. Comment in-place explains why.

@devin-ai-integrationdevin-ai-integrationBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

View 4 additional findings in Devin Review.

Open in Devin Review

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Duplicate failure message on install error in runInstallOrThrow path

Same pattern as runInstall: runInstallWithSpinner already outputs spinner.fail(...) with the failure message at packages/cli/src/cli.ts:430. When runInstallOrThrow throws InstallCommandError, the catch block at line 1379 writes ${err.message}. Aborting.\n — and err.message is "${label} failed (exit ${exitCode})" (set at packages/cli/src/cli.ts:454), producing a second nearly identical error line on stderr.

(Refers to lines 1378-1380)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed in a2bcee3 — the InstallCommandError catch no longer re-prints err.message. The failure line is shown once via spinner.fail inside runInstallWithSpinner.

Comment threadpackages/cli/src/cli.ts Outdated
Comment on lines 440 to 442
if (code !== 0) {
process.stderr.write(`${label} failed (exit ${code}). Aborting.\n`);
process.exit(code);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Duplicate failure message on install error in runInstall

runInstallWithSpinner already writes a failure message via spinner.fail(${label} failed (exit ${code})) at packages/cli/src/cli.ts:430, and dumps captured output at line 431. Then runInstall at line 441 writes a nearly identical message "${label} failed (exit ${code}). Aborting.\n" to stderr before exiting. On a failed install the user sees the same failure reported twice:

Example duplicated output

✗ Installing skills: foo → /path failed (exit 1)
<captured subprocess output>
Installing skills: foo → /path failed (exit 1). Aborting.

Suggested change
if(code!==0){
process.stderr.write(`${label} failed (exit${code}). Aborting.\n`);
process.exit(code);
if(code!==0){
process.exit(code);
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed in a2bcee3runInstall no longer writes the duplicate "Aborting" line; the failure message already comes from spinner.fail inside runInstallWithSpinner.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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)
packages/cli/src/cli.ts (1)

1246-1283: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add SIGTERM handler to the inline mount path for consistent signal cleanup.

The inline mount lifecycle registers only SIGINT, while the other execution path handles both SIGINT and SIGTERM. Without a SIGTERM listener, Node uses its default behavior (immediate exit), bypassing the sync and cleanup logic entirely. A SIGTERM signal during a session would skip syncBack() and leave the session directory behind.

Suggested refactor
- const sigintHandler = () => {+ const handleShutdownSignal = (signal: NodeJS.Signals) => {
if (!isSyncing) {
// Defensive: forward to child if it somehow didn't get the TTY signal.
// No spinner side effects while the harness owns the screen.
if (activeChild && !activeChild.killed && activeChild.exitCode === null) {
try {
- activeChild.kill('SIGINT');+ activeChild.kill(signal);
} catch {
/* ignore — child likely exited between checks */
}
}
return;
}
abortPresses += 1;
if (abortPresses === 1) {
if (syncSpinner) {
syncSpinner.text =
'Aborting sync — partial changes will be propagated. (Ctrl-C again to force quit)';
}
shutdownController.abort();
return;
}
...
};
- process.on('SIGINT', sigintHandler);+ const sigintHandler = () => handleShutdownSignal('SIGINT');+ const sigtermHandler = () => handleShutdownSignal('SIGTERM');+ process.on('SIGINT', sigintHandler);+ process.on('SIGTERM', sigtermHandler);
...
process.removeListener('SIGINT', sigintHandler);
+ process.removeListener('SIGTERM', sigtermHandler);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/cli/src/cli.ts` around lines 1246 - 1283, The inline mount path only
registers sigintHandler for SIGINT so SIGTERM bypasses cleanup; add a SIGTERM
listener that invokes the same sigintHandler to ensure syncSpinner,
shutdownController.abort(), rmSync(sessionRoot, ...) and process.exit handling
run for both signals. Locate the sigintHandler function and replace or augment
the process.on('SIGINT', sigintHandler) registration to also register
process.on('SIGTERM', sigintHandler) (or otherwise wire SIGTERM to call
sigintHandler) so both signals trigger the same teardown logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/cli/src/cli.ts`:
- Around line 411-433: In runInstallWithSpinner, increase spawnSync's
stdout/stderr buffer by adding maxBuffer: 16 * 1024 * 1024 to the options and
include any spawn error text by appending res.error?.message (if present) to the
computed output so buffer exhaustion or spawn errors are visible; keep using
subprocessExitCode(res) but ensure the final output string includes
res.error?.message (and is written to stderr on failure) so ENOBUFS or other
spawn errors are distinguishable from normal command exit codes.
---
Outside diff comments:
In `@packages/cli/src/cli.ts`:
- Around line 1246-1283: The inline mount path only registers sigintHandler for
SIGINT so SIGTERM bypasses cleanup; add a SIGTERM listener that invokes the same
sigintHandler to ensure syncSpinner, shutdownController.abort(),
rmSync(sessionRoot, ...) and process.exit handling run for both signals. Locate
the sigintHandler function and replace or augment the process.on('SIGINT',
sigintHandler) registration to also register process.on('SIGTERM',
sigintHandler) (or otherwise wire SIGTERM to call sigintHandler) so both signals
trigger the same teardown logic.
🪄 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 Plus

Run ID: 22410145-6940-4768-9210-d880b3f5908b

📥 Commits

Reviewing files that changed from the base of the PR and between 3fee405 and 6b651cc.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (3)
  • packages/cli/package.json
  • packages/cli/src/cli.test.ts
  • packages/cli/src/cli.ts

Comment threadpackages/cli/src/cli.ts
- Drop duplicate failure messages on install error: spinner.fail in
runInstallWithSpinner already prints the failure line, so both runInstall
and the InstallCommandError catch were re-printing nearly the same text.
- Stop double-delivering SIGINT to the harness: with stdio: 'inherit' the
child shares the parent's process group, so Ctrl-C at the TTY already
reaches it. Forwarding via child.kill('SIGINT') from our handler
delivered a second SIGINT, which can break harnesses that escalate on
repeated interrupts (e.g. claude treats 1st = cancel, 2nd = quit). The
handler now stays a true no-op while the child runs, only there to
suppress Node's default exit-on-SIGINT.
- Raise spawnSync maxBuffer to 100 MiB for the captured installer output.
Default is 1 MiB; verbose `npx prpm install` / `npx skills add` runs
can blow past it and have spawnSync kill the child with ENOBUFS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for freeto 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

@willwashburn
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Show spinners during persona session install and exit sync - #59

Merged
willwashburn merged 2 commits into
mainfrom
fix-cli-launch-spinners
May 8, 2026
Merged

Show spinners during persona session install and exit sync#59
willwashburn merged 2 commits into
mainfrom
fix-cli-launch-spinners

Conversation

@willwashburn

Copy link
Copy Markdown
Member

Summary

  • Wrap skill install (npx prpm install / npx skills add) in an "Installing skills…" ora spinner with stdio captured. Successful runs collapse to a single ✓ line; failures dump the buffered output after spinner.fail. Replaces the prior raw output that looked broken at session start.
  • Inline the mount lifecycle (formerly launchOnMount) so the sync-back spinner appears the moment the persona child exits — not just when the user presses Ctrl-C. Exiting via /exit no longer looks like a silent hang. Abort reduced from 3 presses to 2 (1st press during sync aborts immediately; 2nd force-quits and rms the session dir).
  • Bump @relayfile/local-mount from ^0.6.14 to ^0.6.15.
  • Isolate AGENT_WORKFORCE_HOME in the --version test so the load-time local-persona scan can't surface warnings from developer-local state.

Test plan

  • pnpm typecheck (after a fresh workspace pnpm build — workload-router's generated types must be up to date)
  • pnpm test from packages/cli — 126/126 pass
  • Manual: launch a persona, watch the "Installing skills…" spinner during start
  • Manual: /exit a persona session, confirm the "Syncing session changes back…" spinner appears immediately and Ctrl-C aborts on first press

🤖 Generated with Claude Code

Two visible UX fixes for the CLI persona launch lifecycle:
- Skill install now runs behind an "Installing skills…" ora spinner with
stdio captured. On success it collapses to a single line; on failure the
buffered output is dumped after spinner.fail so the user still sees what
broke. Replaces the prior raw `npx prpm install` / `npx skills add`
output that looked broken to the eye.
- Exiting a persona session via /exit no longer hangs silently while
relayfile syncs back. The mount lifecycle (createMount → onBeforeLaunch →
startAutoSync → spawn → autoSync.stop → syncBack → cleanup) is now
inlined so we own the gap between child-exit and finalize, and the sync
spinner now starts the moment the child closes — not just on Ctrl-C.
Reduced abort to 2 presses (was 3): 1st press during sync = abort
immediately; 2nd press = force-quit and rm session dir.
Also bumps @relayfile/local-mount from ^0.6.14 to ^0.6.15, and isolates
AGENT_WORKFORCE_HOME in the --version test so the load-time persona scan
can't surface unrelated warnings from the developer's own ~/.agentworkforce.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented May 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 79393cb4-a520-400a-87ce-3036a485ac8a

📥 Commits

Reviewing files that changed from the base of the PR and between 6b651cc and a2bcee3.

📒 Files selected for processing (1)
  • packages/cli/src/cli.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/cli/src/cli.ts

📝 Walkthrough

Walkthrough

The CLI inlines the @relayfile/local-mount lifecycle (createMount replaces launchOnMount), reworks SIGINT and teardown behavior, introduces spinner-backed install helpers, isolates the --version test environment, and bumps the dependency to ^0.6.15.

Changes

Mount-mode execution and install UI refactor

Layer / File(s)Summary
Dependency update and import refactor
packages/cli/package.json, packages/cli/src/cli.ts
@relayfile/local-mount bumped ^0.6.14^0.6.15; import switched from launchOnMount to createMount with AutoSyncHandle.
Install-time UI spinner helpers
packages/cli/src/cli.ts
Added runInstallWithSpinner capturing subprocess stdout/stderr and showing an ora spinner; runInstall and runInstallOrThrow updated to use the spinner helper.
Mount-mode execution refactor with createMount
packages/cli/src/cli.ts
Mount-mode inlines createMount: configures git skip-worktree, optionally runs deferred installs inside the mount, materializes config/sidecar files, records launch metadata, starts autosync, spawns the harness in the mount, then stops autosync and performs explicit syncBack with a spinner; SIGINT forwards to child, aborts on first Ctrl-C during syncing, and forces exit on subsequent Ctrl-C.
Teardown updates
packages/cli/src/cli.ts
Teardown stops the syncing spinner, best-effort stops autosync on error, calls handle.cleanup(), stops launch-metadata, removes SIGINT listener, and avoids duplicate cleanup when installs ran inside mount.
Test isolation
packages/cli/src/cli.test.ts
--version test creates temporary isolated AGENT_WORKFORCE_HOME with personas/, passes it into runCliCapturingStderr, and removes the directory in a finally block.

Sequence Diagram(s)

sequenceDiagram
participant User
participant CLI
participant createMount
participant Installer
participant Harness
participant AutoSync
participant SyncBack
User->>CLI: run CLI in mount-mode
CLI->>createMount: create mount
createMount-->>CLI: mount handle
alt deferred installs
CLI->>Installer: run installs inside mount
Installer-->>CLI: install results
end
CLI->>Harness: spawn child inside mount
Harness-->>CLI: child exits
CLI->>AutoSync: stop autosync (if present)
CLI->>SyncBack: run syncBack with spinner
SyncBack-->>CLI: sync results
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I hopped into mount-mode, bright and spry,
Spinners spinning as installs flew by,
Signals forwarded, syncs that mend,
Tests tucked safe until the end,
A tiny leap — the CLI says hi!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 60.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 nameStatusExplanation
Title check✅ PassedThe title directly matches the main changes: adding spinners during persona session install and exit sync, which are the primary UX improvements in this PR.
Description check✅ PassedThe description comprehensively covers all major changes in the changeset, including skill install spinners, mount lifecycle inlining, dependency bumps, and test isolation, with clear context and rationale.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck 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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-cli-launch-spinners

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

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:6b651cc321

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadpackages/cli/src/cli.ts Outdated
Comment on lines +1250 to +1253
if (activeChild && !activeChild.killed && activeChild.exitCode === null) {
try {
activeChild.kill('SIGINT');
} catch {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid sending SIGINT to child twice during interactive runs

In the non-syncing path this handler always calls activeChild.kill('SIGINT'), but with spawn(..., { stdio: 'inherit' }) the child is already in the foreground process group and receives the terminal Ctrl-C directly. That means each Ctrl-C can deliver two SIGINTs to the harness, which can skip graceful first-interrupt behavior and force premature termination for CLIs that escalate on repeated SIGINT.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good catch — fixed in a2bcee3. The handler is now a true no-op while the child runs (its only purpose is suppressing Node's default exit-on-SIGINT). Removed the activeChild.kill('SIGINT') forward and the activeChild ref. Updated the surrounding comment to call out why the no-op is required.

Comment on lines +419 to +423
const res = spawnSync(bin, args, {
stdio: ['ignore', 'pipe', 'pipe'],
shell: false,
encoding: 'utf8',
...(cwd ? { cwd } : {})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Raise spawnSync maxBuffer when capturing installer output

This install path now captures stdout/stderr via spawnSync pipes but does not set maxBuffer, so Node applies its default per-stream buffer limit (about 1 MiB). Verbose install commands can exceed that limit, causing spawnSync to terminate the subprocess (ENOBUFS/SIGTERM) and report a non-zero exit even when the install would otherwise complete, which is a regression from the previous inherited-stdio behavior.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed in a2bcee3spawnSync now sets maxBuffer: 100 * 1024 * 1024 (100 MiB), well past anything these installers print in practice. Comment in-place explains why.

@devin-ai-integrationdevin-ai-integrationBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

View 4 additional findings in Devin Review.

Open in Devin Review

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Duplicate failure message on install error in runInstallOrThrow path

Same pattern as runInstall: runInstallWithSpinner already outputs spinner.fail(...) with the failure message at packages/cli/src/cli.ts:430. When runInstallOrThrow throws InstallCommandError, the catch block at line 1379 writes ${err.message}. Aborting.\n — and err.message is "${label} failed (exit ${exitCode})" (set at packages/cli/src/cli.ts:454), producing a second nearly identical error line on stderr.

(Refers to lines 1378-1380)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed in a2bcee3 — the InstallCommandError catch no longer re-prints err.message. The failure line is shown once via spinner.fail inside runInstallWithSpinner.

Comment threadpackages/cli/src/cli.ts Outdated
Comment on lines 440 to 442
if (code !== 0) {
process.stderr.write(`${label} failed (exit ${code}). Aborting.\n`);
process.exit(code);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Duplicate failure message on install error in runInstall

runInstallWithSpinner already writes a failure message via spinner.fail(${label} failed (exit ${code})) at packages/cli/src/cli.ts:430, and dumps captured output at line 431. Then runInstall at line 441 writes a nearly identical message "${label} failed (exit ${code}). Aborting.\n" to stderr before exiting. On a failed install the user sees the same failure reported twice:

Example duplicated output

✗ Installing skills: foo → /path failed (exit 1)
<captured subprocess output>
Installing skills: foo → /path failed (exit 1). Aborting.

Suggested change
if(code!==0){
process.stderr.write(`${label} failed (exit${code}). Aborting.\n`);
process.exit(code);
if(code!==0){
process.exit(code);
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed in a2bcee3runInstall no longer writes the duplicate "Aborting" line; the failure message already comes from spinner.fail inside runInstallWithSpinner.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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)
packages/cli/src/cli.ts (1)

1246-1283: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add SIGTERM handler to the inline mount path for consistent signal cleanup.

The inline mount lifecycle registers only SIGINT, while the other execution path handles both SIGINT and SIGTERM. Without a SIGTERM listener, Node uses its default behavior (immediate exit), bypassing the sync and cleanup logic entirely. A SIGTERM signal during a session would skip syncBack() and leave the session directory behind.

Suggested refactor
- const sigintHandler = () => {+ const handleShutdownSignal = (signal: NodeJS.Signals) => {
if (!isSyncing) {
// Defensive: forward to child if it somehow didn't get the TTY signal.
// No spinner side effects while the harness owns the screen.
if (activeChild && !activeChild.killed && activeChild.exitCode === null) {
try {
- activeChild.kill('SIGINT');+ activeChild.kill(signal);
} catch {
/* ignore — child likely exited between checks */
}
}
return;
}
abortPresses += 1;
if (abortPresses === 1) {
if (syncSpinner) {
syncSpinner.text =
'Aborting sync — partial changes will be propagated. (Ctrl-C again to force quit)';
}
shutdownController.abort();
return;
}
...
};
- process.on('SIGINT', sigintHandler);+ const sigintHandler = () => handleShutdownSignal('SIGINT');+ const sigtermHandler = () => handleShutdownSignal('SIGTERM');+ process.on('SIGINT', sigintHandler);+ process.on('SIGTERM', sigtermHandler);
...
process.removeListener('SIGINT', sigintHandler);
+ process.removeListener('SIGTERM', sigtermHandler);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/cli/src/cli.ts` around lines 1246 - 1283, The inline mount path only
registers sigintHandler for SIGINT so SIGTERM bypasses cleanup; add a SIGTERM
listener that invokes the same sigintHandler to ensure syncSpinner,
shutdownController.abort(), rmSync(sessionRoot, ...) and process.exit handling
run for both signals. Locate the sigintHandler function and replace or augment
the process.on('SIGINT', sigintHandler) registration to also register
process.on('SIGTERM', sigintHandler) (or otherwise wire SIGTERM to call
sigintHandler) so both signals trigger the same teardown logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/cli/src/cli.ts`:
- Around line 411-433: In runInstallWithSpinner, increase spawnSync's
stdout/stderr buffer by adding maxBuffer: 16 * 1024 * 1024 to the options and
include any spawn error text by appending res.error?.message (if present) to the
computed output so buffer exhaustion or spawn errors are visible; keep using
subprocessExitCode(res) but ensure the final output string includes
res.error?.message (and is written to stderr on failure) so ENOBUFS or other
spawn errors are distinguishable from normal command exit codes.
---
Outside diff comments:
In `@packages/cli/src/cli.ts`:
- Around line 1246-1283: The inline mount path only registers sigintHandler for
SIGINT so SIGTERM bypasses cleanup; add a SIGTERM listener that invokes the same
sigintHandler to ensure syncSpinner, shutdownController.abort(),
rmSync(sessionRoot, ...) and process.exit handling run for both signals. Locate
the sigintHandler function and replace or augment the process.on('SIGINT',
sigintHandler) registration to also register process.on('SIGTERM',
sigintHandler) (or otherwise wire SIGTERM to call sigintHandler) so both signals
trigger the same teardown logic.
🪄 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 Plus

Run ID: 22410145-6940-4768-9210-d880b3f5908b

📥 Commits

Reviewing files that changed from the base of the PR and between 3fee405 and 6b651cc.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (3)
  • packages/cli/package.json
  • packages/cli/src/cli.test.ts
  • packages/cli/src/cli.ts

Comment threadpackages/cli/src/cli.ts
- Drop duplicate failure messages on install error: spinner.fail in
runInstallWithSpinner already prints the failure line, so both runInstall
and the InstallCommandError catch were re-printing nearly the same text.
- Stop double-delivering SIGINT to the harness: with stdio: 'inherit' the
child shares the parent's process group, so Ctrl-C at the TTY already
reaches it. Forwarding via child.kill('SIGINT') from our handler
delivered a second SIGINT, which can break harnesses that escalate on
repeated interrupts (e.g. claude treats 1st = cancel, 2nd = quit). The
handler now stays a true no-op while the child runs, only there to
suppress Node's default exit-on-SIGINT.
- Raise spawnSync maxBuffer to 100 MiB for the captured installer output.
Default is 1 MiB; verbose `npx prpm install` / `npx skills add` runs
can blow past it and have spawnSync kill the child with ENOBUFS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for freeto 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

@willwashburn