mirror of
https://github.com/garrytan/gstack.git
synced 2026-07-09 17:38:40 +02:00
feat(paths): bin/gstack-paths helper + migrate 8 skills off inline state-root chains
New bin/gstack-paths emits GSTACK_STATE_ROOT, PLAN_ROOT, TMP_ROOT exports for
skill bash blocks to source via eval. Honors GSTACK_HOME → CLAUDE_PLUGIN_DATA →
$HOME/.gstack → .gstack (and parallel chains for plan/tmp roots) so skills work
the same in plugin installs, global installs, and CI containers without HOME.
Eight skills migrate off inline ${CLAUDE_PLUGIN_DATA:-...} or ${GSTACK_HOME:-...}
chains: careful, freeze, guard, unfreeze, investigate, context-save,
context-restore, learn, office-hours, plan-tune, codex. Resolved values are
identical, so existing tests cover correctness; the win is consolidating 11
copy-pasted fallback chains behind one helper.
codex/SKILL.md.tmpl gets a new Step 0.6 Resolve portable roots that sources
gstack-paths once, then replaces hardcoded ~/.claude/plans/*.md and
/tmp/codex-*-XXXXXX.txt with "$PLAN_ROOT"/*.md and "$TMP_ROOT/codex-*-XXXXXX.txt".
Hardening direction credited to the McGluut/gstack fork; this is upstream's
factoring of the per-skill chain the fork inlined.
Tests: test/gstack-paths.test.ts covers all three fallback chains with 8 unit
tests (HOME unset, CLAUDE_PLUGIN_DATA set, GSTACK_HOME wins, etc).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Executable
+61
@@ -0,0 +1,61 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# gstack-paths — output portable state-root paths for skill bash blocks
|
||||||
|
# Usage: eval "$(gstack-paths)" → sets GSTACK_STATE_ROOT, PLAN_ROOT, TMP_ROOT
|
||||||
|
# Or: gstack-paths → prints GSTACK_STATE_ROOT=... etc.
|
||||||
|
#
|
||||||
|
# Resolves three roots with explicit fallback chains so skills work the same
|
||||||
|
# whether installed as a Claude Code plugin (CLAUDE_PLUGIN_DATA / CLAUDE_PLANS_DIR
|
||||||
|
# set), a global ~/.claude/skills/gstack/ install, or a local checkout under
|
||||||
|
# CI / container env where HOME may be unset.
|
||||||
|
#
|
||||||
|
# Chains:
|
||||||
|
# GSTACK_STATE_ROOT: GSTACK_HOME -> CLAUDE_PLUGIN_DATA -> $HOME/.gstack -> .gstack
|
||||||
|
# PLAN_ROOT: GSTACK_PLAN_DIR -> CLAUDE_PLANS_DIR -> $HOME/.claude/plans -> .claude/plans
|
||||||
|
# TMP_ROOT: TMPDIR -> TMP -> .gstack/tmp (and mkdir -p, best-effort)
|
||||||
|
#
|
||||||
|
# Security: output values are not sanitized — callers may receive paths with
|
||||||
|
# shell-special characters if env vars contain them. Skills should always quote
|
||||||
|
# expansions ("$GSTACK_STATE_ROOT", not $GSTACK_STATE_ROOT).
|
||||||
|
set -u
|
||||||
|
|
||||||
|
# State root: where gstack writes projects/, sessions/, analytics/.
|
||||||
|
if [ -n "${GSTACK_HOME:-}" ]; then
|
||||||
|
_state_root="$GSTACK_HOME"
|
||||||
|
elif [ -n "${CLAUDE_PLUGIN_DATA:-}" ]; then
|
||||||
|
_state_root="$CLAUDE_PLUGIN_DATA"
|
||||||
|
elif [ -n "${HOME:-}" ]; then
|
||||||
|
_state_root="$HOME/.gstack"
|
||||||
|
else
|
||||||
|
_state_root=".gstack"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Plan root: where /context-save and /codex consult write plan files.
|
||||||
|
if [ -n "${GSTACK_PLAN_DIR:-}" ]; then
|
||||||
|
_plan_root="$GSTACK_PLAN_DIR"
|
||||||
|
elif [ -n "${CLAUDE_PLANS_DIR:-}" ]; then
|
||||||
|
_plan_root="$CLAUDE_PLANS_DIR"
|
||||||
|
elif [ -n "${HOME:-}" ]; then
|
||||||
|
_plan_root="$HOME/.claude/plans"
|
||||||
|
else
|
||||||
|
_plan_root=".claude/plans"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Tmp root: where ephemeral files (codex stderr captures, etc.) live.
|
||||||
|
# Honor TMPDIR / TMP for Windows + container compat; fall back to a
|
||||||
|
# project-local .gstack/tmp so we never write to a system /tmp that may
|
||||||
|
# be read-only or shared.
|
||||||
|
if [ -n "${TMPDIR:-}" ]; then
|
||||||
|
_tmp_root="$TMPDIR"
|
||||||
|
elif [ -n "${TMP:-}" ]; then
|
||||||
|
_tmp_root="$TMP"
|
||||||
|
else
|
||||||
|
_tmp_root=".gstack/tmp"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Best-effort mkdir; if it fails (read-only fs, permission denied), the caller
|
||||||
|
# will discover that on their own write attempt. Don't fail the eval here.
|
||||||
|
mkdir -p "$_tmp_root" 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "GSTACK_STATE_ROOT=$_state_root"
|
||||||
|
echo "PLAN_ROOT=$_plan_root"
|
||||||
|
echo "TMP_ROOT=$_tmp_root"
|
||||||
+25
-8
@@ -781,6 +781,23 @@ deadlock fixed in #972.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Step 0.6: Resolve portable roots
|
||||||
|
|
||||||
|
Before any mode runs, resolve `$PLAN_ROOT` (where plan files live) and `$TMP_ROOT`
|
||||||
|
(where ephemeral codex stderr / response captures land) via `bin/gstack-paths`.
|
||||||
|
This keeps the skill working whether installed as a Claude Code plugin
|
||||||
|
(`CLAUDE_PLANS_DIR` set), a global `~/.claude/skills/gstack/` install, or a CI
|
||||||
|
container where `HOME` may be unset and `/tmp` may be read-only.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
|
||||||
|
```
|
||||||
|
|
||||||
|
After this, every subsequent bash block in this skill uses `"$PLAN_ROOT"` and
|
||||||
|
`"$TMP_ROOT"` rather than hardcoded `~/.claude/plans` or `/tmp/codex-*`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Step 1: Detect mode
|
## Step 1: Detect mode
|
||||||
|
|
||||||
Parse the user's input to determine which mode to run:
|
Parse the user's input to determine which mode to run:
|
||||||
@@ -798,8 +815,8 @@ Parse the user's input to determine which mode to run:
|
|||||||
C) Something else — I'll provide a prompt
|
C) Something else — I'll provide a prompt
|
||||||
```
|
```
|
||||||
- If no diff, check for plan files scoped to the current project:
|
- If no diff, check for plan files scoped to the current project:
|
||||||
`ls -t ~/.claude/plans/*.md 2>/dev/null | xargs grep -l "$(basename $(pwd))" 2>/dev/null | head -1`
|
`ls -t "$PLAN_ROOT"/*.md 2>/dev/null | xargs grep -l "$(basename $(pwd))" 2>/dev/null | head -1`
|
||||||
If no project-scoped match, fall back to: `ls -t ~/.claude/plans/*.md 2>/dev/null | head -1`
|
If no project-scoped match, fall back to: `ls -t "$PLAN_ROOT"/*.md 2>/dev/null | head -1`
|
||||||
but warn the user: "Note: this plan may be from a different project."
|
but warn the user: "Note: this plan may be from a different project."
|
||||||
- If a plan file exists, offer to review it
|
- If a plan file exists, offer to review it
|
||||||
- Otherwise, ask: "What would you like to ask Codex?"
|
- Otherwise, ask: "What would you like to ask Codex?"
|
||||||
@@ -832,7 +849,7 @@ Run Codex code review against the current branch diff.
|
|||||||
|
|
||||||
1. Create temp files for output capture:
|
1. Create temp files for output capture:
|
||||||
```bash
|
```bash
|
||||||
TMPERR=$(mktemp /tmp/codex-err-XXXXXX.txt)
|
TMPERR=$(mktemp "$TMP_ROOT/codex-err-XXXXXX.txt")
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Run the review (5-minute timeout). **Always** pass the filesystem boundary instruction
|
2. Run the review (5-minute timeout). **Always** pass the filesystem boundary instruction
|
||||||
@@ -1015,7 +1032,7 @@ If the user passed `--xhigh`, use `"xhigh"` instead of `"high"`.
|
|||||||
_REPO_ROOT=$(git rev-parse --show-toplevel) || { echo "ERROR: not in a git repo" >&2; exit 1; }
|
_REPO_ROOT=$(git rev-parse --show-toplevel) || { echo "ERROR: not in a git repo" >&2; exit 1; }
|
||||||
# Fix 1+2: wrap with timeout (gtimeout/timeout fallback chain via probe helper),
|
# Fix 1+2: wrap with timeout (gtimeout/timeout fallback chain via probe helper),
|
||||||
# capture stderr to $TMPERR for auth error detection (was: 2>/dev/null).
|
# capture stderr to $TMPERR for auth error detection (was: 2>/dev/null).
|
||||||
TMPERR=${TMPERR:-$(mktemp /tmp/codex-err-XXXXXX.txt)}
|
TMPERR=${TMPERR:-$(mktemp "$TMP_ROOT/codex-err-XXXXXX.txt")}
|
||||||
_gstack_codex_timeout_wrapper 600 codex exec "<prompt>" -C "$_REPO_ROOT" -s read-only -c 'model_reasoning_effort="high"' --enable web_search_cached --json < /dev/null 2>"$TMPERR" | PYTHONUNBUFFERED=1 python3 -u -c "
|
_gstack_codex_timeout_wrapper 600 codex exec "<prompt>" -C "$_REPO_ROOT" -s read-only -c 'model_reasoning_effort="high"' --enable web_search_cached --json < /dev/null 2>"$TMPERR" | PYTHONUNBUFFERED=1 python3 -u -c "
|
||||||
import sys, json
|
import sys, json
|
||||||
turn_completed_count = 0
|
turn_completed_count = 0
|
||||||
@@ -1094,17 +1111,17 @@ B) Start a new conversation
|
|||||||
|
|
||||||
2. Create temp files:
|
2. Create temp files:
|
||||||
```bash
|
```bash
|
||||||
TMPRESP=$(mktemp /tmp/codex-resp-XXXXXX.txt)
|
TMPRESP=$(mktemp "$TMP_ROOT/codex-resp-XXXXXX.txt")
|
||||||
TMPERR=$(mktemp /tmp/codex-err-XXXXXX.txt)
|
TMPERR=$(mktemp "$TMP_ROOT/codex-err-XXXXXX.txt")
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Plan review auto-detection:** If the user's prompt is about reviewing a plan,
|
3. **Plan review auto-detection:** If the user's prompt is about reviewing a plan,
|
||||||
or if plan files exist and the user said `/codex` with no arguments:
|
or if plan files exist and the user said `/codex` with no arguments:
|
||||||
```bash
|
```bash
|
||||||
setopt +o nomatch 2>/dev/null || true # zsh compat
|
setopt +o nomatch 2>/dev/null || true # zsh compat
|
||||||
ls -t ~/.claude/plans/*.md 2>/dev/null | xargs grep -l "$(basename $(pwd))" 2>/dev/null | head -1
|
ls -t "$PLAN_ROOT"/*.md 2>/dev/null | xargs grep -l "$(basename $(pwd))" 2>/dev/null | head -1
|
||||||
```
|
```
|
||||||
If no project-scoped match, fall back to `ls -t ~/.claude/plans/*.md 2>/dev/null | head -1`
|
If no project-scoped match, fall back to `ls -t "$PLAN_ROOT"/*.md 2>/dev/null | head -1`
|
||||||
but warn: "Note: this plan may be from a different project — verify before sending to Codex."
|
but warn: "Note: this plan may be from a different project — verify before sending to Codex."
|
||||||
|
|
||||||
**IMPORTANT — embed content, don't reference path:** Codex runs sandboxed to the repo
|
**IMPORTANT — embed content, don't reference path:** Codex runs sandboxed to the repo
|
||||||
|
|||||||
+25
-8
@@ -90,6 +90,23 @@ deadlock fixed in #972.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Step 0.6: Resolve portable roots
|
||||||
|
|
||||||
|
Before any mode runs, resolve `$PLAN_ROOT` (where plan files live) and `$TMP_ROOT`
|
||||||
|
(where ephemeral codex stderr / response captures land) via `bin/gstack-paths`.
|
||||||
|
This keeps the skill working whether installed as a Claude Code plugin
|
||||||
|
(`CLAUDE_PLANS_DIR` set), a global `~/.claude/skills/gstack/` install, or a CI
|
||||||
|
container where `HOME` may be unset and `/tmp` may be read-only.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
|
||||||
|
```
|
||||||
|
|
||||||
|
After this, every subsequent bash block in this skill uses `"$PLAN_ROOT"` and
|
||||||
|
`"$TMP_ROOT"` rather than hardcoded `~/.claude/plans` or `/tmp/codex-*`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Step 1: Detect mode
|
## Step 1: Detect mode
|
||||||
|
|
||||||
Parse the user's input to determine which mode to run:
|
Parse the user's input to determine which mode to run:
|
||||||
@@ -107,8 +124,8 @@ Parse the user's input to determine which mode to run:
|
|||||||
C) Something else — I'll provide a prompt
|
C) Something else — I'll provide a prompt
|
||||||
```
|
```
|
||||||
- If no diff, check for plan files scoped to the current project:
|
- If no diff, check for plan files scoped to the current project:
|
||||||
`ls -t ~/.claude/plans/*.md 2>/dev/null | xargs grep -l "$(basename $(pwd))" 2>/dev/null | head -1`
|
`ls -t "$PLAN_ROOT"/*.md 2>/dev/null | xargs grep -l "$(basename $(pwd))" 2>/dev/null | head -1`
|
||||||
If no project-scoped match, fall back to: `ls -t ~/.claude/plans/*.md 2>/dev/null | head -1`
|
If no project-scoped match, fall back to: `ls -t "$PLAN_ROOT"/*.md 2>/dev/null | head -1`
|
||||||
but warn the user: "Note: this plan may be from a different project."
|
but warn the user: "Note: this plan may be from a different project."
|
||||||
- If a plan file exists, offer to review it
|
- If a plan file exists, offer to review it
|
||||||
- Otherwise, ask: "What would you like to ask Codex?"
|
- Otherwise, ask: "What would you like to ask Codex?"
|
||||||
@@ -141,7 +158,7 @@ Run Codex code review against the current branch diff.
|
|||||||
|
|
||||||
1. Create temp files for output capture:
|
1. Create temp files for output capture:
|
||||||
```bash
|
```bash
|
||||||
TMPERR=$(mktemp /tmp/codex-err-XXXXXX.txt)
|
TMPERR=$(mktemp "$TMP_ROOT/codex-err-XXXXXX.txt")
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Run the review (5-minute timeout). **Always** pass the filesystem boundary instruction
|
2. Run the review (5-minute timeout). **Always** pass the filesystem boundary instruction
|
||||||
@@ -254,7 +271,7 @@ If the user passed `--xhigh`, use `"xhigh"` instead of `"high"`.
|
|||||||
_REPO_ROOT=$(git rev-parse --show-toplevel) || { echo "ERROR: not in a git repo" >&2; exit 1; }
|
_REPO_ROOT=$(git rev-parse --show-toplevel) || { echo "ERROR: not in a git repo" >&2; exit 1; }
|
||||||
# Fix 1+2: wrap with timeout (gtimeout/timeout fallback chain via probe helper),
|
# Fix 1+2: wrap with timeout (gtimeout/timeout fallback chain via probe helper),
|
||||||
# capture stderr to $TMPERR for auth error detection (was: 2>/dev/null).
|
# capture stderr to $TMPERR for auth error detection (was: 2>/dev/null).
|
||||||
TMPERR=${TMPERR:-$(mktemp /tmp/codex-err-XXXXXX.txt)}
|
TMPERR=${TMPERR:-$(mktemp "$TMP_ROOT/codex-err-XXXXXX.txt")}
|
||||||
_gstack_codex_timeout_wrapper 600 codex exec "<prompt>" -C "$_REPO_ROOT" -s read-only -c 'model_reasoning_effort="high"' --enable web_search_cached --json < /dev/null 2>"$TMPERR" | PYTHONUNBUFFERED=1 python3 -u -c "
|
_gstack_codex_timeout_wrapper 600 codex exec "<prompt>" -C "$_REPO_ROOT" -s read-only -c 'model_reasoning_effort="high"' --enable web_search_cached --json < /dev/null 2>"$TMPERR" | PYTHONUNBUFFERED=1 python3 -u -c "
|
||||||
import sys, json
|
import sys, json
|
||||||
turn_completed_count = 0
|
turn_completed_count = 0
|
||||||
@@ -333,17 +350,17 @@ B) Start a new conversation
|
|||||||
|
|
||||||
2. Create temp files:
|
2. Create temp files:
|
||||||
```bash
|
```bash
|
||||||
TMPRESP=$(mktemp /tmp/codex-resp-XXXXXX.txt)
|
TMPRESP=$(mktemp "$TMP_ROOT/codex-resp-XXXXXX.txt")
|
||||||
TMPERR=$(mktemp /tmp/codex-err-XXXXXX.txt)
|
TMPERR=$(mktemp "$TMP_ROOT/codex-err-XXXXXX.txt")
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Plan review auto-detection:** If the user's prompt is about reviewing a plan,
|
3. **Plan review auto-detection:** If the user's prompt is about reviewing a plan,
|
||||||
or if plan files exist and the user said `/codex` with no arguments:
|
or if plan files exist and the user said `/codex` with no arguments:
|
||||||
```bash
|
```bash
|
||||||
setopt +o nomatch 2>/dev/null || true # zsh compat
|
setopt +o nomatch 2>/dev/null || true # zsh compat
|
||||||
ls -t ~/.claude/plans/*.md 2>/dev/null | xargs grep -l "$(basename $(pwd))" 2>/dev/null | head -1
|
ls -t "$PLAN_ROOT"/*.md 2>/dev/null | xargs grep -l "$(basename $(pwd))" 2>/dev/null | head -1
|
||||||
```
|
```
|
||||||
If no project-scoped match, fall back to `ls -t ~/.claude/plans/*.md 2>/dev/null | head -1`
|
If no project-scoped match, fall back to `ls -t "$PLAN_ROOT"/*.md 2>/dev/null | head -1`
|
||||||
but warn: "Note: this plan may be from a different project — verify before sending to Codex."
|
but warn: "Note: this plan may be from a different project — verify before sending to Codex."
|
||||||
|
|
||||||
**IMPORTANT — embed content, don't reference path:** Codex runs sandboxed to the repo
|
**IMPORTANT — embed content, don't reference path:** Codex runs sandboxed to the repo
|
||||||
|
|||||||
@@ -701,7 +701,8 @@ Parse the user's input:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p ~/.gstack/projects/$SLUG
|
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p ~/.gstack/projects/$SLUG
|
||||||
CHECKPOINT_DIR="${GSTACK_HOME:-$HOME/.gstack}/projects/$SLUG/checkpoints"
|
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
|
||||||
|
CHECKPOINT_DIR="$GSTACK_STATE_ROOT/projects/$SLUG/checkpoints"
|
||||||
if [ ! -d "$CHECKPOINT_DIR" ]; then
|
if [ ! -d "$CHECKPOINT_DIR" ]; then
|
||||||
echo "NO_CHECKPOINTS"
|
echo "NO_CHECKPOINTS"
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -62,7 +62,8 @@ Parse the user's input:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
{{SLUG_SETUP}}
|
{{SLUG_SETUP}}
|
||||||
CHECKPOINT_DIR="${GSTACK_HOME:-$HOME/.gstack}/projects/$SLUG/checkpoints"
|
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
|
||||||
|
CHECKPOINT_DIR="$GSTACK_STATE_ROOT/projects/$SLUG/checkpoints"
|
||||||
if [ ! -d "$CHECKPOINT_DIR" ]; then
|
if [ ! -d "$CHECKPOINT_DIR" ]; then
|
||||||
echo "NO_CHECKPOINTS"
|
echo "NO_CHECKPOINTS"
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -757,7 +757,8 @@ allowlist: only `a-z 0-9 - .` survive.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p ~/.gstack/projects/$SLUG
|
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p ~/.gstack/projects/$SLUG
|
||||||
CHECKPOINT_DIR="${GSTACK_HOME:-$HOME/.gstack}/projects/$SLUG/checkpoints"
|
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
|
||||||
|
CHECKPOINT_DIR="$GSTACK_STATE_ROOT/projects/$SLUG/checkpoints"
|
||||||
mkdir -p "$CHECKPOINT_DIR"
|
mkdir -p "$CHECKPOINT_DIR"
|
||||||
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
||||||
# Bash-side title sanitize. Pass the raw title as $1 when running this block.
|
# Bash-side title sanitize. Pass the raw title as $1 when running this block.
|
||||||
@@ -843,7 +844,8 @@ Restore later with /context-restore.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p ~/.gstack/projects/$SLUG
|
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p ~/.gstack/projects/$SLUG
|
||||||
CHECKPOINT_DIR="${GSTACK_HOME:-$HOME/.gstack}/projects/$SLUG/checkpoints"
|
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
|
||||||
|
CHECKPOINT_DIR="$GSTACK_STATE_ROOT/projects/$SLUG/checkpoints"
|
||||||
if [ -d "$CHECKPOINT_DIR" ]; then
|
if [ -d "$CHECKPOINT_DIR" ]; then
|
||||||
echo "CHECKPOINT_DIR=$CHECKPOINT_DIR"
|
echo "CHECKPOINT_DIR=$CHECKPOINT_DIR"
|
||||||
# Use find + sort instead of ls -1t: filename YYYYMMDD-HHMMSS prefix is the
|
# Use find + sort instead of ls -1t: filename YYYYMMDD-HHMMSS prefix is the
|
||||||
|
|||||||
@@ -118,7 +118,8 @@ allowlist: only `a-z 0-9 - .` survive.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
{{SLUG_SETUP}}
|
{{SLUG_SETUP}}
|
||||||
CHECKPOINT_DIR="${GSTACK_HOME:-$HOME/.gstack}/projects/$SLUG/checkpoints"
|
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
|
||||||
|
CHECKPOINT_DIR="$GSTACK_STATE_ROOT/projects/$SLUG/checkpoints"
|
||||||
mkdir -p "$CHECKPOINT_DIR"
|
mkdir -p "$CHECKPOINT_DIR"
|
||||||
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
||||||
# Bash-side title sanitize. Pass the raw title as $1 when running this block.
|
# Bash-side title sanitize. Pass the raw title as $1 when running this block.
|
||||||
@@ -204,7 +205,8 @@ Restore later with /context-restore.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
{{SLUG_SETUP}}
|
{{SLUG_SETUP}}
|
||||||
CHECKPOINT_DIR="${GSTACK_HOME:-$HOME/.gstack}/projects/$SLUG/checkpoints"
|
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
|
||||||
|
CHECKPOINT_DIR="$GSTACK_STATE_ROOT/projects/$SLUG/checkpoints"
|
||||||
if [ -d "$CHECKPOINT_DIR" ]; then
|
if [ -d "$CHECKPOINT_DIR" ]; then
|
||||||
echo "CHECKPOINT_DIR=$CHECKPOINT_DIR"
|
echo "CHECKPOINT_DIR=$CHECKPOINT_DIR"
|
||||||
# Use find + sort instead of ls -1t: filename YYYYMMDD-HHMMSS prefix is the
|
# Use find + sort instead of ls -1t: filename YYYYMMDD-HHMMSS prefix is the
|
||||||
|
|||||||
+2
-1
@@ -59,7 +59,8 @@ echo "$FREEZE_DIR"
|
|||||||
2. Ensure trailing slash and save to the freeze state file:
|
2. Ensure trailing slash and save to the freeze state file:
|
||||||
```bash
|
```bash
|
||||||
FREEZE_DIR="${FREEZE_DIR%/}/"
|
FREEZE_DIR="${FREEZE_DIR%/}/"
|
||||||
STATE_DIR="${CLAUDE_PLUGIN_DATA:-$HOME/.gstack}"
|
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
|
||||||
|
STATE_DIR="$GSTACK_STATE_ROOT"
|
||||||
mkdir -p "$STATE_DIR"
|
mkdir -p "$STATE_DIR"
|
||||||
echo "$FREEZE_DIR" > "$STATE_DIR/freeze-dir.txt"
|
echo "$FREEZE_DIR" > "$STATE_DIR/freeze-dir.txt"
|
||||||
echo "Freeze boundary set: $FREEZE_DIR"
|
echo "Freeze boundary set: $FREEZE_DIR"
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ echo "$FREEZE_DIR"
|
|||||||
2. Ensure trailing slash and save to the freeze state file:
|
2. Ensure trailing slash and save to the freeze state file:
|
||||||
```bash
|
```bash
|
||||||
FREEZE_DIR="${FREEZE_DIR%/}/"
|
FREEZE_DIR="${FREEZE_DIR%/}/"
|
||||||
STATE_DIR="${CLAUDE_PLUGIN_DATA:-$HOME/.gstack}"
|
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
|
||||||
|
STATE_DIR="$GSTACK_STATE_ROOT"
|
||||||
mkdir -p "$STATE_DIR"
|
mkdir -p "$STATE_DIR"
|
||||||
echo "$FREEZE_DIR" > "$STATE_DIR/freeze-dir.txt"
|
echo "$FREEZE_DIR" > "$STATE_DIR/freeze-dir.txt"
|
||||||
echo "Freeze boundary set: $FREEZE_DIR"
|
echo "Freeze boundary set: $FREEZE_DIR"
|
||||||
|
|||||||
+2
-1
@@ -68,7 +68,8 @@ echo "$FREEZE_DIR"
|
|||||||
2. Ensure trailing slash and save to the freeze state file:
|
2. Ensure trailing slash and save to the freeze state file:
|
||||||
```bash
|
```bash
|
||||||
FREEZE_DIR="${FREEZE_DIR%/}/"
|
FREEZE_DIR="${FREEZE_DIR%/}/"
|
||||||
STATE_DIR="${CLAUDE_PLUGIN_DATA:-$HOME/.gstack}"
|
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
|
||||||
|
STATE_DIR="$GSTACK_STATE_ROOT"
|
||||||
mkdir -p "$STATE_DIR"
|
mkdir -p "$STATE_DIR"
|
||||||
echo "$FREEZE_DIR" > "$STATE_DIR/freeze-dir.txt"
|
echo "$FREEZE_DIR" > "$STATE_DIR/freeze-dir.txt"
|
||||||
echo "Freeze boundary set: $FREEZE_DIR"
|
echo "Freeze boundary set: $FREEZE_DIR"
|
||||||
|
|||||||
+2
-1
@@ -67,7 +67,8 @@ echo "$FREEZE_DIR"
|
|||||||
2. Ensure trailing slash and save to the freeze state file:
|
2. Ensure trailing slash and save to the freeze state file:
|
||||||
```bash
|
```bash
|
||||||
FREEZE_DIR="${FREEZE_DIR%/}/"
|
FREEZE_DIR="${FREEZE_DIR%/}/"
|
||||||
STATE_DIR="${CLAUDE_PLUGIN_DATA:-$HOME/.gstack}"
|
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
|
||||||
|
STATE_DIR="$GSTACK_STATE_ROOT"
|
||||||
mkdir -p "$STATE_DIR"
|
mkdir -p "$STATE_DIR"
|
||||||
echo "$FREEZE_DIR" > "$STATE_DIR/freeze-dir.txt"
|
echo "$FREEZE_DIR" > "$STATE_DIR/freeze-dir.txt"
|
||||||
echo "Freeze boundary set: $FREEZE_DIR"
|
echo "Freeze boundary set: $FREEZE_DIR"
|
||||||
|
|||||||
@@ -763,7 +763,8 @@ After forming your root cause hypothesis, lock edits to the affected module to p
|
|||||||
**If FREEZE_AVAILABLE:** Identify the narrowest directory containing the affected files. Write it to the freeze state file:
|
**If FREEZE_AVAILABLE:** Identify the narrowest directory containing the affected files. Write it to the freeze state file:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
STATE_DIR="${CLAUDE_PLUGIN_DATA:-$HOME/.gstack}"
|
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
|
||||||
|
STATE_DIR="$GSTACK_STATE_ROOT"
|
||||||
mkdir -p "$STATE_DIR"
|
mkdir -p "$STATE_DIR"
|
||||||
echo "<detected-directory>/" > "$STATE_DIR/freeze-dir.txt"
|
echo "<detected-directory>/" > "$STATE_DIR/freeze-dir.txt"
|
||||||
echo "Debug scope locked to: <detected-directory>/"
|
echo "Debug scope locked to: <detected-directory>/"
|
||||||
|
|||||||
@@ -88,7 +88,8 @@ After forming your root cause hypothesis, lock edits to the affected module to p
|
|||||||
**If FREEZE_AVAILABLE:** Identify the narrowest directory containing the affected files. Write it to the freeze state file:
|
**If FREEZE_AVAILABLE:** Identify the narrowest directory containing the affected files. Write it to the freeze state file:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
STATE_DIR="${CLAUDE_PLUGIN_DATA:-$HOME/.gstack}"
|
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
|
||||||
|
STATE_DIR="$GSTACK_STATE_ROOT"
|
||||||
mkdir -p "$STATE_DIR"
|
mkdir -p "$STATE_DIR"
|
||||||
echo "<detected-directory>/" > "$STATE_DIR/freeze-dir.txt"
|
echo "<detected-directory>/" > "$STATE_DIR/freeze-dir.txt"
|
||||||
echo "Debug scope locked to: <detected-directory>/"
|
echo "Debug scope locked to: <detected-directory>/"
|
||||||
|
|||||||
+2
-2
@@ -780,8 +780,8 @@ Show summary statistics about the project's learnings.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)"
|
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)"
|
||||||
GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
|
||||||
LEARN_FILE="$GSTACK_HOME/projects/$SLUG/learnings.jsonl"
|
LEARN_FILE="$GSTACK_STATE_ROOT/projects/$SLUG/learnings.jsonl"
|
||||||
if [ -f "$LEARN_FILE" ]; then
|
if [ -f "$LEARN_FILE" ]; then
|
||||||
TOTAL=$(wc -l < "$LEARN_FILE" | tr -d ' ')
|
TOTAL=$(wc -l < "$LEARN_FILE" | tr -d ' ')
|
||||||
echo "TOTAL: $TOTAL entries"
|
echo "TOTAL: $TOTAL entries"
|
||||||
|
|||||||
+2
-2
@@ -141,8 +141,8 @@ Show summary statistics about the project's learnings.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)"
|
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)"
|
||||||
GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
|
||||||
LEARN_FILE="$GSTACK_HOME/projects/$SLUG/learnings.jsonl"
|
LEARN_FILE="$GSTACK_STATE_ROOT/projects/$SLUG/learnings.jsonl"
|
||||||
if [ -f "$LEARN_FILE" ]; then
|
if [ -f "$LEARN_FILE" ]; then
|
||||||
TOTAL=$(wc -l < "$LEARN_FILE" | tr -d ' ')
|
TOTAL=$(wc -l < "$LEARN_FILE" | tr -d ' ')
|
||||||
echo "TOTAL: $TOTAL entries"
|
echo "TOTAL: $TOTAL entries"
|
||||||
|
|||||||
@@ -1430,7 +1430,8 @@ After counting signals, append a session entry to the builder profile. This is t
|
|||||||
source of truth for all closing state (tier, resource dedup, journey tracking).
|
source of truth for all closing state (tier, resource dedup, journey tracking).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir -p "${GSTACK_HOME:-$HOME/.gstack}"
|
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
|
||||||
|
mkdir -p "$GSTACK_STATE_ROOT"
|
||||||
```
|
```
|
||||||
|
|
||||||
Append one JSON line with these fields (substitute actual values from this session):
|
Append one JSON line with these fields (substitute actual values from this session):
|
||||||
@@ -1445,7 +1446,8 @@ Append one JSON line with these fields (substitute actual values from this sessi
|
|||||||
- `topics`: array of 2-3 topic keywords that describe what this session was about
|
- `topics`: array of 2-3 topic keywords that describe what this session was about
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
echo '{"date":"TIMESTAMP","mode":"MODE","project_slug":"SLUG","signal_count":N,"signals":SIGNALS_ARRAY,"design_doc":"DOC_PATH","assignment":"ASSIGNMENT_TEXT","resources_shown":[],"topics":TOPICS_ARRAY}' >> "${GSTACK_HOME:-$HOME/.gstack}/builder-profile.jsonl"
|
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
|
||||||
|
echo '{"date":"TIMESTAMP","mode":"MODE","project_slug":"SLUG","signal_count":N,"signals":SIGNALS_ARRAY,"design_doc":"DOC_PATH","assignment":"ASSIGNMENT_TEXT","resources_shown":[],"topics":TOPICS_ARRAY}' >> "$GSTACK_STATE_ROOT/builder-profile.jsonl"
|
||||||
```
|
```
|
||||||
|
|
||||||
This entry is append-only. The `resources_shown` field will be updated via a second append
|
This entry is append-only. The `resources_shown` field will be updated via a second append
|
||||||
@@ -1803,7 +1805,8 @@ This must feel earned, not broadcast. If the evidence doesn't support it, skip e
|
|||||||
with a narrative arc (not a data table). The arc tells the STORY of their journey in
|
with a narrative arc (not a data table). The arc tells the STORY of their journey in
|
||||||
second person, referencing specific things they said across sessions. Then open it:
|
second person, referencing specific things they said across sessions. Then open it:
|
||||||
```bash
|
```bash
|
||||||
open "${GSTACK_HOME:-$HOME/.gstack}/builder-journey.md"
|
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
|
||||||
|
open "$GSTACK_STATE_ROOT/builder-journey.md"
|
||||||
```
|
```
|
||||||
|
|
||||||
Then proceed to Founder Resources below.
|
Then proceed to Founder Resources below.
|
||||||
@@ -1905,7 +1908,8 @@ PAUL GRAHAM ESSAYS:
|
|||||||
1. Log the selected resource URLs to the builder profile (single source of truth).
|
1. Log the selected resource URLs to the builder profile (single source of truth).
|
||||||
Append a resource-tracking entry:
|
Append a resource-tracking entry:
|
||||||
```bash
|
```bash
|
||||||
echo '{"date":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'","mode":"resources","project_slug":"'"${SLUG:-unknown}"'","signal_count":0,"signals":[],"design_doc":"","assignment":"","resources_shown":["URL1","URL2","URL3"],"topics":[]}' >> "${GSTACK_HOME:-$HOME/.gstack}/builder-profile.jsonl"
|
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
|
||||||
|
echo '{"date":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'","mode":"resources","project_slug":"'"${SLUG:-unknown}"'","signal_count":0,"signals":[],"design_doc":"","assignment":"","resources_shown":["URL1","URL2","URL3"],"topics":[]}' >> "$GSTACK_STATE_ROOT/builder-profile.jsonl"
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Log the selection to analytics:
|
2. Log the selection to analytics:
|
||||||
|
|||||||
@@ -445,7 +445,8 @@ After counting signals, append a session entry to the builder profile. This is t
|
|||||||
source of truth for all closing state (tier, resource dedup, journey tracking).
|
source of truth for all closing state (tier, resource dedup, journey tracking).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir -p "${GSTACK_HOME:-$HOME/.gstack}"
|
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
|
||||||
|
mkdir -p "$GSTACK_STATE_ROOT"
|
||||||
```
|
```
|
||||||
|
|
||||||
Append one JSON line with these fields (substitute actual values from this session):
|
Append one JSON line with these fields (substitute actual values from this session):
|
||||||
@@ -460,7 +461,8 @@ Append one JSON line with these fields (substitute actual values from this sessi
|
|||||||
- `topics`: array of 2-3 topic keywords that describe what this session was about
|
- `topics`: array of 2-3 topic keywords that describe what this session was about
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
echo '{"date":"TIMESTAMP","mode":"MODE","project_slug":"SLUG","signal_count":N,"signals":SIGNALS_ARRAY,"design_doc":"DOC_PATH","assignment":"ASSIGNMENT_TEXT","resources_shown":[],"topics":TOPICS_ARRAY}' >> "${GSTACK_HOME:-$HOME/.gstack}/builder-profile.jsonl"
|
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
|
||||||
|
echo '{"date":"TIMESTAMP","mode":"MODE","project_slug":"SLUG","signal_count":N,"signals":SIGNALS_ARRAY,"design_doc":"DOC_PATH","assignment":"ASSIGNMENT_TEXT","resources_shown":[],"topics":TOPICS_ARRAY}' >> "$GSTACK_STATE_ROOT/builder-profile.jsonl"
|
||||||
```
|
```
|
||||||
|
|
||||||
This entry is append-only. The `resources_shown` field will be updated via a second append
|
This entry is append-only. The `resources_shown` field will be updated via a second append
|
||||||
@@ -758,7 +760,8 @@ This must feel earned, not broadcast. If the evidence doesn't support it, skip e
|
|||||||
with a narrative arc (not a data table). The arc tells the STORY of their journey in
|
with a narrative arc (not a data table). The arc tells the STORY of their journey in
|
||||||
second person, referencing specific things they said across sessions. Then open it:
|
second person, referencing specific things they said across sessions. Then open it:
|
||||||
```bash
|
```bash
|
||||||
open "${GSTACK_HOME:-$HOME/.gstack}/builder-journey.md"
|
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
|
||||||
|
open "$GSTACK_STATE_ROOT/builder-journey.md"
|
||||||
```
|
```
|
||||||
|
|
||||||
Then proceed to Founder Resources below.
|
Then proceed to Founder Resources below.
|
||||||
@@ -860,7 +863,8 @@ PAUL GRAHAM ESSAYS:
|
|||||||
1. Log the selected resource URLs to the builder profile (single source of truth).
|
1. Log the selected resource URLs to the builder profile (single source of truth).
|
||||||
Append a resource-tracking entry:
|
Append a resource-tracking entry:
|
||||||
```bash
|
```bash
|
||||||
echo '{"date":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'","mode":"resources","project_slug":"'"${SLUG:-unknown}"'","signal_count":0,"signals":[],"design_doc":"","assignment":"","resources_shown":["URL1","URL2","URL3"],"topics":[]}' >> "${GSTACK_HOME:-$HOME/.gstack}/builder-profile.jsonl"
|
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
|
||||||
|
echo '{"date":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'","mode":"resources","project_slug":"'"${SLUG:-unknown}"'","signal_count":0,"signals":[],"design_doc":"","assignment":"","resources_shown":["URL1","URL2","URL3"],"topics":[]}' >> "$GSTACK_STATE_ROOT/builder-profile.jsonl"
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Log the selection to analytics:
|
2. Log the selection to analytics:
|
||||||
|
|||||||
+8
-4
@@ -783,7 +783,8 @@ Power-user shortcuts (one-word invocations) — handle these too:
|
|||||||
# Ensure profile exists
|
# Ensure profile exists
|
||||||
~/.claude/skills/gstack/bin/gstack-developer-profile --read >/dev/null
|
~/.claude/skills/gstack/bin/gstack-developer-profile --read >/dev/null
|
||||||
# Update declared dimensions atomically
|
# Update declared dimensions atomically
|
||||||
_PROFILE="${GSTACK_HOME:-$HOME/.gstack}/developer-profile.json"
|
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
|
||||||
|
_PROFILE="$GSTACK_STATE_ROOT/developer-profile.json"
|
||||||
bun -e "
|
bun -e "
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const p = JSON.parse(fs.readFileSync('$_PROFILE','utf-8'));
|
const p = JSON.parse(fs.readFileSync('$_PROFILE','utf-8'));
|
||||||
@@ -844,7 +845,8 @@ Parse the JSON. Present in **plain English**, not raw floats:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)"
|
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)"
|
||||||
_LOG="${GSTACK_HOME:-$HOME/.gstack}/projects/$SLUG/question-log.jsonl"
|
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
|
||||||
|
_LOG="$GSTACK_STATE_ROOT/projects/$SLUG/question-log.jsonl"
|
||||||
if [ ! -f "$_LOG" ]; then
|
if [ ! -f "$_LOG" ]; then
|
||||||
echo "NO_LOG"
|
echo "NO_LOG"
|
||||||
else
|
else
|
||||||
@@ -937,7 +939,8 @@ is a trust boundary (Codex #15 in the design doc).
|
|||||||
|
|
||||||
3. After Y, write:
|
3. After Y, write:
|
||||||
```bash
|
```bash
|
||||||
_PROFILE="${GSTACK_HOME:-$HOME/.gstack}/developer-profile.json"
|
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
|
||||||
|
_PROFILE="$GSTACK_STATE_ROOT/developer-profile.json"
|
||||||
bun -e "
|
bun -e "
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const p = JSON.parse(fs.readFileSync('$_PROFILE','utf-8'));
|
const p = JSON.parse(fs.readFileSync('$_PROFILE','utf-8'));
|
||||||
@@ -978,7 +981,8 @@ the user decides whether declared is wrong or behavior is wrong.
|
|||||||
```bash
|
```bash
|
||||||
~/.claude/skills/gstack/bin/gstack-question-preference --stats
|
~/.claude/skills/gstack/bin/gstack-question-preference --stats
|
||||||
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)"
|
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)"
|
||||||
_LOG="${GSTACK_HOME:-$HOME/.gstack}/projects/$SLUG/question-log.jsonl"
|
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
|
||||||
|
_LOG="$GSTACK_STATE_ROOT/projects/$SLUG/question-log.jsonl"
|
||||||
[ -f "$_LOG" ] && echo "TOTAL_LOGGED: $(wc -l < "$_LOG" | tr -d ' ')" || echo "TOTAL_LOGGED: 0"
|
[ -f "$_LOG" ] && echo "TOTAL_LOGGED: $(wc -l < "$_LOG" | tr -d ' ')" || echo "TOTAL_LOGGED: 0"
|
||||||
~/.claude/skills/gstack/bin/gstack-developer-profile --profile | bun -e "
|
~/.claude/skills/gstack/bin/gstack-developer-profile --profile | bun -e "
|
||||||
const p = JSON.parse(await Bun.stdin.text());
|
const p = JSON.parse(await Bun.stdin.text());
|
||||||
|
|||||||
@@ -144,7 +144,8 @@ Power-user shortcuts (one-word invocations) — handle these too:
|
|||||||
# Ensure profile exists
|
# Ensure profile exists
|
||||||
~/.claude/skills/gstack/bin/gstack-developer-profile --read >/dev/null
|
~/.claude/skills/gstack/bin/gstack-developer-profile --read >/dev/null
|
||||||
# Update declared dimensions atomically
|
# Update declared dimensions atomically
|
||||||
_PROFILE="${GSTACK_HOME:-$HOME/.gstack}/developer-profile.json"
|
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
|
||||||
|
_PROFILE="$GSTACK_STATE_ROOT/developer-profile.json"
|
||||||
bun -e "
|
bun -e "
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const p = JSON.parse(fs.readFileSync('$_PROFILE','utf-8'));
|
const p = JSON.parse(fs.readFileSync('$_PROFILE','utf-8'));
|
||||||
@@ -205,7 +206,8 @@ Parse the JSON. Present in **plain English**, not raw floats:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)"
|
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)"
|
||||||
_LOG="${GSTACK_HOME:-$HOME/.gstack}/projects/$SLUG/question-log.jsonl"
|
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
|
||||||
|
_LOG="$GSTACK_STATE_ROOT/projects/$SLUG/question-log.jsonl"
|
||||||
if [ ! -f "$_LOG" ]; then
|
if [ ! -f "$_LOG" ]; then
|
||||||
echo "NO_LOG"
|
echo "NO_LOG"
|
||||||
else
|
else
|
||||||
@@ -298,7 +300,8 @@ is a trust boundary (Codex #15 in the design doc).
|
|||||||
|
|
||||||
3. After Y, write:
|
3. After Y, write:
|
||||||
```bash
|
```bash
|
||||||
_PROFILE="${GSTACK_HOME:-$HOME/.gstack}/developer-profile.json"
|
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
|
||||||
|
_PROFILE="$GSTACK_STATE_ROOT/developer-profile.json"
|
||||||
bun -e "
|
bun -e "
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const p = JSON.parse(fs.readFileSync('$_PROFILE','utf-8'));
|
const p = JSON.parse(fs.readFileSync('$_PROFILE','utf-8'));
|
||||||
@@ -339,7 +342,8 @@ the user decides whether declared is wrong or behavior is wrong.
|
|||||||
```bash
|
```bash
|
||||||
~/.claude/skills/gstack/bin/gstack-question-preference --stats
|
~/.claude/skills/gstack/bin/gstack-question-preference --stats
|
||||||
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)"
|
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)"
|
||||||
_LOG="${GSTACK_HOME:-$HOME/.gstack}/projects/$SLUG/question-log.jsonl"
|
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
|
||||||
|
_LOG="$GSTACK_STATE_ROOT/projects/$SLUG/question-log.jsonl"
|
||||||
[ -f "$_LOG" ] && echo "TOTAL_LOGGED: $(wc -l < "$_LOG" | tr -d ' ')" || echo "TOTAL_LOGGED: 0"
|
[ -f "$_LOG" ] && echo "TOTAL_LOGGED: $(wc -l < "$_LOG" | tr -d ' ')" || echo "TOTAL_LOGGED: 0"
|
||||||
~/.claude/skills/gstack/bin/gstack-developer-profile --profile | bun -e "
|
~/.claude/skills/gstack/bin/gstack-developer-profile --profile | bun -e "
|
||||||
const p = JSON.parse(await Bun.stdin.text());
|
const p = JSON.parse(await Bun.stdin.text());
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { describe, test, expect } from 'bun:test';
|
||||||
|
import { spawnSync } from 'child_process';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
const ROOT = path.resolve(import.meta.dir, '..');
|
||||||
|
const BIN = path.join(ROOT, 'bin', 'gstack-paths');
|
||||||
|
|
||||||
|
function run(env: Record<string, string | undefined>): Record<string, string> {
|
||||||
|
const result = spawnSync(BIN, [], {
|
||||||
|
env: { PATH: process.env.PATH, ...env } as Record<string, string>,
|
||||||
|
encoding: 'utf-8',
|
||||||
|
});
|
||||||
|
if (result.status !== 0) {
|
||||||
|
throw new Error(`gstack-paths failed (status ${result.status}): ${result.stderr}`);
|
||||||
|
}
|
||||||
|
const out: Record<string, string> = {};
|
||||||
|
for (const line of result.stdout.split('\n')) {
|
||||||
|
const eq = line.indexOf('=');
|
||||||
|
if (eq > 0) out[line.slice(0, eq)] = line.slice(eq + 1);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('gstack-paths', () => {
|
||||||
|
test('GSTACK_HOME wins over CLAUDE_PLUGIN_DATA and HOME', () => {
|
||||||
|
const got = run({
|
||||||
|
GSTACK_HOME: '/tmp/explicit-state',
|
||||||
|
CLAUDE_PLUGIN_DATA: '/tmp/plugin-data',
|
||||||
|
HOME: '/tmp/home',
|
||||||
|
});
|
||||||
|
expect(got.GSTACK_STATE_ROOT).toBe('/tmp/explicit-state');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('CLAUDE_PLUGIN_DATA wins over HOME when GSTACK_HOME unset', () => {
|
||||||
|
const got = run({
|
||||||
|
CLAUDE_PLUGIN_DATA: '/tmp/plugin-data',
|
||||||
|
HOME: '/tmp/home',
|
||||||
|
});
|
||||||
|
expect(got.GSTACK_STATE_ROOT).toBe('/tmp/plugin-data');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('HOME-derived state root when GSTACK_HOME and CLAUDE_PLUGIN_DATA unset', () => {
|
||||||
|
const got = run({ HOME: '/tmp/myhome' });
|
||||||
|
expect(got.GSTACK_STATE_ROOT).toBe('/tmp/myhome/.gstack');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('CWD fallback when HOME also unset (container env)', () => {
|
||||||
|
const got = run({ HOME: '' });
|
||||||
|
expect(got.GSTACK_STATE_ROOT).toBe('.gstack');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('PLAN_ROOT chain: GSTACK_PLAN_DIR > CLAUDE_PLANS_DIR > HOME > CWD', () => {
|
||||||
|
expect(run({ GSTACK_PLAN_DIR: '/tmp/explicit', HOME: '/h' }).PLAN_ROOT).toBe('/tmp/explicit');
|
||||||
|
expect(run({ CLAUDE_PLANS_DIR: '/tmp/claude', HOME: '/h' }).PLAN_ROOT).toBe('/tmp/claude');
|
||||||
|
expect(run({ HOME: '/tmp/myhome' }).PLAN_ROOT).toBe('/tmp/myhome/.claude/plans');
|
||||||
|
expect(run({ HOME: '' }).PLAN_ROOT).toBe('.claude/plans');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('TMP_ROOT chain: TMPDIR > TMP > .gstack/tmp', () => {
|
||||||
|
expect(run({ TMPDIR: '/tmp/x', HOME: '/h' }).TMP_ROOT).toBe('/tmp/x');
|
||||||
|
expect(run({ TMP: '/tmp/y', HOME: '/h' }).TMP_ROOT).toBe('/tmp/y');
|
||||||
|
expect(run({ HOME: '' }).TMP_ROOT).toBe('.gstack/tmp');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('emits all three exports on every invocation', () => {
|
||||||
|
const got = run({ HOME: '/tmp/h' });
|
||||||
|
expect(got).toHaveProperty('GSTACK_STATE_ROOT');
|
||||||
|
expect(got).toHaveProperty('PLAN_ROOT');
|
||||||
|
expect(got).toHaveProperty('TMP_ROOT');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('output is shell-evalable: only KEY=VALUE lines, no extra prose', () => {
|
||||||
|
const result = spawnSync(BIN, [], {
|
||||||
|
env: { PATH: process.env.PATH, HOME: '/tmp/h' } as Record<string, string>,
|
||||||
|
encoding: 'utf-8',
|
||||||
|
});
|
||||||
|
const lines = result.stdout.split('\n').filter(Boolean);
|
||||||
|
for (const line of lines) {
|
||||||
|
expect(line).toMatch(/^[A-Z_]+=.*/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
+2
-1
@@ -29,7 +29,8 @@ echo '{"skill":"unfreeze","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$(bas
|
|||||||
## Clear the boundary
|
## Clear the boundary
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
STATE_DIR="${CLAUDE_PLUGIN_DATA:-$HOME/.gstack}"
|
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
|
||||||
|
STATE_DIR="$GSTACK_STATE_ROOT"
|
||||||
if [ -f "$STATE_DIR/freeze-dir.txt" ]; then
|
if [ -f "$STATE_DIR/freeze-dir.txt" ]; then
|
||||||
PREV=$(cat "$STATE_DIR/freeze-dir.txt")
|
PREV=$(cat "$STATE_DIR/freeze-dir.txt")
|
||||||
rm -f "$STATE_DIR/freeze-dir.txt"
|
rm -f "$STATE_DIR/freeze-dir.txt"
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ echo '{"skill":"unfreeze","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$(bas
|
|||||||
## Clear the boundary
|
## Clear the boundary
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
STATE_DIR="${CLAUDE_PLUGIN_DATA:-$HOME/.gstack}"
|
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
|
||||||
|
STATE_DIR="$GSTACK_STATE_ROOT"
|
||||||
if [ -f "$STATE_DIR/freeze-dir.txt" ]; then
|
if [ -f "$STATE_DIR/freeze-dir.txt" ]; then
|
||||||
PREV=$(cat "$STATE_DIR/freeze-dir.txt")
|
PREV=$(cat "$STATE_DIR/freeze-dir.txt")
|
||||||
rm -f "$STATE_DIR/freeze-dir.txt"
|
rm -f "$STATE_DIR/freeze-dir.txt"
|
||||||
|
|||||||
Reference in New Issue
Block a user