diff --git a/install.sh b/install.sh index 3efc935..7edfebd 100755 --- a/install.sh +++ b/install.sh @@ -119,6 +119,7 @@ GTBIN="$HOME/.agents/skills/sume-gt-mq/bin" ln -sfn "$BIN/agent-human-stream.sh" "$HOME/.local/bin/agent-human-stream" ln -sfn "$BIN/claude-human-stream.sh" "$HOME/.local/bin/claude-human-stream" ln -sfn "$BIN/sume-bg-launch.sh" "$HOME/.local/bin/sume-bg-launch" +ln -sfn "$BIN/agent-holders.sh" "$HOME/.local/bin/agent-holders" ln -sfn "$GTBIN/cstack-clone.sh" "$HOME/.local/bin/cstack-clone" ln -sfn "$GTBIN/cstack-clone-rm.sh" "$HOME/.local/bin/cstack-clone-rm" ln -sfn "$GTBIN/cstack-mirror-sync.sh" "$HOME/.local/bin/cstack-mirror-sync" @@ -126,7 +127,9 @@ ln -sfn "$GTBIN/cstack-gt-wait-merge.sh" "$HOME/.local/bin/cstack-gt-wait-merge" chmod +x "$BIN/agent-human-stream.sh" "$BIN/agent-human-stream.py" \ "$BIN/agent-human-stream.test.py" \ "$BIN/claude-human-stream.sh" "$BIN/claude-human-stream.py" \ - "$BIN/sume-bg-launch.sh" \ + "$BIN/sume-bg-launch.sh" "$BIN/sume-bg-launch.test.sh" \ + "$BIN/agent-holders.sh" "$BIN/agent-registry.py" \ + "$BIN/grok-desk-hook.sh" "$BIN/grok-desk-hook.test.sh" \ "$HOME/.agents/skills/sume-main-agent-orchestration/check-wiring.sh" \ "$HOME/.agents/skills/sume-gt-mq/install-symlinks.sh" \ "$GTBIN/cstack-clone.sh" "$GTBIN/cstack-clone-rm.sh" \ @@ -177,6 +180,22 @@ if [ -x "$GTBIN/cstack-gt-wait-merge.test.sh" ]; then echo "== cstack-gt-wait-merge matcher ==" "$GTBIN/cstack-gt-wait-merge.test.sh" fi +if [ -x "$BIN/sume-bg-launch.test.sh" ]; then + echo "== sume-bg-launch steer-kill (agent-holders) ==" + "$BIN/sume-bg-launch.test.sh" +fi +if [ -x "$BIN/grok-desk-hook.test.sh" ]; then + echo "== grok-desk-hook ==" + "$BIN/grok-desk-hook.test.sh" +fi + +# Grok Build hooks → desk registry (Stop / StopFailure / SessionEnd / PostCompact). +# Sibling of tokenmaxxing-grok.json; absolute path because Grok runs hooks +# from any cwd. Re-written on every install so the path follows the SoT. +echo "== Grok Build desk hook ==" +mkdir -p "$HOME/.grok/hooks" +sed "s|__HOOK__|$BIN/grok-desk-hook.sh|g" "$DESK/grok-hooks/sume-desk.json.tmpl" > "$HOME/.grok/hooks/sume-desk.json" +echo "installed: ~/.grok/hooks/sume-desk.json → $BIN/grok-desk-hook.sh" if [ -x "$HOME/.agents/skills/sume-main-agent-orchestration/check-wiring.sh" ]; then echo "== wiring check ==" @@ -195,6 +214,21 @@ else echo " Docs: $ROOT/docs/TOKENMAXXING.md" fi +echo "" +echo "== Grok Build (desk pin) ==" +# `grok` on PATH is the tokenmaxxing shim; the binary that actually runs is +# config.json grokBin (pinned download), not ~/.grok/bin/grok. Say which. +GROK_PIN="$(python3 -c 'import json,os; print(json.load(open(os.path.expanduser("~/.config/tokenmaxxing/config.json"))).get("grokBin",""))' 2>/dev/null || true)" +if [ -n "$GROK_PIN" ] && [ -x "$GROK_PIN" ]; then + echo "runs: $GROK_PIN ($("$GROK_PIN" --version 2>/dev/null | head -1))" +else + echo "runs: $(command -v grok 2>/dev/null || echo 'grok not on PATH') (no tokenmaxxing grokBin pin)" +fi +if [ -x "$HOME/.grok/bin/grok" ]; then + echo "bin: ~/.grok/bin/grok ($("$HOME/.grok/bin/grok" --version 2>/dev/null | head -1)) — NOT what desk workers run when the pin above is set" +fi +echo "SoT: docs/TOKENMAXXING.md § Grok Build pool" + if [ -x "$HOME/.local/bin/cstack-mirror-sync" ]; then echo "== cstack mirror (sume-com objects) ==" "$HOME/.local/bin/cstack-mirror-sync" || echo "NOTE: seed later with cstack-mirror-sync" diff --git a/sume-desk/grok-hooks/sume-desk.json.tmpl b/sume-desk/grok-hooks/sume-desk.json.tmpl new file mode 100644 index 0000000..3e99fbc --- /dev/null +++ b/sume-desk/grok-hooks/sume-desk.json.tmpl @@ -0,0 +1,17 @@ +{ + "description": "Sume desk: registry rows for agent-human-stream (sumelabs/sume#5706)", + "hooks": { + "Stop": [ + {"hooks": [{"type": "command", "command": "\"__HOOK__\" stop", "timeout": 5}]} + ], + "StopFailure": [ + {"hooks": [{"type": "command", "command": "\"__HOOK__\" stop_failure", "timeout": 5}]} + ], + "SessionEnd": [ + {"hooks": [{"type": "command", "command": "\"__HOOK__\" session_end", "timeout": 5}]} + ], + "PostCompact": [ + {"hooks": [{"type": "command", "command": "\"__HOOK__\" post_compact", "timeout": 5}]} + ] + } +} diff --git a/sume-desk/skills/sume-main-agent-orchestration/bin/agent-human-stream.sh b/sume-desk/skills/sume-main-agent-orchestration/bin/agent-human-stream.sh index 118a16b..c8d3dfc 100755 --- a/sume-desk/skills/sume-main-agent-orchestration/bin/agent-human-stream.sh +++ b/sume-desk/skills/sume-main-agent-orchestration/bin/agent-human-stream.sh @@ -77,7 +77,8 @@ Human-readable wrapper around Claude Code and Grok Build headless NDJSON. agent-human-stream --claude "…" # same as --backend claude agent-human-stream --grok "…" # same as --backend grok agent-human-stream --resume --prompt-file - agent-human-stream --sessions + agent-human-stream --sessions [--backend grok|claude] + agent-human-stream --export # grok export → ~/.cstack/state/opus-live/.transcript.md agent-human-stream --self-test Auto backend (default): @@ -136,6 +137,7 @@ NAME="" CONTINUE=0 FORK=0 LIST_SESSIONS=0 +EXPORT_SESSION="" SELF_TEST=0 PROMPT="" PROMPT_FILE="" @@ -158,6 +160,10 @@ while [[ $# -gt 0 ]]; do LIST_SESSIONS=1 shift ;; + --export) + EXPORT_SESSION=${2:?--export needs a session uuid} + shift 2 + ;; --self-test) SELF_TEST=1 shift @@ -282,10 +288,47 @@ if [[ "$LIST_SESSIONS" -eq 1 ]]; then echo "(no registry yet: $REGISTRY)" >&2 exit 0 fi - tail -n 30 "$REGISTRY" + # --sessions [--backend grok|claude]: last 30 rows (filtered), one line each. + python3 - "$REGISTRY" "$BACKEND" <<'PYSESS' +import json, sys +path, backend = sys.argv[1], sys.argv[2].lower() +rows = [] +try: + for line in open(path, encoding="utf-8"): + try: + rec = json.loads(line) + except json.JSONDecodeError: + continue + if backend in ("claude", "grok") and rec.get("backend") != backend: + continue + rows.append(rec) +except OSError: + pass +for rec in rows[-30:]: + extra = " ".join( + f"{k}={rec[k]}" for k in ("exit_code", "landed", "context_pct", "total_cost_usd", "child_pid") if rec.get(k) is not None + ) + print(f"{rec.get('ts','')} {rec.get('event',''):<8} {rec.get('backend') or '-':<6} {(rec.get('session_id') or '-')[:8]} {rec.get('name') or '-'} model={rec.get('model') or '-'} {extra}".rstrip()) +PYSESS exit 0 fi +if [[ -n "$EXPORT_SESSION" ]]; then + # --export : Grok transcript (Markdown) next to the live logs; works from any cwd. + if ! command -v grok >/dev/null 2>&1; then + echo "error: grok not on PATH" >&2 + exit 127 + fi + mkdir -p "$LIVE_DIR" + _out="$LIVE_DIR/${EXPORT_SESSION}.transcript.md" + if grok export "$EXPORT_SESSION" "$_out"; then + echo "$_out" + exit 0 + fi + echo "error: grok export failed for $EXPORT_SESSION (grok sessions list is cwd-scoped; export by uuid is global)" >&2 + exit 1 +fi + if [[ "$SELF_TEST" -eq 1 ]]; then _got="$(_grok_effort_wire mid)" if [[ "$_got" != "medium" ]]; then diff --git a/sume-desk/skills/sume-main-agent-orchestration/bin/grok-desk-hook.sh b/sume-desk/skills/sume-main-agent-orchestration/bin/grok-desk-hook.sh new file mode 100755 index 0000000..d0db932 --- /dev/null +++ b/sume-desk/skills/sume-main-agent-orchestration/bin/grok-desk-hook.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Grok Build hook → desk registry row. Installed by cstack install.sh as +# ~/.grok/hooks/sume-desk.json for Stop / SessionEnd / PostCompact / +# StopFailure (sumelabs/sume#5706 Slice D). +# +# Contract (Grok 1.0.11 hooks reference, same as tokenmaxxing's hook): stdin is +# a camelCase JSON envelope (sessionId, hookEventName, reason, error, …). A +# hook that exits 0 with NO stdout allows the event; never print, never block. +# +# grok-desk-hook.sh # event = stop|session_end|post_compact|stop_failure +set -uo pipefail + +EVENT="${1:-unknown}" +REGISTRY="${AGENT_HUMAN_STREAM_REGISTRY:-${CSTACK_STATE:-$HOME/.cstack/state}/opus-sessions.jsonl}" +RAW="$(cat 2>/dev/null || true)" + +python3 - "$EVENT" "$REGISTRY" "$RAW" <<'PYHOOK' >/dev/null 2>&1 || true +import json, os, sys, time +event, path, raw = sys.argv[1], sys.argv[2], sys.argv[3] +try: + env = json.loads(raw) if raw.strip() else {} +except json.JSONDecodeError: + env = {} +if not isinstance(env, dict): + env = {} +row = { + "ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + "event": f"hook_{event}", + "backend": "grok", + "session_id": env.get("sessionId") or env.get("session_id"), + "cwd": env.get("cwd") or os.getcwd(), + "name": os.environ.get("AGENT_HUMAN_STREAM_NAME") or None, + "pid": int(os.environ.get("AGENT_HUMAN_STREAM_PID") or "0") or None, + "hook_event": env.get("hookEventName") or event, + "reason": env.get("reason"), + "error": env.get("error"), + "trigger": env.get("trigger"), + "tokens_before": env.get("tokensBefore") or env.get("tokens_before"), + "tokens_after": env.get("tokensAfter") or env.get("tokens_after"), + "supervisor_id": os.environ.get("TOKENMAXXING_GROK_SUPERVISOR_ID"), +} +os.makedirs(os.path.dirname(path) or ".", exist_ok=True) +with open(path, "a", encoding="utf-8") as f: + f.write(json.dumps(row, ensure_ascii=False) + "\n") +PYHOOK +exit 0 diff --git a/sume-desk/skills/sume-main-agent-orchestration/bin/grok-desk-hook.test.sh b/sume-desk/skills/sume-main-agent-orchestration/bin/grok-desk-hook.test.sh new file mode 100755 index 0000000..3c6b0bc --- /dev/null +++ b/sume-desk/skills/sume-main-agent-orchestration/bin/grok-desk-hook.test.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# grok-desk-hook: silent, exit 0, one registry row per event. +set -euo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT +export AGENT_HUMAN_STREAM_REGISTRY="$TMP/reg.jsonl" +fail() { echo "FAIL: $*" >&2; exit 1; } + +out=$(printf '{"hookEventName":"Stop","sessionId":"01a05e1f-90bb-7561-b9a9-8f5e4f9558c6","reason":"end_turn"}' | "$ROOT/grok-desk-hook.sh" stop) +[[ -z "$out" ]] || fail "hook printed to stdout: $out" +grep -q '"event": "hook_stop"' "$TMP/reg.jsonl" || fail "no hook_stop row" +grep -q '"reason": "end_turn"' "$TMP/reg.jsonl" || fail "reason missing" +printf '{"hookEventName":"PostCompact","sessionId":"x","trigger":"auto","tokensBefore":400000,"tokensAfter":90000}' | "$ROOT/grok-desk-hook.sh" post_compact +grep -q '"event": "hook_post_compact"' "$TMP/reg.jsonl" || fail "no post_compact row" +grep -q '"tokens_before": 400000' "$TMP/reg.jsonl" || fail "tokens_before missing" +printf 'not json' | "$ROOT/grok-desk-hook.sh" stop_failure || fail "hook must exit 0 on garbage" +[[ $(grep -c . "$TMP/reg.jsonl") -eq 3 ]] || fail "expected 3 rows" +AGENT_HUMAN_STREAM_REGISTRY="/nonexistent-dir-$$/reg.jsonl" "$ROOT/grok-desk-hook.sh" session_end /dev/null 2>&1; then + ok "agent-holders on PATH" +else + bad "agent-holders not on PATH (steer-kill helper)" +fi + if command -v cstack-clone >/dev/null 2>&1; then ok "cstack-clone on PATH" else