Files
gstack/bin/gstack-session-kind
T
Garry TanandClaude Fable 5 5486cd6620 feat(session-kind): explicit GSTACK_SESSION_KIND override; skill-start spawned gates keyed on kind (#2733)
Claude Code subagents inherit the parent env byte-for-byte, so ambient
markers classify them as the parent's kind and the spawned classification
was unreachable outside OpenClaw. GSTACK_SESSION_KIND=spawned (step 0,
spawned-only by design) lets a dispatching skill mark its subagent per
command. skill-start now keys SPAWNED_SESSION and the spawned-session
instruction block on the resolved kind (was raw OPENCLAW_SESSION),
suppresses CONDUCTOR_SESSION for spawned sessions, gates all 11
interactive-onboarding blocks plus their ack-at-emit marker writes on
kind != spawned, and adds a destructive-gate carve-out to the spawned
block (conservative-continue, never prose-STOP).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 23:33:40 +00:00

74 lines
3.3 KiB
Bash
Executable File

#!/usr/bin/env bash
# gstack-session-kind — classify the current agent session so skills know whether
# a human can answer an interactive prompt (AskUserQuestion).
#
# Usage: gstack-session-kind → prints one of: spawned | headless | interactive
#
# Used by the preamble (generate-preamble-bash.ts) which echoes
# SESSION_KIND: <value>
# so the AskUserQuestion-failure fallback rule can branch without a shell-out at
# failure time:
# spawned → orchestrator session (OpenClaw). Auto-choose recommended option
# per the skill's SPAWNED_SESSION block. Never prose, never BLOCKED.
# headless → no human present (claude -p evals / CI). BLOCK on AUQ failure.
# interactive → a human is present. Prose-fallback on AUQ failure.
#
# Detection is best-effort. On ANY ambiguity it prints `interactive` — BLOCK only on
# a positive headless signal, since a stray prose message in an unmarked one-shot
# `-p` run just ends the turn (harmless), whereas wrongly BLOCKING a real human is not.
#
# GSTACK_SESSION_KIND=spawned is the explicit per-command override (step 0 below),
# outranking every ambient marker. Deliberately narrow — only "spawned" is honored:
# Claude Code subagents inherit the parent env byte-for-byte (#2733), so a
# dispatching skill marks its subagent by prefixing the gstack-skill-start
# invocation. "headless" already has GSTACK_HEADLESS; other values are reserved
# and ignored (fall through to detection).
#
# Why env vars and not TTY/entrypoint: an interactive Conductor session reports
# CLAUDE_CODE_ENTRYPOINT=sdk-ts with no TTY — identical to a headless SDK eval. The
# signals that actually discriminate are the host/orchestrator/CI env markers below.
set -euo pipefail
# 0. Explicit spawned marker — set per-command by a dispatching skill (e.g.
# /ship Step 18 prefixes the document-release subagent's gstack-skill-start
# with GSTACK_SESSION_KIND=spawned; #2733: Claude Code subagents inherit
# the parent env byte-for-byte, so ambient markers misclassify them as the
# parent's kind). Only "spawned" is honored: headless already has
# GSTACK_HEADLESS, and letting an env var force "interactive" over CI
# markers would be a misclassification footgun. Other values are reserved
# and ignored (fall through) — same contract as empty GSTACK_HEADLESS.
if [ "${GSTACK_SESSION_KIND:-}" = "spawned" ]; then
echo "spawned"
exit 0
fi
# 1. Orchestrator-spawned session (OpenClaw). Authoritative block lives in the skill;
# we only surface the classification.
if [ -n "${OPENCLAW_SESSION:-}" ]; then
echo "spawned"
exit 0
fi
# 2. Explicit headless override (set by the eval/E2E harness for determinism).
if [ -n "${GSTACK_HEADLESS:-}" ]; then
echo "headless"
exit 0
fi
# 3. Positive interactive-host signals: a human-driven host is present.
# - Conductor app sets CONDUCTOR_* workspace vars.
# - Plain interactive `claude` CLI sets CLAUDE_CODE_ENTRYPOINT=cli.
if [ -n "${CONDUCTOR_WORKSPACE_PATH:-}" ] || [ -n "${CONDUCTOR_PORT:-}" ] || [ "${CLAUDE_CODE_ENTRYPOINT:-}" = "cli" ]; then
echo "interactive"
exit 0
fi
# 4. CI / automation markers with no interactive host → headless.
if [ -n "${CI:-}" ] || [ -n "${GITHUB_ACTIONS:-}" ]; then
echo "headless"
exit 0
fi
# 5. No positive headless signal → assume a human is present (degrade-safe default).
echo "interactive"