Skip to content

feat(cli): make local-assistant lifecycle ops programmatically reusable#32619

Merged
vex-assistant-bot[bot] merged 3 commits into
mainfrom
devin/lum-2051-cli-lib-programmatic-reuse
May 30, 2026
Merged

feat(cli): make local-assistant lifecycle ops programmatically reusable#32619
vex-assistant-bot[bot] merged 3 commits into
mainfrom
devin/lum-2051-cli-lib-programmatic-reuse

Conversation

@ashleeradka
Copy link
Copy Markdown
Contributor

@ashleeradka ashleeradka commented May 29, 2026

Prompt / plan

Foundation for the Electron-shell work tracked in LUM-2051 (macOS Electron project). The browser SPA needs privileged local-assistant operations (hatch, retire, guardian token); a host with OS/process access must perform them. Today the only non-terminal consumer — the dev-only apps/web/vite-plugin-local-mode.ts — gets them by spawning the CLI as a subprocess and scraping stdout (it regex-matches Hatching local assistant:\s+(.+) to recover the new assistant id, and reads the guardian access token off stdout.trim()). A second host (the Electron main process, a Node process that can import these functions) hits a wall: hatchLocal/retireLocal return void and write everything through console.log, and cli/src/lib/* isn't exported from the package, so there's no way to get a result back without scraping logs.

This PR reshapes those ops to be consumable in-process, without changing any CLI behavior. It does not add the Electron IPC handlers themselves — that's downstream work (Noa's LUM-1997–2000) that consumes this surface.

What changed

  • Injectable progress/log sink (cli/src/lib/lifecycle-reporter.ts): a LifecycleReporter interface (progress, log, warn, error) plus a default consoleLifecycleReporter. The console reporter routes log/warn/error to the console and mirrors progress to the existing HATCH_PROGRESS: stdout channel via emitProgress.
  • hatchLocal now returns HatchLocalResult (assistantId, runtimeUrl, localUrl, species, optional guardianAccessToken) instead of void, and reports through options.reporter (defaulting to the console reporter) instead of calling console.*/emitProgress directly.
  • retireLocal now returns RetireLocalResult (assistantId, archived, optional archivePath, optional sharedDataDir) and takes an optional reporter arg (defaulting to the console reporter).
  • Curated package exports for hatch-local, retire-local, guardian-token, and lifecycle-reporter so a second host can import them. The guardian-token helpers already returned structured data; they only needed exporting.

Why it's safe (behavior preservation)

  • CLI commands (hatch, retire, recover, teleport) are unchanged — they don't pass a reporter, so they get the default console reporter and produce byte-for-byte identical terminal output.
  • The HATCH_PROGRESS:{step,total,label} stdout protocol is consumed by the already-shipped Swift macOS app (clients/macos/.../HatchingStepView.swift), which can run against a newer CLI. The default reporter still emits those exact lines under VELLUM_DESKTOP_APP, so the cross-component wire contract is preserved (asserted by a unit test). Per the macOS↔platform skew policy, an older client running a newer CLI keeps working.
  • Return-type widening (void → result object) is additive: existing callers await and ignore the return.
  • Reporter injection is an opt-in seam for non-CLI callers, not a shared logger for the CLI. Per cli/AGENTS.md, user-facing CLI output still goes through console.log/console.error (via the default reporter).

Alternatives considered and rejected

  • Keep subprocess-spawn + stdout-scrape for Electron too — brittle in a packaged app (needs bun on PATH/bundled, cold-start cost) and recovers results by regex-matching log lines; exactly the fragility this removes.
  • Talk to the running daemon over IPC/HTTP — wrong layer: hatch/retire are instance-lifecycle ops that run when no daemon exists yet (the reason cli/ is a separate package).
  • Stand up a local HTTP service both hosts call — that's the current Vite plugin, and re-introducing an HTTP surface in Electron throws away the reason IPC is safe.
  • Wildcard ./src/lib/* export — exposes the entire lib/ surface; a curated list keeps the public seam intentional.

Scope boundary / known follow-up

This PR routes the top-level lifecycle output of hatchLocal/retireLocal (their own progress + log lines) through the reporter. The nested startup helpers in lib/local.ts (startLocalDaemon, startGateway) still console.log/console.warn directly, so an in-process caller that supplies a custom reporter will not yet observe every line through it (flagged by Codex review, P2). Threading the reporter deeper into local.ts is intentionally not in this PR: those helpers are shared infrastructure with many callers, and the deeper plumbing belongs with the first in-process caller (the Electron adapter) so it can be validated against a real consumer rather than landed speculatively here. Tracked as the natural next layer on top of this foundation. For the CLI today this is a non-issue — the default reporter already routes to the console, so output is unchanged.

Test plan

  • cd cli && bunx tsc --noEmit — clean.
  • cd cli && bun run lint — clean (one pre-existing unrelated warning in roadmap.ts).
  • cli/src/lib/__tests__/lifecycle-reporter.test.ts: console routing, HATCH_PROGRESS emission under VELLUM_DESKTOP_APP, and suppression when unset.
  • Existing cli/src/__tests__/{retire,teleport}.test.ts stay green, including the toHaveBeenCalledWith assertions that pin the CLI→retireLocal/hatchLocal call shapes.
  • A dedicated retire-local unit test was intentionally not added: teleport.test.ts registers a process-global mock.module("../lib/retire-local.js") (and one for hatch-local.js) that is never restored, so any sibling test file resolves the mocked function in the full suite. The structured returns are covered by tsc; the command-level tests exercise the call sites; reporter routing is covered by the lifecycle-reporter test.

Relates to LUM-2051.

Link to Devin session: https://app.devin.ai/sessions/15bca57bd4c64a3085cfb80e1f26355a
Requested by: @ashleeradka

Add an injectable LifecycleReporter so hatchLocal/retireLocal can report
progress and log output through a sink instead of writing to the console
directly, and return structured results instead of void. The CLI keeps its
exact terminal output (and the HATCH_PROGRESS stdout protocol under
VELLUM_DESKTOP_APP) via a default console reporter, while in-process callers
can inject their own reporter and read results without scraping stdout.

Curate package exports for hatch-local, retire-local, guardian-token, and
lifecycle-reporter so a second host (e.g. the Electron main process) can import
these functions directly.

LUM-2051

Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>
@devin-ai-integration
Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@linear
Copy link
Copy Markdown

linear Bot commented May 29, 2026

LUM-2051

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

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: a8e38369a0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +234 to +236
runtimeUrl = await startGateway(watch, resources, {
signingKey,
bootstrapSecret,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Route nested hatch output through the reporter

When an in-process caller supplies options.reporter, this still invokes the nested lifecycle helpers without any reporting hook; startGateway (and the preceding startLocalDaemon) write status and warning messages directly with console.log/console.warn in cli/src/lib/local.ts. In a normal successful hatch this means the new reusable API still pollutes stdout/stderr and the caller cannot observe all lifecycle output via LifecycleReporter, defeating the purpose of avoiding subprocess stdout scraping. Please thread the reporter through these helpers or otherwise suppress their console output when a custom reporter is provided.

Useful? React with 👍 / 👎.

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.

Good catch, and correct — hatchLocal's own progress/log lines now route through the reporter, but the nested startLocalDaemon/startGateway helpers in lib/local.ts still console.log/console.warn directly, so an in-process caller with a custom reporter won't yet see every line through it.

Deliberately scoping that out of this PR rather than fixing it here. Threading the reporter through local.ts touches shared infrastructure with several callers, and it's better landed alongside the first real in-process consumer (the Electron adapter) so it can be validated against an actual LifecycleReporter rather than plumbed speculatively. Captured this as the explicit next layer in the PR description's "Scope boundary / known follow-up" section.

For the CLI today it's a non-issue: the default consoleLifecycleReporter already routes everything to the console, so output is unchanged. Resolving this thread with that decision noted.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 3 additional findings.

Open in Devin Review

…le mock

teleport.test.ts registers a process-global mock.module for
../lib/retire-local.js and never restores it, so a separate retire-local
test file resolves the mocked retireLocal in the full suite. The structured
return is covered by tsc and the command-level retire/teleport tests; reporter
routing is covered by lifecycle-reporter.test.ts.

Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>
@devin-ai-integration
Copy link
Copy Markdown
Contributor

devin-ai-integration Bot commented May 29, 2026

@noanflaherty flagging you here since this is the foundation your local-mode unbundling + the Electron IPC tickets (LUM-1997–2000) sit on — wanted to align before more gets built on top.

What this PR does: makes cli/src/lib's hatchLocal/retireLocal (+ the existing guardian-token helpers) consumable in-process — structured return values instead of void, an injectable LifecycleReporter instead of hard-coded console.log, and curated package exports. CLI behavior is byte-for-byte unchanged (default console reporter; HATCH_PROGRESS contract preserved).

Why it helps your work: the Electron main process can now import { hatchLocal } and read { assistantId, runtimeUrl, guardianAccessToken } off the return value, instead of the Vite plugin's current approach of spawning the CLI and regex-scraping stdout. So the Electron equivalents of the Vite endpoints become thin adapters over these functions rather than reimplementations.

What it deliberately does NOT do: it doesn't add any Electron IPC handler, and it doesn't touch your tickets or the ATL findings — purely additive cli/-only groundwork.

Two things I'd like to align on for what happens next:

  1. Who builds the first IPC handler on top of this, and where? Happy to take a net-new ticket to land one reference adapter (e.g. hatch) over these exports as the pattern for the rest — staying entirely out of your ticket numbers — or leave all of that to you. Your call.
  2. Reporter depth. The next layer is threading the LifecycleReporter through lib/local.ts (startLocalDaemon/startGateway) so an in-process caller observes all lifecycle output, not just the top-level lines (see the "Scope boundary" note + Codex thread). That's best validated against the first real consumer — fits naturally with whoever takes on question 1 above.

No coordination blocker either way — this can merge independently and your tickets consume it whenever they're ready. Just want to make sure we're not about to build the same thing twice.

@socket-security
Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedjszip@​3.10.199100848070
Addedopenai@​6.29.074100100100100
Addedmarked@​18.0.0100851009680
Addedrrule@​2.8.19910010082100
Addedmadge@​8.0.09910010082100
Addedpostgres@​3.4.89910010084100
Addedminimatch@​10.2.49910010090100
Addedknip@​5.86.0991009595100
Addedplaywright@​1.58.210010010099100

View full report

Copy link
Copy Markdown
Contributor

@vex-assistant-bot vex-assistant-bot Bot left a comment

Choose a reason for hiding this comment

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

APPROVE — reviewed at 12b5a9f3

Value: Eliminates subprocess spawning + stdout regex-scraping as the only path to hatchLocal/retireLocal results — gives the Electron main process a clean in-process API to hatch, retire, and receive the guardian access token directly, without parsing Hatching local assistant:\s+(.+) off stdout.

Full analysis

lifecycle-reporter.ts

Clean, minimal interface. progress / log / warn / error — nothing over-engineered. consoleLifecycleReporter is a faithful shim that preserves exact existing terminal output AND the HATCH_PROGRESS: stdout contract under VELLUM_DESKTOP_APP, so no existing subscriber breaks.

hatch-local.ts

All console.log/warn/error and emitProgress calls replaced with reporter.*. The reporter field in HatchLocalOptions defaults to consoleLifecycleReporter, so the CLI commands get zero behavioral change. HatchLocalResult surfaces exactly what an in-process caller needs: assistantId, runtimeUrl, localUrl, species, and guardianAccessToken? — the token is the linchpin (currently only recoverable by reading guardian-token.json off disk or scraping stdout).

logHatchNextSteps call updated to (message) => reporter.log(message) — correct.

The keepAlive path correctly uses reporter.log throughout. No missed console.* calls in the happy path or error paths.

retire-local.ts

Same pattern — reporter param with default. RetireLocalResult adds archived, archivePath?, sharedDataDir?. All three early-return paths now return structured data. Clean.

lifecycle-reporter.test.ts

Covers the two meaningful behaviors: (1) HATCH_PROGRESS: emission gated on VELLUM_DESKTOP_APP, (2) suppression without it. log/warn/error routing is verified. afterEach properly restores VELLUM_DESKTOP_APP. Good.

Dropped retire-local.test.ts

Legitimate removal — teleport.test.ts registers a mock.module for ../lib/retire-local.js that leaks globally and poisons a separate retire-local.test.ts. Coverage is preserved: RetireLocalResult shape is enforced by tsc, reporter routing is covered by lifecycle-reporter.test.ts, and command-level retire/teleport tests cover the command surface.

Codex P2: startLocalDaemon / startGateway still use console.*

Valid observation, correctly scoped out. Threading the reporter through lib/local.ts touches shared infrastructure with multiple callers — doing it speculatively before the first real in-process consumer (Electron adapter) exists would mean plumbing with no real validation target. Right call to defer to the LUM-1997–2000 work.

package.json exports

hatch-local, retire-local, guardian-token, lifecycle-reporter — exactly the surface Electron needs, nothing over-exported.

Vellum Constitution — Yours: the assistant's lifecycle operations now belong to the host that runs them, not to a terminal process that has to be scraped.

@vex-assistant-bot vex-assistant-bot Bot merged commit 493c235 into main May 30, 2026
3 checks passed
@vex-assistant-bot vex-assistant-bot Bot deleted the devin/lum-2051-cli-lib-programmatic-reuse branch May 30, 2026 13:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants