From 21f516cffa305eefab991747f1419d92a96d5946 Mon Sep 17 00:00:00 2001 From: fujibee Date: Tue, 23 Jun 2026 23:10:33 -0700 Subject: [PATCH 1/3] feat(types): add grok-build agent type (#214) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an agmsg agent type for xAI's official Grok Build CLI (binary 'grok', x.ai/cli) so it can join a team alongside Claude Code, Codex, Gemini, Cursor, and Hermes. Grok Build deliberately adopts Claude-Code-style conventions, so this is a manifest + path/detect remap rather than new infrastructure — the existing Stop-hook delivery script reads session_id from the hook input JSON on stdin, which Grok Build emits the same way, so no env remap is needed. - scripts/drivers/types/grok-build/{type.conf,template.md,_delivery.sh}: a turn/off type (no Monitor-tool equivalent, so delivery_modes='turn off', default turn). detect=GROK_SESSION_ID for whoami; cli=grok. The _delivery.sh writes a dedicated .grok/hooks/agmsg.json (copilot model). - install.sh: ship the skill to ~/.grok/skills// and accept --agent-type grok-build, both via the type registry. - tests: grok-build delivery (turn/off/reject), install (skill drop + --agent-type), join, and the registry's nine-built-ins count. C-GATED (#214): the hook filename and JSON schema in _delivery.sh and the delivery tests are the assumed Claude-Code shape — xAI's official schema page was not directly fetchable. They are scaffolded and must be confirmed against a real install with 'grok inspect' before release; everything else (manifest, template, whoami detect, install placement, join) is schema-independent. --- install.sh | 25 +++- scripts/drivers/types/grok-build/_delivery.sh | 52 +++++++ scripts/drivers/types/grok-build/template.md | 136 ++++++++++++++++++ scripts/drivers/types/grok-build/type.conf | 9 ++ tests/test_delivery.bats | 43 ++++++ tests/test_install.bats | 33 +++++ tests/test_team.bats | 6 + tests/test_type_registry.bats | 4 +- 8 files changed, 303 insertions(+), 5 deletions(-) create mode 100644 scripts/drivers/types/grok-build/_delivery.sh create mode 100644 scripts/drivers/types/grok-build/template.md create mode 100644 scripts/drivers/types/grok-build/type.conf diff --git a/install.sh b/install.sh index 0f545ae..c0cb6a1 100755 --- a/install.sh +++ b/install.sh @@ -162,7 +162,7 @@ while [[ $# -gt 0 ]]; do echo "Options:" echo " --cmd Command & skill folder name (default: agmsg)" echo " Claude Code: /, Codex/Gemini/Antigravity: \$" - echo " --agent-type Agent type: claude-code, codex, gemini, antigravity, opencode, hermes, cursor" + echo " --agent-type Agent type: claude-code, codex, gemini, antigravity, opencode, hermes, cursor, grok-build" echo " Selects which template becomes SKILL.md (matches the" echo " arg passed to join.sh / whoami.sh)" echo " --update Update skill scripts only (preserve DB and teams)" @@ -241,7 +241,7 @@ if [ "$UPDATE_ONLY" = true ]; then # shared SKILL.md; their dedicated copies are dropped separately below.) TPL_TYPE="codex" case "$AGENT_TYPE" in - gemini|antigravity|opencode|hermes|cursor) TPL_TYPE="$AGENT_TYPE" ;; + gemini|antigravity|opencode|hermes|cursor|grok-build) TPL_TYPE="$AGENT_TYPE" ;; esac sed "s/__SKILL_NAME__/$SKILL_NAME/g" "$(agmsg_type_template_path "$TPL_TYPE")" > "$SKILL_DIR/SKILL.md" # Recursive copy so nested helper dirs (scripts/lib/, scripts/drivers/types/) @@ -280,6 +280,12 @@ if [ "$UPDATE_ONLY" = true ]; then mkdir -p "$HERMES_SKILL_DIR" sed "s/__SKILL_NAME__/$SKILL_NAME/g" "$(agmsg_type_template_path hermes)" > "$HERMES_SKILL_DIR/SKILL.md" fi + # Refresh / install the Grok Build skill (same reasoning as Copilot above). + GROK_SKILL_DIR="$HOME/.grok/skills/$SKILL_NAME" + if [ -d "$HOME/.grok" ]; then + mkdir -p "$GROK_SKILL_DIR" + sed "s/__SKILL_NAME__/$SKILL_NAME/g" "$(agmsg_type_template_path grok-build)" > "$GROK_SKILL_DIR/SKILL.md" + fi cp "$SCRIPT_DIR/openai.yaml" "$SKILL_DIR/agents/openai.yaml" 2>/dev/null || true chmod +x "$SKILL_DIR/scripts/"*.sh chmod +x "$SKILL_DIR/scripts/drivers/types/codex/"*.sh 2>/dev/null || true @@ -337,7 +343,7 @@ mkdir -p "$SKILL_DIR"/{scripts,types,db,agents} # codex template by default; gemini/antigravity/opencode get their own. TPL_TYPE="codex" case "$AGENT_TYPE" in - gemini|antigravity|opencode|hermes|cursor) TPL_TYPE="$AGENT_TYPE" ;; + gemini|antigravity|opencode|hermes|cursor|grok-build) TPL_TYPE="$AGENT_TYPE" ;; esac sed "s/__SKILL_NAME__/$CMD_NAME/g" "$(agmsg_type_template_path "$TPL_TYPE")" > "$SKILL_DIR/SKILL.md" # Recursive copy so nested helper dirs (scripts/lib/, scripts/drivers/types/) ship @@ -423,6 +429,19 @@ if [ -d "$HOME/.hermes" ]; then echo " + installed /$CMD_NAME skill to ~/.hermes/skills/" fi +# --- Install Grok Build skill --- +# Grok Build reads skills from ~/.grok/skills//SKILL.md (it also accepts +# the cross-vendor ~/.agents/skills/ fallback, but the shared SKILL.md is +# Codex-typed and would mis-identify a Grok session — keep the Grok copy +# separate, same pattern as Copilot). Delivery (turn) registers a Stop hook under +# ~/.grok/hooks/ via `delivery.sh set` per project. +GROK_SKILL_DIR="$HOME/.grok/skills/$CMD_NAME" +if [ -d "$HOME/.grok" ]; then + mkdir -p "$GROK_SKILL_DIR" + sed "s/__SKILL_NAME__/$CMD_NAME/g" "$(agmsg_type_template_path grok-build)" > "$GROK_SKILL_DIR/SKILL.md" + echo " + installed /$CMD_NAME skill to ~/.grok/skills/" +fi + # Codex sandbox writable_roots are configured by configure_codex_sandbox() at # the "Done" step below — the single source of truth for db/, teams/, and run/. # (A legacy inline copy used to run here too, which double-mutated the array and diff --git a/scripts/drivers/types/grok-build/_delivery.sh b/scripts/drivers/types/grok-build/_delivery.sh new file mode 100644 index 0000000..1a6f95b --- /dev/null +++ b/scripts/drivers/types/grok-build/_delivery.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# grok-build delivery plug — dedicated JSON hook file (.grok/hooks/agmsg.json). +# +# Grok Build has no Monitor-tool equivalent, so the manifest declares +# delivery_modes="turn off"; delivery.sh's central gate rejects monitor/both +# before this runs (and before any file is touched). Modeled on the copilot +# plug: a fully agmsg-owned hook file we write/remove wholesale (not a merge +# into a shared settings file). The Stop hook runs check-inbox.sh, which reads +# `session_id` from the hook input JSON on stdin — Grok Build emits the same +# Claude-Code-shaped event, so no env remap is needed here. Uses +# resolve_hooks_file + SKILL_DIR from delivery.sh's sourced context. +# +# C-GATED (#214): the exact hook filename + JSON schema below are the assumed +# Claude-Code shape (xAI's official schema page was not directly fetchable). Do +# NOT treat this as final — confirm against a real install with `grok inspect` +# and adjust the file path (manifest hooks_file=) and the JSON keys if they +# differ. Everything else in this type (manifest, template, whoami detect, +# install SKILL placement, join) is schema-independent and stands as-is. +agmsg_delivery_apply() { + local type="$1" + local project="$2" + local mode="$3" + local hooks_file + hooks_file=$(resolve_hooks_file "$type" "$project") + + # Strip first so re-applying turn is an idempotent rewrite and turn->off + # cleanly removes the file. + rm -f "$hooks_file" + + if [ "$mode" = "turn" ]; then + mkdir -p "$(dirname "$hooks_file")" + local cmd="'$SKILL_DIR/scripts/check-inbox.sh' '$type' '$project'" + local cmd_json + cmd_json=$(agmsg_sqlite_mem "SELECT json_quote('$(printf '%s' "$cmd" | sed "s/'/''/g")');") + # Stop trigger (PascalCase) so the input payload's snake_case session_id + # field matches what check-inbox.sh already parses. + cat < "$hooks_file" +{ + "version": 1, + "hooks": { + "Stop": [ + { + "type": "command", + "bash": $cmd_json, + "timeoutSec": 30 + } + ] + } +} +EOF + fi +} diff --git a/scripts/drivers/types/grok-build/template.md b/scripts/drivers/types/grok-build/template.md new file mode 100644 index 0000000..12db84a --- /dev/null +++ b/scripts/drivers/types/grok-build/template.md @@ -0,0 +1,136 @@ +--- +name: __SKILL_NAME__ +description: Cross-agent messaging via SQLite. Send messages between Claude Code, Codex, Gemini CLI, Grok Build, and other agents. No daemon, no network, no dependencies beyond bash and sqlite3. +--- + +Agent messaging command. **IMPORTANT: Always use the provided scripts. NEVER directly read or edit config files, DB, or team data. There is NO register.sh — use join.sh to join a team.** + +## Identity + +If you already know your AGENT and TEAMS from a previous `/__SKILL_NAME__` call in this session, skip to **Execute** below. + +Otherwise, run: `~/.agents/skills/__SKILL_NAME__/scripts/whoami.sh "$(pwd)" grok-build` + +Four possible outputs: + +**A) Single identity:** +`agent= teams= type=grok-build project=` +→ Remember AGENT and TEAMS, then go to **Execute**. + +**B) Multiple identities:** +`multiple=true agents= teams= type=grok-build project=` +→ Ask the user which agent name to use for this session, then go to **Execute**. + +**C) Not in a team:** +`not_joined=true available_teams=` (or `available_teams=none`) +→ Show the user the available teams from the output, then: + + > **First-time setup required.** + > Joining a team so this agent can send and receive messages. + > - **Team name**: a group of agents that can message each other (available: ) + > - **Agent name**: this agent's identity within the team + + 1. Ask: "Enter a team name (joins existing or creates new)" + 2. Ask: "Enter a name for this agent" + 3. **You MUST use join.sh** — run: `~/.agents/skills/__SKILL_NAME__/scripts/join.sh grok-build "$(pwd)"` + 4. Show the result and explain: + + > **Joined!** You can now use `/__SKILL_NAME__` to check and send messages. + > - `/__SKILL_NAME__` — check inbox + > - `/__SKILL_NAME__ send ` — send a message + > - `/__SKILL_NAME__ team` — list team members + > - `/__SKILL_NAME__ history` — message history + + 5. **REQUIRED — Do NOT skip this step.** Ask the user to pick a delivery mode using exactly this prompt: + + ``` + Choose delivery mode for incoming messages: + + 1) turn — Check inbox at the end of each assistant turn + Stop hook pulls after each response. + + 2) off — No automatic delivery + Manual /__SKILL_NAME__ only. + + [1]: + ``` + + - **Wait for the user's answer before proceeding.** Empty input means `1` (turn). + - Map the chosen number to a mode (`1`→`turn`, `2`→`off`) and run: + `~/.agents/skills/__SKILL_NAME__/scripts/delivery.sh set grok-build "$(pwd)"` + - Grok Build has no Monitor-tool equivalent, so `monitor` and `both` modes are not offered here. + + 6. Then check inbox for the newly joined team. + +**D) Suggestions for reuse:** +`suggest=true agents= teams= type=grok-build project= available_teams=` +→ No exact registration exists for this project, but there are same-type agent names registered elsewhere. + + 1. Show the suggested agent names to the user. + 2. Ask whether to reuse one of those names or choose a new one. + 3. Ask for the team name to join (existing or new). + 4. Run: `~/.agents/skills/__SKILL_NAME__/scripts/join.sh grok-build "$(pwd)"` + 5. Then continue with the normal post-join flow above. + +## Execute + +**Only use scripts in `~/.agents/skills/__SKILL_NAME__/scripts/` — do not read or modify files under `teams/` or `db/` directly.** + +**If no arguments provided (DEFAULT action — always do this when the command is invoked without arguments):** +1. **IMMEDIATELY** run inbox check for each TEAM: `~/.agents/skills/__SKILL_NAME__/scripts/inbox.sh $TEAM $AGENT` +2. Do NOT ask the user what to do — just run the inbox check. +3. If there are messages, read and respond appropriately. To reply: + `~/.agents/skills/__SKILL_NAME__/scripts/send.sh $TEAM $AGENT ""` + +If argument is "history": +1. Run: `~/.agents/skills/__SKILL_NAME__/scripts/history.sh $TEAM $AGENT` + +If argument is "team": +1. For each TEAM, run: `~/.agents/skills/__SKILL_NAME__/scripts/team.sh $TEAM` + +If argument starts with "send" (e.g. "send misaki check the server"): +1. Parse target agent and message from the arguments +2. Determine which team the target agent belongs to, then run: + `~/.agents/skills/__SKILL_NAME__/scripts/send.sh $TEAM $AGENT ""` + +If argument is "config": +1. Run: `~/.agents/skills/__SKILL_NAME__/scripts/config.sh show` +2. Show the output to the user. + +If argument starts with "config set" (e.g. "config set hook.check_interval 30"): +1. Parse key and value from the arguments. +2. Run: `~/.agents/skills/__SKILL_NAME__/scripts/config.sh set ` + + +If argument starts with "actas" followed by an agent name (e.g. "actas alice"): +1. Parse the new role name. +2. Run `~/.agents/skills/__SKILL_NAME__/scripts/identities.sh "$(pwd)" grok-build` to see whether the role is already registered for this (project, type). +3. If the name does not appear in the output, join under the existing team. For a single team, run `~/.agents/skills/__SKILL_NAME__/scripts/join.sh grok-build "$(pwd)"`. For multiple teams, ask the user which team to join the new role into. +4. Set the session's active FROM to `` for every `send.sh` call until another `actas`. +5. Tell the user: "Now acting as ``. Sends will use `` as the from agent. (Grok Build has no Monitor tool, so receive still covers all of your registered roles in this project.)" + +If argument starts with "drop" followed by an agent name (e.g. "drop alice"): +1. Parse the role name. +2. Run `~/.agents/skills/__SKILL_NAME__/scripts/reset.sh "$(pwd)" grok-build ` to remove that role's registration. +3. If the session's active FROM was ``, clear that state. +4. Tell the user: "Dropped role `` from this project." + +If argument is "mode" (no further args): +1. Run: `~/.agents/skills/__SKILL_NAME__/scripts/delivery.sh status grok-build "$(pwd)"` +2. Show the output to the user. + +If argument starts with "mode" followed by a mode name (e.g. "mode turn"): +1. Parse the mode. Grok Build supports only `turn` and `off` — reject `monitor` and `both` with: "Grok Build has no Monitor tool; only `turn` or `off` modes are supported." +2. Run: `~/.agents/skills/__SKILL_NAME__/scripts/delivery.sh set grok-build "$(pwd)"` + +If argument is "hook on" (legacy alias): +1. Run: `~/.agents/skills/__SKILL_NAME__/scripts/delivery.sh set turn grok-build "$(pwd)"` +2. Tell the user: "Delivery mode set to 'turn' (legacy hook on behavior)." + +If argument is "hook off" (legacy alias): +1. Run: `~/.agents/skills/__SKILL_NAME__/scripts/delivery.sh set off grok-build "$(pwd)"` +2. Tell the user: "Delivery mode set to 'off'." + +If argument is "reset": +1. Run: `~/.agents/skills/__SKILL_NAME__/scripts/reset.sh "$(pwd)" grok-build` +2. Tell the user the result. diff --git a/scripts/drivers/types/grok-build/type.conf b/scripts/drivers/types/grok-build/type.conf new file mode 100644 index 0000000..bbdbd76 --- /dev/null +++ b/scripts/drivers/types/grok-build/type.conf @@ -0,0 +1,9 @@ +# agmsg agent-type manifest — read-only key=value DATA. NEVER sourced. +name=grok-build +template=template.md +cli=grok +detect=GROK_SESSION_ID +detect_proc=grok grok-* +hooks_file=.grok/hooks/agmsg.json +monitor=no +delivery_modes=turn off diff --git a/tests/test_delivery.bats b/tests/test_delivery.bats index bdc1b1e..20db89f 100644 --- a/tests/test_delivery.bats +++ b/tests/test_delivery.bats @@ -1455,3 +1455,46 @@ JSON kill "$watch_pid" 2>/dev/null || true wait 2>/dev/null || true } + +# --- grok-build (turn|off via a dedicated .grok/hooks/agmsg.json) --- +# C-GATED (#214): the hook file path + JSON schema asserted here are the assumed +# Claude-Code shape (xAI's official schema page was not directly fetchable). If +# `grok inspect` on a real install shows a different filename/keys, update the +# manifest hooks_file=, the _delivery.sh writer, and these assertions together. + +@test "delivery set turn (grok-build): writes .grok/hooks/agmsg.json with version + Stop entry" { + run bash "$SCRIPTS/delivery.sh" set turn grok-build "$TEST_PROJECT" + [ "$status" -eq 0 ] + [[ "$output" =~ "Delivery mode set to 'turn'" ]] + local hook_file="$TEST_PROJECT/.grok/hooks/agmsg.json" + [ -f "$hook_file" ] + local v + v=$(sqlite_mem "SELECT json_extract(readfile('$(rf "$hook_file")'), '\$.version');") + [ "$v" = "1" ] + local cmd + cmd=$(sqlite_mem "SELECT json_extract(readfile('$(rf "$hook_file")'), '\$.hooks.Stop[0].bash');") + [[ "$cmd" =~ "check-inbox.sh" ]] + [[ "$cmd" =~ "grok-build" ]] +} + +@test "delivery set off (grok-build): removes the hook file" { + bash "$SCRIPTS/delivery.sh" set turn grok-build "$TEST_PROJECT" + [ -f "$TEST_PROJECT/.grok/hooks/agmsg.json" ] + run bash "$SCRIPTS/delivery.sh" set off grok-build "$TEST_PROJECT" + [ "$status" -eq 0 ] + [ ! -f "$TEST_PROJECT/.grok/hooks/agmsg.json" ] +} + +@test "delivery set monitor (grok-build): rejected; no hook file written" { + run bash "$SCRIPTS/delivery.sh" set monitor grok-build "$TEST_PROJECT" + [ "$status" -ne 0 ] + [[ "$output" =~ "not supported" ]] + [ ! -f "$TEST_PROJECT/.grok/hooks/agmsg.json" ] +} + +@test "delivery set both (grok-build): does NOT delete an existing turn hook" { + bash "$SCRIPTS/delivery.sh" set turn grok-build "$TEST_PROJECT" >/dev/null + run bash "$SCRIPTS/delivery.sh" set both grok-build "$TEST_PROJECT" + [ "$status" -ne 0 ] + [ -f "$TEST_PROJECT/.grok/hooks/agmsg.json" ] +} diff --git a/tests/test_install.bats b/tests/test_install.bats index 9c1585c..d2afd21 100644 --- a/tests/test_install.bats +++ b/tests/test_install.bats @@ -558,3 +558,36 @@ PY # The refresh is gated on an existing agmsg shim — it must not opt the user in. [ ! -e "$FAKE_HOME/.agents/bin/codex" ] } + +# --- grok-build skill (~/.grok/skills//SKILL.md) --- + +@test "install: drops a Grok Build SKILL.md when ~/.grok exists" { + mkdir -p "$FAKE_HOME/.grok" + HOME="$FAKE_HOME" bash "$REPO_ROOT/install.sh" --cmd agmsg + local grok_skill="$FAKE_HOME/.grok/skills/agmsg/SKILL.md" + [ -f "$grok_skill" ] + grep -q "whoami.sh \"\$(pwd)\" grok-build" "$grok_skill" + ! grep -q "whoami.sh \"\$(pwd)\" codex" "$grok_skill" + grep -q "^name: agmsg" "$grok_skill" +} + +@test "install: skips Grok Build skill when ~/.grok is absent" { + rm -rf "$FAKE_HOME/.grok" + HOME="$FAKE_HOME" bash "$REPO_ROOT/install.sh" --cmd agmsg + [ ! -d "$FAKE_HOME/.grok" ] +} + +@test "install --update: installs Grok Build skill for upgraders without prior skill" { + HOME="$FAKE_HOME" bash "$REPO_ROOT/install.sh" --cmd agmsg + [ ! -d "$FAKE_HOME/.grok/skills/agmsg" ] + mkdir -p "$FAKE_HOME/.grok" + HOME="$FAKE_HOME" bash "$REPO_ROOT/install.sh" --update + [ -f "$FAKE_HOME/.grok/skills/agmsg/SKILL.md" ] + grep -q "whoami.sh \"\$(pwd)\" grok-build" "$FAKE_HOME/.grok/skills/agmsg/SKILL.md" +} + +@test "install: --agent-type grok-build makes shared SKILL.md Grok-typed" { + HOME="$FAKE_HOME" bash "$REPO_ROOT/install.sh" --cmd agmsg --agent-type grok-build + grep -q "whoami.sh \"\$(pwd)\" grok-build" "$SK/SKILL.md" + ! grep -q "whoami.sh \"\$(pwd)\" codex" "$SK/SKILL.md" +} diff --git a/tests/test_team.bats b/tests/test_team.bats index 55fcdb7..5a6ceaf 100644 --- a/tests/test_team.bats +++ b/tests/test_team.bats @@ -385,3 +385,9 @@ teardown() { [ "$status" -eq 0 ] [ -f "$TEST_SKILL_DIR/teams/myteam/config.json" ] } + +@test "join: accepts grok-build" { + run bash "$SCRIPTS/join.sh" myteam alice grok-build /tmp/proj + [ "$status" -eq 0 ] + [ -f "$TEST_SKILL_DIR/teams/myteam/config.json" ] +} diff --git a/tests/test_type_registry.bats b/tests/test_type_registry.bats index 4c995e8..2c17604 100644 --- a/tests/test_type_registry.bats +++ b/tests/test_type_registry.bats @@ -29,11 +29,11 @@ write_node_launcher_fixtures() { printf '// stub node launcher fixture\n' > "$nd/nodetype-launcher.mjs" } -@test "type-registry: known_types lists the eight built-ins" { +@test "type-registry: known_types lists the nine built-ins" { run env -i PATH="$PATH" bash -c \ "source '$SCRIPTS/lib/type-registry.sh'; agmsg_known_types | sort -u | paste -sd, -" [ "$status" -eq 0 ] - [ "$output" = "antigravity,claude-code,codex,copilot,cursor,gemini,hermes,opencode" ] + [ "$output" = "antigravity,claude-code,codex,copilot,cursor,gemini,grok-build,hermes,opencode" ] } @test "type-registry: is_known_type accepts a built-in and rejects a bogus type" { From 66d23e00f6ea711082b9e564a37cd5a655a78820 Mon Sep 17 00:00:00 2001 From: fujibee Date: Tue, 23 Jun 2026 23:55:43 -0700 Subject: [PATCH 2/3] feat(grok-build): finalize hook schema, vendor-neutral session-id, spawn Confirmed the Grok Build hook contract against a real install (xAI local docs ~/.grok/docs/user-guide/10-hooks.md) and finalize the type: - _delivery.sh: write the confirmed nested Claude hook shape { hooks: { Stop: [ { hooks: [ { type:command, command:, timeout:N } ] } ] } } at .grok/hooks/agmsg.json (no top-level version), command as the absolute check-inbox.sh path. Replaces the provisional copilot-flat scaffold. - session-start.sh / check-inbox.sh: resolve the session id from snake_case session_id (Claude) -> camelCase sessionId (Grok Build / Cursor) -> $GROK_SESSION_ID env. Additive and snake-first, so claude-code is unchanged. Grok emits camelCase stdin, which previously degraded to a synthesized id and broke per-session dedup (#93) and the delivery watermark. - type.conf: spawnable=yes. spawn.sh already launches direct-CLI types via `grok "/agmsg actas "` and skips the readiness wait for monitor=no types, so no spawn.sh change is needed. - install.sh: infer grok-build on --update from an existing grok-typed SKILL.md. - tests: nested hook shape, session-id resolution (camelCase / env / snake-wins), status detection, --update inference, and grok-build in the spawnable set. Refs #214. --- install.sh | 2 + scripts/check-inbox.sh | 10 ++- scripts/drivers/types/grok-build/_delivery.sh | 42 ++++++----- scripts/drivers/types/grok-build/type.conf | 1 + scripts/session-start.sh | 11 ++- tests/test_delivery.bats | 69 ++++++++++++++++--- tests/test_type_registry.bats | 4 +- 7 files changed, 107 insertions(+), 32 deletions(-) diff --git a/install.sh b/install.sh index c0cb6a1..37e3754 100755 --- a/install.sh +++ b/install.sh @@ -232,6 +232,8 @@ if [ "$UPDATE_ONLY" = true ]; then AGENT_TYPE="antigravity" elif grep -q "whoami.sh.*gemini" "$SKILL_DIR/SKILL.md" 2>/dev/null; then AGENT_TYPE="gemini" + elif grep -q "whoami.sh.*grok-build" "$SKILL_DIR/SKILL.md" 2>/dev/null; then + AGENT_TYPE="grok-build" else AGENT_TYPE="codex" fi diff --git a/scripts/check-inbox.sh b/scripts/check-inbox.sh index dc067b7..46a07b5 100755 --- a/scripts/check-inbox.sh +++ b/scripts/check-inbox.sh @@ -40,11 +40,17 @@ if echo "$INPUT" | grep -q '"stop_hook_active"[[:space:]]*:[[:space:]]*true' 2>/ fi # Defer to the monitor watcher when one is alive for this session. -# Avoids double-delivery when delivery.mode = both. session_id is sent in -# the hook input JSON for Stop events. +# Avoids double-delivery when delivery.mode = both. The session id field name +# differs by vendor: Claude Code emits snake_case "session_id"; Grok Build (and +# Cursor) emit camelCase "sessionId". Try snake first (claude-code unaffected), +# then camel, then the GROK_SESSION_ID env Grok injects into every hook. SESSION_ID=$(printf '%s' "$INPUT" \ | sed -n 's/.*"session_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \ | head -1) +[ -z "$SESSION_ID" ] && SESSION_ID=$(printf '%s' "$INPUT" \ + | sed -n 's/.*"sessionId"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \ + | head -1) +[ -z "$SESSION_ID" ] && SESSION_ID="${GROK_SESSION_ID:-}" if [ -n "$SESSION_ID" ]; then # The monitor watcher keys its pidfile (and its actas owner, below) on the # per-process instance id (#93), not the bare session_id. Normalize to the diff --git a/scripts/drivers/types/grok-build/_delivery.sh b/scripts/drivers/types/grok-build/_delivery.sh index 1a6f95b..194cf15 100644 --- a/scripts/drivers/types/grok-build/_delivery.sh +++ b/scripts/drivers/types/grok-build/_delivery.sh @@ -5,17 +5,18 @@ # delivery_modes="turn off"; delivery.sh's central gate rejects monitor/both # before this runs (and before any file is touched). Modeled on the copilot # plug: a fully agmsg-owned hook file we write/remove wholesale (not a merge -# into a shared settings file). The Stop hook runs check-inbox.sh, which reads -# `session_id` from the hook input JSON on stdin — Grok Build emits the same -# Claude-Code-shaped event, so no env remap is needed here. Uses -# resolve_hooks_file + SKILL_DIR from delivery.sh's sourced context. +# into a shared settings file). The Stop hook runs check-inbox.sh between turns. +# Uses resolve_hooks_file + SKILL_DIR from delivery.sh's sourced context. # -# C-GATED (#214): the exact hook filename + JSON schema below are the assumed -# Claude-Code shape (xAI's official schema page was not directly fetchable). Do -# NOT treat this as final — confirm against a real install with `grok inspect` -# and adjust the file path (manifest hooks_file=) and the JSON keys if they -# differ. Everything else in this type (manifest, template, whoami detect, -# install SKILL placement, join) is schema-independent and stands as-is. +# Schema confirmed against a real Grok Build install (xAI local docs +# ~/.grok/docs/user-guide/10-hooks.md): the hook file is the Claude-Code-shaped +# nested form { "hooks": { "Stop": [ { "hooks": [ { "type":"command", +# "command":, "timeout":N } ] } ] } } at /.grok/hooks/agmsg.json +# (no top-level "version"). Grok injects GROK_SESSION_ID/CLAUDE_PROJECT_DIR into +# every hook and emits the session id on stdin as camelCase "sessionId"; the +# shared session-start.sh / check-inbox.sh resolve that (snake_case session_id -> +# camelCase sessionId -> $GROK_SESSION_ID) so no per-type remap is needed here. +# `command` is the absolute check-inbox.sh path ($SKILL_DIR is absolute). agmsg_delivery_apply() { local type="$1" local project="$2" @@ -32,17 +33,20 @@ agmsg_delivery_apply() { local cmd="'$SKILL_DIR/scripts/check-inbox.sh' '$type' '$project'" local cmd_json cmd_json=$(agmsg_sqlite_mem "SELECT json_quote('$(printf '%s' "$cmd" | sed "s/'/''/g")');") - # Stop trigger (PascalCase) so the input payload's snake_case session_id - # field matches what check-inbox.sh already parses. + # Stop = "agent turn ends" (lifecycle event, no matcher). Nested Claude shape: + # Stop[].hooks[] each { type:"command", command:, timeout: }. cat < "$hooks_file" { - "version": 1, "hooks": { "Stop": [ { - "type": "command", - "bash": $cmd_json, - "timeoutSec": 30 + "hooks": [ + { + "type": "command", + "command": $cmd_json, + "timeout": 30 + } + ] } ] } @@ -50,3 +54,9 @@ agmsg_delivery_apply() { EOF fi } + +# Status derives the mode from the hook file's existence, not by parsing the +# Claude/Codex nested-settings shape the default status reader expects — our +# hook file is the dedicated agmsg-owned form (present => turn, absent => off). +# Same override copilot uses. Schema-independent: only checks file presence. +agmsg_delivery_status() { rulefile_status "$@"; } diff --git a/scripts/drivers/types/grok-build/type.conf b/scripts/drivers/types/grok-build/type.conf index bbdbd76..c8cfc2f 100644 --- a/scripts/drivers/types/grok-build/type.conf +++ b/scripts/drivers/types/grok-build/type.conf @@ -2,6 +2,7 @@ name=grok-build template=template.md cli=grok +spawnable=yes detect=GROK_SESSION_ID detect_proc=grok grok-* hooks_file=.grok/hooks/agmsg.json diff --git a/scripts/session-start.sh b/scripts/session-start.sh index 32a9e71..85d420e 100755 --- a/scripts/session-start.sh +++ b/scripts/session-start.sh @@ -64,15 +64,22 @@ else agmsg_session_start_default fi -# Read hook input JSON from stdin. session_id field is sent for SessionStart. +# Read hook input JSON from stdin. The session id field name differs by vendor: +# Claude Code emits snake_case "session_id"; Grok Build (and Cursor) emit +# camelCase "sessionId". Try snake first (claude-code unaffected), then camel, +# then the GROK_SESSION_ID env Grok injects into every hook. INPUT=$(cat 2>/dev/null || true) SESSION_ID="" if [ -n "$INPUT" ]; then SESSION_ID=$(printf '%s' "$INPUT" \ | sed -n 's/.*"session_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \ | head -1) + [ -z "$SESSION_ID" ] && SESSION_ID=$(printf '%s' "$INPUT" \ + | sed -n 's/.*"sessionId"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \ + | head -1) fi -# Fallback so the instruction is still actionable even outside CC's hook flow. +[ -z "$SESSION_ID" ] && SESSION_ID="${GROK_SESSION_ID:-}" +# Fallback so the instruction is still actionable even outside a hook flow. [ -z "$SESSION_ID" ] && SESSION_ID="unknown-$$" mkdir -p "$RUN_DIR" 2>/dev/null || true diff --git a/tests/test_delivery.bats b/tests/test_delivery.bats index 20db89f..4ee15b5 100644 --- a/tests/test_delivery.bats +++ b/tests/test_delivery.bats @@ -359,6 +359,47 @@ JSON [ ! -f "$TEST_SKILL_DIR/run/cc-instance.$dead_pid" ] } +# --- session-id resolution: vendor field-name differences (grok/cursor) --- +# Grok Build emits the session id on stdin as camelCase "sessionId" and injects +# GROK_SESSION_ID into every hook; Claude uses snake_case "session_id". The +# shared resolver tries snake -> camel -> $GROK_SESSION_ID. The Monitor +# directive echoes the resolved id as the watch.sh command's session arg, so we +# assert through that. (Exercised via claude-code since the resolver is shared.) + +@test "session-start: resolves camelCase sessionId from stdin (grok/cursor field)" { + env AGMSG_RESOLVE_PROJECT=0 bash "$SCRIPTS/join.sh" team alice claude-code "$TEST_PROJECT" >/dev/null + bash "$SCRIPTS/delivery.sh" set monitor claude-code "$TEST_PROJECT" >/dev/null + run env AGMSG_RESOLVE_PROJECT=0 bash "$SCRIPTS/session-start.sh" claude-code "$TEST_PROJECT" <<<'{"sessionId":"grokCamelSID"}' + [ "$status" -eq 0 ] + local cmdline + cmdline=$(printf '%s\n' "$output" | sed -n 's/^[[:space:]]*command: //p') + eval "set -- $cmdline" + [[ "$2" =~ grokCamelSID ]] +} + +@test "session-start: falls back to GROK_SESSION_ID env when stdin lacks a session id" { + env AGMSG_RESOLVE_PROJECT=0 bash "$SCRIPTS/join.sh" team alice claude-code "$TEST_PROJECT" >/dev/null + bash "$SCRIPTS/delivery.sh" set monitor claude-code "$TEST_PROJECT" >/dev/null + run env AGMSG_RESOLVE_PROJECT=0 GROK_SESSION_ID=grokEnvSID bash "$SCRIPTS/session-start.sh" claude-code "$TEST_PROJECT" <<<'{}' + [ "$status" -eq 0 ] + local cmdline + cmdline=$(printf '%s\n' "$output" | sed -n 's/^[[:space:]]*command: //p') + eval "set -- $cmdline" + [[ "$2" =~ grokEnvSID ]] +} + +@test "session-start: snake_case session_id still wins over camelCase (claude-code unaffected)" { + env AGMSG_RESOLVE_PROJECT=0 bash "$SCRIPTS/join.sh" team alice claude-code "$TEST_PROJECT" >/dev/null + bash "$SCRIPTS/delivery.sh" set monitor claude-code "$TEST_PROJECT" >/dev/null + run env AGMSG_RESOLVE_PROJECT=0 bash "$SCRIPTS/session-start.sh" claude-code "$TEST_PROJECT" <<<'{"session_id":"snakeWins","sessionId":"camelLoses"}' + [ "$status" -eq 0 ] + local cmdline + cmdline=$(printf '%s\n' "$output" | sed -n 's/^[[:space:]]*command: //p') + eval "set -- $cmdline" + [[ "$2" =~ snakeWins ]] + [[ ! "$2" =~ camelLoses ]] +} + # --- SessionEnd hook integration --- has_session_end() { @@ -1457,22 +1498,21 @@ JSON } # --- grok-build (turn|off via a dedicated .grok/hooks/agmsg.json) --- -# C-GATED (#214): the hook file path + JSON schema asserted here are the assumed -# Claude-Code shape (xAI's official schema page was not directly fetchable). If -# `grok inspect` on a real install shows a different filename/keys, update the -# manifest hooks_file=, the _delivery.sh writer, and these assertions together. +# Schema confirmed against a real Grok Build install (xAI local docs +# ~/.grok/docs/user-guide/10-hooks.md): nested Claude shape +# { "hooks": { "Stop": [ { "hooks": [ { "type":"command", "command":, +# "timeout":N } ] } ] } }, no top-level "version". -@test "delivery set turn (grok-build): writes .grok/hooks/agmsg.json with version + Stop entry" { +@test "delivery set turn (grok-build): writes .grok/hooks/agmsg.json with nested Stop entry" { run bash "$SCRIPTS/delivery.sh" set turn grok-build "$TEST_PROJECT" [ "$status" -eq 0 ] [[ "$output" =~ "Delivery mode set to 'turn'" ]] local hook_file="$TEST_PROJECT/.grok/hooks/agmsg.json" [ -f "$hook_file" ] - local v - v=$(sqlite_mem "SELECT json_extract(readfile('$(rf "$hook_file")'), '\$.version');") - [ "$v" = "1" ] - local cmd - cmd=$(sqlite_mem "SELECT json_extract(readfile('$(rf "$hook_file")'), '\$.hooks.Stop[0].bash');") + local cmd typ + cmd=$(sqlite_mem "SELECT json_extract(readfile('$(rf "$hook_file")'), '\$.hooks.Stop[0].hooks[0].command');") + typ=$(sqlite_mem "SELECT json_extract(readfile('$(rf "$hook_file")'), '\$.hooks.Stop[0].hooks[0].type');") + [ "$typ" = "command" ] [[ "$cmd" =~ "check-inbox.sh" ]] [[ "$cmd" =~ "grok-build" ]] } @@ -1498,3 +1538,12 @@ JSON [ "$status" -ne 0 ] [ -f "$TEST_PROJECT/.grok/hooks/agmsg.json" ] } + +@test "delivery status (grok-build): derives mode from hook file existence" { + run bash "$SCRIPTS/delivery.sh" status grok-build "$TEST_PROJECT" + [[ "$output" =~ "mode: off" ]] + + bash "$SCRIPTS/delivery.sh" set turn grok-build "$TEST_PROJECT" + run bash "$SCRIPTS/delivery.sh" status grok-build "$TEST_PROJECT" + [[ "$output" =~ "mode: turn" ]] +} diff --git a/tests/test_type_registry.bats b/tests/test_type_registry.bats index 2c17604..abf0152 100644 --- a/tests/test_type_registry.bats +++ b/tests/test_type_registry.bats @@ -64,7 +64,7 @@ write_node_launcher_fixtures() { [ "$status" -ne 0 ] } -@test "type-registry: spawnable set is exactly claude-code, codex and hermes" { +@test "type-registry: spawnable set is exactly claude-code, codex, grok-build and hermes" { run env -i PATH="$PATH" bash -c \ "source '$SCRIPTS/lib/type-registry.sh' while IFS= read -r t; do @@ -72,7 +72,7 @@ write_node_launcher_fixtures() { [ \"\$(agmsg_type_get \"\$t\" spawnable)\" = yes ] && echo \"\$t\" done <<< \"\$(agmsg_known_types | sort -u)\" | paste -sd, -" [ "$status" -eq 0 ] - [ "$output" = "claude-code,codex,hermes" ] + [ "$output" = "claude-code,codex,grok-build,hermes" ] } @test "type-registry: detection manifests carry the expected env / proc keys" { From 5f7067a10a4da2efcc8c4e1b0ff6e60f001d0053 Mon Sep 17 00:00:00 2001 From: fujibee Date: Wed, 24 Jun 2026 15:22:15 -0700 Subject: [PATCH 3/3] refactor(grok-build): switch delivery to rule-file self-poll, not hooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Real-machine dogfood found Grok Build's passive hooks (SessionStart/Stop) discard their stdout — they cannot inject context. A Stop hook running check-inbox.sh would therefore deliver nothing while still marking messages read = silent loss. Grok's only delivery path is the agent reading a tool's output, so switch to the existing rule-file self-poll model (gemini/opencode): - _delivery.sh: write a .grok/rules/agmsg.md rule (which Grok always scans into context each turn) telling the agent to poll inbox.sh each turn. inbox.sh prints unread messages and marks them read in the same call the agent sees = loss-safe; check-inbox.sh's hook-control JSON and cooldown are wrong for agent-read tool output. - type.conf: hooks_file .grok/hooks/agmsg.json -> .grok/rules/agmsg.md. Rule files need no folder-trust (Grok's trust gate is for execution, not rules) and are read even outside a git repo, so spawn works with a plain launch. - template.md: describe turn mode as rule-driven self-poll. - tests: rule-file assertions; grok-build spawns the plain grok CLI (no flag). Session-id resolution (camelCase / GROK_SESSION_ID) and spawnable=yes are kept. Refs #214. --- scripts/drivers/types/grok-build/_delivery.sh | 92 +++++++++---------- scripts/drivers/types/grok-build/template.md | 3 +- scripts/drivers/types/grok-build/type.conf | 2 +- tests/test_delivery.bats | 48 +++++----- tests/test_spawn.bats | 18 +++- 5 files changed, 89 insertions(+), 74 deletions(-) diff --git a/scripts/drivers/types/grok-build/_delivery.sh b/scripts/drivers/types/grok-build/_delivery.sh index 194cf15..714e5e7 100644 --- a/scripts/drivers/types/grok-build/_delivery.sh +++ b/scripts/drivers/types/grok-build/_delivery.sh @@ -1,62 +1,60 @@ #!/usr/bin/env bash -# grok-build delivery plug — dedicated JSON hook file (.grok/hooks/agmsg.json). +# grok-build delivery plug — markdown rule file (.grok/rules/agmsg.md). # -# Grok Build has no Monitor-tool equivalent, so the manifest declares -# delivery_modes="turn off"; delivery.sh's central gate rejects monitor/both -# before this runs (and before any file is touched). Modeled on the copilot -# plug: a fully agmsg-owned hook file we write/remove wholesale (not a merge -# into a shared settings file). The Stop hook runs check-inbox.sh between turns. -# Uses resolve_hooks_file + SKILL_DIR from delivery.sh's sourced context. +# Why a rule file (not a hook): Grok Build's passive hooks (SessionStart/Stop) +# discard their stdout — they cannot inject anything into the conversation. A +# Stop hook running check-inbox.sh would therefore deliver NOTHING while still +# marking messages read = silent loss. So grok integrates the same way as +# gemini / antigravity / opencode: a markdown rule under /.grok/rules/, +# which Grok always scans into context each turn. The rule tells the agent to +# poll its own inbox; the agent runs the check as a tool call and reads the +# output — the one delivery path Grok actually supports. +# +# The rule points at inbox.sh (not check-inbox.sh): inbox.sh prints the unread +# messages in plain text AND marks them read in the same call, so the agent sees +# exactly what gets consumed (loss-safe). check-inbox.sh wraps its output in +# Claude hook-control JSON and carries hook-only cooldown/watcher logic that is +# wrong for an agent reading tool output. # -# Schema confirmed against a real Grok Build install (xAI local docs -# ~/.grok/docs/user-guide/10-hooks.md): the hook file is the Claude-Code-shaped -# nested form { "hooks": { "Stop": [ { "hooks": [ { "type":"command", -# "command":, "timeout":N } ] } ] } } at /.grok/hooks/agmsg.json -# (no top-level "version"). Grok injects GROK_SESSION_ID/CLAUDE_PROJECT_DIR into -# every hook and emits the session id on stdin as camelCase "sessionId"; the -# shared session-start.sh / check-inbox.sh resolve that (snake_case session_id -> -# camelCase sessionId -> $GROK_SESSION_ID) so no per-type remap is needed here. -# `command` is the absolute check-inbox.sh path ($SKILL_DIR is absolute). +# Rule files need no folder-trust (Grok's trust gate is for execution — +# hooks/MCP/LSP — not rules), and a project-level .grok/rules is read even +# outside a git repo, so this also works for spawned sessions. delivery_modes is +# "turn off": turn => rule present (self-poll active), off => rule removed. +# Uses resolve_hooks_file + SKILL_DIR from delivery.sh's sourced context. agmsg_delivery_apply() { local type="$1" local project="$2" local mode="$3" - local hooks_file - hooks_file=$(resolve_hooks_file "$type" "$project") + local rule_file + rule_file=$(resolve_hooks_file "$type" "$project") - # Strip first so re-applying turn is an idempotent rewrite and turn->off - # cleanly removes the file. - rm -f "$hooks_file" + # Always start clean; turn rewrites the rule, off leaves it absent. + rm -f "$rule_file" if [ "$mode" = "turn" ]; then - mkdir -p "$(dirname "$hooks_file")" - local cmd="'$SKILL_DIR/scripts/check-inbox.sh' '$type' '$project'" - local cmd_json - cmd_json=$(agmsg_sqlite_mem "SELECT json_quote('$(printf '%s' "$cmd" | sed "s/'/''/g")');") - # Stop = "agent turn ends" (lifecycle event, no matcher). Nested Claude shape: - # Stop[].hooks[] each { type:"command", command:, timeout: }. - cat < "$hooks_file" -{ - "hooks": { - "Stop": [ - { - "hooks": [ - { - "type": "command", - "command": $cmd_json, - "timeout": 30 - } - ] - } - ] - } -} + mkdir -p "$(dirname "$rule_file")" + cat < "$rule_file" +# agmsg — check your inbox each turn + +You belong to one or more agmsg teams. Before you respond to the user on each +turn, check your agmsg inbox so you never miss a teammate's message. + +1. Identify yourself (once per session is enough): + \`$SKILL_DIR/scripts/whoami.sh '$project' $type\` + It prints your \`agent=\` name and \`teams=\` list. +2. For each team, show and consume unread messages: + \`$SKILL_DIR/scripts/inbox.sh \` + This prints unread messages AND marks them read in the same call, so nothing + is lost. +3. If any messages were shown, relay them to the user before continuing with + their request. + +There is no background watcher for Grok Build — this self-check is how delivery +works. Removing this file turns automatic delivery off. EOF fi } -# Status derives the mode from the hook file's existence, not by parsing the -# Claude/Codex nested-settings shape the default status reader expects — our -# hook file is the dedicated agmsg-owned form (present => turn, absent => off). -# Same override copilot uses. Schema-independent: only checks file presence. +# Status is the rule file's presence: present => turn, absent => off (no monitor +# for a self-poll type). Same shared helper the other rule-file types use. agmsg_delivery_status() { rulefile_status "$@"; } diff --git a/scripts/drivers/types/grok-build/template.md b/scripts/drivers/types/grok-build/template.md index 12db84a..1a81d57 100644 --- a/scripts/drivers/types/grok-build/template.md +++ b/scripts/drivers/types/grok-build/template.md @@ -47,7 +47,8 @@ Four possible outputs: Choose delivery mode for incoming messages: 1) turn — Check inbox at the end of each assistant turn - Stop hook pulls after each response. + A .grok/rules/agmsg.md rule has you self-check inbox.sh + each turn (Grok hooks can't push, so delivery is self-poll). 2) off — No automatic delivery Manual /__SKILL_NAME__ only. diff --git a/scripts/drivers/types/grok-build/type.conf b/scripts/drivers/types/grok-build/type.conf index c8cfc2f..5565932 100644 --- a/scripts/drivers/types/grok-build/type.conf +++ b/scripts/drivers/types/grok-build/type.conf @@ -5,6 +5,6 @@ cli=grok spawnable=yes detect=GROK_SESSION_ID detect_proc=grok grok-* -hooks_file=.grok/hooks/agmsg.json +hooks_file=.grok/rules/agmsg.md monitor=no delivery_modes=turn off diff --git a/tests/test_delivery.bats b/tests/test_delivery.bats index 4ee15b5..13fd075 100644 --- a/tests/test_delivery.bats +++ b/tests/test_delivery.bats @@ -1497,49 +1497,49 @@ JSON wait 2>/dev/null || true } -# --- grok-build (turn|off via a dedicated .grok/hooks/agmsg.json) --- -# Schema confirmed against a real Grok Build install (xAI local docs -# ~/.grok/docs/user-guide/10-hooks.md): nested Claude shape -# { "hooks": { "Stop": [ { "hooks": [ { "type":"command", "command":, -# "timeout":N } ] } ] } }, no top-level "version". +# --- grok-build (turn|off via a markdown rule file .grok/rules/agmsg.md) --- +# Grok passive hooks can't inject (stdout is discarded), so grok delivers via the +# rule-file self-poll model (like gemini/opencode): a .grok/rules/agmsg.md that +# tells the agent to poll inbox.sh each turn. turn => rule present, off => absent. -@test "delivery set turn (grok-build): writes .grok/hooks/agmsg.json with nested Stop entry" { +@test "delivery set turn (grok-build): writes .grok/rules/agmsg.md self-poll rule" { run bash "$SCRIPTS/delivery.sh" set turn grok-build "$TEST_PROJECT" [ "$status" -eq 0 ] [[ "$output" =~ "Delivery mode set to 'turn'" ]] - local hook_file="$TEST_PROJECT/.grok/hooks/agmsg.json" - [ -f "$hook_file" ] - local cmd typ - cmd=$(sqlite_mem "SELECT json_extract(readfile('$(rf "$hook_file")'), '\$.hooks.Stop[0].hooks[0].command');") - typ=$(sqlite_mem "SELECT json_extract(readfile('$(rf "$hook_file")'), '\$.hooks.Stop[0].hooks[0].type');") - [ "$typ" = "command" ] - [[ "$cmd" =~ "check-inbox.sh" ]] - [[ "$cmd" =~ "grok-build" ]] -} - -@test "delivery set off (grok-build): removes the hook file" { + local rule_file="$TEST_PROJECT/.grok/rules/agmsg.md" + [ -f "$rule_file" ] + # The rule points at inbox.sh (clean display + same-call mark = loss-safe), + # not the hook-only check-inbox.sh, and references this type + project. + run cat "$rule_file" + [[ "$output" == *"inbox.sh"* ]] + [[ "$output" != *"check-inbox.sh"* ]] + [[ "$output" == *"grok-build"* ]] + [[ "$output" == *"$TEST_PROJECT"* ]] +} + +@test "delivery set off (grok-build): removes the rule file" { bash "$SCRIPTS/delivery.sh" set turn grok-build "$TEST_PROJECT" - [ -f "$TEST_PROJECT/.grok/hooks/agmsg.json" ] + [ -f "$TEST_PROJECT/.grok/rules/agmsg.md" ] run bash "$SCRIPTS/delivery.sh" set off grok-build "$TEST_PROJECT" [ "$status" -eq 0 ] - [ ! -f "$TEST_PROJECT/.grok/hooks/agmsg.json" ] + [ ! -f "$TEST_PROJECT/.grok/rules/agmsg.md" ] } -@test "delivery set monitor (grok-build): rejected; no hook file written" { +@test "delivery set monitor (grok-build): rejected; no rule file written" { run bash "$SCRIPTS/delivery.sh" set monitor grok-build "$TEST_PROJECT" [ "$status" -ne 0 ] [[ "$output" =~ "not supported" ]] - [ ! -f "$TEST_PROJECT/.grok/hooks/agmsg.json" ] + [ ! -f "$TEST_PROJECT/.grok/rules/agmsg.md" ] } -@test "delivery set both (grok-build): does NOT delete an existing turn hook" { +@test "delivery set both (grok-build): rejected; does NOT delete an existing turn rule" { bash "$SCRIPTS/delivery.sh" set turn grok-build "$TEST_PROJECT" >/dev/null run bash "$SCRIPTS/delivery.sh" set both grok-build "$TEST_PROJECT" [ "$status" -ne 0 ] - [ -f "$TEST_PROJECT/.grok/hooks/agmsg.json" ] + [ -f "$TEST_PROJECT/.grok/rules/agmsg.md" ] } -@test "delivery status (grok-build): derives mode from hook file existence" { +@test "delivery status (grok-build): derives mode from rule file existence" { run bash "$SCRIPTS/delivery.sh" status grok-build "$TEST_PROJECT" [[ "$output" =~ "mode: off" ]] diff --git a/tests/test_spawn.bats b/tests/test_spawn.bats index 4de694f..beca2f0 100644 --- a/tests/test_spawn.bats +++ b/tests/test_spawn.bats @@ -10,7 +10,7 @@ setup() { # a terminal. PATH is prepended so the stubs win. export STUB_BIN="$TEST_SKILL_DIR/stub-bin" mkdir -p "$STUB_BIN" - for bin in claude codex; do + for bin in claude codex grok; do printf '#!/usr/bin/env bash\nexit 0\n' > "$STUB_BIN/$bin" chmod +x "$STUB_BIN/$bin" done @@ -151,6 +151,22 @@ teardown() { [[ "$output" == *"$PROJ"* ]] } +@test "spawn: grok-build launches the plain grok CLI with the actas prompt" { + # grok-build is spawnable and monitor=no, so spawn skips the readiness wait. + # Delivery is a rule file (no hook), so no folder-trust flag is needed — + # the launch is the bare `grok "/ actas "`, like claude-code. + bash "$SCRIPTS/join.sh" myteam existing claude-code "$PROJ" + run bash "$SCRIPTS/spawn.sh" grok-build alice --project "$PROJ" --no-wait + [ "$status" -eq 0 ] + boot="$(cat "$CAPTURE")" + [ -f "$boot" ] + run cat "$boot" + [[ "$output" == *"grok"* ]] + [[ "$output" == *"actas"* ]] + [[ "$output" == *"alice"* ]] + [[ "$output" != *"--trust"* ]] +} + @test "spawn: actas prompt uses the install command name (not hardcoded agmsg)" { # Rename the skill dir to a custom command name and re-point SCRIPTS so the # script resolves SKILL_DIR basename = the custom name.