Skip to content
This repository was archived by the owner on Aug 17, 2026. It is now read-only.

feat(grunt/tui): bare gruntcode auto-spawns serve + attaches (one-command wakeable TUI) - #5

Merged
terrxo merged 1 commit into
devfrom
grunt-feat/auto-serve-attach
May 26, 2026
Merged

terrxo merged 1 commit into
devfrom
grunt-feat/auto-serve-attach

Conversation

@terrxo

@terrxo terrxo commented May 26, 2026

Copy link
Copy Markdown

Closes hivemind anomalyco#224.

What

Bare gruntcode now just works as a wakeable TUI. No serve + attach dance.

gruntcode --peer-id nik-tab-1 --project ~/Developer/Projects/foo
# done. wakeable. ready for hivemind_wake_peer.

Behind the scenes: probes :4096 for an existing daemon, spawns one if missing, re-execs into gruntcode attach.

Implementation

  • New packages/opencode/src/cli/cmd/tui/auto-serve.ts — ensureServeDaemon() returns {ok: true, url, bin} or {ok: false, reason}
  • thread.ts handler runs ensureServeDaemon() early; on success re-execs into attach with same args; on failure falls through to in-process worker (preserves upstream behavior)

Escape hatches

  • OPENCODE_DISABLE_AUTO_SERVE=1 keeps the legacy in-process worker path
  • OPENCODE_AUTO_SERVE_PORT / OPENCODE_AUTO_SERVE_HOST override defaults

Verified end-to-end

  1. Killed existing daemon, confirmed :4096 closed
  2. Launched bare TUI from patched source
  3. Process tree showed: bun run → child gruntcode serve AND child gruntcode attach
  4. External curl -X POST :4096/session/<id>/prompt_async accepted, daemon LLM ran, response "BARE-CMD-WORKS" written to opencode.db

Known follow-up

The wake response doesn't always render visually in the TUI yet (the message lands in the daemon's DB but the SSE subscription path established by patch 2c needs more work for newly-created sessions). Tracking in hivemind anomalyco#221 — the wake itself works for autonomous coordination, just the human-watcher UX needs polish.

bun typecheck clean.

…ommand wakeable TUI)

## Goal

Bare `gruntcode` (no subcommand) should Just Work — the launched TUI is
automatically wakeable. No `serve` + `attach` two-command dance, no
shell wrapper.

Nik framing 2026-05-26: "like i run gruntcode and things can get woken
up via hivemind".

## Behavior of bare `gruntcode` now

1. Probes http://127.0.0.1:4096/session — if a daemon is already serving
   there, attach to it.
2. If no daemon: spawns one as a detached child (`gruntcode serve
   --port 4096 --hostname 127.0.0.1 --print-logs`), waits up to 10s for
   readiness.
3. Re-execs into `gruntcode attach <url>` with all the relevant args
   passed through (--peer-id, --dir, --continue, --session, --fork). The
   user sees an unchanged TUI; implementation detail (the daemon) is
   hidden behind the bare command.
4. Daemon survives the TUI exit — second bare `gruntcode` invocation
   reuses it.

## Escape hatches

- `OPENCODE_DISABLE_AUTO_SERVE=1` skips the auto-serve and uses the
  legacy in-process worker (the path used today by upstream opencode).
- `OPENCODE_AUTO_SERVE_PORT` / `OPENCODE_AUTO_SERVE_HOST` override the
  defaults (4096 / 127.0.0.1).

## Implementation

- New `packages/opencode/src/cli/cmd/tui/auto-serve.ts` exports
  `ensureServeDaemon()`. Returns `{ok: true, url, bin}` or `{ok: false, reason}`.
  Logic:
  - Resolves gruntcode binary path (argv0 if compiled binary; `which
    gruntcode` if running via `bun run` in dev mode; fallback to `which
    opencode`).
  - Probes /session with 1.5s timeout.
  - If 200 → returns the URL.
  - If connection refused → spawns daemon as detached child, polls
    /session every 200ms until ready or 10s timeout.
- `packages/opencode/src/cli/cmd/tui/thread.ts` handler calls
  `ensureServeDaemon()` early. On success: builds attach argv from
  current args and re-execs via child_process.spawn with stdio inherited.
  On failure: logs the reason and falls through to the existing
  in-process worker path.

## Verification

End-to-end manual test:
1. Killed existing daemon. Confirmed `curl :4096/session` returns connection refused.
2. Launched bare TUI from patched source: `bun run src/index.ts --peer-id nik-test --project /tmp`.
3. Process tree showed: bun run → child `gruntcode serve --port 4096 ...` AND child
   `gruntcode attach http://127.0.0.1:4096 --dir /tmp --peer-id nik-test`.
4. `curl :4096/session` returned 200 with the active session.
5. `curl -X POST :4096/session/<id>/prompt_async` accepted (204) and the
   daemon log showed the LLM running. The wake response 'BARE-CMD-WORKS'
   was written to opencode.db (verified via `/session/<id>/message`).

## Known follow-up

The wake response sometimes doesn't render visually in the TUI even
though it's persisted server-side. This is a separate refinement of
patch 2c (SSE subscription) that needs investigation — the wake is
audit-complete and the system works for autonomous coordination, but
human-watcher UX needs polish. Tracked in hivemind anomalyco#221.

Refs hivemind anomalyco#224.
@terrxo
terrxo merged commit 1f6b6f3 into dev May 26, 2026
3 of 10 checks passed
@github-actions

Copy link
Copy Markdown

This PR doesn't fully meet our contributing guidelines and PR template.

What needs to be fixed:

  • PR description is missing required template sections. Please use the PR template.

Please edit this PR description to address the above within 2 hours, or it will be automatically closed.

If you believe this was flagged incorrectly, please let a maintainer know.

@github-actions

Copy link
Copy Markdown

Hey! Your PR title feat(grunt/tui): bare gruntcode auto-spawns serve + attaches (one-command wakeable TUI) doesn't follow conventional commit format.

Please update it to start with one of:

  • feat: or feat(scope): new feature
  • fix: or fix(scope): bug fix
  • docs: or docs(scope): documentation changes
  • chore: or chore(scope): maintenance tasks
  • refactor: or refactor(scope): code refactoring
  • test: or test(scope): adding or updating tests

Where scope is the package name (e.g., app, desktop, opencode).

See CONTRIBUTING.md for details.

@terrxo
terrxo deleted the grunt-feat/auto-serve-attach branch May 26, 2026 21:42
terrxo added a commit that referenced this pull request May 27, 2026
…alyco#266 Phase 1) (#21)

Bridges gruntcode's per-turn lifecycle to the hivemind-mcp loop primitive
shipped in hivemind-mcp v0.8.2 (PR #18) + extended in v0.8.3 (PR #20
await-review). Every step-finish fires hivemind_record_turn_end into the
MCP; every successful tool-result fires hivemind_loop_progress. The MCP
decides whether to auto-wake the same peer (continue), wake the parent
(escalate / await-review), or no-op. Workers self-drive toward goals
WITHOUT Nik typing.

This is Phase 1 of the loop primitive program (anomalyco#266). Phase 0 (schema +
tools + decision logic) lives in hivemind-mcp; this PR wires the gruntcode
side that emits the events.

Implementation:

- packages/opencode/src/session/hivemind-loop-hook.ts (new):
  - HivemindLoopHook.recordTurnEnd(...) — called from step-finish handler
  - HivemindLoopHook.loopProgress(...) — called from tool-result handler
  - Both take MCP.Service as Option (Effect.serviceOption) so the
    processor degrades cleanly when MCP isn't in context (tests don't
    have to provide MCP just to satisfy a single hook env requirement)
  - Both fire-and-forget via Effect.forkIn(scope) + Effect.ignore — the
    hook MUST NEVER block or throw into the TUI (loop primitive correctness
    rule; the loop is supposed to PREVENT failures, not cause them)
  - findHivemindClient: fast-path matches client named 'hivemind'; slow-
    path scans tools() map for the tool-name suffix (handles users who
    renamed their MCP under a non-conventional key)
  - buildTurnSummary deferred until AFTER client resolution to skip the
    Database.use read when we're no-opping anyway

- packages/opencode/src/effect/runtime-flags.ts:
  - New hivemindLoopEnabled flag (OPENCODE_HIVEMIND_LOOP_ENABLED env var,
    default false). Opt-in per-tab in Phase 1; planned default-on in
    Phase 2 after validating in the wild.

- packages/opencode/src/session/processor.ts:
  - Yield MCP.Service via Effect.serviceOption (doesn't leak through
    Handle.process's never-environment signature; processor still has
    MCP at runtime via prompt.ts's layer wiring)
  - case 'step-finish' (after value.reason captured, after summary fork):
    fires HivemindLoopHook.recordTurnEnd
  - case 'tool-result' (after completeToolCall): fires
    HivemindLoopHook.loopProgress

- packages/opencode/test/session/hivemind-loop-hook.test.ts (new):
  - 5 unit tests covering the gating paths:
    * flag off + MCP=None → silent no-op
    * flag on + MCP=None → silent no-op (no error)
    * flag off + (loopProgress) → silent no-op
    * flag on + MCP=None + (loopProgress) → silent no-op
    * flag on + MCP=Some (stub w/ empty clients()) → silent no-op (the
      findHivemindClient miss path)

- GRUNTCODE.md:
  - Documented as patch #5 with the feature flag + behavior.

Testing:
- bun typecheck (from packages/opencode): green.
- bun test test/session/ : 357 pass / 5 skip / 1 todo / 0 fail (was 352
  pre-hook; +5 new hook tests). No regressions across the session module
  (processor-effect.test.ts + compaction.test.ts + all peers in /session
  still green).
- The wake fire-path is exercised end-to-end by the production loop
  primitive once Phase 1 is enabled per-tab; that's better validated by
  live use than by mocked MCP calls in unit tests.

Refs:
- hivemind anomalyco#266 (parent — Phase 1)
- hivemind-mcp PR #18 (Phase 0 — schema + 6 tools)
- hivemind-mcp PR #20 (await-review decision branch — receiver-side already
  live on hivemind-mcp main as of 65a1020)
- AGENTS.md + hivemind-peers.md three-layer goal-loop contract (the
  behavioral rules this PR mechanically enforces)
terrxo added a commit that referenced this pull request May 28, 2026
…ommand wakeable TUI) (#5)

## Goal

Bare `gruntcode` (no subcommand) should Just Work — the launched TUI is
automatically wakeable. No `serve` + `attach` two-command dance, no
shell wrapper.

Nik framing 2026-05-26: "like i run gruntcode and things can get woken
up via hivemind".

## Behavior of bare `gruntcode` now

1. Probes http://127.0.0.1:4096/session — if a daemon is already serving
   there, attach to it.
2. If no daemon: spawns one as a detached child (`gruntcode serve
   --port 4096 --hostname 127.0.0.1 --print-logs`), waits up to 10s for
   readiness.
3. Re-execs into `gruntcode attach <url>` with all the relevant args
   passed through (--peer-id, --dir, --continue, --session, --fork). The
   user sees an unchanged TUI; implementation detail (the daemon) is
   hidden behind the bare command.
4. Daemon survives the TUI exit — second bare `gruntcode` invocation
   reuses it.

## Escape hatches

- `OPENCODE_DISABLE_AUTO_SERVE=1` skips the auto-serve and uses the
  legacy in-process worker (the path used today by upstream opencode).
- `OPENCODE_AUTO_SERVE_PORT` / `OPENCODE_AUTO_SERVE_HOST` override the
  defaults (4096 / 127.0.0.1).

## Implementation

- New `packages/opencode/src/cli/cmd/tui/auto-serve.ts` exports
  `ensureServeDaemon()`. Returns `{ok: true, url, bin}` or `{ok: false, reason}`.
  Logic:
  - Resolves gruntcode binary path (argv0 if compiled binary; `which
    gruntcode` if running via `bun run` in dev mode; fallback to `which
    opencode`).
  - Probes /session with 1.5s timeout.
  - If 200 → returns the URL.
  - If connection refused → spawns daemon as detached child, polls
    /session every 200ms until ready or 10s timeout.
- `packages/opencode/src/cli/cmd/tui/thread.ts` handler calls
  `ensureServeDaemon()` early. On success: builds attach argv from
  current args and re-execs via child_process.spawn with stdio inherited.
  On failure: logs the reason and falls through to the existing
  in-process worker path.

## Verification

End-to-end manual test:
1. Killed existing daemon. Confirmed `curl :4096/session` returns connection refused.
2. Launched bare TUI from patched source: `bun run src/index.ts --peer-id nik-test --project /tmp`.
3. Process tree showed: bun run → child `gruntcode serve --port 4096 ...` AND child
   `gruntcode attach http://127.0.0.1:4096 --dir /tmp --peer-id nik-test`.
4. `curl :4096/session` returned 200 with the active session.
5. `curl -X POST :4096/session/<id>/prompt_async` accepted (204) and the
   daemon log showed the LLM running. The wake response 'BARE-CMD-WORKS'
   was written to opencode.db (verified via `/session/<id>/message`).

## Known follow-up

The wake response sometimes doesn't render visually in the TUI even
though it's persisted server-side. This is a separate refinement of
patch 2c (SSE subscription) that needs investigation — the wake is
audit-complete and the system works for autonomous coordination, but
human-watcher UX needs polish. Tracked in hivemind anomalyco#221.

Refs hivemind anomalyco#224.
terrxo added a commit that referenced this pull request May 28, 2026
…alyco#266 Phase 1) (#21)

Bridges gruntcode's per-turn lifecycle to the hivemind-mcp loop primitive
shipped in hivemind-mcp v0.8.2 (PR #18) + extended in v0.8.3 (PR #20
await-review). Every step-finish fires hivemind_record_turn_end into the
MCP; every successful tool-result fires hivemind_loop_progress. The MCP
decides whether to auto-wake the same peer (continue), wake the parent
(escalate / await-review), or no-op. Workers self-drive toward goals
WITHOUT Nik typing.

This is Phase 1 of the loop primitive program (anomalyco#266). Phase 0 (schema +
tools + decision logic) lives in hivemind-mcp; this PR wires the gruntcode
side that emits the events.

Implementation:

- packages/opencode/src/session/hivemind-loop-hook.ts (new):
  - HivemindLoopHook.recordTurnEnd(...) — called from step-finish handler
  - HivemindLoopHook.loopProgress(...) — called from tool-result handler
  - Both take MCP.Service as Option (Effect.serviceOption) so the
    processor degrades cleanly when MCP isn't in context (tests don't
    have to provide MCP just to satisfy a single hook env requirement)
  - Both fire-and-forget via Effect.forkIn(scope) + Effect.ignore — the
    hook MUST NEVER block or throw into the TUI (loop primitive correctness
    rule; the loop is supposed to PREVENT failures, not cause them)
  - findHivemindClient: fast-path matches client named 'hivemind'; slow-
    path scans tools() map for the tool-name suffix (handles users who
    renamed their MCP under a non-conventional key)
  - buildTurnSummary deferred until AFTER client resolution to skip the
    Database.use read when we're no-opping anyway

- packages/opencode/src/effect/runtime-flags.ts:
  - New hivemindLoopEnabled flag (OPENCODE_HIVEMIND_LOOP_ENABLED env var,
    default false). Opt-in per-tab in Phase 1; planned default-on in
    Phase 2 after validating in the wild.

- packages/opencode/src/session/processor.ts:
  - Yield MCP.Service via Effect.serviceOption (doesn't leak through
    Handle.process's never-environment signature; processor still has
    MCP at runtime via prompt.ts's layer wiring)
  - case 'step-finish' (after value.reason captured, after summary fork):
    fires HivemindLoopHook.recordTurnEnd
  - case 'tool-result' (after completeToolCall): fires
    HivemindLoopHook.loopProgress

- packages/opencode/test/session/hivemind-loop-hook.test.ts (new):
  - 5 unit tests covering the gating paths:
    * flag off + MCP=None → silent no-op
    * flag on + MCP=None → silent no-op (no error)
    * flag off + (loopProgress) → silent no-op
    * flag on + MCP=None + (loopProgress) → silent no-op
    * flag on + MCP=Some (stub w/ empty clients()) → silent no-op (the
      findHivemindClient miss path)

- GRUNTCODE.md:
  - Documented as patch #5 with the feature flag + behavior.

Testing:
- bun typecheck (from packages/opencode): green.
- bun test test/session/ : 357 pass / 5 skip / 1 todo / 0 fail (was 352
  pre-hook; +5 new hook tests). No regressions across the session module
  (processor-effect.test.ts + compaction.test.ts + all peers in /session
  still green).
- The wake fire-path is exercised end-to-end by the production loop
  primitive once Phase 1 is enabled per-tab; that's better validated by
  live use than by mocked MCP calls in unit tests.

Refs:
- hivemind anomalyco#266 (parent — Phase 1)
- hivemind-mcp PR #18 (Phase 0 — schema + 6 tools)
- hivemind-mcp PR #20 (await-review decision branch — receiver-side already
  live on hivemind-mcp main as of 65a1020)
- AGENTS.md + hivemind-peers.md three-layer goal-loop contract (the
  behavioral rules this PR mechanically enforces)
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant