diff --git a/install.sh b/install.sh index 0f545ae..37e3754 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)" @@ -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 @@ -241,7 +243,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 +282,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 +345,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 +431,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/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 new file mode 100644 index 0000000..714e5e7 --- /dev/null +++ b/scripts/drivers/types/grok-build/_delivery.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# grok-build delivery plug — markdown rule file (.grok/rules/agmsg.md). +# +# 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. +# +# 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 rule_file + rule_file=$(resolve_hooks_file "$type" "$project") + + # Always start clean; turn rewrites the rule, off leaves it absent. + rm -f "$rule_file" + + if [ "$mode" = "turn" ]; then + 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 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 new file mode 100644 index 0000000..1a81d57 --- /dev/null +++ b/scripts/drivers/types/grok-build/template.md @@ -0,0 +1,137 @@ +--- +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 + 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. + + [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..5565932 --- /dev/null +++ b/scripts/drivers/types/grok-build/type.conf @@ -0,0 +1,10 @@ +# agmsg agent-type manifest — read-only key=value DATA. NEVER sourced. +name=grok-build +template=template.md +cli=grok +spawnable=yes +detect=GROK_SESSION_ID +detect_proc=grok grok-* +hooks_file=.grok/rules/agmsg.md +monitor=no +delivery_modes=turn off 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 bdc1b1e..13fd075 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() { @@ -1455,3 +1496,54 @@ JSON kill "$watch_pid" 2>/dev/null || true wait 2>/dev/null || true } + +# --- 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/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 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/rules/agmsg.md" ] + run bash "$SCRIPTS/delivery.sh" set off grok-build "$TEST_PROJECT" + [ "$status" -eq 0 ] + [ ! -f "$TEST_PROJECT/.grok/rules/agmsg.md" ] +} + +@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/rules/agmsg.md" ] +} + +@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/rules/agmsg.md" ] +} + +@test "delivery status (grok-build): derives mode from rule 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_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_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. 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..abf0152 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" { @@ -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" {