From 13850ec388e46e5b98f97a57531832876590200f Mon Sep 17 00:00:00 2001 From: maxpetrusenkoagent Date: Sun, 14 Jun 2026 10:08:01 -0400 Subject: [PATCH] feat: allow repo-local gstack artifacts --- autoplan/SKILL.md | 4 +- autoplan/SKILL.md.tmpl | 2 +- bin/gstack-config | 17 ++++++- bin/gstack-paths | 28 +++++++++-- canary/SKILL.md | 4 +- canary/SKILL.md.tmpl | 2 +- context-restore/SKILL.md | 4 +- context-restore/SKILL.md.tmpl | 2 +- context-save/SKILL.md | 10 ++-- context-save/SKILL.md.tmpl | 4 +- design-consultation/SKILL.md | 2 +- design-review/SKILL.md | 4 +- design-review/SKILL.md.tmpl | 2 +- health/SKILL.md | 10 ++-- health/SKILL.md.tmpl | 6 +-- land-and-deploy/SKILL.md | 14 +++--- land-and-deploy/SKILL.md.tmpl | 10 ++-- office-hours/SKILL.md | 6 +-- office-hours/SKILL.md.tmpl | 4 +- office-hours/sections/design-and-handoff.md | 6 +-- .../sections/design-and-handoff.md.tmpl | 4 +- plan-ceo-review/SKILL.md | 8 ++-- plan-ceo-review/SKILL.md.tmpl | 8 ++-- plan-ceo-review/sections/review-sections.md | 4 +- .../sections/review-sections.md.tmpl | 2 +- plan-devex-review/SKILL.md | 5 +- plan-devex-review/SKILL.md.tmpl | 5 +- plan-eng-review/SKILL.md | 5 +- plan-eng-review/SKILL.md.tmpl | 5 +- qa-only/SKILL.md | 8 ++-- qa-only/SKILL.md.tmpl | 4 +- qa/SKILL.md | 8 ++-- qa/SKILL.md.tmpl | 4 +- scripts/resolvers/utility.ts | 4 +- test/gstack-paths.test.ts | 48 ++++++++++++++++++- ...tup-plan-tune-hooks-noninteractive.test.ts | 40 ++++++++++++++++ 36 files changed, 215 insertions(+), 88 deletions(-) diff --git a/autoplan/SKILL.md b/autoplan/SKILL.md index 5d5f6334c5..59a09ee40f 100644 --- a/autoplan/SKILL.md +++ b/autoplan/SKILL.md @@ -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: diff --git a/autoplan/SKILL.md.tmpl b/autoplan/SKILL.md.tmpl index 888cddabbc..2a7520f264 100644 --- a/autoplan/SKILL.md.tmpl +++ b/autoplan/SKILL.md.tmpl @@ -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: diff --git a/bin/gstack-config b/bin/gstack-config index 01defcef86..d3cef8c2f6 100755 --- a/bin/gstack-config +++ b/bin/gstack-config @@ -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 ;; diff --git a/bin/gstack-paths b/bin/gstack-paths index 1a7e073065..90db58e16d 100755 --- a/bin/gstack-paths +++ b/bin/gstack-paths @@ -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 -> /.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" diff --git a/canary/SKILL.md b/canary/SKILL.md index 1a47571a18..b22823f69c 100644 --- a/canary/SKILL.md +++ b/canary/SKILL.md @@ -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":"","status":"","url":"","duration_min":,"alerts":}` diff --git a/canary/SKILL.md.tmpl b/canary/SKILL.md.tmpl index d1eb2950ab..bf32ac6500 100644 --- a/canary/SKILL.md.tmpl +++ b/canary/SKILL.md.tmpl @@ -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":"","status":"","url":"","duration_min":,"alerts":}` diff --git a/context-restore/SKILL.md b/context-restore/SKILL.md index d9bf73a727..1514146df7 100644 --- a/context-restore/SKILL.md +++ b/context-restore/SKILL.md @@ -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 diff --git a/context-restore/SKILL.md.tmpl b/context-restore/SKILL.md.tmpl index 55889f6e06..624fddbe7d 100644 --- a/context-restore/SKILL.md.tmpl +++ b/context-restore/SKILL.md.tmpl @@ -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 diff --git a/context-save/SKILL.md b/context-save/SKILL.md index 05f06724e9..e4a227b218 100644 --- a/context-save/SKILL.md +++ b/context-save/SKILL.md @@ -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 diff --git a/context-save/SKILL.md.tmpl b/context-save/SKILL.md.tmpl index a3702bc954..5491c40cf6 100644 --- a/context-save/SKILL.md.tmpl +++ b/context-save/SKILL.md.tmpl @@ -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 diff --git a/design-consultation/SKILL.md b/design-consultation/SKILL.md index f5e15a5c7f..11c5bbd39f 100644 --- a/design-consultation/SKILL.md +++ b/design-consultation/SKILL.md @@ -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 ``` diff --git a/design-review/SKILL.md b/design-review/SKILL.md index ff95aeef34..4c2d788e64 100644 --- a/design-review/SKILL.md +++ b/design-review/SKILL.md @@ -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 diff --git a/design-review/SKILL.md.tmpl b/design-review/SKILL.md.tmpl index bdcda48e29..e2d5cb33d7 100644 --- a/design-review/SKILL.md.tmpl +++ b/design-review/SKILL.md.tmpl @@ -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 diff --git a/health/SKILL.md b/health/SKILL.md index 02696dafb6..d629bc0239 100644 --- a/health/SKILL.md +++ b/health/SKILL.md @@ -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:** diff --git a/health/SKILL.md.tmpl b/health/SKILL.md.tmpl index f92eb7347e..343aa6b3c9 100644 --- a/health/SKILL.md.tmpl +++ b/health/SKILL.md.tmpl @@ -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:** diff --git a/land-and-deploy/SKILL.md b/land-and-deploy/SKILL.md index e576fdf7ac..87628603d4 100644 --- a/land-and-deploy/SKILL.md +++ b/land-and-deploy/SKILL.md @@ -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: diff --git a/land-and-deploy/SKILL.md.tmpl b/land-and-deploy/SKILL.md.tmpl index 98976ad020..682d333e9e 100644 --- a/land-and-deploy/SKILL.md.tmpl +++ b/land-and-deploy/SKILL.md.tmpl @@ -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: diff --git a/office-hours/SKILL.md b/office-hours/SKILL.md index cbf3353268..1b23da7c59 100644 --- a/office-hours/SKILL.md +++ b/office-hours/SKILL.md @@ -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 "\|\|" ~/.gstack/projects/$SLUG/*-design-*.md 2>/dev/null +grep -li "\|\|" "$PROJECT_DIR"/*-design-*.md 2>/dev/null ``` If matches found, read the matching design docs and surface them: diff --git a/office-hours/SKILL.md.tmpl b/office-hours/SKILL.md.tmpl index 8568fe73cc..f27bc37f3a 100644 --- a/office-hours/SKILL.md.tmpl +++ b/office-hours/SKILL.md.tmpl @@ -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 "\|\|" ~/.gstack/projects/$SLUG/*-design-*.md 2>/dev/null +grep -li "\|\|" "$PROJECT_DIR"/*-design-*.md 2>/dev/null ``` If matches found, read the matching design docs and surface them: diff --git a/office-hours/sections/design-and-handoff.md b/office-hours/sections/design-and-handoff.md index 9d3f2e19b1..fa0649b88e 100644 --- a/office-hours/sections/design-and-handoff.md +++ b/office-hours/sections/design-and-handoff.md @@ -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."** diff --git a/office-hours/sections/design-and-handoff.md.tmpl b/office-hours/sections/design-and-handoff.md.tmpl index 8acfe1f329..b49584b1a5 100644 --- a/office-hours/sections/design-and-handoff.md.tmpl +++ b/office-hours/sections/design-and-handoff.md.tmpl @@ -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."** diff --git a/plan-ceo-review/SKILL.md b/plan-ceo-review/SKILL.md index e7d4bf68d9..672f74159c 100644 --- a/plan-ceo-review/SKILL.md +++ b/plan-ceo-review/SKILL.md @@ -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/ ``` diff --git a/plan-ceo-review/SKILL.md.tmpl b/plan-ceo-review/SKILL.md.tmpl index fd95b5d134..41e42c5b12 100644 --- a/plan-ceo-review/SKILL.md.tmpl +++ b/plan-ceo-review/SKILL.md.tmpl @@ -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/ ``` diff --git a/plan-ceo-review/sections/review-sections.md b/plan-ceo-review/sections/review-sections.md index 9da3ee88ee..f0f7bc5bc7 100644 --- a/plan-ceo-review/sections/review-sections.md +++ b/plan-ceo-review/sections/review-sections.md @@ -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 diff --git a/plan-ceo-review/sections/review-sections.md.tmpl b/plan-ceo-review/sections/review-sections.md.tmpl index 133c8a27a7..3c2ede116a 100644 --- a/plan-ceo-review/sections/review-sections.md.tmpl +++ b/plan-ceo-review/sections/review-sections.md.tmpl @@ -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 diff --git a/plan-devex-review/SKILL.md b/plan-devex-review/SKILL.md index af3a3eeec0..615aefd5dc 100644 --- a/plan-devex-review/SKILL.md +++ b/plan-devex-review/SKILL.md @@ -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. diff --git a/plan-devex-review/SKILL.md.tmpl b/plan-devex-review/SKILL.md.tmpl index 1ef723c109..17d0ae5542 100644 --- a/plan-devex-review/SKILL.md.tmpl +++ b/plan-devex-review/SKILL.md.tmpl @@ -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. diff --git a/plan-eng-review/SKILL.md b/plan-eng-review/SKILL.md index 1b5b3d90dc..64741c8723 100644 --- a/plan-eng-review/SKILL.md +++ b/plan-eng-review/SKILL.md @@ -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. diff --git a/plan-eng-review/SKILL.md.tmpl b/plan-eng-review/SKILL.md.tmpl index 73953afe41..9733e98ddb 100644 --- a/plan-eng-review/SKILL.md.tmpl +++ b/plan-eng-review/SKILL.md.tmpl @@ -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. diff --git a/qa-only/SKILL.md b/qa-only/SKILL.md index 74a1fed929..2425f52d8a 100644 --- a/qa-only/SKILL.md +++ b/qa-only/SKILL.md @@ -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 diff --git a/qa-only/SKILL.md.tmpl b/qa-only/SKILL.md.tmpl index 75c4123cc5..86cb253a88 100644 --- a/qa-only/SKILL.md.tmpl +++ b/qa-only/SKILL.md.tmpl @@ -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 diff --git a/qa/SKILL.md b/qa/SKILL.md index 053b707a5c..cf07213d01 100644 --- a/qa/SKILL.md +++ b/qa/SKILL.md @@ -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 diff --git a/qa/SKILL.md.tmpl b/qa/SKILL.md.tmpl index 11997f7b87..52779c8fec 100644 --- a/qa/SKILL.md.tmpl +++ b/qa/SKILL.md.tmpl @@ -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 diff --git a/scripts/resolvers/utility.ts b/scripts/resolvers/utility.ts index 3d2e368a29..9513f4aa04 100644 --- a/scripts/resolvers/utility.ts +++ b/scripts/resolvers/utility.ts @@ -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 { diff --git a/test/gstack-paths.test.ts b/test/gstack-paths.test.ts index 42c13c3acd..fc0319a828 100644 --- a/test/gstack-paths.test.ts +++ b/test/gstack-paths.test.ts @@ -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): Record { +function run(env: Record, cwd?: string): Record { const result = spawnSync('bash', [BIN], { env: { PATH: process.env.PATH, USERPROFILE: '', ...env } as Record, 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, diff --git a/test/setup-plan-tune-hooks-noninteractive.test.ts b/test/setup-plan-tune-hooks-noninteractive.test.ts index 9a0f03dede..add44aa4cc 100644 --- a/test/setup-plan-tune-hooks-noninteractive.test.ts +++ b/test/setup-plan-tune-hooks-noninteractive.test.ts @@ -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'); + }); +});