feat: allow repo-local gstack artifacts

This commit is contained in:
maxpetrusenkoagent 2026-06-14 10:08:01 -04:00
parent cab774cced
commit 13850ec388
36 changed files with 215 additions and 88 deletions

View File

@ -958,10 +958,10 @@ instructions instead of reviewing the plan.
Before doing anything, save the plan file's current state to an external file:
```bash
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p ~/.gstack/projects/$SLUG
eval "$(~/.claude/skills/gstack/bin/gstack-paths 2>/dev/null)" && eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && PROJECT_DIR="$GSTACK_ARTIFACTS_ROOT/projects/$SLUG" && mkdir -p "$PROJECT_DIR"
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-')
DATETIME=$(date +%Y%m%d-%H%M%S)
echo "RESTORE_PATH=$HOME/.gstack/projects/$SLUG/${BRANCH}-autoplan-restore-${DATETIME}.md"
echo "RESTORE_PATH=$PROJECT_DIR/${BRANCH}-autoplan-restore-${DATETIME}.md"
```
Write the plan file's full contents to the restore path with this header:

View File

@ -169,7 +169,7 @@ Before doing anything, save the plan file's current state to an external file:
{{SLUG_SETUP}}
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-')
DATETIME=$(date +%Y%m%d-%H%M%S)
echo "RESTORE_PATH=$HOME/.gstack/projects/$SLUG/${BRANCH}-autoplan-restore-${DATETIME}.md"
echo "RESTORE_PATH=$PROJECT_DIR/${BRANCH}-autoplan-restore-${DATETIME}.md"
```
Write the plan file's full contents to the restore path with this header:

View File

@ -63,6 +63,14 @@ CONFIG_HEADER='# gstack configuration — edit freely, changes take effect on ne
# # See docs/designs/PLAN_TUNING_V1.md for rationale.
#
# ─── Artifacts sync (renamed from gbrain_sync_mode in v1.27.0.0) ─────
# artifacts_root: state # state | repo
# # state — write project artifacts under the gstack
# # state root (default: ~/.gstack/projects)
# # repo — write project artifacts under the current
# # repo at .gstack/projects so authors can
# # review/commit them with the project.
# # Used by gstack-paths as GSTACK_ARTIFACTS_ROOT.
#
# artifacts_sync_mode: off # off | artifacts-only | full
# # off — no sync (default)
# # artifacts-only — sync plans/designs/retros/learnings only
@ -117,6 +125,7 @@ lookup_default() {
gstack_contributor) echo "false" ;;
skip_eng_review) echo "false" ;;
workspace_root) echo "$HOME/conductor/workspaces" ;;
artifacts_root) echo "state" ;;
cross_project_learnings) echo "" ;; # intentionally empty → unset triggers first-time prompt
artifacts_sync_mode) echo "off" ;;
artifacts_sync_mode_prompted) echo "false" ;;
@ -287,6 +296,10 @@ case "${1:-}" in
echo "Warning: artifacts_sync_mode '$VALUE' not recognized. Valid values: off, artifacts-only, full. Using off." >&2
VALUE="off"
fi
if [ "$KEY" = "artifacts_root" ] && [ "$VALUE" != "state" ] && [ "$VALUE" != "repo" ]; then
echo "Warning: artifacts_root '$VALUE' not recognized. Valid values: state, repo. Using state." >&2
VALUE="state"
fi
# redact_repo_visibility: a LOCAL override for repos gh/glab can't read (e.g.
# self-hosted GitLab). It lives in ~/.gstack/config.yaml (never committed), so
# it can't be used to weaken the gate repo-wide for other contributors.
@ -331,7 +344,7 @@ case "${1:-}" in
for KEY in proactive routing_declined telemetry auto_upgrade update_check \
skill_prefix checkpoint_mode checkpoint_push explain_level \
codex_reviews gstack_contributor skip_eng_review workspace_root \
artifacts_sync_mode artifacts_sync_mode_prompted plan_tune_hooks; do
artifacts_root artifacts_sync_mode artifacts_sync_mode_prompted plan_tune_hooks; 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
@ -347,7 +360,7 @@ case "${1:-}" in
for KEY in proactive routing_declined telemetry auto_upgrade update_check \
skill_prefix checkpoint_mode checkpoint_push explain_level \
codex_reviews gstack_contributor skip_eng_review workspace_root \
artifacts_sync_mode artifacts_sync_mode_prompted plan_tune_hooks; do
artifacts_root artifacts_sync_mode artifacts_sync_mode_prompted plan_tune_hooks; do
printf ' %-24s %s\n' "$KEY:" "$(lookup_default "$KEY")"
done
;;

View File

@ -1,6 +1,6 @@
#!/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
# Usage: eval "$(gstack-paths)" → sets GSTACK_STATE_ROOT, GSTACK_ARTIFACTS_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
@ -9,9 +9,10 @@
# CI / container env where HOME may be unset.
#
# Chains:
# GSTACK_STATE_ROOT: GSTACK_HOME -> CLAUDE_PLUGIN_DATA (only when CLAUDE_PLUGIN_ROOT=*gstack*) -> $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)
# GSTACK_STATE_ROOT: GSTACK_HOME -> CLAUDE_PLUGIN_DATA (only when CLAUDE_PLUGIN_ROOT=*gstack*) -> $HOME/.gstack -> .gstack
# GSTACK_ARTIFACTS_ROOT: config artifacts_root=repo -> <git-root>/.gstack, otherwise GSTACK_STATE_ROOT
# 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
@ -33,6 +34,24 @@ else
_state_root=".gstack"
fi
# Artifact root: where skills write user-facing project artifacts under
# projects/$SLUG (design docs, review reports, test plans, checkpoints).
# Defaults to the state root for backward compatibility. If the user sets
# `artifacts_root: repo`, write those artifacts to the current repository's
# `.gstack/` directory so they can be reviewed/committed with the project.
_artifacts_root="$_state_root"
_config_file="$_state_root/config.yaml"
_artifacts_mode=""
if [ -f "$_config_file" ]; then
_artifacts_mode=$(grep -E '^artifacts_root:' "$_config_file" 2>/dev/null | tail -1 | awk '{print $2}' | tr -d '[:space:]' || true)
fi
if [ "$_artifacts_mode" = "repo" ]; then
_repo_root=$(git rev-parse --show-toplevel 2>/dev/null || true)
if [ -n "$_repo_root" ]; then
_artifacts_root="$_repo_root/.gstack"
fi
fi
# Plan root: where /context-save and /codex consult write plan files.
if [ -n "${GSTACK_PLAN_DIR:-}" ]; then
_plan_root="$GSTACK_PLAN_DIR"
@ -61,5 +80,6 @@ fi
mkdir -p "$_tmp_root" 2>/dev/null || true
echo "GSTACK_STATE_ROOT=$_state_root"
echo "GSTACK_ARTIFACTS_ROOT=$_artifacts_root"
echo "PLAN_ROOT=$_plan_root"
echo "TMP_ROOT=$_tmp_root"

View File

@ -952,8 +952,8 @@ Save report to `.gstack/canary-reports/{date}-canary.md` and `.gstack/canary-rep
Log the result for the review dashboard:
```bash
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)"
mkdir -p ~/.gstack/projects/$SLUG
eval "$(~/.claude/skills/gstack/bin/gstack-paths 2>/dev/null)" && eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && PROJECT_DIR="$GSTACK_ARTIFACTS_ROOT/projects/$SLUG"
mkdir -p "$PROJECT_DIR"
```
Write a JSONL entry: `{"skill":"canary","timestamp":"<ISO>","status":"<HEALTHY/DEGRADED/BROKEN>","url":"<url>","duration_min":<N>,"alerts":<N>}`

View File

@ -198,7 +198,7 @@ Log the result for the review dashboard:
```bash
{{SLUG_EVAL}}
mkdir -p ~/.gstack/projects/$SLUG
mkdir -p "$PROJECT_DIR"
```
Write a JSONL entry: `{"skill":"canary","timestamp":"<ISO>","status":"<HEALTHY/DEGRADED/BROKEN>","url":"<url>","duration_min":<N>,"alerts":<N>}`

View File

@ -745,9 +745,9 @@ Parse the user's input:
### Step 1: Find saved contexts
```bash
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p ~/.gstack/projects/$SLUG
eval "$(~/.claude/skills/gstack/bin/gstack-paths 2>/dev/null)" && eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && PROJECT_DIR="$GSTACK_ARTIFACTS_ROOT/projects/$SLUG" && mkdir -p "$PROJECT_DIR"
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
CHECKPOINT_DIR="$GSTACK_STATE_ROOT/projects/$SLUG/checkpoints"
CHECKPOINT_DIR="$PROJECT_DIR/checkpoints"
if [ ! -d "$CHECKPOINT_DIR" ]; then
echo "NO_CHECKPOINTS"
else

View File

@ -63,7 +63,7 @@ Parse the user's input:
```bash
{{SLUG_SETUP}}
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
CHECKPOINT_DIR="$GSTACK_STATE_ROOT/projects/$SLUG/checkpoints"
CHECKPOINT_DIR="$PROJECT_DIR/checkpoints"
if [ ! -d "$CHECKPOINT_DIR" ]; then
echo "NO_CHECKPOINTS"
else

View File

@ -740,7 +740,7 @@ If the user types `/context-save resume` or `/context-save restore`, tell them:
### Step 1: Gather state
```bash
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p ~/.gstack/projects/$SLUG
eval "$(~/.claude/skills/gstack/bin/gstack-paths 2>/dev/null)" && eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && PROJECT_DIR="$GSTACK_ARTIFACTS_ROOT/projects/$SLUG" && mkdir -p "$PROJECT_DIR"
```
Collect the current working state:
@ -800,9 +800,9 @@ inject shell metacharacters into any subsequent command. The sanitizer is an
allowlist: only `a-z 0-9 - .` survive.
```bash
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p ~/.gstack/projects/$SLUG
eval "$(~/.claude/skills/gstack/bin/gstack-paths 2>/dev/null)" && eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && PROJECT_DIR="$GSTACK_ARTIFACTS_ROOT/projects/$SLUG" && mkdir -p "$PROJECT_DIR"
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
CHECKPOINT_DIR="$GSTACK_STATE_ROOT/projects/$SLUG/checkpoints"
CHECKPOINT_DIR="$PROJECT_DIR/checkpoints"
mkdir -p "$CHECKPOINT_DIR"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
# Bash-side title sanitize. Pass the raw title as $1 when running this block.
@ -887,9 +887,9 @@ Restore later with /context-restore.
### Step 1: Gather saved contexts
```bash
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p ~/.gstack/projects/$SLUG
eval "$(~/.claude/skills/gstack/bin/gstack-paths 2>/dev/null)" && eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && PROJECT_DIR="$GSTACK_ARTIFACTS_ROOT/projects/$SLUG" && mkdir -p "$PROJECT_DIR"
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
CHECKPOINT_DIR="$GSTACK_STATE_ROOT/projects/$SLUG/checkpoints"
CHECKPOINT_DIR="$PROJECT_DIR/checkpoints"
if [ -d "$CHECKPOINT_DIR" ]; then
echo "CHECKPOINT_DIR=$CHECKPOINT_DIR"
# Use find + sort instead of ls -1t: filename YYYYMMDD-HHMMSS prefix is the

View File

@ -119,7 +119,7 @@ allowlist: only `a-z 0-9 - .` survive.
```bash
{{SLUG_SETUP}}
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
CHECKPOINT_DIR="$GSTACK_STATE_ROOT/projects/$SLUG/checkpoints"
CHECKPOINT_DIR="$PROJECT_DIR/checkpoints"
mkdir -p "$CHECKPOINT_DIR"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
# Bash-side title sanitize. Pass the raw title as $1 when running this block.
@ -206,7 +206,7 @@ Restore later with /context-restore.
```bash
{{SLUG_SETUP}}
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
CHECKPOINT_DIR="$GSTACK_STATE_ROOT/projects/$SLUG/checkpoints"
CHECKPOINT_DIR="$PROJECT_DIR/checkpoints"
if [ -d "$CHECKPOINT_DIR" ]; then
echo "CHECKPOINT_DIR=$CHECKPOINT_DIR"
# Use find + sort instead of ls -1t: filename YYYYMMDD-HHMMSS prefix is the

View File

@ -781,7 +781,7 @@ Look for office-hours output:
```bash
setopt +o nomatch 2>/dev/null || true # zsh compat
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)"
eval "$(~/.claude/skills/gstack/bin/gstack-paths 2>/dev/null)" && eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && PROJECT_DIR="$GSTACK_ARTIFACTS_ROOT/projects/$SLUG"
ls ~/.gstack/projects/$SLUG/*office-hours* 2>/dev/null | head -5
ls .context/*office-hours* .context/attachments/*office-hours* 2>/dev/null | head -5
```

View File

@ -1853,9 +1853,9 @@ Write the report to `$REPORT_DIR` (already set up in the setup phase):
**Also write a summary to the project index:**
```bash
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p ~/.gstack/projects/$SLUG
eval "$(~/.claude/skills/gstack/bin/gstack-paths 2>/dev/null)" && eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && PROJECT_DIR="$GSTACK_ARTIFACTS_ROOT/projects/$SLUG" && mkdir -p "$PROJECT_DIR"
```
Write a one-line summary to `~/.gstack/projects/{slug}/{user}-{branch}-design-audit-{datetime}.md` with a pointer to the full report in `$REPORT_DIR`.
Write a one-line summary to `$PROJECT_DIR/{user}-{branch}-design-audit-{datetime}.md` with a pointer to the full report in `$REPORT_DIR`.
**Per-finding additions** (beyond standard design audit report):
- Fix Status: verified / best-effort / reverted / deferred

View File

@ -268,7 +268,7 @@ Write the report to `$REPORT_DIR` (already set up in the setup phase):
```bash
{{SLUG_SETUP}}
```
Write a one-line summary to `~/.gstack/projects/{slug}/{user}-{branch}-design-audit-{datetime}.md` with a pointer to the full report in `$REPORT_DIR`.
Write a one-line summary to `$PROJECT_DIR/{user}-{branch}-design-audit-{datetime}.md` with a pointer to the full report in `$REPORT_DIR`.
**Per-finding additions** (beyond standard design audit report):
- Fix Status: verified / best-effort / reverted / deferred

View File

@ -913,10 +913,10 @@ DETAILS: Lint (3 warnings)
## Step 5: Persist to Health History
```bash
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p ~/.gstack/projects/$SLUG
eval "$(~/.claude/skills/gstack/bin/gstack-paths 2>/dev/null)" && eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && PROJECT_DIR="$GSTACK_ARTIFACTS_ROOT/projects/$SLUG" && mkdir -p "$PROJECT_DIR"
```
Append one JSONL line to `~/.gstack/projects/$SLUG/health-history.jsonl`:
Append one JSONL line to `$PROJECT_DIR/health-history.jsonl`:
```json
{"ts":"2026-03-31T14:30:00Z","branch":"main","score":9.1,"typecheck":10,"lint":8,"test":10,"deadcode":7,"shell":10,"gbrain":10,"duration_s":23}
@ -937,12 +937,12 @@ and start new tracking from the first post-D6 run.
## Step 6: Trend Analysis + Recommendations
Read the last 10 entries from `~/.gstack/projects/$SLUG/health-history.jsonl` (if the
Read the last 10 entries from `$PROJECT_DIR/health-history.jsonl` (if the
file exists and has prior entries).
```bash
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p ~/.gstack/projects/$SLUG
tail -10 ~/.gstack/projects/$SLUG/health-history.jsonl 2>/dev/null || echo "NO_HISTORY"
eval "$(~/.claude/skills/gstack/bin/gstack-paths 2>/dev/null)" && eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && PROJECT_DIR="$GSTACK_ARTIFACTS_ROOT/projects/$SLUG" && mkdir -p "$PROJECT_DIR"
tail -10 "$PROJECT_DIR/health-history.jsonl" 2>/dev/null || echo "NO_HISTORY"
```
**If prior entries exist, show the trend:**

View File

@ -232,7 +232,7 @@ DETAILS: Lint (3 warnings)
{{SLUG_SETUP}}
```
Append one JSONL line to `~/.gstack/projects/$SLUG/health-history.jsonl`:
Append one JSONL line to `$PROJECT_DIR/health-history.jsonl`:
```json
{"ts":"2026-03-31T14:30:00Z","branch":"main","score":9.1,"typecheck":10,"lint":8,"test":10,"deadcode":7,"shell":10,"gbrain":10,"duration_s":23}
@ -253,12 +253,12 @@ and start new tracking from the first post-D6 run.
## Step 6: Trend Analysis + Recommendations
Read the last 10 entries from `~/.gstack/projects/$SLUG/health-history.jsonl` (if the
Read the last 10 entries from `$PROJECT_DIR/health-history.jsonl` (if the
file exists and has prior entries).
```bash
{{SLUG_SETUP}}
tail -10 ~/.gstack/projects/$SLUG/health-history.jsonl 2>/dev/null || echo "NO_HISTORY"
tail -10 "$PROJECT_DIR/health-history.jsonl" 2>/dev/null || echo "NO_HISTORY"
```
**If prior entries exist, show the trend:**

View File

@ -882,12 +882,12 @@ Check whether this project has been through a successful `/land-and-deploy` befo
and whether the deploy configuration has changed since then:
```bash
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)"
if [ ! -f ~/.gstack/projects/$SLUG/land-deploy-confirmed ]; then
eval "$(~/.claude/skills/gstack/bin/gstack-paths 2>/dev/null)" && eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && PROJECT_DIR="$GSTACK_ARTIFACTS_ROOT/projects/$SLUG"
if [ ! -f "$PROJECT_DIR/land-deploy-confirmed" ]; then
echo "FIRST_RUN"
else
# Check if deploy config has changed since confirmation
SAVED_HASH=$(cat ~/.gstack/projects/$SLUG/land-deploy-confirmed 2>/dev/null)
SAVED_HASH=$(cat "$PROJECT_DIR/land-deploy-confirmed" 2>/dev/null)
CURRENT_HASH=$(sed -n '/## Deploy Configuration/,/^## /p' CLAUDE.md 2>/dev/null | shasum -a 256 | cut -d' ' -f1)
# Also hash workflow files that affect deploy behavior
WORKFLOW_HASH=$(find .github/workflows -maxdepth 1 \( -name '*deploy*' -o -name '*cd*' \) 2>/dev/null | xargs cat 2>/dev/null | shasum -a 256 | cut -d' ' -f1)
@ -1081,10 +1081,10 @@ Present the full dry-run results to the user via AskUserQuestion:
Save the deploy config fingerprint so we can detect future changes:
```bash
mkdir -p ~/.gstack/projects/$SLUG
mkdir -p "$PROJECT_DIR"
CURRENT_HASH=$(sed -n '/## Deploy Configuration/,/^## /p' CLAUDE.md 2>/dev/null | shasum -a 256 | cut -d' ' -f1)
WORKFLOW_HASH=$(find .github/workflows -maxdepth 1 \( -name '*deploy*' -o -name '*cd*' \) 2>/dev/null | xargs cat 2>/dev/null | shasum -a 256 | cut -d' ' -f1)
echo "${CURRENT_HASH}-${WORKFLOW_HASH}" > ~/.gstack/projects/$SLUG/land-deploy-confirmed
echo "${CURRENT_HASH}-${WORKFLOW_HASH}" > "$PROJECT_DIR/land-deploy-confirmed"
```
Continue to Step 2.
@ -1803,8 +1803,8 @@ Save report to `.gstack/deploy-reports/{date}-pr{number}-deploy.md`.
Log to the review dashboard:
```bash
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)"
mkdir -p ~/.gstack/projects/$SLUG
eval "$(~/.claude/skills/gstack/bin/gstack-paths 2>/dev/null)" && eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && PROJECT_DIR="$GSTACK_ARTIFACTS_ROOT/projects/$SLUG"
mkdir -p "$PROJECT_DIR"
```
Write a JSONL entry with timing data:

View File

@ -111,11 +111,11 @@ and whether the deploy configuration has changed since then:
```bash
{{SLUG_EVAL}}
if [ ! -f ~/.gstack/projects/$SLUG/land-deploy-confirmed ]; then
if [ ! -f "$PROJECT_DIR/land-deploy-confirmed" ]; then
echo "FIRST_RUN"
else
# Check if deploy config has changed since confirmation
SAVED_HASH=$(cat ~/.gstack/projects/$SLUG/land-deploy-confirmed 2>/dev/null)
SAVED_HASH=$(cat "$PROJECT_DIR/land-deploy-confirmed" 2>/dev/null)
CURRENT_HASH=$(sed -n '/## Deploy Configuration/,/^## /p' CLAUDE.md 2>/dev/null | shasum -a 256 | cut -d' ' -f1)
# Also hash workflow files that affect deploy behavior
WORKFLOW_HASH=$(find .github/workflows -maxdepth 1 \( -name '*deploy*' -o -name '*cd*' \) 2>/dev/null | xargs cat 2>/dev/null | shasum -a 256 | cut -d' ' -f1)
@ -276,10 +276,10 @@ Present the full dry-run results to the user via AskUserQuestion:
Save the deploy config fingerprint so we can detect future changes:
```bash
mkdir -p ~/.gstack/projects/$SLUG
mkdir -p "$PROJECT_DIR"
CURRENT_HASH=$(sed -n '/## Deploy Configuration/,/^## /p' CLAUDE.md 2>/dev/null | shasum -a 256 | cut -d' ' -f1)
WORKFLOW_HASH=$(find .github/workflows -maxdepth 1 \( -name '*deploy*' -o -name '*cd*' \) 2>/dev/null | xargs cat 2>/dev/null | shasum -a 256 | cut -d' ' -f1)
echo "${CURRENT_HASH}-${WORKFLOW_HASH}" > ~/.gstack/projects/$SLUG/land-deploy-confirmed
echo "${CURRENT_HASH}-${WORKFLOW_HASH}" > "$PROJECT_DIR/land-deploy-confirmed"
```
Continue to Step 2.
@ -966,7 +966,7 @@ Log to the review dashboard:
```bash
{{SLUG_EVAL}}
mkdir -p ~/.gstack/projects/$SLUG
mkdir -p "$PROJECT_DIR"
```
Write a JSONL entry with timing data:

View File

@ -850,7 +850,7 @@ rm -f /tmp/.gstack-brain-context-$$.md 2>/dev/null || true
Understand the project and the area the user wants to change.
```bash
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)"
eval "$(~/.claude/skills/gstack/bin/gstack-paths 2>/dev/null)" && eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && PROJECT_DIR="$GSTACK_ARTIFACTS_ROOT/projects/$SLUG"
```
1. Read `CLAUDE.md`, `TODOS.md` (if they exist).
@ -859,7 +859,7 @@ eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)"
4. **List existing design docs for this project:**
```bash
setopt +o nomatch 2>/dev/null || true # zsh compat
ls -t ~/.gstack/projects/$SLUG/*-design-*.md 2>/dev/null
ls -t "$PROJECT_DIR"/*-design-*.md 2>/dev/null
```
If design docs exist, list them: "Prior designs for this project: [titles + dates]"
@ -1156,7 +1156,7 @@ After the user states the problem (first question in Phase 2A or 2B), search exi
Extract 3-5 significant keywords from the user's problem statement and grep across design docs:
```bash
setopt +o nomatch 2>/dev/null || true # zsh compat
grep -li "<keyword1>\|<keyword2>\|<keyword3>" ~/.gstack/projects/$SLUG/*-design-*.md 2>/dev/null
grep -li "<keyword1>\|<keyword2>\|<keyword3>" "$PROJECT_DIR"/*-design-*.md 2>/dev/null
```
If matches found, read the matching design docs and surface them:

View File

@ -87,7 +87,7 @@ Understand the project and the area the user wants to change.
4. **List existing design docs for this project:**
```bash
setopt +o nomatch 2>/dev/null || true # zsh compat
ls -t ~/.gstack/projects/$SLUG/*-design-*.md 2>/dev/null
ls -t "$PROJECT_DIR"/*-design-*.md 2>/dev/null
```
If design docs exist, list them: "Prior designs for this project: [titles + dates]"
@ -341,7 +341,7 @@ After the user states the problem (first question in Phase 2A or 2B), search exi
Extract 3-5 significant keywords from the user's problem statement and grep across design docs:
```bash
setopt +o nomatch 2>/dev/null || true # zsh compat
grep -li "<keyword1>\|<keyword2>\|<keyword3>" ~/.gstack/projects/$SLUG/*-design-*.md 2>/dev/null
grep -li "<keyword1>\|<keyword2>\|<keyword3>" "$PROJECT_DIR"/*-design-*.md 2>/dev/null
```
If matches found, read the matching design docs and surface them:

View File

@ -5,7 +5,7 @@
Write the design document to the project directory.
```bash
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p ~/.gstack/projects/$SLUG
eval "$(~/.claude/skills/gstack/bin/gstack-paths 2>/dev/null)" && eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && PROJECT_DIR="$GSTACK_ARTIFACTS_ROOT/projects/$SLUG" && mkdir -p "$PROJECT_DIR"
USER=$(whoami)
DATETIME=$(date +%Y%m%d-%H%M%S)
```
@ -13,11 +13,11 @@ DATETIME=$(date +%Y%m%d-%H%M%S)
**Design lineage:** Before writing, check for existing design docs on this branch:
```bash
setopt +o nomatch 2>/dev/null || true # zsh compat
PRIOR=$(ls -t ~/.gstack/projects/$SLUG/*-$BRANCH-design-*.md 2>/dev/null | head -1)
PRIOR=$(ls -t "$PROJECT_DIR"/*-$BRANCH-design-*.md 2>/dev/null | head -1)
```
If `$PRIOR` exists, the new doc gets a `Supersedes:` field referencing it. This creates a revision chain — you can trace how a design evolved across office hours sessions.
Write to `~/.gstack/projects/{slug}/{user}-{branch}-design-{datetime}.md`.
Write to `$PROJECT_DIR/{user}-{branch}-design-{datetime}.md`.
After writing the design doc, tell the user:
**"Design doc saved to: {full path}. Other skills (/plan-ceo-review, /plan-eng-review) will find it automatically."**

View File

@ -11,11 +11,11 @@ DATETIME=$(date +%Y%m%d-%H%M%S)
**Design lineage:** Before writing, check for existing design docs on this branch:
```bash
setopt +o nomatch 2>/dev/null || true # zsh compat
PRIOR=$(ls -t ~/.gstack/projects/$SLUG/*-$BRANCH-design-*.md 2>/dev/null | head -1)
PRIOR=$(ls -t "$PROJECT_DIR"/*-$BRANCH-design-*.md 2>/dev/null | head -1)
```
If `$PRIOR` exists, the new doc gets a `Supersedes:` field referencing it. This creates a revision chain — you can trace how a design evolved across office hours sessions.
Write to `~/.gstack/projects/{slug}/{user}-{branch}-design-{datetime}.md`.
Write to `$PROJECT_DIR/{user}-{branch}-design-{datetime}.md`.
After writing the design doc, tell the user:
**"Design doc saved to: {full path}. Other skills (/plan-ceo-review, /plan-eng-review) will find it automatically."**

View File

@ -875,10 +875,11 @@ Then read CLAUDE.md, TODOS.md, and any existing architecture docs.
**Design doc check:**
```bash
setopt +o nomatch 2>/dev/null || true # zsh compat
eval "$(~/.claude/skills/gstack/bin/gstack-paths 2>/dev/null)" && eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && PROJECT_DIR="$GSTACK_ARTIFACTS_ROOT/projects/$SLUG"
SLUG=$(~/.claude/skills/gstack/browse/bin/remote-slug 2>/dev/null || basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)")
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-' || echo 'no-branch')
DESIGN=$(ls -t ~/.gstack/projects/$SLUG/*-$BRANCH-design-*.md 2>/dev/null | head -1)
[ -z "$DESIGN" ] && DESIGN=$(ls -t ~/.gstack/projects/$SLUG/*-design-*.md 2>/dev/null | head -1)
DESIGN=$(ls -t "$PROJECT_DIR"/*-$BRANCH-design-*.md 2>/dev/null | head -1)
[ -z "$DESIGN" ] && DESIGN=$(ls -t "$PROJECT_DIR"/*-design-*.md 2>/dev/null | head -1)
[ -n "$DESIGN" ] && echo "Design doc found: $DESIGN" || echo "No design doc found"
```
If a design doc exists (from `/office-hours`), read it. Use it as the source of truth for the problem statement, constraints, and chosen approach. If it has a `Supersedes:` field, note that this is a revised design.
@ -1216,7 +1217,8 @@ eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p ~/.gst
Before writing, check for existing CEO plans in the ceo-plans/ directory. If any are >30 days old or their branch has been merged/deleted, offer to archive them:
```bash
mkdir -p ~/.gstack/projects/$SLUG/ceo-plans/archive
eval "$(~/.claude/skills/gstack/bin/gstack-paths 2>/dev/null)" && eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && PROJECT_DIR="$GSTACK_ARTIFACTS_ROOT/projects/$SLUG" && mkdir -p "$PROJECT_DIR"
mkdir -p "$PROJECT_DIR/ceo-plans/archive"
# For each stale plan: mv ~/.gstack/projects/$SLUG/ceo-plans/{old-plan}.md ~/.gstack/projects/$SLUG/ceo-plans/archive/
```

View File

@ -136,10 +136,11 @@ Then read CLAUDE.md, TODOS.md, and any existing architecture docs.
**Design doc check:**
```bash
setopt +o nomatch 2>/dev/null || true # zsh compat
{{SLUG_EVAL}}
SLUG=$(~/.claude/skills/gstack/browse/bin/remote-slug 2>/dev/null || basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)")
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-' || echo 'no-branch')
DESIGN=$(ls -t ~/.gstack/projects/$SLUG/*-$BRANCH-design-*.md 2>/dev/null | head -1)
[ -z "$DESIGN" ] && DESIGN=$(ls -t ~/.gstack/projects/$SLUG/*-design-*.md 2>/dev/null | head -1)
DESIGN=$(ls -t "$PROJECT_DIR"/*-$BRANCH-design-*.md 2>/dev/null | head -1)
[ -z "$DESIGN" ] && DESIGN=$(ls -t "$PROJECT_DIR"/*-design-*.md 2>/dev/null | head -1)
[ -n "$DESIGN" ] && echo "Design doc found: $DESIGN" || echo "No design doc found"
```
If a design doc exists (from `/office-hours`), read it. Use it as the source of truth for the problem statement, constraints, and chosen approach. If it has a `Supersedes:` field, note that this is a revised design.
@ -327,7 +328,8 @@ eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p ~/.gst
Before writing, check for existing CEO plans in the ceo-plans/ directory. If any are >30 days old or their branch has been merged/deleted, offer to archive them:
```bash
mkdir -p ~/.gstack/projects/$SLUG/ceo-plans/archive
{{SLUG_SETUP}}
mkdir -p "$PROJECT_DIR/ceo-plans/archive"
# For each stale plan: mv ~/.gstack/projects/$SLUG/ceo-plans/{old-plan}.md ~/.gstack/projects/$SLUG/ceo-plans/archive/
```

View File

@ -581,8 +581,8 @@ the review is complete and the context is no longer needed.
```bash
setopt +o nomatch 2>/dev/null || true # zsh compat
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)"
rm -f ~/.gstack/projects/$SLUG/*-$BRANCH-ceo-handoff-*.md 2>/dev/null || true
eval "$(~/.claude/skills/gstack/bin/gstack-paths 2>/dev/null)" && eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && PROJECT_DIR="$GSTACK_ARTIFACTS_ROOT/projects/$SLUG"
rm -f "$PROJECT_DIR"/*-$BRANCH-ceo-handoff-*.md 2>/dev/null || true
```
## Review Log

View File

@ -376,7 +376,7 @@ the review is complete and the context is no longer needed.
```bash
setopt +o nomatch 2>/dev/null || true # zsh compat
{{SLUG_EVAL}}
rm -f ~/.gstack/projects/$SLUG/*-$BRANCH-ceo-handoff-*.md 2>/dev/null || true
rm -f "$PROJECT_DIR"/*-$BRANCH-ceo-handoff-*.md 2>/dev/null || true
```
## Review Log

View File

@ -899,10 +899,11 @@ Then read:
**Design doc check:**
```bash
setopt +o nomatch 2>/dev/null || true
eval "$(~/.claude/skills/gstack/bin/gstack-paths 2>/dev/null)" && eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && PROJECT_DIR="$GSTACK_ARTIFACTS_ROOT/projects/$SLUG"
SLUG=$(~/.claude/skills/gstack/browse/bin/remote-slug 2>/dev/null || basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)")
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-' || echo 'no-branch')
DESIGN=$(ls -t ~/.gstack/projects/$SLUG/*-$BRANCH-design-*.md 2>/dev/null | head -1)
[ -z "$DESIGN" ] && DESIGN=$(ls -t ~/.gstack/projects/$SLUG/*-design-*.md 2>/dev/null | head -1)
DESIGN=$(ls -t "$PROJECT_DIR"/*-$BRANCH-design-*.md 2>/dev/null | head -1)
[ -z "$DESIGN" ] && DESIGN=$(ls -t "$PROJECT_DIR"/*-design-*.md 2>/dev/null | head -1)
[ -n "$DESIGN" ] && echo "Design doc found: $DESIGN" || echo "No design doc found"
```
If a design doc exists, read it.

View File

@ -97,10 +97,11 @@ Then read:
**Design doc check:**
```bash
setopt +o nomatch 2>/dev/null || true
{{SLUG_EVAL}}
SLUG=$(~/.claude/skills/gstack/browse/bin/remote-slug 2>/dev/null || basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)")
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-' || echo 'no-branch')
DESIGN=$(ls -t ~/.gstack/projects/$SLUG/*-$BRANCH-design-*.md 2>/dev/null | head -1)
[ -z "$DESIGN" ] && DESIGN=$(ls -t ~/.gstack/projects/$SLUG/*-design-*.md 2>/dev/null | head -1)
DESIGN=$(ls -t "$PROJECT_DIR"/*-$BRANCH-design-*.md 2>/dev/null | head -1)
[ -z "$DESIGN" ] && DESIGN=$(ls -t "$PROJECT_DIR"/*-design-*.md 2>/dev/null | head -1)
[ -n "$DESIGN" ] && echo "Design doc found: $DESIGN" || echo "No design doc found"
```
If a design doc exists, read it.

View File

@ -824,10 +824,11 @@ sections. Read a section in full before doing its step; do not work from memory.
### Design Doc Check
```bash
setopt +o nomatch 2>/dev/null || true # zsh compat
eval "$(~/.claude/skills/gstack/bin/gstack-paths 2>/dev/null)" && eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && PROJECT_DIR="$GSTACK_ARTIFACTS_ROOT/projects/$SLUG"
SLUG=$(~/.claude/skills/gstack/browse/bin/remote-slug 2>/dev/null || basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)")
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-' || echo 'no-branch')
DESIGN=$(ls -t ~/.gstack/projects/$SLUG/*-$BRANCH-design-*.md 2>/dev/null | head -1)
[ -z "$DESIGN" ] && DESIGN=$(ls -t ~/.gstack/projects/$SLUG/*-design-*.md 2>/dev/null | head -1)
DESIGN=$(ls -t "$PROJECT_DIR"/*-$BRANCH-design-*.md 2>/dev/null | head -1)
[ -z "$DESIGN" ] && DESIGN=$(ls -t "$PROJECT_DIR"/*-design-*.md 2>/dev/null | head -1)
[ -n "$DESIGN" ] && echo "Design doc found: $DESIGN" || echo "No design doc found"
```
If a design doc exists, read it. Use it as the source of truth for the problem statement, constraints, and chosen approach. If it has a `Supersedes:` field, note that this is a revised design — check the prior version for context on what changed and why.

View File

@ -87,10 +87,11 @@ When evaluating architecture, think "boring by default." When reviewing tests, t
### Design Doc Check
```bash
setopt +o nomatch 2>/dev/null || true # zsh compat
{{SLUG_EVAL}}
SLUG=$(~/.claude/skills/gstack/browse/bin/remote-slug 2>/dev/null || basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)")
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-' || echo 'no-branch')
DESIGN=$(ls -t ~/.gstack/projects/$SLUG/*-$BRANCH-design-*.md 2>/dev/null | head -1)
[ -z "$DESIGN" ] && DESIGN=$(ls -t ~/.gstack/projects/$SLUG/*-design-*.md 2>/dev/null | head -1)
DESIGN=$(ls -t "$PROJECT_DIR"/*-$BRANCH-design-*.md 2>/dev/null | head -1)
[ -z "$DESIGN" ] && DESIGN=$(ls -t "$PROJECT_DIR"/*-design-*.md 2>/dev/null | head -1)
[ -n "$DESIGN" ] && echo "Design doc found: $DESIGN" || echo "No design doc found"
```
If a design doc exists, read it. Use it as the source of truth for the problem statement, constraints, and chosen approach. If it has a `Supersedes:` field, note that this is a revised design — check the prior version for context on what changed and why.

View File

@ -836,8 +836,8 @@ Before falling back to git diff heuristics, check for richer test plan sources:
1. **Project-scoped test plans:** Check `~/.gstack/projects/` for recent `*-test-plan-*.md` files for this repo
```bash
setopt +o nomatch 2>/dev/null || true # zsh compat
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)"
ls -t ~/.gstack/projects/$SLUG/*-test-plan-*.md 2>/dev/null | head -1
eval "$(~/.claude/skills/gstack/bin/gstack-paths 2>/dev/null)" && eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && PROJECT_DIR="$GSTACK_ARTIFACTS_ROOT/projects/$SLUG"
ls -t "$PROJECT_DIR"/*-test-plan-*.md 2>/dev/null | head -1
```
2. **Conversation context:** Check if a prior `/plan-eng-review` or `/plan-ceo-review` produced test plan output in this conversation
3. **Use whichever source is richer.** Fall back to git diff analysis only if neither is available.
@ -1132,9 +1132,9 @@ Write the report to both local and project-scoped locations:
**Project-scoped:** Write test outcome artifact for cross-session context:
```bash
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p ~/.gstack/projects/$SLUG
eval "$(~/.claude/skills/gstack/bin/gstack-paths 2>/dev/null)" && eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && PROJECT_DIR="$GSTACK_ARTIFACTS_ROOT/projects/$SLUG" && mkdir -p "$PROJECT_DIR"
```
Write to `~/.gstack/projects/{slug}/{user}-{branch}-test-outcome-{datetime}.md`
Write to `$PROJECT_DIR/{user}-{branch}-test-outcome-{datetime}.md`
### Output Structure

View File

@ -66,7 +66,7 @@ Before falling back to git diff heuristics, check for richer test plan sources:
```bash
setopt +o nomatch 2>/dev/null || true # zsh compat
{{SLUG_EVAL}}
ls -t ~/.gstack/projects/$SLUG/*-test-plan-*.md 2>/dev/null | head -1
ls -t "$PROJECT_DIR"/*-test-plan-*.md 2>/dev/null | head -1
```
2. **Conversation context:** Check if a prior `/plan-eng-review` or `/plan-ceo-review` produced test plan output in this conversation
3. **Use whichever source is richer.** Fall back to git diff analysis only if neither is available.
@ -87,7 +87,7 @@ Write the report to both local and project-scoped locations:
```bash
{{SLUG_SETUP}}
```
Write to `~/.gstack/projects/{slug}/{user}-{branch}-test-outcome-{datetime}.md`
Write to `$PROJECT_DIR/{user}-{branch}-test-outcome-{datetime}.md`
### Output Structure

View File

@ -1068,8 +1068,8 @@ Before falling back to git diff heuristics, check for richer test plan sources:
1. **Project-scoped test plans:** Check `~/.gstack/projects/` for recent `*-test-plan-*.md` files for this repo
```bash
setopt +o nomatch 2>/dev/null || true # zsh compat
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)"
ls -t ~/.gstack/projects/$SLUG/*-test-plan-*.md 2>/dev/null | head -1
eval "$(~/.claude/skills/gstack/bin/gstack-paths 2>/dev/null)" && eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && PROJECT_DIR="$GSTACK_ARTIFACTS_ROOT/projects/$SLUG"
ls -t "$PROJECT_DIR"/*-test-plan-*.md 2>/dev/null | head -1
```
2. **Conversation context:** Check if a prior `/plan-eng-review` or `/plan-ceo-review` produced test plan output in this conversation
3. **Use whichever source is richer.** Fall back to git diff analysis only if neither is available.
@ -1546,9 +1546,9 @@ Write the report to both local and project-scoped locations:
**Project-scoped:** Write test outcome artifact for cross-session context:
```bash
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p ~/.gstack/projects/$SLUG
eval "$(~/.claude/skills/gstack/bin/gstack-paths 2>/dev/null)" && eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && PROJECT_DIR="$GSTACK_ARTIFACTS_ROOT/projects/$SLUG" && mkdir -p "$PROJECT_DIR"
```
Write to `~/.gstack/projects/{slug}/{user}-{branch}-test-outcome-{datetime}.md`
Write to `$PROJECT_DIR/{user}-{branch}-test-outcome-{datetime}.md`
**Per-issue additions** (beyond standard report template):
- Fix Status: verified / best-effort / reverted / deferred

View File

@ -110,7 +110,7 @@ Before falling back to git diff heuristics, check for richer test plan sources:
```bash
setopt +o nomatch 2>/dev/null || true # zsh compat
{{SLUG_EVAL}}
ls -t ~/.gstack/projects/$SLUG/*-test-plan-*.md 2>/dev/null | head -1
ls -t "$PROJECT_DIR"/*-test-plan-*.md 2>/dev/null | head -1
```
2. **Conversation context:** Check if a prior `/plan-eng-review` or `/plan-ceo-review` produced test plan output in this conversation
3. **Use whichever source is richer.** Fall back to git diff analysis only if neither is available.
@ -313,7 +313,7 @@ Write the report to both local and project-scoped locations:
```bash
{{SLUG_SETUP}}
```
Write to `~/.gstack/projects/{slug}/{user}-{branch}-test-outcome-{datetime}.md`
Write to `$PROJECT_DIR/{user}-{branch}-test-outcome-{datetime}.md`
**Per-issue additions** (beyond standard report template):
- Fix Status: verified / best-effort / reverted / deferred

View File

@ -1,11 +1,11 @@
import type { TemplateContext } from './types';
export function generateSlugEval(ctx: TemplateContext): string {
return `eval "$(${ctx.paths.binDir}/gstack-slug 2>/dev/null)"`;
return `eval "$(${ctx.paths.binDir}/gstack-paths 2>/dev/null)" && eval "$(${ctx.paths.binDir}/gstack-slug 2>/dev/null)" && PROJECT_DIR="$GSTACK_ARTIFACTS_ROOT/projects/$SLUG"`;
}
export function generateSlugSetup(ctx: TemplateContext): string {
return `eval "$(${ctx.paths.binDir}/gstack-slug 2>/dev/null)" && mkdir -p ~/.gstack/projects/$SLUG`;
return `eval "$(${ctx.paths.binDir}/gstack-paths 2>/dev/null)" && eval "$(${ctx.paths.binDir}/gstack-slug 2>/dev/null)" && PROJECT_DIR="$GSTACK_ARTIFACTS_ROOT/projects/$SLUG" && mkdir -p "$PROJECT_DIR"`;
}
export function generateBaseBranchDetect(_ctx: TemplateContext): string {

View File

@ -1,5 +1,7 @@
import { describe, test, expect } from 'bun:test';
import { spawnSync } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
const ROOT = path.resolve(import.meta.dir, '..');
@ -15,10 +17,11 @@ const BIN = path.join(ROOT, 'bin', 'gstack-paths');
// HOME from USERPROFILE at shell startup if HOME is unset/empty, which
// silently breaks the "HOME unset" test scenarios. Clearing USERPROFILE
// alongside HOME prevents that auto-population on Windows runners.
function run(env: Record<string, string | undefined>): Record<string, string> {
function run(env: Record<string, string | undefined>, cwd?: string): Record<string, string> {
const result = spawnSync('bash', [BIN], {
env: { PATH: process.env.PATH, USERPROFILE: '', ...env } as Record<string, string>,
encoding: 'utf-8',
cwd,
});
if (result.status !== 0) {
throw new Error(`gstack-paths failed (status ${result.status}): ${result.stderr}`);
@ -100,10 +103,53 @@ describe('gstack-paths', () => {
test('emits all three exports on every invocation', () => {
const got = run({ HOME: '/tmp/h' });
expect(got).toHaveProperty('GSTACK_STATE_ROOT');
expect(got).toHaveProperty('GSTACK_ARTIFACTS_ROOT');
expect(got).toHaveProperty('PLAN_ROOT');
expect(got).toHaveProperty('TMP_ROOT');
});
test('artifacts_root=repo writes skill artifacts under the current repo', () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-artifacts-root-'));
const home = path.join(tmp, 'home');
const repo = path.join(tmp, 'repo');
fs.mkdirSync(path.join(home, '.gstack'), { recursive: true });
fs.mkdirSync(repo, { recursive: true });
fs.writeFileSync(path.join(home, '.gstack', 'config.yaml'), 'artifacts_root: repo\n');
spawnSync('git', ['init'], { cwd: repo, encoding: 'utf-8' });
const got = run({ HOME: home }, repo);
expect(got.GSTACK_STATE_ROOT).toBe(path.join(home, '.gstack'));
expect(got.GSTACK_ARTIFACTS_ROOT).toBe(path.join(fs.realpathSync(repo), '.gstack'));
});
test('SLUG_SETUP uses GSTACK_ARTIFACTS_ROOT instead of hardcoded ~/.gstack', async () => {
const { generateSlugEval, generateSlugSetup } = await import('../scripts/resolvers/utility');
const ctx = {
skillName: 'test-skill',
tmplPath: 'test/SKILL.md.tmpl',
host: 'claude',
paths: {
skillRoot: '~/.claude/skills/gstack',
localSkillRoot: '.claude/skills/gstack',
binDir: '~/.claude/skills/gstack/bin',
browseDir: '~/.claude/skills/gstack/browse/dist',
designDir: '~/.claude/skills/gstack/design/dist',
makePdfDir: '~/.claude/skills/gstack/make-pdf/dist',
},
} as const;
const rendered = generateSlugSetup({
...ctx,
});
const evalRendered = generateSlugEval({ ...ctx });
expect(rendered).toContain('gstack-paths');
expect(rendered).toContain('"$GSTACK_ARTIFACTS_ROOT/projects/$SLUG"');
expect(evalRendered).toContain('gstack-paths');
expect(evalRendered).toContain('PROJECT_DIR="$GSTACK_ARTIFACTS_ROOT/projects/$SLUG"');
expect(rendered).not.toContain('~/.gstack/projects/$SLUG');
});
test('output is shell-evalable: only KEY=VALUE lines, no extra prose', () => {
const result = spawnSync('bash', [BIN], {
env: { PATH: process.env.PATH, USERPROFILE: '', HOME: '/tmp/h' } as Record<string, string>,

View File

@ -121,3 +121,43 @@ describe('gstack-config: plan_tune_hooks key', () => {
expect(got).toBe('prompt');
});
});
describe('gstack-config: artifacts_root key', () => {
let tmpHome: string;
let env: NodeJS.ProcessEnv;
beforeAll(() => {
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-artifacts-root-cfg-'));
env = { ...process.env, GSTACK_HOME: tmpHome };
});
afterAll(() => {
fs.rmSync(tmpHome, { recursive: true, force: true });
});
test('defaults to state and appears in defaults/list output', () => {
const out = execSync(`${GSTACK_CONFIG} get artifacts_root`, {
encoding: 'utf-8',
env,
}).trim();
expect(out).toBe('state');
const defaults = execSync(`${GSTACK_CONFIG} defaults`, { encoding: 'utf-8', env });
expect(defaults).toContain('artifacts_root');
const list = execSync(`${GSTACK_CONFIG} list`, { encoding: 'utf-8', env });
expect(list).toContain('artifacts_root');
});
test('accepts state/repo and rejects out-of-domain values', () => {
for (const v of ['state', 'repo']) {
execSync(`${GSTACK_CONFIG} set artifacts_root ${v}`, { encoding: 'utf-8', env });
const got = execSync(`${GSTACK_CONFIG} get artifacts_root`, { encoding: 'utf-8', env }).trim();
expect(got).toBe(v);
}
const res = execSync(`${GSTACK_CONFIG} set artifacts_root elsewhere 2>&1`, { encoding: 'utf-8', env });
expect(res.toLowerCase()).toContain('not recognized');
const got = execSync(`${GSTACK_CONFIG} get artifacts_root`, { encoding: 'utf-8', env }).trim();
expect(got).toBe('state');
});
});