Skip to content

chore: re-pin agent-bundle preview d30d9acb6; hauler dashboard uses spawnServeApp - #122

Merged
ScriptedAlchemy merged 6 commits into
masterfrom
feat/serve-app-command
Sep 5, 2026
Merged

ScriptedAlchemy merged 6 commits into
masterfrom
feat/serve-app-command

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Sep 5, 2026 •

Copy link
Copy Markdown
Owner

Adopts spawnServeApp from agent-bundle/serve-app-command for hauler dashboard and re-pins the agent-bundle / @agent-bundle/runtime preview.

Pin

cd0b4a62c → d30d9acb6 (agent-bundle main head): includes #582 (agent-bundle/serve-app-command, AB4837), #588 (package dist/ held to AB6005), #575 (Rslib 1.0 / Rsbuild 2.2), #565 (serve-app binds the host's own opening call), #585 (AB4770–AB4772). It does not include #578 (composite plugin root; still open), so nothing about artifact/<host> layout, install/receipt paths, or the ~/.codex/config.toml marketplace source changes here. Same mechanics as #83/#85: URLs in package.json, pnpm install regenerated pnpm-lock.yaml (agent-bundle's own dependency bumps shrink it ~1000 lines), README pin mention.

src/cli/dashboard.ts (181 → 90 lines) + src/lib/dashboard-serve.ts (new, 130 lines)

Deleted the node_modules crawl for the framework manifest (frameworkCliPath), the stdout URL regex (servedUrl), and the spawn/relay/abort→SIGTERM code (serve). spawnServeApp now owns CLI resolution, spawning agent-bundle serve-app, ready-line parsing, stdout→stderr relay, and signal teardown; ServeAppCommandError (framework-not-installed, artifact-missing, exited-before-ready, …) becomes the command's message plus "In an MCP host, call hauler_status instead". Unchanged: --target / --port / --no-open, the opening hauler_status call, call-tool auto-approve, foreground until Ctrl-C, checkout-only scope. locateProject stays in the route (the helper takes root / artifact as inputs); the outcome mapping lives in serveDashboard (src/lib/dashboard-serve.ts, imported by the route) with an injectable spawn, so it is unit-tested. After the ready line the helper reports a refused SIGTERM only through close(), so serveDashboard closes on abort itself and turns a stop-failed into the result (exit 1, message names the pid to kill by hand) instead of awaiting a closed that never settles. No compiler import in the routed bin (AB4837 never fired).

Gate

pnpm check green locally (validate, build — #588's dist AB6005 passed as-is: the helper is inlined into dist/bin/cargo-hauler.js and all four artifact/*/bin/cargo-hauler.mjs, no bare agent-bundle specifier — typecheck, diagnostics, rstest 1016, routes 37); tests/dashboard-serve.test.ts (19 cases with a fake that mirrors the helper's signal / close() contract and counts SIGTERMs: option mapping and defaults, every ServeAppCommandError kind before ready incl. the child's exit code / signal, self-exit by code and by signal, abort → close, abort → stop-failed surfaced, pre-aborted signal, unexpected error rethrown). Smoke: node dist/bin/hauler.js dashboard --no-open --port 0 → MCP App hauler/dashboard at http://127.0.0.1:40577/ …; curl → HTTP 200 text/html (<title>hauler/dashboard</title>); SIGINT → Aborted., process tree gone in ~0.5 s; the running daemon untouched.

Against #107 §4

This sheds four of the six glue responsibilities #107 §4 lists — crawling node_modules for the framework manifest, spawning agent-bundle serve-app, regex-parsing its human stdout, and hand-relaying abort→SIGTERM — into a typed framework contract. Two remain and are structural: locating checkout/artifact by relative paths (locateProject; the helper takes them as inputs) and checkout-only operation (framework-not-installed / artifact-missing still fire from the npm package or an installed pack). Both disappear only with agent-bundle #564/#594's framework-generated browser command, at which point src/cli/dashboard.ts is deleted wholesale as #107 intends; nothing here entrenches it. When #578 lands, the only piece to revisit is locateProject's artifact/<host>/bin heuristic and the --target enum — the same rewrite #107 §1 already lists; the spawnServeApp call passes root / artifact / target through and needs no undoing.

Self-review

Implementation lane: claude-fable-5-1-thinking-max. Reviewer: gpt-5.6-sol-medium (two passes).

Pass 1 (ae87e12) — 0 blockers, 2 should-fix, both fixed in 56f62d2:

  1. should-fix dashboard.ts:115 post-ready stop-failed unobservable, served.closed pending forever — exitOrStopFailure races closed against close() on abort and maps the rejection onto the result.
  2. should-fix no automated adapter test — logic extracted to src/lib/dashboard-serve.ts with an injectable spawn; tests/dashboard-serve.test.ts added.

Pass 2 (56f62d2) — 0 blockers, 2 should-fix + 1 nit, fixed in 761fa9a:

  1. should-fix double SIGTERM: the helper's own abort listener and close() both stopped the child — the helper now gets a private signal that follows the request only until spawn settles (untilReady); after the ready line close() is the single owner.
  2. should-fix fakes ignored options.signal — fakeHelper honours it like the real helper and every scenario asserts the stop count (0 / 1).
  3. nit changeset omitted spawn-failed / aborted / stop-failed — listed.

Pass 3 (d094382) — 0 blockers, 1 should-fix, dismissed with a test (2190c2c): "helper.signal.aborted blindly awaits closed; a refused post-ready SIGTERM would hang". The state helper.signal.aborted && served is reachable only when the abort came before the ready line, the helper's SIGTERM was accepted (a refusal rejects spawn with stop-failed, now tested), and the child printed its ready line while dying — its exit is on the way and a close() would be the second SIGTERM the previous pass objected to. An abort cannot land between the ready line and the hand-over: the ready line settles spawn from a stdout event and the continuation runs in the following microtasks, before any signal handler or timer. Both pre-ready races are now tests (readyWhileDying, refuseStop); the reasoning is in the code comment.

Rebased onto master at c5694dc (#121); pnpm check green after the rebase (1050 unit, 37 routes).

@changeset-bot

changeset-bot Bot commented Sep 5, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2190c2c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
cargo-hauler Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 5, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-05T06:22:13.843307Z 7538120 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

ScriptedAlchemy added a commit that referenced this pull request Sep 5, 2026
ScriptedAlchemy added a commit that referenced this pull request Sep 5, 2026
… test the serve-app adapter with an injected spawn

Reviewer findings on #122. After the ready line the helper reports a refused
SIGTERM only through close(), so an abort whose stop failed left closed
pending for ever; serveDashboard now closes on abort itself and turns that
stop-failed into the result (with the pid to kill by hand). The route's logic
moved to src/lib/dashboard-serve.ts, which the route imports, so the option
mapping, every ServeAppCommandError kind, and the abort paths have unit tests.
ScriptedAlchemy added a commit that referenced this pull request Sep 5, 2026
… close() after it

Second-pass finding on #122: passing the request signal to the helper and
also calling close() on abort sent the server two SIGTERMs, which can bypass
a one-shot graceful handler. The helper now gets a private signal that
follows the request only until readiness; after it a single close() stops
the server and surfaces stop-failed. The test fake honours options.signal
like the real helper and asserts exactly one stop per scenario. Changeset
lists every surfaced failure kind.
…pawnServeApp

The pkg.pr.new preview of agent-bundle main d30d9acb6bd106762bb975ac58d6bf437c176091
(#588; includes #582 `agent-bundle/serve-app-command` + AB4837, #575 Rslib 1.0 /
Rsbuild 2.2, #565 serve-app binds the host's own opening call, #585 AB4770–AB4772).

src/cli/dashboard.ts: `hauler dashboard` calls `spawnServeApp` from
`agent-bundle/serve-app-command` (agent-bundle#558) instead of its own
child-process plumbing. The helper now owns resolving the installed framework
CLI (was `frameworkCliPath`), spawning `agent-bundle serve-app` (was `serve`),
parsing the ready line (was the `servedUrl` regex), relaying the child's stdout
to stderr, and turning the request signal into SIGTERM; its typed
`ServeAppCommandError` becomes the command's failure message. What stays is
`locateProject` — where this install keeps its artifact — since the helper
takes `root`/`artifact` as inputs. Flags, the `hauler_status` opening call,
foreground-until-Ctrl-C, and the checkout-only scope are unchanged; the routed
bin still never imports the compiler (AB4837), and the built bins inline the
helper (no bare specifier, AB6005 clean including the package build's dist).

Verified: validate, build (no new diagnostics), typecheck, diagnostics,
tests/dashboard.test.ts + tests/version-consistency.test.ts (104) and
tests/route-unit/routes.test.ts (7). Smoke: `hauler dashboard --no-open
--port 0` printed `MCP App hauler/dashboard at http://127.0.0.1:40577/ (tool
hauler_status; Ctrl-C stops the server)`; curl of that URL returned HTTP 200
text/html (`<title>hauler/dashboard</title>`); SIGINT ended the whole tree
(`Aborted.`).
… test the serve-app adapter with an injected spawn

Reviewer findings on #122. After the ready line the helper reports a refused
SIGTERM only through close(), so an abort whose stop failed left closed
pending for ever; serveDashboard now closes on abort itself and turns that
stop-failed into the result (with the pid to kill by hand). The route's logic
moved to src/lib/dashboard-serve.ts, which the route imports, so the option
mapping, every ServeAppCommandError kind, and the abort paths have unit tests.
… close() after it

Second-pass finding on #122: passing the request signal to the helper and
also calling close() on abort sent the server two SIGTERMs, which can bypass
a one-shot graceful handler. The helper now gets a private signal that
follows the request only until readiness; after it a single close() stops
the server and surfaces stop-failed. The test fake honours options.signal
like the real helper and asserts exactly one stop per scenario. Changeset
lists every surfaced failure kind.
…ing; refused stop) and document why no post-ready window exists
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant