Skip to content

feat(grok): add first-class Grok Build integration#81

Open
kiala9 wants to merge 8 commits into
aannoo:mainfrom
kiala9:feat/grok-build-integration
Open

feat(grok): add first-class Grok Build integration#81
kiala9 wants to merge 8 commits into
aannoo:mainfrom
kiala9:feat/grok-build-integration

Conversation

@kiala9

@kiala9 kiala9 commented Jul 11, 2026

Copy link
Copy Markdown

Summary

Adds first-class Grok Build (grok) support to hcom: hook install, PTY launch/resume/fork, message delivery, transcript parsing, config, docs, and focused tests.

Validated on WSL/Windows Terminal against Grok Build (hooks + PTY + multi-agent send/reply).

Delivery protocol (aligned with Grok StopHookJson)

Grok discards stdout on observe-only hooks (SessionStart / UserPromptSubmit / PostToolUse). The only reliable model-injection gate is Stop → hookSpecificOutput.additionalContext.

hcom send → notify → PTY injects short wake sentinel only ("hcom: wake") + Enter
                   → turn starts (pending kept; no force-ack)
                   → genuine Stop(end_turn) → additionalContext = full <hcom> body + commit_delivery_ack
                   → agent replies via hcom send

Key rules after owner review alignment (d482ea5):

  1. PTY = wake only — never blind full-body inject / Ctrl-U / force-ack
  2. Observe hooks — no delivery on UPS/PostToolUse/SessionStart stdout
  3. Stop onlyadditionalContext (+ snake_case twin); never followup_message
  4. Session-end Stop — skip deliver/ack (channel_closed, shutdown, …)
  5. $GROK_HOME for hooks path; dual UPS races avoided by keeping pending until Stop

What works

  • hcom grok / hcom grok-build — launch with hooks install
  • hcom hooks add grok / hcom hooks remove grok~/.grok/hooks/hcom.json
  • hcom r <name> / hcom f <name>--resume + --fork-session
  • Message delivery to/from other tools (Claude, Codex, Cursor, …)
  • hcom transcript via ~/.grok/sessions/**/updates.jsonl
  • Wake-mode delivery + Enter retry on slow WSL (no premature ST_ACTIVE confirm)

New files

  • src/hooks/grok.rs — hook install/verify/remove + lifecycle (Stop gate)
  • src/transcript/grok.rsupdates.jsonl parser

Testing

Automated

  • cargo test grok — hooks, Stop additionalContext shape, host detection, delivery wake tests
  • cargo fmt / clippy cleanups on the branch

Manual (author, home/~ workspace)

  • Wake sentinel only in transcript user turn (hcom: wake)
  • Synthetic hcom grok-stop with pending → stdout hookSpecificOutput.additionalContext + log grok.stop.additional_context + cursor advance
  • Live multi-agent: bozo → reviewfix-mika → PONG-PROTO ack (agent also may self-pull via hcom listen, which advances cursor; pure mid-turn Stop body inject depends on ending turn without listen)

Usage

hcom hooks add grok
hcom grok
hcom send @<name> -- "hello from another agent"

Known limits / follow-ups

  • Composer scrape still returns None for Grok (wake uses force-Enter after sentinel; no Ctrl-U)
  • Full composer ownership / dual-queue with Grok Build submit queue still open
  • SessionStart bootstrap must use launch-time channels (--rules / skill), not hook stdout
  • Agents that hcom listen mid-turn advance last_event_id and can skip the Stop additionalContext path (listen is intentional CLI consumption)

Offline patch kit

~/workspaces/hcom-patches/grok-build-integration tracks tip d482ea5 for brew/offline apply.

Add Grok Build as a released hcom tool so agents can launch, message,
and coordinate with `hcom grok` the same way as Claude/Cursor/Kimi.

What works:
- `hcom grok` / `hcom grok-build` — PTY launch with hooks install
- `hcom hooks add grok` / `remove` — `~/.grok/hooks/hcom.json`
- `hcom r` / `hcom f` — resume + fork (`--resume`, `--fork-session`)
- Full-body PTY inject delivery (Grok ignores Claude exit-2 Stop)
- Claude-compat host detection so dual hooks do not double-queue
- Transcript parse of `~/.grok/sessions/**/updates.jsonl`
- Config: `grok_args` / `HCOM_GROK_ARGS`

Delivery notes (validated end-to-end):
- Grok loads Claude settings hooks and strips HCOM_PTY_MODE
- Bare `<hcom>` wake + WT/WSL control sequences corrupt the composer
- Inject plain-text message body, single Enter, ack immediately
- Skip followup_message when the prompt already carries the body

Tests: cargo test green for grok hooks, integration_spec, transcript.
@kiala9 kiala9 changed the title feat: add first-class Grok Build (grok) integration feat(grok): add first-class Grok Build integration Jul 11, 2026
kiala9 added 5 commits July 12, 2026 02:34
Remove a duplicated #[test] attribute, restore #[test] on
phase1_timeout_is_ten_seconds, and drop a useless borrow that
fails clippy -D warnings on CI.
Grok full-body inject was writing the whole body then force-Entering
after a fixed 350ms and acking immediately. On slow WSL/Grok Build the
composer often still had not absorbed the paste, so Enter was dropped
and the body sat unsent with no retry.

- Pace inject in small chunks with short gaps
- Scale post-inject settle with payload length (cap 4s)
- After first Enter+ack, confirm status left listening; re-Enter up to 3x
commit_delivery_ack sets ST_ACTIVE immediately, which made the post-Enter
confirm path believe Grok had submitted even when the body still sat in
the composer (UPS only fired after a manual Enter ~25s later).

Defer bus ack until real UPS (prompt|trigger) or pending is cleared by
the skip-followup hook path; re-Enter while still pending; double-\r
burst for slow WSL paste digest.
HCOM_GROK_DELIVERY=wake|full (also ~/.hcom/grok_delivery_mode) selects
PTY inject strategy. Wake mode pastes only "hcom: wake" then Enter so
GB can queue quickly; bus body is delivered via native UPS followup_message.

Claude-compat UPS defers bare-wake delivery to native grok hooks to avoid
dual followup races. E2E on headless silo: inject→enter ~1s, model saw
BARE-WAKE-TEST payload and replied PONG-BARE.
CI rust-tests/windows-build failed on fmt only (inject_text_paced
safe_text chain). No logic change.
@aannoo

aannoo commented Jul 21, 2026

Copy link
Copy Markdown
Owner

thanks for the pr looks good. here is AI review may or may not be relevant you can check what you want to do.

PR 81 review: Grok Build integration

Verdict: do not merge. The hook delivery path is structurally broken, the PTY path bypasses every safety mechanism hcom has, and content is mutated with no working fallback. The integration scaffolding (registration, launch, lifecycle, resume, transcripts) has value and can merge after targeted fixes, but the delivery architecture needs a redesign before released: true.

What was reviewed

PR 81 at 0d83923 (27 files, +2133 lines) applied against hcom main. Cross-referenced against the Grok Build source at c5c4ce03 (v0.2.106). No build or test execution — static source analysis only.

The hook delivery path is broken at the protocol level

Observe-only events cannot deliver messages

Grok classifies hook events by gate type. SessionStart, UserPromptSubmit, PostToolUse, SessionEnd, and notifications are GateKind::Observe. The command runner's Observe branch (xai-grok-hooks/src/runner/command.rs:230-238) returns Success or Failed based solely on exit code. Stdout is never parsed. It is read, measured for logging, and discarded.

PR 81 emits message payloads from three observe-only events:

  • handle_sessionstart (src/hooks/grok.rs:388-434): emits additional_context and hookSpecificOutput.additionalContext for bootstrap
  • handle_userpromptsubmit (src/hooks/grok.rs:437-482): emits followup_message
  • handle_posttooluse (src/hooks/grok.rs:499-519): emits additional_context and hookSpecificOutput.additionalContext

None of these outputs reach the model. Every message that attempts delivery through these hooks is silently lost.

Stop uses the wrong output field

Stop is a real blocking gate (GateKind::Stop). Its stdout is parsed as StopHookJson (xai-grok-hooks/src/runner/mod.rs:64-75), which accepts:

struct StopHookJson {
    decision: Option<String>,
    reason: Option<String>,
    continue_: Option<bool>,       // serde rename "continue"
    stop_reason: Option<String>,   // serde rename "stopReason"
    hook_specific_output: Option<StopHookSpecificOutputJson>,
}

struct StopHookSpecificOutputJson {
    additional_context: Option<String>,
}

PR 81's handle_stop (src/hooks/grok.rs:548-551) emits:

{"followup_message": "..."}

followup_message is not a field on StopHookJson. Serde deserializes this as an empty StopHookJson with all None fields. The message never reaches the model.

Acknowledgment fires on pipe flush, not model delivery

dispatch_grok_hook (src/hooks/grok.rs:612-618) commits the delivery ack after serde_json::to_writer + stdout.flush() succeeds. This proves only that bytes were written to a pipe. It does not prove Grok parsed them, accepted them, or exposed them to the model.

For observe-only events this is a guaranteed silent loss: hcom marks the message as delivered while Grok drops the output.

For Stop, the wrong field means the same outcome: empty parse, no continuation, message lost, cursor advanced.

Session-end Stop creates a deterministic loss case

Grok fires a separate session-end Stop when the user exits. dispatch_session_end_stop (xai-grok-shell/src/session/acp_session_impl/stop_gate.rs:112-141) runs hooks in Stop mode but calls demote_ignored_blocks on the results — block decisions are discarded because no turn remains.

PR 81's handle_stop does not filter on the Stop reason. It checks a status field for cancellation strings but does not distinguish reason: "end_turn" from reason: "channel_closed" / "shutdown". A message pending at session exit will be emitted through followup_message, Grok will ignore the output, and hcom will advance the cursor.

The PTY full-body path bypasses five safety mechanisms

1. Prompt readiness disabled

require_ready_prompt: false,

hcom's ready-prompt check ensures the tool's normal input interface is visible. Disabling it means hcom will inject text into any screen state.

2. Empty prompt check disabled

require_prompt_empty: false,

hcom's empty-prompt check prevents submitting content when the user has an unsent draft. The PR acknowledges that Grok's input box cannot be scraped — but the correct response is "don't inject," not "inject anyway."

3. Ctrl-U without observation

clear_composer_best_effort sends Ctrl-U before every Grok delivery. Grok's textarea implements Ctrl-U as delete-from-cursor-to-line-start, not clear-all. Text after the cursor survives. hcom cannot observe the result because it has no composer parser. A user draft can be partially deleted and then combined with the hcom message.

4. Force Enter without ownership

After injecting text and sleeping for a length-scaled settle time, hcom presses Enter without verifying that the composer contains exactly the injected text. This is the fundamental ownership check that hcom added and hardened across multiple tools and PRs. Bypassing it means:

  • An unrelated user prompt can be submitted
  • A partial or corrupted inject can be submitted
  • An empty composer can receive a spurious Enter

5. Force-ack after failed verification

After three Enter attempts without confirmation, hcom force-acknowledges the pending batch:

// Last resort: clear bus (composer may still need manual Enter).
if let Some(prepared) = prepare_pending_messages(db, &current_name) {
    commit_delivery_ack(db, &prepared.ack);
}

This produces at-most-once delivery under uncertainty: when hcom cannot prove delivery, it discards the messages. This conflicts with hcom's own direction of travel — bounded retries that park delivery while leaving messages pending.

Content is mutated and truncated

sanitize_grok_inject_text (src/delivery.rs):

  • <[, >] (breaks XML, HTML, Rust/C++ generics, shell redirection, comparisons)
  • Newlines and control characters → spaces (destroys code blocks, stack traces, structured data)
  • All whitespace collapsed
  • Capped at 2,000 characters

The code comment says "full body still arrives via UserPromptSubmit hooks when present." But UserPromptSubmit is observe-only — stdout is ignored. There is no working fallback for the truncated or mutated content.

Dual hook ownership creates race conditions

Grok loads Claude-compat hooks from ~/.claude/settings.json alongside native hooks from ~/.grok/hooks/. PR 81 adds Grok-specific delivery behavior to both:

  • Native Grok hooks (src/hooks/grok.rs): PostToolUse emits additional_context, Stop emits followup_message
  • Claude-compat hooks (src/hooks/claude.rs): PostToolUse emits followup_message when is_grok_host(), Stop delegates to handle_poll_for_grok which emits followup_message, UserPromptSubmit has Grok-specific body-matching logic

Both hook sets fire for each event. The code tries to coordinate with prompt_already_carries_hcom_body, which does 48-char prefix matching and pattern matching for "request #" + arrows. This is a heuristic that can false-positive on user content and false-negative when formatting differs between the two representations.

One owner per tool, not two racing owners with heuristic deduplication.

Other defects

$GROK_HOME ignored

Grok resolves its home as $GROK_HOME or ~/.grok (xai-grok-config/src/paths.rs:34-38). PR 81 hardcodes tool_config_root().join(".grok") for hooks and home/.grok/sessions for transcripts, with config_dir_env: None in the integration spec.

Resume argument handling drops valid values

merge_grok_args (src/commands/resume.rs) maintains a VALUE_FLAGS list but is missing:

  • --worktree [NAME]
  • --worktree-ref / --ref REF
  • --leader-socket PATH
  • --debug-file FILE
  • --system-prompt-override PROMPT

All non-flag positional tokens are dropped as "stale task prompts." --worktree feature --ref main becomes --worktree --ref.

Transcript chunk assembly

content_text (src/transcript/grok.rs:32-55) .trim()s every chunk before use. Streamed chunks like "Hello " + "world" become "Hello" + "world""Helloworld" when joined. Grok's own ACP stream concatenates raw chunks without trimming.

All tools are marked is_error: false. Tool results, failure updates, and _detailed mode are ignored.

One-shot mode treated as persistent

-p / --single is documented as one-shot scripting mode. The PR advertises it as hcom grok -p "task" while treating Grok as a PTY-managed persistent agent. A one-shot process exits and cannot receive later messages.

Delivery choices exposed by the reviewed Grok code

Two-stage sentinel delivery

Cursor and Copilot use the same basic delivery architecture:

message arrives while the agent is idle
→ hcom proves the normal composer is visible and empty
→ PTY submits only <hcom>
→ bootstrap says <hcom> is not a task: do not answer, do not run tools, end immediately
→ the wake turn reaches its end-of-turn hook
→ the hook supplies the real hcom body
→ the model handles the body in the second stage

The host-specific difference is only how the second stage is created:

  • Cursor: Stop returns followup_message; Cursor submits that body as a new user prompt.
  • Copilot: AgentStop returns decision:block with the body in reason; Copilot prevents termination and continues with that feedback.
  • Grok: the reviewed Stop schema accepts hookSpecificOutput.additionalContext; static analysis shows that context is fed back into the model at Stop. Grok does not expose Cursor's followup_message field, and its exact continuation fields are Grok-specific rather than Copilot's schema.

For Grok, the corresponding idle flow is therefore:

prove Grok's composer is visible and empty
→ submit a small <hcom> sentinel
→ bootstrap makes Grok end the wake turn
→ genuine Stop(reason="end_turn") fires
→ Stop additionalContext contains the real batch
→ Grok processes the batch in the continuation stage

For a message that arrives while Grok is already working, the PTY wake is unnecessary:

message remains pending
→ current Grok turn reaches genuine Stop(reason="end_turn")
→ Stop additionalContext supplies the real batch
→ Grok continues with it

UserPromptSubmit and PostToolUse cannot carry the body because their stdout is ignored. Stop-added context is limited to roughly 10,000 characters and eight continuations, and the same boundary is not available on interruption, refusal, maximum-turn termination, API error, or shutdown.

Historical-Codex-style retrieval

short PTY sentinel or Stop feedback
→ bootstrap/injected context tells Grok to run an hcom read command
→ exact message body arrives as tool output
→ the retrieved batch is acknowledged

The sentinel still creates the wake turn, but the second stage retrieves the body through a tool call instead of carrying it directly in Stop output. This avoids the Stop context-size limit and requires separate read and acknowledgment semantics.

Full-text PTY prompt

Grok is idle
→ prove the normal composer is visible and empty
→ paste the exact message body
→ verify the composer contains exactly that body
→ submit it as a normal user prompt

This skips the sentinel/Stop second stage and places the real body directly in the composer. It is idle-only and cannot inject a new prompt while Grok is already processing a turn.

PR 81's current path mutates and truncates the body, cannot prove an empty composer, uses Ctrl-U without observing the result, submits without exact ownership, and force-acknowledges on uncertainty.

ACP

hcom opens or controls a Grok ACP session
→ messages are sent through the structured session protocol
→ responses and lifecycle events return through ACP

This bypasses both PTY sentinel delivery and file-hook payload delivery. Static analysis shows an ACP session implementation but does not establish attachment to an already-running interactive Grok TUI session.

Other hcom delivery models

Model What happens
Prompt-start hook payload Claude PTY, Codex, Kimi, and Gemini attach the real body during the same turn started by the PTY trigger
Blocking Stop listener Claude print/vanilla Stop waits for a message and then resumes Claude with the body; no PTY wake
Plugin/extension API OpenCode, Kilo, Pi, and OMP call a host prompt/message API directly
Manual retrieval The agent runs hcom listen without an automatic wake path

Merge recommendation

Merge after fixes

Component Fix needed
Tool::Grok registration, aliases, config, help None
Hook installation/removal Resolve $GROK_HOME consistently
Launch and lifecycle hooks (SessionStart, SessionEnd, PreToolUse) Remove delivery/bootstrap from SessionStart stdout
Status tracking and TUI integration None
Resume/fork Fix arg handling: preserve unknown flags verbatim, reject -p/--single
Transcript parser Remove chunk trimming; add real fixtures with chunk boundaries

Remove or replace

Component Reason
PostToolUse message delivery Observe-only; stdout ignored
UserPromptSubmit message delivery Observe-only; stdout ignored
followup_message from Stop Not in StopHookJson schema
Current full-body PTY implementation Content mutation, no ownership, no verification
sanitize_grok_inject_text Semantic corruption with no working fallback
Ctrl-U / force-Enter / double-Enter Unsafe without composer parser
Force-ack after failed delivery Violates pending-message invariant
Grok-specific delivery logic in claude.rs One delivery owner per tool
prompt_already_carries_hcom_body Heuristic deduplication for a race that shouldn't exist
Current GrokDeliveryMode / HCOM_GROK_DELIVERY paths They select delivery paths that are not protocol-correct as implemented

Delivery choices requiring implementation work

Choice Missing implementation
Two-stage sentinel delivery Add a Grok composer parser and safe <hcom> submission for idle wake; deliver the real body only from genuine Stop(reason=="end_turn") using Grok's supported continuation schema
Active Stop delivery Leave messages pending until genuine Stop, obey Stop context/continuation limits, and acknowledge only after the supported handoff
Historical-Codex-style retrieval Add a non-destructive read plus exact batch acknowledgment after the tool result is observed
Full-text PTY prompt Add lossless paste, exact composer parsing, post-paste ownership verification, submission correlation, and failure-safe acknowledgment
ACP Define whether hcom creates a separate managed Grok session or can attach to an existing interactive session
Bootstrap Use --rules, --append-system-prompt, or another supported launch-time channel rather than SessionStart stdout

kiala9 added 2 commits July 25, 2026 17:48
Owner review (PR aannoo#81): observe-only hooks discard stdout; Stop only
accepts hookSpecificOutput.additionalContext (not followup_message).

- SessionStart/UPS/PostToolUse: status only — no message payloads/acks
- Stop: deliver pending via additionalContext on genuine end_turn only;
  skip session-end reasons (channel_closed/shutdown/…)
- Claude-compat: no Grok delivery ownership (native grok-stop only)
- PTY: short wake sentinel only; no full-body sanitize/Ctrl-U/force-ack
- Respect $GROK_HOME; resume VALUE_FLAGS for worktree/debug/system-prompt
- Transcript: do not trim stream chunks; surface tool is_error
- Help: -p is one-shot, not multi-turn hcom agent
Empty commit so PR aannoo#81 CI re-runs on the current delivery protocol tip.
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