Files
gstack/bin/gstack-config
Garry Tan f44de365c5 v1.27.0.0 feat: /setup-gbrain Path 4 (remote MCP) + brain → artifacts rename (#1351)
* feat: gstack-gbrain-mcp-verify helper for remote MCP probe

Probes a remote gbrain MCP endpoint with bearer auth. POSTs initialize,
classifies failures into NETWORK / AUTH / MALFORMED with one-line
remediation hints, and runs a tools/list capability probe to detect
sources_add MCP support (forward-compat for when gbrain ships URL ingest).

Token consumed from GBRAIN_MCP_TOKEN env, never argv. Required to set
both 'application/json' AND 'text/event-stream' in Accept; that gotcha
costs 10 minutes of debugging when missed (regression-tested).

Live-verified against wintermute (gbrain v0.27.1).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: gstack-artifacts-init + gstack-artifacts-url helpers

artifacts-init replaces brain-init with provider choice (gh / glab /
manual), per-user gstack-artifacts-$USER repo, HTTPS-canonical storage in
~/.gstack-artifacts-remote.txt, and a "send this to your brain admin"
hookup printout. Always prints the command, never auto-executes — gbrain
v0.26.x has no admin-scope MCP probe (codex Finding #3).

artifacts-url centralizes HTTPS↔SSH/host/owner-repo conversion so callers
don't each string-mangle (codex Finding #10). The remote-conflict check in
artifacts-init compares at the canonical level so re-running with HTTPS
input doesn't trip on a stored SSH URL for the same logical repo.

The "URL form not supported" branch prints a two-line clone-then-path
form for gbrain v0.26.x; the supported branch is a one-liner with --url
ready for when gbrain ships URL ingest.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: extend gstack-gbrain-detect with mcp_mode + artifacts_remote

Adds two new fields to detect's JSON output:

- gbrain_mcp_mode: local-stdio | remote-http | none
  Resolved via 3-tier fallback (codex Finding D3): claude mcp get --json
  → claude mcp list text-grep → ~/.claude.json jq read. If Anthropic moves
  the file format, the first two tiers absorb it.

- gstack_artifacts_remote: HTTPS URL from ~/.gstack-artifacts-remote.txt
  Falls back to ~/.gstack-brain-remote.txt during the v1.27.0.0 migration
  window so detect doesn't return empty between upgrade and migration.

Existing detect tests still pass (15/15). New 19 tests cover every fallback
tier independently, plus a schema regression for /sync-gbrain compat.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: setup-gbrain Path 4 (remote MCP) + artifacts rename

Path 4 lets users paste an HTTPS MCP URL + bearer token and registers it
as an HTTP-transport MCP without needing a local gbrain CLI install. The
flow:

- Step 2 gains a fourth option (Remote gbrain MCP)
- Step 4 adds Path 4 sub-flow: collect URL, secret-read bearer, verify
  via gstack-gbrain-mcp-verify (NETWORK / AUTH / MALFORMED classifier)
- Step 5 (local doctor), Step 7.5 (transcript ingest), Step 5a's stdio
  branch all skip on Path 4
- Step 5a adds an HTTP+bearer registration form: claude mcp add
  --transport http --header "Authorization: Bearer ..."
- Step 7 renamed "session memory sync" → "artifacts sync" and now calls
  gstack-artifacts-init (which always prints the brain-admin hookup
  command — no auto-execute, codex Finding #3)
- Step 8 CLAUDE.md block branches: remote-http includes URL + server
  version (never the token); local-stdio keeps engine + config-file
- Step 9 smoke test on Path 4 prints the curl-equivalent for
  post-restart verification (MCP tools aren't visible mid-session)
- Step 10 verdict block has separate templates per mode

Idempotency: re-running with gbrain_mcp_mode=remote-http already in
detect output skips Step 2 entirely and goes to verification.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: rename gbrain_sync_mode → artifacts_sync_mode (v1.27.0.0 prep)

Hard rename, no dual-read alias (codex Finding D4). The on-disk migration
script (Phase C, separate commit) renames the config key in users'
~/.gstack/config.yaml and any CLAUDE.md blocks.

Touched call sites:
- bin/gstack-config defaults + validation + list/defaults output
- bin/gstack-gbrain-detect (gstack_brain_sync_mode field still emitted
  with the same name for downstream-tool compat; reads new key)
- bin/gstack-brain-sync, bin/gstack-brain-enqueue, bin/gstack-brain-uninstall
- bin/gstack-timeline-log (comment ref)
- scripts/resolvers/preamble/generate-brain-sync-block.ts: renames key,
  branches on gbrain_mcp_mode=remote-http to emit "ARTIFACTS_SYNC:
  remote-mode (managed by brain server <host>)" instead of the local
  mode/queue/last_push line (codex Finding #11)
- bin/gstack-brain-restore + bin/gstack-gbrain-source-wireup: read
  ~/.gstack-artifacts-remote.txt with ~/.gstack-brain-remote.txt fallback
  during the migration window
- bin/gstack-artifacts-init: tolerant of unrecognized URL forms (local
  paths, file://, self-hosted gitea) so test infrastructure and unusual
  remotes work without canonicalization
- test/brain-sync.test.ts: gstack-brain-init → gstack-artifacts-init
- test/skill-e2e-brain-privacy-gate.test.ts: artifacts_sync_mode keys
- test/gen-skill-docs.test.ts: budget 35K → 36.5K for the new MCP-mode
  probe in the preamble resolver
- health/SKILL.md.tmpl, sync-gbrain/SKILL.md.tmpl: comment + verdict line

Hard delete:
- bin/gstack-brain-init (replaced by bin/gstack-artifacts-init in v1.27.0.0)
- test/gstack-brain-init-gh-mock.test.ts (replaced by gstack-artifacts-init.test.ts)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: regenerate SKILL.md files after artifacts-sync rename

Mechanical regen via \`bun run gen:skill-docs --host all\`. All */SKILL.md
files reflect the renamed config key (gbrain_sync_mode →
artifacts_sync_mode), the renamed remote-helper file
(~/.gstack-artifacts-remote.txt with brain fallback), the renamed init
script (gstack-artifacts-init), and the new ARTIFACTS_SYNC: remote-mode
status line that fires when a remote-http MCP is registered.

Golden fixtures (test/fixtures/golden/*-ship-SKILL.md) refreshed to match
the regenerated default-ship output.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: v1.27.0.0 migration — gstack-brain → gstack-artifacts rename

Journaled, interruption-safe migration. Six steps, each writes to
~/.gstack/.migrations/v1.27.0.0.journal on success; re-entry resumes
from the next un-done step. On final success, journal is replaced by
~/.gstack/.migrations/v1.27.0.0.done.

Steps:
1. gh_repo_renamed       gh/glab repo rename gstack-brain-$USER →
                         gstack-artifacts-$USER (idempotent: detects
                         already-renamed and skips)
2. remote_txt_renamed    mv ~/.gstack-brain-remote.txt → artifacts file,
                         rewriting URL path to match the new repo name
3. config_key_renamed    sed -i in ~/.gstack/config.yaml flips
                         gbrain_sync_mode → artifacts_sync_mode
4. claude_md_block       sed flips "- Memory sync:" → "- Artifacts sync:"
                         in cwd CLAUDE.md and ~/.gstack/CLAUDE.md
5. sources_swapped       gbrain sources add NEW (verify) → remove OLD
                         (codex Finding #6: add-before-remove ordering,
                         no downtime window). On remote-MCP mode, prints
                         commands for the brain admin instead of executing.
6. done                  touchfile + delete journal

User opt-out: any "n" or "skip-for-now" answer at the initial prompt
writes a marker file that prevents re-prompting; user can re-invoke
via /setup-gbrain --rerun-migration.

11 unit tests cover: nothing-to-migrate, GitHub happy path, idempotent
re-run, journal-resume mid-flight, remote-MCP print-only path,
add-before-remove ordering verification, add-fail → old source stays
registered, CLAUDE.md field rewrite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: regression suite + E2E for v1.27.0.0 rename

Three new regression tests guard the rename's blast radius (per codex
Findings #1, #8, #9, #12):

- test/no-stale-gstack-brain-refs.test.ts: greps bin/, scripts/, *.tmpl,
  test/ for forbidden identifiers (gstack-brain-init, gbrain_sync_mode);
  fails CI if any non-allowlisted file references them.
- test/post-rename-doc-regen.test.ts: confirms gen-skill-docs output has
  no stale references in any */SKILL.md (the cross-product blind spot).
- test/setup-gbrain-path4-structure.test.ts: structural lint over the
  Path 4 prose contract — STOP gates after verify failure, never-write-
  token rules, mode-aware CLAUDE.md block, bearer always via env-var.

Two new gate-tier E2E tests (deterministic stub HTTP server, fixed inputs):

- test/skill-e2e-setup-gbrain-remote.test.ts: Path 4 happy path. Stubs
  an HTTP MCP server, drives the skill via Agent SDK with a stubbed
  bearer, asserts claude.json gets the http MCP entry, CLAUDE.md gets
  the remote-http block, the secret token NEVER leaks to CLAUDE.md.
- test/skill-e2e-setup-gbrain-bad-token.test.ts: stub server returns 401;
  asserts the AUTH classifier hint surfaces, no MCP registration occurs,
  CLAUDE.md is unchanged. Regression guard for the "verify failed → STOP"
  rule.

touchfiles.ts: setup-gbrain-remote and setup-gbrain-bad-token added at
gate-tier so CI catches Path 4 regressions on every PR.

Plus a few comment refs flipped: bin/gstack-jsonl-merge, bin/gstack-timeline-log
(legacy gstack-brain-init mentions in headers).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* release: v1.27.0.0 — /setup-gbrain Path 4 + brain → artifacts rename

Bumps VERSION 1.26.4.0 → 1.27.0.0 (MINOR per CLAUDE.md scale-aware bump
guidance: ~1500 line net change including a new path in /setup-gbrain,
two new bin helpers, a journaled migration, 59 new tests, and a config
key rename across the codebase).

CHANGELOG entry covers: Path 4 (Remote MCP) end-to-end, the brain →
artifacts rename, the journaled migration, the verify-helper error
classifier, the artifacts-init multi-host provider choice. Includes
the canonical Garry-voice headline + numbers table + audience close
per the release-summary format.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: demote setup-gbrain Path 4 E2E to periodic-tier

The Agent SDK E2E tests for Path 4 (skill-e2e-setup-gbrain-remote and
skill-e2e-setup-gbrain-bad-token) are inherently non-deterministic —
the model interprets "follow Path 4 only" prompts flexibly and can
skip Step 8 (CLAUDE.md write) or shortcut past the verify helper, which
makes the gate-tier assertions flaky.

The deterministic gate coverage for Path 4 is in
test/setup-gbrain-path4-structure.test.ts: a fast structural lint that
catches AUQ-pacing regressions and prose contract drift in <200ms with
zero token spend. That test is the right tool for catching the failure
mode the gate-tier was meant to guard against.

The Agent SDK E2E tests stay available on-demand for periodic-tier runs
(EVALS=1 EVALS_TIER=periodic bun test test/skill-e2e-setup-gbrain-*.test.ts).
Also tightened the verify-error assertion to the literal field shape
("error_class": "AUTH") instead of a substring match that false-matches
the parent claude session's "needs-auth" MCP discovery markers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: sync package.json version to 1.27.0.0

VERSION was bumped to 1.27.0.0 in f6ec11eb but package.json was not
updated in the same commit. The gen-skill-docs.test.ts assertion
"package.json version matches VERSION file" caught the drift.

This is the DRIFT_STALE_PKG case the /ship Step 12 idempotency check
is designed for; the fix is the documented sync-only repair (no
re-bump, package.json synced to existing VERSION).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 19:37:53 -07:00

199 lines
10 KiB
Bash
Executable File

#!/usr/bin/env bash
# gstack-config — read/write ~/.gstack/config.yaml
#
# Usage:
# gstack-config get <key> — read a config value (falls back to DEFAULTS)
# gstack-config set <key> <value> — write a config value
# gstack-config list — show all config (values + defaults)
# gstack-config defaults — show just the defaults table
#
# Env overrides (for testing):
# GSTACK_HOME — override ~/.gstack state directory (aligns with writer scripts)
# GSTACK_STATE_DIR — legacy alias for GSTACK_HOME (kept for backwards compat)
set -euo pipefail
STATE_DIR="${GSTACK_HOME:-${GSTACK_STATE_DIR:-$HOME/.gstack}}"
CONFIG_FILE="$STATE_DIR/config.yaml"
# Annotated header for new config files. Written once on first `set`.
# Default semantics: DEFAULTS table below is the canonical source. Header text
# is documentation that must stay in sync with DEFAULTS.
CONFIG_HEADER='# gstack configuration — edit freely, changes take effect on next skill run.
# Docs: https://github.com/garrytan/gstack
#
# ─── Behavior ────────────────────────────────────────────────────────
# proactive: true # Auto-invoke skills when your request matches one.
# # Set to false to only run skills you type explicitly.
#
# routing_declined: false # Set to true to skip the CLAUDE.md routing injection
# # prompt. Set back to false to be asked again.
#
# ─── Telemetry ───────────────────────────────────────────────────────
# telemetry: off # off | anonymous | community
# # off — no data sent, no local analytics (default)
# # anonymous — counter only, no device ID
# # community — usage data + stable device ID
#
# ─── Updates ─────────────────────────────────────────────────────────
# auto_upgrade: false # true = silently upgrade on session start
# update_check: true # false = suppress version check notifications
#
# ─── Skill naming ────────────────────────────────────────────────────
# skill_prefix: false # true = namespace skills as /gstack-qa, /gstack-ship
# # false = short names /qa, /ship
#
# ─── Checkpoint ──────────────────────────────────────────────────────
# checkpoint_mode: explicit # explicit | continuous
# # explicit — commit only when you run /ship or /checkpoint
# # continuous — auto-commit after each significant change
# # with WIP: prefix + [gstack-context] body
#
# checkpoint_push: false # true = push WIP commits to remote as you go
# # false = keep WIP commits local only (default)
# # Pushing can trigger CI/deploy hooks — opt in carefully.
#
# ─── Writing style (V1) ──────────────────────────────────────────────
# explain_level: default # default = jargon-glossed, outcome-framed prose
# # (V1 default — more accessible for everyone)
# # terse = V0 prose style, no glosses, no outcome-framing layer
# # (for power users who know the terms)
# # Unknown values default to "default" with a warning.
# # See docs/designs/PLAN_TUNING_V1.md for rationale.
#
# ─── Artifacts sync (renamed from gbrain_sync_mode in v1.27.0.0) ─────
# artifacts_sync_mode: off # off | artifacts-only | full
# # off — no sync (default)
# # artifacts-only — sync plans/designs/retros/learnings only
# # (skip behavioral data: question-log,
# # developer-profile, timeline)
# # full — sync everything allowlisted
# # Set by the first-run privacy stop-gate. See docs/gbrain-sync.md.
#
# artifacts_sync_mode_prompted: false
# # Set to true once the privacy gate has asked the user.
# # Flip back to false to be re-prompted.
#
# ─── Advanced ────────────────────────────────────────────────────────
# codex_reviews: enabled # disabled = skip Codex adversarial reviews in /ship
# gstack_contributor: false # true = file field reports when gstack misbehaves
# skip_eng_review: false # true = skip eng review gate in /ship (not recommended)
#
# ─── Workspace-aware ship ────────────────────────────────────────────
# workspace_root: $HOME/conductor/workspaces # Where /ship looks for sibling
# # Conductor worktrees when picking a VERSION slot.
# # Set to "null" to disable sibling scanning entirely.
# # Non-Conductor users can point this at any directory
# # that holds parallel worktrees of the same repo.
#
'
# DEFAULTS table — canonical default values for known keys.
# `get <key>` returns DEFAULTS[key] when the key is absent from the config file
# AND the env override is not set. Keep in sync with the CONFIG_HEADER comments.
lookup_default() {
case "$1" in
proactive) echo "true" ;;
routing_declined) echo "false" ;;
telemetry) echo "off" ;;
auto_upgrade) echo "false" ;;
update_check) echo "true" ;;
skill_prefix) echo "false" ;;
checkpoint_mode) echo "explicit" ;;
checkpoint_push) echo "false" ;;
codex_reviews) echo "enabled" ;;
gstack_contributor) echo "false" ;;
skip_eng_review) echo "false" ;;
workspace_root) echo "$HOME/conductor/workspaces" ;;
cross_project_learnings) echo "" ;; # intentionally empty → unset triggers first-time prompt
artifacts_sync_mode) echo "off" ;;
artifacts_sync_mode_prompted) echo "false" ;;
*) echo "" ;;
esac
}
case "${1:-}" in
get)
KEY="${2:?Usage: gstack-config get <key>}"
# Validate key (alphanumeric + underscore only)
if ! printf '%s' "$KEY" | grep -qE '^[a-zA-Z0-9_]+$'; then
echo "Error: key must contain only alphanumeric characters and underscores" >&2
exit 1
fi
VALUE=$(grep -E "^${KEY}:" "$CONFIG_FILE" 2>/dev/null | tail -1 | awk '{print $2}' | tr -d '[:space:]' || true)
if [ -z "$VALUE" ]; then
VALUE=$(lookup_default "$KEY")
fi
printf '%s' "$VALUE"
;;
set)
KEY="${2:?Usage: gstack-config set <key> <value>}"
VALUE="${3:?Usage: gstack-config set <key> <value>}"
# Validate key (alphanumeric + underscore only)
if ! printf '%s' "$KEY" | grep -qE '^[a-zA-Z0-9_]+$'; then
echo "Error: key must contain only alphanumeric characters and underscores" >&2
exit 1
fi
# V1: whitelist values for keys with closed value domains. Unknown values warn + default.
if [ "$KEY" = "explain_level" ] && [ "$VALUE" != "default" ] && [ "$VALUE" != "terse" ]; then
echo "Warning: explain_level '$VALUE' not recognized. Valid values: default, terse. Using default." >&2
VALUE="default"
fi
if [ "$KEY" = "artifacts_sync_mode" ] && [ "$VALUE" != "off" ] && [ "$VALUE" != "artifacts-only" ] && [ "$VALUE" != "full" ]; then
echo "Warning: artifacts_sync_mode '$VALUE' not recognized. Valid values: off, artifacts-only, full. Using off." >&2
VALUE="off"
fi
mkdir -p "$STATE_DIR"
# Write annotated header on first creation
if [ ! -f "$CONFIG_FILE" ]; then
printf '%s' "$CONFIG_HEADER" > "$CONFIG_FILE"
fi
# Escape sed special chars in value and drop embedded newlines
ESC_VALUE="$(printf '%s' "$VALUE" | head -1 | sed 's/[&/\]/\\&/g')"
if grep -qE "^${KEY}:" "$CONFIG_FILE" 2>/dev/null; then
# Portable in-place edit (BSD sed uses -i '', GNU sed uses -i without arg)
_tmpfile="$(mktemp "${CONFIG_FILE}.XXXXXX")"
sed "/^${KEY}:/s/.*/${KEY}: ${ESC_VALUE}/" "$CONFIG_FILE" > "$_tmpfile" && mv "$_tmpfile" "$CONFIG_FILE"
else
echo "${KEY}: ${VALUE}" >> "$CONFIG_FILE"
fi
# Auto-relink skills when prefix setting changes (skip during setup to avoid recursive call)
if [ "$KEY" = "skill_prefix" ] && [ -z "${GSTACK_SETUP_RUNNING:-}" ]; then
GSTACK_RELINK="$(dirname "$0")/gstack-relink"
[ -x "$GSTACK_RELINK" ] && "$GSTACK_RELINK" || true
fi
;;
list)
if [ -f "$CONFIG_FILE" ]; then
cat "$CONFIG_FILE"
fi
echo ""
echo "# ─── Active values (including defaults for unset keys) ───"
for KEY in proactive routing_declined telemetry auto_upgrade update_check \
skill_prefix checkpoint_mode checkpoint_push codex_reviews \
gstack_contributor skip_eng_review workspace_root \
artifacts_sync_mode artifacts_sync_mode_prompted; do
VALUE=$(grep -E "^${KEY}:" "$CONFIG_FILE" 2>/dev/null | tail -1 | awk '{print $2}' | tr -d '[:space:]' || true)
SOURCE="default"
if [ -n "$VALUE" ]; then
SOURCE="set"
else
VALUE=$(lookup_default "$KEY")
fi
printf ' %-24s %s (%s)\n' "$KEY:" "$VALUE" "$SOURCE"
done
;;
defaults)
echo "# gstack-config defaults"
for KEY in proactive routing_declined telemetry auto_upgrade update_check \
skill_prefix checkpoint_mode checkpoint_push codex_reviews \
gstack_contributor skip_eng_review workspace_root \
artifacts_sync_mode artifacts_sync_mode_prompted; do
printf ' %-24s %s\n' "$KEY:" "$(lookup_default "$KEY")"
done
;;
*)
echo "Usage: gstack-config {get|set|list|defaults} [key] [value]"
exit 1
;;
esac