mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-01 19:00:40 +02:00
* 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> * fix(hooks): spawned-session escape in Conductor AUQ deny; override coverage in AUQ-error fallback (#2733) Hooks inherit the harness env, so a per-command GSTACK_SESSION_KIND prefix inside a subagent's bash can never reach them. Levers added: a deterministic [conductor][spawned] auto-choose deny for env-level spawned sessions (OPENCLAW_SESSION or session-wide GSTACK_SESSION_KIND), and a spawned escape sentence appended to both hooks' prose directives so a marked subagent that slips and calls AUQ resolves to auto-choose instead of prose-STOP. The sentence lives in one shared constant (hosts/claude/hooks/spawned-directive.ts) so the two paths can never drift; destructive semantics are unified to conservative-continue. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ship): Step 18 marks the document-release subagent spawned — env prefix + auto-choose prompt (#2733) The dispatch prompt now (1) frames the run as a SPAWNED subagent whose LAST line is machine-parsed, (2) instructs prefixing the preamble's gstack-skill-start invocation with GSTACK_SESSION_KIND=spawned on the same command line (template bash blocks don't share exports), and (3) resolves every AUQ gate to auto-choosing the recommended option, conservative on no-recommendation, never destructive. The JSON contract gains a required "decisions" array (auto-chosen gates, printed to the ship console — never embedded in the public PR body) and a placement clause so the skill's own doc-health summary stops competing with the LAST-line JSON. Tripwire pins added; codex/factory goldens refreshed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(auq-format): proactive SESSION_KIND=spawned rule ordered above the Conductor rule (#2733) The spawned classification previously existed only in the failure-fallback branch — a spawned session was invited to call AskUserQuestion and reach auto-choose via the deny/error detour, and a spawned session inside a Conductor workspace hit the Conductor prose-STOP rule first. The Tool resolution list now leads with the spawned rule (auto-choose recommended, never prose, never BLOCKED, destructive gates resolve conservative), the self-check carries the never-reach-this-checklist clause, and all tier>=2 SKILL.md renders are regenerated. Context-budget fixture refreshed in the same commit per the ratchet protocol (the AUQ section is eager in every tier>=2 skill). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(e2e): spawned document-release subagent returns the JSON contract through a firing gate (#2733) The behavioral proof the bug shipped without: ship-docsync stubs the skill (no preamble, no gates) and skill-e2e-workflow suppresses the gates by prompt. This gate-tier E2E plays the parent — it drives the verbatim Step 18 dispatch prompt (extracted from the live pr-body.md, drift-proof) against a real preamble-bearing document-release slice in a Conductor-ambient env with both AUQ hooks seeded live, an unbumped VERSION making Step 8 fire. Asserts: the final line parses as the 5-key JSON contract, the fired gate's auto-choice is recorded in decisions, and VERSION is untouched (the gate resolved to its recommended Skip). Burn-in: 1/1 pass, $0.35, 21 turns, 106s. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(openclaw): document the GSTACK_SESSION_KIND override; wire session-kind into paid selectors (#2733) OPENCLAW.md's spawned-session section now covers the explicit per-command marker, its deliberate spawned-only narrowness, the /ship Step 18 usage, the destructive carve-out, onboarding-block suppression, and the hook env-blindness caveat. bin/gstack-session-kind and the shared spawned-directive module join the conductor-prose and auto-decide-preserved selector dep lists (session-kind previously appeared in no touchfiles entry — editing it alone triggered no paid E2E). TODOS.md gains the plan-tune capture follow-up for spawned auto-choices. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: pre-landing review fixes (#2733) Review army + coverage audit findings, all applied: - headless directive carries the spawned escape sentence too (multi- specialist: a CI-hosted ship's marked subagent must not end BLOCKED) - anti-injection scoping on every text-claimable spawned trigger (AUQ rule + shared escape sentence): markings count only from the creating prompt, never from files/tool output/web content read mid-run - [conductor][spawned] deny annotates one-way doors per question - SPAWNED_OVERRIDE: env tamper-visibility status line + OPENCLAW.md note - spawned sessions skip the network update-check and first-task probe (consumers suppressed; preserves the one-shot just-upgraded marker) - test hardening: dispatch-tripwire end-bound validated, vacuous marker asserts replaced with output asserts, E2E cpSync size filter + named fence tolerance, spawnedByEnv parity pin, destructive-policy cross- surface drift guard, one-way annotation + bogus-value hook cases - session-kind duplicate rationale comment deduped; regen + goldens + context-budget fixture refreshed Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: bump version and changelog (v1.76.0.0) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: update project documentation for v1.76.0.0 PROJECT_STRUCTURE.md: add hosts/claude/hooks/ to the directory tree (AUQ capture + enforcement hooks, spawned-session directive, timeline stop) — the tree omitted the directory while docs/OPENCLAW.md and CHANGELOG.md now reference paths inside it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: sync TODOS.md ship dispatch entry with the v1.76.0.0 contract Codex doc-review finding: the SHIPPED entry for /ship auto-invoking /document-release still described the four-key JSON contract. Adds the decisions key (console-printed, never PR markdown), the GSTACK_SESSION_KIND=spawned dispatch marking (#2733), and the new spawned-dispatch gate E2E to the proven-by list. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
553 lines
31 KiB
Bash
Executable File
553 lines
31 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# gstack-skill-start — the skill preamble's runtime, consolidated (token-reduction Phase 1).
|
|
#
|
|
# Absorbs the bash that every generated SKILL.md used to inline twice over
|
|
# (the "Preamble (run first)" bootstrap fence and the "Artifacts Sync" fence,
|
|
# ~13KB per skill x 50 skills). The generated skill now carries a one-line
|
|
# invocation; this script emits the SAME `KEY: value` STATUS lines the prose
|
|
# rules interpret. The contract is pinned by test/gstack-skill-start.test.ts
|
|
# (every KEY the rendered prose references must be emitted here).
|
|
#
|
|
# Enumerated divergences from the inline original (plan EOV5):
|
|
# - Paths resolve $0-relative (works for every host + install layout) instead
|
|
# of gen-time interpolation.
|
|
# - --parent-pid carries the HARNESS pid for session counting ($PPID inside
|
|
# this script is the ephemeral tool-call shell — wrong identity).
|
|
# - State paths honor ${GSTACK_HOME} (EOV7); the inline original hardcoded
|
|
# ~/.gstack in the bootstrap half.
|
|
# - SESSION_ID / TEL_START are echoed (the inline shell vars never survived
|
|
# across Bash tool calls, so skill-end durations were already broken; the
|
|
# echo makes them real).
|
|
# - SKILL_START_PROTO handshake (OV5): prose treats a missing/unexpected
|
|
# proto as a degraded install and applies safe defaults.
|
|
# - Passthrough sub-command output is sanitized (OV4): GSTACK_INSTRUCTION
|
|
# markers cannot be injected into the blessed tool result by learnings,
|
|
# update-check, or first-task text.
|
|
#
|
|
# Error style (F3): per-line `|| true`, never `set -e` — a mid-script failure
|
|
# must not drop later STATUS lines.
|
|
|
|
# Heredoc bodies in the 512B-64KiB window can deadlock bash 5.1+'s pipe-backed
|
|
# heredoc path when the reader stalls; compat level 50 restores the tempfile
|
|
# path. This script is bash-3.2-clean, so the compat level costs nothing
|
|
# (same guard as bin/gstack-brain-sync; pinned by
|
|
# test/heredoc-pipe-deadlock.test.ts).
|
|
BASH_COMPAT=50
|
|
|
|
SKILL_NAME=""
|
|
MODEL_OVERLAY="none"
|
|
PARENT_PID="$PPID"
|
|
BRAIN_HEALTH="no"
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--skill) SKILL_NAME="$2"; shift 2 ;;
|
|
--model) MODEL_OVERLAY="$2"; shift 2 ;;
|
|
--parent-pid) PARENT_PID="$2"; shift 2 ;;
|
|
--brain-health) BRAIN_HEALTH="yes"; shift ;;
|
|
*) shift ;;
|
|
esac
|
|
done
|
|
[ -n "$SKILL_NAME" ] || SKILL_NAME="unknown"
|
|
|
|
_SCRIPT_DIR=$(cd "$(dirname "$0")" 2>/dev/null && pwd)
|
|
_BIN="$_SCRIPT_DIR"
|
|
_GH="${GSTACK_HOME:-$HOME/.gstack}"
|
|
|
|
# OV4: strip instruction markers from any passthrough text before echoing it
|
|
# into the blessed tool result. Prior-session/repo content must not be able to
|
|
# mint directive blocks — nor forge a SESSION_ID: status line (the token that
|
|
# authenticates instruction blocks), so line-leading spoofs are neutralized too.
|
|
_sanitize() { sed -e 's/GSTACK_INSTRUCTION/GSTACK-INSTRUCTION-(stripped)/g' -e 's/^SESSION_ID:/SESSION-ID-(stripped):/'; }
|
|
|
|
echo "SKILL_START_PROTO: 1"
|
|
|
|
# Session kind resolves FIRST (env-only checks, cheap). Spawned sessions skip
|
|
# the network-bound update-check: its only consumer (the upgrade-flow block)
|
|
# is emission-gated on kind != spawned below, so the work would be discarded —
|
|
# and running it would consume the one-shot just-upgraded marker meant for the
|
|
# next human session (#2733 review).
|
|
_SESSION_KIND=$("$_BIN/gstack-session-kind" 2>/dev/null || echo "interactive")
|
|
case "$_SESSION_KIND" in spawned|headless|interactive) ;; *) _SESSION_KIND="interactive" ;; esac
|
|
|
|
_UPD=""
|
|
if [ "$_SESSION_KIND" != "spawned" ]; then
|
|
_UPD=$("$_BIN/gstack-update-check" 2>/dev/null || true)
|
|
[ -n "$_UPD" ] && printf '%s\n' "$_UPD" | _sanitize || true
|
|
fi
|
|
|
|
mkdir -p "$_GH/sessions" 2>/dev/null || true
|
|
touch "$_GH/sessions/$PARENT_PID" 2>/dev/null || true
|
|
find "$_GH/sessions" -mmin +120 -type f -exec rm {} + 2>/dev/null || true
|
|
|
|
_PROACTIVE=$("$_BIN/gstack-config" get proactive 2>/dev/null || echo "true")
|
|
_PROACTIVE_PROMPTED=$([ -f "$_GH/.proactive-prompted" ] && echo "yes" || echo "no")
|
|
# Branch name is embedded in concatenated JSON below; strip anything outside
|
|
# the gstack-slug BRANCH alphabet so a hostile ref name can't forge fields.
|
|
_BRANCH=$(git branch --show-current 2>/dev/null | tr -cd 'a-zA-Z0-9._/-')
|
|
_BRANCH=${_BRANCH:-unknown}
|
|
echo "BRANCH: $_BRANCH"
|
|
_SKILL_PREFIX=$("$_BIN/gstack-config" get skill_prefix 2>/dev/null || echo "false")
|
|
echo "PROACTIVE: $_PROACTIVE"
|
|
echo "PROACTIVE_PROMPTED: $_PROACTIVE_PROMPTED"
|
|
echo "SKILL_PREFIX: $_SKILL_PREFIX"
|
|
REPO_MODE=""
|
|
eval "$("$_BIN/gstack-repo-mode" 2>/dev/null)" 2>/dev/null || true
|
|
REPO_MODE=${REPO_MODE:-unknown}
|
|
echo "REPO_MODE: $REPO_MODE"
|
|
echo "SESSION_KIND: $_SESSION_KIND"
|
|
# Tamper visibility (#2733 review): a repo-provided settings env block or a CI
|
|
# wrapper could set GSTACK_SESSION_KIND or OPENCLAW_SESSION for a real human's
|
|
# session, silently flipping every gate to auto-choose. Surface the env-driven
|
|
# override loudly, naming the driver, so the transcript shows WHY this session
|
|
# is spawned. Gated on the RESOLVED kind so a broken session-kind fallback
|
|
# never prints a contradictory override line next to SESSION_KIND: interactive.
|
|
if [ "$_SESSION_KIND" = "spawned" ]; then
|
|
if [ "${GSTACK_SESSION_KIND:-}" = "spawned" ]; then
|
|
echo "SPAWNED_OVERRIDE: env (GSTACK_SESSION_KIND)"
|
|
elif [ -n "${OPENCLAW_SESSION:-}" ]; then
|
|
echo "SPAWNED_OVERRIDE: env (OPENCLAW_SESSION)"
|
|
fi
|
|
fi
|
|
# Conductor host: AskUserQuestion is unreliable there (native disabled, MCP
|
|
# variant flaky); skills render decisions as prose. Gated on !headless so an
|
|
# eval/CI run INSIDE Conductor still BLOCKs rather than rendering prose to
|
|
# nobody, and on !spawned so an orchestrator- or parent-skill-spawned session
|
|
# inside a Conductor workspace auto-chooses per its spawned-session block
|
|
# instead of rendering prose to nobody (#2733).
|
|
if [ "$_SESSION_KIND" != "headless" ] && [ "$_SESSION_KIND" != "spawned" ] && { [ -n "${CONDUCTOR_WORKSPACE_PATH:-}" ] || [ -n "${CONDUCTOR_PORT:-}" ]; }; then
|
|
echo "CONDUCTOR_SESSION: true"
|
|
fi
|
|
_ACTIVATED=$([ -f "$_GH/.activated" ] && echo "yes" || echo "no")
|
|
_FIRST_LOOP_SHOWN=$([ -f "$_GH/.first-loop-tip-shown" ] && echo "yes" || echo "no")
|
|
echo "ACTIVATED: $_ACTIVATED"
|
|
echo "FIRST_LOOP_SHOWN: $_FIRST_LOOP_SHOWN"
|
|
# First-run project detection: only on the first-ever skill run (off the hot
|
|
# path after). Spawned excluded like headless (#2733 review): spawned sessions
|
|
# never write .activated, so without the exclusion the ~17-subprocess probe
|
|
# would re-run on EVERY spawned start while its only consumers stay suppressed.
|
|
_FIRST_TASK=""
|
|
if [ "$_ACTIVATED" = "no" ] && [ "$_SESSION_KIND" != "headless" ] && [ "$_SESSION_KIND" != "spawned" ]; then
|
|
_FIRST_TASK=$("$_BIN/gstack-first-task-detect" 2>/dev/null || true)
|
|
fi
|
|
printf 'FIRST_TASK: %s\n' "$_FIRST_TASK" | _sanitize
|
|
_LAKE_SEEN=$([ -f "$_GH/.completeness-intro-seen" ] && echo "yes" || echo "no")
|
|
echo "LAKE_INTRO: $_LAKE_SEEN"
|
|
_TEL=$("$_BIN/gstack-config" get telemetry 2>/dev/null || true)
|
|
_TEL_PROMPTED=$([ -f "$_GH/.telemetry-prompted" ] && echo "yes" || echo "no")
|
|
_TEL_START=$(date +%s)
|
|
# PID+epoch alone is guessable; a random suffix makes the block-binding token
|
|
# unforgeable by content reflected into the same tool result. Falls back to
|
|
# the plain form when urandom is unavailable.
|
|
_SID_RAND=$(od -An -N4 -tx4 /dev/urandom 2>/dev/null | tr -d ' \n' || true)
|
|
_SESSION_ID="$PARENT_PID-$_TEL_START${_SID_RAND:+-$_SID_RAND}"
|
|
echo "TELEMETRY: ${_TEL:-off}"
|
|
echo "TEL_PROMPTED: $_TEL_PROMPTED"
|
|
echo "SESSION_ID: $_SESSION_ID"
|
|
echo "TEL_START: $_TEL_START"
|
|
_EXPLAIN_LEVEL=$("$_BIN/gstack-config" get explain_level 2>/dev/null || echo "default")
|
|
if [ "$_EXPLAIN_LEVEL" != "default" ] && [ "$_EXPLAIN_LEVEL" != "terse" ]; then _EXPLAIN_LEVEL="default"; fi
|
|
echo "EXPLAIN_LEVEL: $_EXPLAIN_LEVEL"
|
|
_QUESTION_TUNING=$("$_BIN/gstack-config" get question_tuning 2>/dev/null || echo "false")
|
|
echo "QUESTION_TUNING: $_QUESTION_TUNING"
|
|
_UPDATE_CHECK=$("$_BIN/gstack-config" get update_check 2>/dev/null || echo "true")
|
|
echo "UPDATE_CHECK: $_UPDATE_CHECK"
|
|
mkdir -p "$_GH/analytics" 2>/dev/null || true
|
|
if [ "$_TEL" != "off" ]; then
|
|
echo '{"skill":"'"$SKILL_NAME"'","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$(_repo=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null | tr -cd 'a-zA-Z0-9._-'); echo "${_repo:-unknown}")'"}' >> "$_GH/analytics/skill-usage.jsonl" 2>/dev/null || true
|
|
fi
|
|
for _PF in $(find "$_GH/analytics" -maxdepth 1 -name '.pending-*' 2>/dev/null); do
|
|
if [ -f "$_PF" ]; then
|
|
if [ "$_TEL" != "off" ] && [ -x "$_BIN/gstack-telemetry-log" ]; then
|
|
"$_BIN/gstack-telemetry-log" --event-type skill_run --skill _pending_finalize --outcome unknown --session-id "$_SESSION_ID" 2>/dev/null || true
|
|
fi
|
|
rm -f "$_PF" 2>/dev/null || true
|
|
fi
|
|
# Deliberate throttle: drain at most ONE orphaned pending file per skill
|
|
# start so a backlog can't stall the preamble.
|
|
break
|
|
done
|
|
eval "$("$_BIN/gstack-slug" 2>/dev/null)" 2>/dev/null || true
|
|
_LEARN_FILE="$_GH/projects/${SLUG:-unknown}/learnings.jsonl"
|
|
if [ -f "$_LEARN_FILE" ]; then
|
|
_LEARN_COUNT=$(wc -l < "$_LEARN_FILE" 2>/dev/null | tr -d ' ')
|
|
echo "LEARNINGS: $_LEARN_COUNT entries loaded"
|
|
if [ "$_LEARN_COUNT" -gt 5 ] 2>/dev/null; then
|
|
"$_BIN/gstack-learnings-search" --limit 3 2>/dev/null | _sanitize || true
|
|
fi
|
|
else
|
|
echo "LEARNINGS: 0"
|
|
fi
|
|
"$_BIN/gstack-timeline-log" '{"skill":"'"$SKILL_NAME"'","event":"started","branch":"'"$_BRANCH"'","session":"'"$_SESSION_ID"'"}' 2>/dev/null &
|
|
_HAS_ROUTING="no"
|
|
for _RF in CLAUDE.md AGENTS.md; do
|
|
if [ -f "$_RF" ] && grep -q "## Skill routing" "$_RF" 2>/dev/null; then
|
|
_HAS_ROUTING="yes"
|
|
fi
|
|
done
|
|
_ROUTING_DECLINED=$("$_BIN/gstack-config" get routing_declined 2>/dev/null || echo "false")
|
|
echo "HAS_ROUTING: $_HAS_ROUTING"
|
|
echo "ROUTING_DECLINED: $_ROUTING_DECLINED"
|
|
_VENDORED="no"
|
|
if [ -d ".claude/skills/gstack" ] && [ ! -L ".claude/skills/gstack" ]; then
|
|
if [ -f ".claude/skills/gstack/VERSION" ] || [ -d ".claude/skills/gstack/.git" ]; then
|
|
_VENDORED="yes"
|
|
fi
|
|
fi
|
|
echo "VENDORED_GSTACK: $_VENDORED"
|
|
echo "MODEL_OVERLAY: $MODEL_OVERLAY"
|
|
_CHECKPOINT_MODE=$("$_BIN/gstack-config" get checkpoint_mode 2>/dev/null || echo "explicit")
|
|
_CHECKPOINT_PUSH=$("$_BIN/gstack-config" get checkpoint_push 2>/dev/null || echo "false")
|
|
echo "CHECKPOINT_MODE: $_CHECKPOINT_MODE"
|
|
echo "CHECKPOINT_PUSH: $_CHECKPOINT_PUSH"
|
|
# Plan-mode hint for skills that branch on plan-mode state. Detected best-effort
|
|
# from CLAUDE_PLAN_FILE (set by the harness when plan mode is active); "inactive"
|
|
# is the safe default (file+execute pipeline).
|
|
if [ -n "${CLAUDE_PLAN_FILE:-}${GSTACK_PLAN_MODE_FORCE:-}" ]; then
|
|
GSTACK_PLAN_MODE="active"
|
|
elif [ "${GSTACK_PLAN_MODE:-}" = "active" ]; then
|
|
GSTACK_PLAN_MODE="active"
|
|
else
|
|
GSTACK_PLAN_MODE="inactive"
|
|
fi
|
|
echo "GSTACK_PLAN_MODE: $GSTACK_PLAN_MODE"
|
|
# Keyed on the resolved kind, not raw OPENCLAW_SESSION (#2733): the explicit
|
|
# GSTACK_SESSION_KIND=spawned override must light this up too.
|
|
[ "$_SESSION_KIND" = "spawned" ] && echo "SPAWNED_SESSION: true" || true
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Artifacts sync (the former "Artifacts Sync (skill start)" fence, verbatim
|
|
# modulo GSTACK_HOME + $0-relative bins).
|
|
# ---------------------------------------------------------------------------
|
|
if [ -f "$HOME/.gstack-artifacts-remote.txt" ]; then
|
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-artifacts-remote.txt"
|
|
else
|
|
_BRAIN_REMOTE_FILE="$HOME/.gstack-brain-remote.txt"
|
|
fi
|
|
_BRAIN_SYNC_BIN="$_BIN/gstack-brain-sync"
|
|
_BRAIN_CONFIG_BIN="$_BIN/gstack-config"
|
|
|
|
# /sync-gbrain context-load hint. Per-worktree pin via kubectl-style
|
|
# .gbrain-source at the git toplevel; empty output when gbrain is not
|
|
# configured (zero context cost for non-gbrain users).
|
|
_GBRAIN_CONFIG="$HOME/.gbrain/config.json"
|
|
if [ -f "$_GBRAIN_CONFIG" ] && command -v gbrain >/dev/null 2>&1; then
|
|
_GBRAIN_VERSION_OK=$(gbrain --version 2>/dev/null | grep -c '^gbrain ' || true)
|
|
if [ "$_GBRAIN_VERSION_OK" -gt 0 ] 2>/dev/null; then
|
|
_GBRAIN_PIN_PATH=""
|
|
_REPO_TOP=$(git rev-parse --show-toplevel 2>/dev/null || echo "")
|
|
if [ -n "$_REPO_TOP" ] && [ -f "$_REPO_TOP/.gbrain-source" ]; then
|
|
_GBRAIN_PIN_PATH="$_REPO_TOP/.gbrain-source"
|
|
fi
|
|
if [ -n "$_GBRAIN_PIN_PATH" ]; then
|
|
echo "GBrain configured. Prefer \`gbrain search\`/\`gbrain query\` over Grep for"
|
|
echo "semantic questions; use \`gbrain code-def\`/\`code-refs\`/\`code-callers\` for"
|
|
echo "symbol-aware code lookup. See \"## GBrain Search Guidance\" in CLAUDE.md."
|
|
echo "Run /sync-gbrain to refresh."
|
|
else
|
|
echo "GBrain configured but this worktree isn't pinned yet. Run \`/sync-gbrain --full\`"
|
|
echo "before relying on \`gbrain search\` for code questions in this worktree."
|
|
echo "Falls back to Grep until pinned."
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
_BRAIN_SYNC_MODE=$("$_BRAIN_CONFIG_BIN" get artifacts_sync_mode 2>/dev/null || echo off)
|
|
|
|
# Remote-MCP mode detection (Path 4 of /setup-gbrain): read claude.json
|
|
# directly (no subprocess to claude CLI on the hot path). Both registration
|
|
# scopes are read (#2499): nearest-ancestor project scope first, then user
|
|
# scope; project-local BEATS user scope; the ancestor match accepts both path
|
|
# separators (Windows project keys are backslash-formed).
|
|
_GBRAIN_MCP_MODE="none"
|
|
_GBRAIN_MCP_ENTRY=""
|
|
# Cheap substring pre-filter: ~/.claude.json can be megabytes on real installs;
|
|
# skip the full jq parse when no gbrain server is registered at all.
|
|
if command -v jq >/dev/null 2>&1 && [ -f "$HOME/.claude.json" ] && grep -q '"gbrain"' "$HOME/.claude.json" 2>/dev/null; then
|
|
_GBRAIN_MCP_ENTRY=$(jq -c --arg cwd "$PWD" '((.projects // {}) | to_entries | map(select((.key as $k | $cwd == $k or ($cwd | startswith($k + "/")) or ($cwd | startswith($k + "\\"))) and ((try .value.mcpServers.gbrain catch null) != null))) | sort_by(.key | length) | last | .value.mcpServers.gbrain) // .mcpServers.gbrain // empty' "$HOME/.claude.json" 2>/dev/null)
|
|
_GBRAIN_MCP_TYPE=$(printf '%s' "$_GBRAIN_MCP_ENTRY" | jq -r '.type // .transport // empty' 2>/dev/null)
|
|
case "$_GBRAIN_MCP_TYPE" in
|
|
url|http|sse) _GBRAIN_MCP_MODE="remote-http" ;;
|
|
stdio) _GBRAIN_MCP_MODE="local-stdio" ;;
|
|
esac
|
|
fi
|
|
|
|
if [ -f "$_BRAIN_REMOTE_FILE" ] && [ ! -d "$_GH/.git" ] && [ "$_BRAIN_SYNC_MODE" = "off" ]; then
|
|
_BRAIN_NEW_URL=$(head -1 "$_BRAIN_REMOTE_FILE" 2>/dev/null | tr -d '[:space:]')
|
|
if [ -n "$_BRAIN_NEW_URL" ]; then
|
|
printf 'ARTIFACTS_SYNC: artifacts repo detected: %s\n' "$_BRAIN_NEW_URL" | _sanitize
|
|
echo "ARTIFACTS_SYNC: run 'gstack-brain-restore' to pull your cross-machine artifacts (or 'gstack-config set artifacts_sync_mode off' to dismiss forever)"
|
|
fi
|
|
fi
|
|
|
|
if [ -d "$_GH/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
|
_BRAIN_LAST_PULL_FILE="$_GH/.brain-last-pull"
|
|
_BRAIN_NOW=$(date +%s)
|
|
_BRAIN_DO_PULL=1
|
|
if [ -f "$_BRAIN_LAST_PULL_FILE" ]; then
|
|
_BRAIN_LAST=$(cat "$_BRAIN_LAST_PULL_FILE" 2>/dev/null || echo 0)
|
|
case "$_BRAIN_LAST" in ''|*[!0-9]*) _BRAIN_LAST=0 ;; esac
|
|
_BRAIN_AGE=$(( _BRAIN_NOW - _BRAIN_LAST ))
|
|
[ "$_BRAIN_AGE" -lt 86400 ] && _BRAIN_DO_PULL=0
|
|
fi
|
|
if [ "$_BRAIN_DO_PULL" = "1" ]; then
|
|
# Daily artifacts pull is a brain-sync-class sink: receipt-before-send,
|
|
# fail-closed (same wiring as bin/gstack-brain-sync's fetch/push).
|
|
. "$_BIN/gstack-egress-lib.sh" 2>/dev/null || true
|
|
_PULL_HOST=$(cd "$_GH" 2>/dev/null && git remote get-url origin 2>/dev/null | sed -E 's|^[a-z+]+://([^/@]*@)?([^/:]+).*|\2|; s|^([^@]+@)?([^:]+):.*|\2|' | head -1)
|
|
if command -v _receipted_git >/dev/null 2>&1; then
|
|
# Non-interactive + slow-network bounded (same guards as gstack-update-check):
|
|
# a hung remote or a credential prompt must not stall the preamble.
|
|
( cd "$_GH" && GSTACK_HOME="$_GH" GIT_TERMINAL_PROMPT=0 GIT_HTTP_LOW_SPEED_LIMIT=1000 GIT_HTTP_LOW_SPEED_TIME=5 \
|
|
_receipted_git closed brain-sync "${_PULL_HOST:-unknown}" curated-memory-git-fetch "artifacts_sync_mode!=off" \
|
|
git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
|
# Stamp only when the receipted path actually ran — a missing egress lib
|
|
# must surface as a retry next start, not a silent 24h suppression.
|
|
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
|
fi
|
|
fi
|
|
"$_BRAIN_SYNC_BIN" --once 2>/dev/null || true
|
|
fi
|
|
|
|
if [ "$_GBRAIN_MCP_MODE" = "remote-http" ]; then
|
|
# Remote-MCP mode: local artifacts sync is a no-op by design (the brain
|
|
# admin's server pulls from GitHub/GitLab).
|
|
_GBRAIN_HOST=$(printf '%s' "${_GBRAIN_MCP_ENTRY:-}" | jq -r '.url // empty' 2>/dev/null | sed -E 's|^https?://([^/:]+).*|\1|' | head -1 | tr -cd 'A-Za-z0-9._-')
|
|
echo "ARTIFACTS_SYNC: remote-mode (managed by brain server ${_GBRAIN_HOST:-remote})"
|
|
elif [ -d "$_GH/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
|
_BRAIN_QUEUE_DEPTH=0
|
|
# Spool-dir queue (one file per record); legacy .brain-queue.jsonl lines
|
|
# counted too until the drain migrates them.
|
|
[ -d "$_GH/.brain-queue.d" ] && _BRAIN_QUEUE_DEPTH=$(find "$_GH/.brain-queue.d" -maxdepth 1 -name '*.json' 2>/dev/null | wc -l | tr -d ' ')
|
|
[ -f "$_GH/.brain-queue.jsonl" ] && _BRAIN_QUEUE_DEPTH=$(( _BRAIN_QUEUE_DEPTH + $(wc -l < "$_GH/.brain-queue.jsonl" | tr -d ' ') ))
|
|
[ -f "$_GH/.brain-queue.jsonl.migrating" ] && _BRAIN_QUEUE_DEPTH=$(( _BRAIN_QUEUE_DEPTH + $(wc -l < "$_GH/.brain-queue.jsonl.migrating" | tr -d ' ') ))
|
|
_BRAIN_LAST_PUSH="never"
|
|
# First line only + charset clamp: this file lives in a git checkout pulled
|
|
# from the artifacts remote, so its content is not trusted for STATUS output.
|
|
[ -f "$_GH/.brain-last-push" ] && _BRAIN_LAST_PUSH=$(head -1 "$_GH/.brain-last-push" 2>/dev/null | tr -cd 'A-Za-z0-9._:+-' || echo never)
|
|
_BRAIN_LAST_PUSH=${_BRAIN_LAST_PUSH:-never}
|
|
echo "ARTIFACTS_SYNC: mode=$_BRAIN_SYNC_MODE | last_push=$_BRAIN_LAST_PUSH | queue=$_BRAIN_QUEUE_DEPTH"
|
|
else
|
|
echo "ARTIFACTS_SYNC: off"
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Instruction-emission layer (token-reduction Phase 2). One-time onboarding
|
|
# text used to be inlined unconditionally in every SKILL.md (~7KB/skill);
|
|
# it now appears ONLY when its runtime gate fires, wrapped in blocks the
|
|
# preamble prose tells the model to follow. Security scoping (F4/OV4): each
|
|
# header carries this run's SESSION_ID; the prose rule requires the match and
|
|
# accepts blocks only from this command's direct tool result, so page/file
|
|
# content cannot mint directives (passthrough output above is _sanitize-d).
|
|
# Ack ownership (OV6): display-only tips write their done-marker here at
|
|
# emit; interactive flows carry their ack commands inside the block for the
|
|
# model to run after the interaction.
|
|
# ---------------------------------------------------------------------------
|
|
_ROOT_DIR=$(dirname "$_BIN")
|
|
_emit_block() { echo "GSTACK_INSTRUCTION_BEGIN: $1 $_SESSION_ID"; cat; echo "GSTACK_INSTRUCTION_END"; }
|
|
|
|
# Spawned sessions get NO interactive-onboarding blocks (#2733): no human is
|
|
# watching, so an emitted prompt gets auto-answered (config writes nobody
|
|
# approved) and ack-at-emit markers get consumed invisibly — the next HUMAN
|
|
# session would never see the one-time prompt. Gating EMISSION here (not just
|
|
# the prose-skip inside the spawned-session block) also gates the script-side
|
|
# marker/state writes: .activated, .first-loop-tip-shown, and scaffold
|
|
# telemetry stay untouched, so onboarding fires intact on the next human run.
|
|
# The spawned-session block and the privacy-stop-gate below sit OUTSIDE this
|
|
# guard: the former is the gate's inverse, the latter is interactive-only.
|
|
if [ "$_SESSION_KIND" != "spawned" ]; then
|
|
|
|
# Upgrade flow (gated: update-check emitted something above).
|
|
if [ -n "$_UPD" ]; then
|
|
_emit_block upgrade-flow <<EOI
|
|
The update-check output above applies. If it shows \`UPGRADE_AVAILABLE <old> <new>\`: read \`$_ROOT_DIR/gstack-upgrade/SKILL.md\` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If it shows \`JUST_UPGRADED <from> <to>\`: print "Running gstack v{to} (just updated!)". If \`SPAWNED_SESSION\` is true, skip feature discovery. After upgrade prompts, continue the workflow.
|
|
EOI
|
|
fi
|
|
# Feature discovery (one prompt per session, marker-gated).
|
|
if [ ! -f "$_ROOT_DIR/.feature-prompted-continuous-checkpoint" ]; then
|
|
_emit_block feature-checkpoint <<EOI
|
|
Feature discovery: AskUserQuestion for Continuous checkpoint auto-commits. If accepted, run \`$_BIN/gstack-config set checkpoint_mode continuous\`. Always run: \`touch "$_ROOT_DIR/.feature-prompted-continuous-checkpoint"\`.
|
|
EOI
|
|
elif [ ! -f "$_ROOT_DIR/.feature-prompted-model-overlay" ]; then
|
|
_emit_block feature-overlay <<EOI
|
|
Feature discovery: inform "Model overlays are active. MODEL_OVERLAY shows the patch." Always run: \`touch "$_ROOT_DIR/.feature-prompted-model-overlay"\`.
|
|
EOI
|
|
fi
|
|
|
|
# Writing-style migration (gate previously dormant: nothing echoed
|
|
# WRITING_STYLE_PENDING; the script now computes it from the marker files).
|
|
if [ -f "$_GH/.writing-style-prompt-pending" ] && [ ! -f "$_GH/.writing-style-prompted" ]; then
|
|
_emit_block writing-style-migration <<EOI
|
|
Ask once about writing style:
|
|
|
|
> v1 prompts are simpler: first-use jargon glosses, outcome-framed questions, shorter prose. Keep default or restore terse?
|
|
|
|
Options:
|
|
- A) Keep the new default (recommended — good writing helps everyone)
|
|
- B) Restore V0 prose — set \`explain_level: terse\`
|
|
|
|
If A: leave \`explain_level\` unset (defaults to \`default\`). If B: run \`$_BIN/gstack-config set explain_level terse\`.
|
|
Always run (regardless of choice): \`rm -f "$_GH/.writing-style-prompt-pending" && touch "$_GH/.writing-style-prompted"\`.
|
|
EOI
|
|
fi
|
|
|
|
# Lake intro (one-time; the offer is interactive, so the model acks).
|
|
if [ "$_LAKE_SEEN" = "no" ]; then
|
|
_emit_block lake-intro <<EOI
|
|
Say: "gstack follows the **Boil the Ocean** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" and offer to open it. Only run \`open https://garryslist.org/posts/boil-the-ocean\` if the user says yes. Always run: \`touch "$_GH/.completeness-intro-seen"\`.
|
|
EOI
|
|
fi
|
|
|
|
# Telemetry opt-in (interactive; consent — the model acks after answering).
|
|
if [ "$_TEL_PROMPTED" = "no" ] && [ "$_LAKE_SEEN" = "yes" ]; then
|
|
_emit_block telemetry-prompt <<EOI
|
|
Ask telemetry once via AskUserQuestion:
|
|
|
|
> Help gstack get better. Share usage data only: skill, duration, crashes, stable device ID. No code or file paths. Your repo name is recorded locally only and stripped before any upload.
|
|
|
|
Options:
|
|
- A) Help gstack get better! (recommended)
|
|
- B) No thanks
|
|
|
|
If A: run \`$_BIN/gstack-config set telemetry community\`. If B, ask the follow-up:
|
|
|
|
> Anonymous mode sends only aggregate usage, no unique ID.
|
|
|
|
Options: A) Sure, anonymous is fine B) No thanks, fully off
|
|
If B→A: \`$_BIN/gstack-config set telemetry anonymous\`. If B→B: \`$_BIN/gstack-config set telemetry off\`.
|
|
Always run: \`touch "$_GH/.telemetry-prompted"\`.
|
|
EOI
|
|
fi
|
|
|
|
# Proactive-suggestions opt-in (interactive).
|
|
if [ "$_PROACTIVE_PROMPTED" = "no" ] && [ "$_TEL_PROMPTED" = "yes" ]; then
|
|
_emit_block proactive-prompt <<EOI
|
|
Ask once:
|
|
|
|
> Let gstack proactively suggest skills, like /qa for "does this work?" or /investigate for bugs?
|
|
|
|
Options: A) Keep it on (recommended) B) Turn it off — I'll type /commands myself
|
|
If A: \`$_BIN/gstack-config set proactive true\`. If B: \`$_BIN/gstack-config set proactive false\`.
|
|
Always run: \`touch "$_GH/.proactive-prompted"\`.
|
|
EOI
|
|
fi
|
|
|
|
# First-run guidance (display-only tips: marker + scaffold telemetry written
|
|
# HERE at emit, per OV6 — a lost tip is acceptable; a re-firing loop is not).
|
|
if [ "$_ACTIVATED" = "no" ]; then
|
|
_FT_TIP=""
|
|
case "$_FIRST_TASK" in
|
|
greenfield) _FT_TIP="Fresh repo — shape it first with \`/spec\` or \`/office-hours\`." ;;
|
|
code_node|code_python|code_rust|code_go|code_ruby|code_ios) _FT_TIP="There's code here — \`/qa\` to see it work, or \`/investigate\` if something's off." ;;
|
|
branch_ahead) _FT_TIP="Unshipped work on this branch — \`/review\` then \`/ship\`." ;;
|
|
dirty_default) _FT_TIP="Uncommitted changes — \`/review\` before committing." ;;
|
|
clean_default) _FT_TIP="Pick one: \`/spec\`, \`/investigate\`, or \`/qa\`." ;;
|
|
esac
|
|
if [ -n "$_FT_TIP" ]; then
|
|
_emit_block first-run-tip <<EOI
|
|
Show this one project-specific line as a heads-up, then CONTINUE with whatever the user actually asked — do NOT halt their task: $_FT_TIP
|
|
EOI
|
|
"$_BIN/gstack-telemetry-log" --event-type first_task_scaffold_shown --skill "$_FIRST_TASK" --outcome shown 2>/dev/null || true
|
|
fi
|
|
touch "$_GH/.activated" 2>/dev/null || true
|
|
elif [ "$_FIRST_LOOP_SHOWN" = "no" ]; then
|
|
_emit_block first-loop-tip <<EOI
|
|
Say once as a heads-up (then continue): Tip: gstack pays off when you complete one loop — **plan → review → ship**. A common first loop: \`/office-hours\` or \`/spec\` to shape it, \`/plan-eng-review\` to lock it, then \`/ship\`.
|
|
EOI
|
|
touch "$_GH/.first-loop-tip-shown" 2>/dev/null || true
|
|
fi
|
|
|
|
# Routing injection (interactive: AUQ + CLAUDE.md append + commit). The body
|
|
# is a quoted heredoc (markdown backticks must stay literal), so the resolved
|
|
# bin path is substituted via sed on the __BIN__ placeholder.
|
|
if [ "$_HAS_ROUTING" = "no" ] && [ "$_ROUTING_DECLINED" = "false" ] && [ "$_PROACTIVE_PROMPTED" = "yes" ]; then
|
|
sed "s|__BIN__|$_BIN|g" <<'EOI' | _emit_block routing-injection
|
|
gstack works best when the project CLAUDE.md includes skill routing rules. If no CLAUDE.md exists in the project root, create it. Use AskUserQuestion:
|
|
|
|
> gstack works best when your project's CLAUDE.md includes skill routing rules.
|
|
|
|
Options: A) Add routing rules to CLAUDE.md (recommended) B) No thanks, I'll invoke skills manually
|
|
|
|
If A: append this section to the end of CLAUDE.md, then commit it (`git add CLAUDE.md && git commit -m "chore: add gstack skill routing rules to CLAUDE.md"`):
|
|
|
|
## Skill routing
|
|
|
|
When the user's request matches an available skill, invoke it via the Skill tool. When in doubt, invoke the skill.
|
|
|
|
Key routing rules:
|
|
- Product ideas/brainstorming → invoke /office-hours
|
|
- Strategy/scope → invoke /plan-ceo-review
|
|
- Architecture → invoke /plan-eng-review
|
|
- Design system/plan review → invoke /design-consultation or /plan-design-review
|
|
- Full review pipeline → invoke /autoplan
|
|
- Bugs/errors → invoke /investigate
|
|
- QA/testing site behavior → invoke /qa or /qa-only
|
|
- Code review/diff check → invoke /review
|
|
- Visual polish → invoke /design-review
|
|
- Ship/deploy/PR → invoke /ship or /land-and-deploy
|
|
- Save progress → invoke /context-save
|
|
- Resume context → invoke /context-restore
|
|
- Author a backlog-ready spec/issue → invoke /spec
|
|
|
|
If B: run `__BIN__/gstack-config set routing_declined true` and say they can re-enable with `__BIN__/gstack-config set routing_declined false`. This only happens once per project.
|
|
EOI
|
|
fi
|
|
|
|
# Vendoring deprecation (interactive; slug-scoped marker).
|
|
if [ "$_VENDORED" = "yes" ] && [ ! -f "$_GH/.vendoring-warned-${SLUG:-unknown}" ]; then
|
|
_emit_block vendoring-deprecation <<EOI
|
|
This project has gstack vendored in \`.claude/skills/gstack/\`. Vendoring is deprecated. Warn once via AskUserQuestion:
|
|
|
|
> Migrate to team mode?
|
|
|
|
Options: A) Yes, migrate to team mode now B) No, I'll handle it myself
|
|
If A: 1) \`git rm -r .claude/skills/gstack/\` 2) \`echo '.claude/skills/gstack/' >> .gitignore\` 3) \`$_BIN/gstack-team-init required\` (or \`optional\`) 4) \`git add .claude/ .gitignore CLAUDE.md && git commit -m "chore: migrate gstack from vendored to team mode"\` 5) Tell the user: "Done. Each developer now runs: \`cd ~/.claude/skills/gstack && ./setup --team\`"
|
|
If B: say "OK, you're on your own to keep the vendored copy up to date."
|
|
Always run (regardless of choice): \`touch "$_GH/.vendoring-warned-${SLUG:-unknown}"\`.
|
|
EOI
|
|
fi
|
|
|
|
fi # end interactive-onboarding guard (kind != spawned)
|
|
|
|
# Spawned-session rules (NOT one-time — every spawned session gets the full
|
|
# behavioral instruction, per plan OV6 move-with-care). Keyed on the resolved
|
|
# kind, not raw OPENCLAW_SESSION (#2733): the GSTACK_SESSION_KIND=spawned
|
|
# override must emit this block too — it is the only runtime text that
|
|
# actually says "auto-choose".
|
|
if [ "$_SESSION_KIND" = "spawned" ]; then
|
|
_emit_block spawned-session <<EOI
|
|
You are running inside a session spawned by an AI orchestrator (e.g., OpenClaw, or a parent skill such as /ship that dispatched you as a subagent). In spawned sessions: do NOT use AskUserQuestion for interactive prompts — auto-choose the recommended option (exception: never auto-choose a destructive or irreversible option such as delete, force-push, or overwrite — take the conservative non-destructive choice and record it in the completion report); do NOT run upgrade checks, telemetry prompts, routing injection, or lake intro (skip any such instruction blocks above); focus on completing the task and reporting results via prose output; end with a completion report: what shipped, decisions made, anything uncertain.
|
|
EOI
|
|
fi
|
|
|
|
# Privacy stop-gate (Phase 2, user-approved Q2: consent question surfaces only
|
|
# when consent is actually pending; still fired through AskUserQuestion).
|
|
# Interactive sessions ONLY: this block is emitted AFTER the spawned-session
|
|
# block, so its "skip instruction blocks above" rule does not cover it — a
|
|
# spawned session's auto-choose would accept data-sync consent no human gave.
|
|
# Deferral is safe (EOV8): the config key stays unset, so the gate fires on
|
|
# the next interactive run. Same reasoning suppresses it in headless runs.
|
|
_ARTIFACTS_PROMPTED=$("$_BRAIN_CONFIG_BIN" get artifacts_sync_mode_prompted 2>/dev/null || echo "false")
|
|
if [ "$_SESSION_KIND" = "interactive" ] && [ "$_BRAIN_SYNC_MODE" = "off" ] && [ "$_ARTIFACTS_PROMPTED" != "true" ] && command -v gbrain >/dev/null 2>&1; then
|
|
_emit_block privacy-stop-gate <<EOI
|
|
Privacy stop-gate — ask once via AskUserQuestion:
|
|
|
|
> gstack can publish your artifacts (CEO plans, designs, reports) to a private GitHub repo that GBrain indexes across machines. How much should sync?
|
|
|
|
Options: A) Everything allowlisted (recommended) B) Only artifacts C) Decline, keep everything local
|
|
After answer run: \`$_BIN/gstack-config set artifacts_sync_mode <full|artifacts-only|off>\` and \`$_BIN/gstack-config set artifacts_sync_mode_prompted true\`. If A/B and \`~/.gstack/.git\` is missing, ask whether to run \`gstack-artifacts-init\`. Do not block the skill.
|
|
EOI
|
|
fi
|
|
|
|
# BRAIN_HEALTH block: only for hosts whose render passes --brain-health
|
|
# (gbrain/hermes) — gen-time host conditional preserved as a flag.
|
|
if [ "$BRAIN_HEALTH" = "yes" ] && command -v gbrain >/dev/null 2>&1; then
|
|
_BRAIN_JSON=$(gbrain doctor --fast --json 2>/dev/null || echo '{}')
|
|
_BRAIN_SCORE=$(echo "$_BRAIN_JSON" | grep -o '"health_score":[0-9]*' | cut -d: -f2)
|
|
_BRAIN_FAILS=$(echo "$_BRAIN_JSON" | grep -o '"status":"fail"' | wc -l | tr -d ' ')
|
|
_BRAIN_WARNS=$(echo "$_BRAIN_JSON" | grep -o '"status":"warn"' | wc -l | tr -d ' ')
|
|
echo "BRAIN_HEALTH: ${_BRAIN_SCORE:-unknown} (${_BRAIN_FAILS:-0} failures, ${_BRAIN_WARNS:-0} warnings)"
|
|
if [ "${_BRAIN_SCORE:-100}" -lt 50 ] 2>/dev/null; then
|
|
echo "$_BRAIN_JSON" | grep -o '"name":"[^"]*","status":"[^"]*","message":"[^"]*"' | _sanitize || true
|
|
fi
|
|
fi
|