Files
gstack/bin/gstack-config
Garry Tan e4041f7a7f v1.11.0.0 feat(ship): workspace-aware version allocation (#1168)
* feat: bin/gstack-next-version util + workspace_root config key

Host-aware (GitHub + GitLab + unknown) VERSION allocator. Queries the open
PR queue, fetches each PR's VERSION at head, scans configurable Conductor
sibling worktrees for WIP work, and picks the next free slot at the
requested bump level.

Pure reader, never writes files. /ship consumes the JSON and decides.

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

* test: fixture tests for gstack-next-version

21 pure-function tests covering parseVersion / bumpVersion / cmpVersion /
pickNextSlot (with 8 collision scenarios) / markActiveSiblings (4 cases)
plus one CLI smoke test against the live repo.

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

* feat(scripts): detect-bump + compare-pr-version helpers

Shared between /ship (legacy path) and the CI version-gate job.
detect-bump: derive bump level from VERSION diff. compare-pr-version:
CI gate logic with three exit paths (pass / block / fail-open).

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

* feat(ci): version-gate + pr-title-sync workflows (GitHub + GitLab)

Merge-time collision gate. Fail-open on util errors (network, auth, bug),
fail-closed on confirmed collisions. pr-title-sync rewrites the PR title
when VERSION changes on push, only for titles that already carry the
v<X.Y.Z.W> prefix (custom titles left alone).

GitLab CI mirrors both jobs for host parity.

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

* feat(skills): queue-aware /ship + drift abort in /land-and-deploy + advisory in /review

ship Step 12: queue-aware version pick (FRESH path) + drift detection
(ALREADY_BUMPED path). Prompts user to rebump when queue moved, runs
the full ship metadata path (VERSION, package.json, CHANGELOG header,
PR title) on the rebump so nothing goes stale.

ship Step 19: PR title format v<X.Y.Z.W> <type>: <summary> — version
ALWAYS first. Rerun path updates title (not just body) when VERSION
changed.

land-and-deploy Step 3.4: detect drift, ABORT with instruction to
rerun /ship. Never auto-mutates from land.

review Step 3.4: advisory one-line queue status. Non-blocking.

Goldens refreshed for all three hosts (claude/codex/factory).

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

* feat(skill): /landing-report read-only queue dashboard

Standalone skill that renders the current PR queue, sibling worktrees,
and what all four bump levels would claim. Pure reader. Useful when
running many parallel Conductor workspaces to see what's in flight
before shipping anything.

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

* docs: versioning invariant in CLAUDE.md

Document that VERSION is a monotonic sequence, not a strict semver
commitment. Bump level expresses intent; queue-advance within a level
is permitted. Prevents future re-litigation of the workspace-aware
ship design.

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

* chore: bump version and changelog (v1.8.0.0)

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

* fix(ship): exclude current PR from queue-awareness (self-reference bug)

Version gate flagged PR #1168 as stale because the util counted the PR
itself as a queued claim. The exclude filter removes that self-reference.

New --exclude-pr <N> flag on bin/gstack-next-version. CI workflows pass
github.event.pull_request.number / CI_MERGE_REQUEST_IID. Local /ship
auto-detects via gh pr view when the flag isn't passed, with a warning
recording the auto-exclusion so it's observable.

Caught during the first live ship through the v1.8.0.0 gate — the kind
of dogfood the whole release is designed for.

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

* Merge remote-tracking branch 'origin/main' into garrytan/workspace-aware-ship

Rebumped v1.8.0.0 -> v1.11.0.0 (minor-past main's v1.10.1.0) using
bin/gstack-next-version — the same queue-aware path this branch introduces.
CHANGELOG repositioned so v1.11.0.0 sits above main's new entries
(v1.10.1.0 / v1.10.0.0 / v1.9.0.0).

Conflicts resolved:
- VERSION, package.json: rebumped to v1.11.0.0 (util-picked)
- bin/gstack-config: merged both lists (workspace_root + gbrain keys)
- CHANGELOG.md: hoisted v1.11.0.0 entry above main's new entries

Pre-existing failures in main (4) documented but not fixed in this PR:
1. gstack-brain-sync secret scan > blocks bearer-json (brain-sync tests)
2. no files larger than 2MB (security-bench fixture, already TODO'd)
3. selectTests > skill-specific change (touchfiles scoping)
4. Opus 4.7 overlay pacing directive (expectation stale after v1.10.1.0
   removed the Fan out nudge)

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

* ci: re-trigger PR workflows after merge

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 23:03:27 -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.
#
# ─── GBrain sync (v1.7+) ─────────────────────────────────────────────
# gbrain_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.
#
# gbrain_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
gbrain_sync_mode) echo "off" ;;
gbrain_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" = "gbrain_sync_mode" ] && [ "$VALUE" != "off" ] && [ "$VALUE" != "artifacts-only" ] && [ "$VALUE" != "full" ]; then
echo "Warning: gbrain_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 \
gbrain_sync_mode gbrain_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 \
gbrain_sync_mode gbrain_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