Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion autoplan/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@ Then prepend a one-line HTML comment to the plan file:
### Step 2: Read context

- Read CLAUDE.md, TODOS.md, git log -30, git diff against the base branch --stat
- Discover design docs: `ls -t ~/.gstack/projects/$SLUG/*-design-*.md 2>/dev/null | head -1`
- Discover design docs: `~/.claude/skills/gstack/bin/gstack-find-artifact design-doc 2>/dev/null || true`
- Detect UI scope: grep the plan for view/rendering terms (component, screen, form,
button, modal, layout, dashboard, sidebar, nav, dialog). Require 2+ matches. Exclude
false positives ("page" alone, "UI" in acronyms).
Expand Down
2 changes: 1 addition & 1 deletion autoplan/SKILL.md.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ Then prepend a one-line HTML comment to the plan file:
### Step 2: Read context

- Read CLAUDE.md, TODOS.md, git log -30, git diff against the base branch --stat
- Discover design docs: `ls -t ~/.gstack/projects/$SLUG/*-design-*.md 2>/dev/null | head -1`
- Discover design docs: `~/.claude/skills/gstack/bin/gstack-find-artifact design-doc 2>/dev/null || true`
- Detect UI scope: grep the plan for view/rendering terms (component, screen, form,
button, modal, layout, dashboard, sidebar, nav, dialog). Require 2+ matches. Exclude
false positives ("page" alone, "UI" in acronyms).
Expand Down
87 changes: 87 additions & 0 deletions bin/gstack-find-artifact
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#!/usr/bin/env bash
# gstack-find-artifact — locate the best project artifact for the current repo/branch
# Usage: gstack-find-artifact design-doc|ceo-plan
set -euo pipefail

ARTIFACT="${1:-}"
if [ -z "$ARTIFACT" ]; then
echo "Usage: gstack-find-artifact design-doc|ceo-plan" >&2
exit 1
fi

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null || true)"
SLUG="${SLUG:-}"
BRANCH="${BRANCH:-}"

if [ -z "$SLUG" ]; then
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
SLUG="$(basename "$ROOT" | tr -cd 'a-zA-Z0-9._-')"
fi

if [ -z "$BRANCH" ]; then
BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-' | tr -cd 'a-zA-Z0-9._-' || echo 'no-branch')"
fi

GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
PROJECT_DIR="$GSTACK_HOME/projects/$SLUG"

shopt -s nullglob

latest_file() {
[ "$#" -gt 0 ] || return 1
ls -t "$@" 2>/dev/null | head -1
}

case "$ARTIFACT" in
design-doc)
branch_docs=("$PROJECT_DIR"/*-"$BRANCH"-design-*.md)
if [ ${#branch_docs[@]} -gt 0 ]; then
latest_file "${branch_docs[@]}"
exit 0
fi

all_docs=("$PROJECT_DIR"/*-design-*.md)
if [ ${#all_docs[@]} -gt 0 ]; then
latest_file "${all_docs[@]}"
exit 0
fi
;;

ceo-plan)
plan_dir="$PROJECT_DIR/ceo-plans"
all_plans=("$plan_dir"/*.md)
if [ ${#all_plans[@]} -gt 0 ]; then
same_branch_active=()
active_plans=()
for plan in "${all_plans[@]}"; do
if grep -q '^status: ACTIVE$' "$plan" 2>/dev/null; then
active_plans+=("$plan")
if grep -q "^Branch: $BRANCH\$" "$plan" 2>/dev/null; then
same_branch_active+=("$plan")
fi
fi
done

if [ ${#same_branch_active[@]} -gt 0 ]; then
latest_file "${same_branch_active[@]}"
exit 0
fi

if [ ${#active_plans[@]} -gt 0 ]; then
latest_file "${active_plans[@]}"
exit 0
fi

latest_file "${all_plans[@]}"
exit 0
fi
;;

*)
echo "Unknown artifact type: $ARTIFACT" >&2
exit 1
;;
esac

exit 1
12 changes: 11 additions & 1 deletion bin/gstack-review-log
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,17 @@
# Usage: gstack-review-log '{"skill":"...","timestamp":"...","status":"..."}'
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null)"
eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null || true)"
SLUG="${SLUG:-}"
BRANCH="${BRANCH:-}"
if [ -z "$SLUG" ]; then
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
SLUG="$(basename "$ROOT" | tr -cd 'a-zA-Z0-9._-')"
fi
if [ -z "$BRANCH" ]; then
BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-' | tr -cd 'a-zA-Z0-9._-' || echo 'no-branch')"
fi
GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
mkdir -p "$GSTACK_HOME/projects/$SLUG"
echo "$1" >> "$GSTACK_HOME/projects/$SLUG/reviews.jsonl"
echo "$1" >> "$GSTACK_HOME/projects/$SLUG/$BRANCH-reviews.jsonl"
25 changes: 23 additions & 2 deletions bin/gstack-review-read
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,30 @@
# Usage: gstack-review-read
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null)"
eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null || true)"
SLUG="${SLUG:-}"
BRANCH="${BRANCH:-}"
if [ -z "$SLUG" ]; then
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
SLUG="$(basename "$ROOT" | tr -cd 'a-zA-Z0-9._-')"
fi
if [ -z "$BRANCH" ]; then
BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-' | tr -cd 'a-zA-Z0-9._-' || echo 'no-branch')"
fi
GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
cat "$GSTACK_HOME/projects/$SLUG/$BRANCH-reviews.jsonl" 2>/dev/null || echo "NO_REVIEWS"
PROJECT_DIR="$GSTACK_HOME/projects/$SLUG"
PROJECT_LOG="$PROJECT_DIR/reviews.jsonl"
if [ -s "$PROJECT_LOG" ]; then
cat "$PROJECT_LOG"
else
shopt -s nullglob
LEGACY_LOGS=("$PROJECT_DIR"/*-reviews.jsonl)
if [ ${#LEGACY_LOGS[@]} -gt 0 ]; then
cat "${LEGACY_LOGS[@]}"
else
echo "NO_REVIEWS"
fi
fi
echo "---CONFIG---"
"$SCRIPT_DIR/gstack-config" get skip_eng_review 2>/dev/null || echo "false"
echo "---HEAD---"
Expand Down
3 changes: 1 addition & 2 deletions plan-ceo-review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -399,8 +399,7 @@ Then read CLAUDE.md, TODOS.md, and any existing architecture docs.
```bash
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=$(~/.claude/skills/gstack/bin/gstack-find-artifact design-doc 2>/dev/null || true)
[ -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.
Expand Down
3 changes: 1 addition & 2 deletions plan-ceo-review/SKILL.md.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,7 @@ Then read CLAUDE.md, TODOS.md, and any existing architecture docs.
```bash
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=$(~/.claude/skills/gstack/bin/gstack-find-artifact design-doc 2>/dev/null || true)
[ -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.
Expand Down
8 changes: 4 additions & 4 deletions plan-eng-review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,13 +343,13 @@ When evaluating architecture, think "boring by default." When reviewing tests, t

### Design Doc Check
```bash
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=$(~/.claude/skills/gstack/bin/gstack-find-artifact design-doc 2>/dev/null || true)
CEO_PLAN=$(~/.claude/skills/gstack/bin/gstack-find-artifact ceo-plan 2>/dev/null || true)
[ -n "$DESIGN" ] && echo "Design doc found: $DESIGN" || echo "No design doc found"
[ -n "$CEO_PLAN" ] && echo "CEO plan found: $CEO_PLAN" || echo "No CEO plan 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.
If a CEO plan exists, read it too. Use it as the cross-branch scope and vision handoff, especially when the current branch has not produced a fresh design doc yet.

## Prerequisite Skill Offer

Expand Down
8 changes: 4 additions & 4 deletions plan-eng-review/SKILL.md.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,13 @@ When evaluating architecture, think "boring by default." When reviewing tests, t

### Design Doc Check
```bash
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=$(~/.claude/skills/gstack/bin/gstack-find-artifact design-doc 2>/dev/null || true)
CEO_PLAN=$(~/.claude/skills/gstack/bin/gstack-find-artifact ceo-plan 2>/dev/null || true)
[ -n "$DESIGN" ] && echo "Design doc found: $DESIGN" || echo "No design doc found"
[ -n "$CEO_PLAN" ] && echo "CEO plan found: $CEO_PLAN" || echo "No CEO plan 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.
If a CEO plan exists, read it too. Use it as the cross-branch scope and vision handoff, especially when the current branch has not produced a fresh design doc yet.

{{BENEFITS_FROM}}

Expand Down
122 changes: 122 additions & 0 deletions test/workflow-handoff.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "fs";
import { join } from "path";
import { tmpdir } from "os";
import { spawnSync } from "child_process";

const ROOT = join(import.meta.dir, "..");
const reviewLogBin = join(ROOT, "bin", "gstack-review-log");
const reviewReadBin = join(ROOT, "bin", "gstack-review-read");
const findArtifactBin = join(ROOT, "bin", "gstack-find-artifact");

function run(cmd: string, args: string[], cwd: string, env: Record<string, string>) {
const result = spawnSync(cmd, args, {
cwd,
env: { ...process.env, ...env },
encoding: "utf-8",
timeout: 10000,
});
return result;
}

function initRepo(): { repoDir: string; stateDir: string; projectDir: string } {
const repoDir = mkdtempSync(join(tmpdir(), "gstack-workflow-"));
const stateDir = mkdtempSync(join(tmpdir(), "gstack-state-"));

run("git", ["init", "-b", "main"], repoDir, {});
run("git", ["config", "user.name", "Test User"], repoDir, {});
run("git", ["config", "user.email", "test@example.com"], repoDir, {});
run("git", ["remote", "add", "origin", "https://github.com/example-org/example-repo.git"], repoDir, {});
writeFileSync(join(repoDir, "README.md"), "test\n");
run("git", ["add", "README.md"], repoDir, {});
run("git", ["commit", "-m", "initial"], repoDir, {});

const projectDir = join(stateDir, "projects", "example-org-example-repo");
mkdirSync(projectDir, { recursive: true });

return { repoDir, stateDir, projectDir };
}

describe("workflow handoff helpers", () => {
let repoDir: string;
let stateDir: string;
let projectDir: string;

beforeEach(() => {
({ repoDir, stateDir, projectDir } = initRepo());
});

afterEach(() => {
rmSync(repoDir, { recursive: true, force: true });
rmSync(stateDir, { recursive: true, force: true });
});

test("gstack-review-read sees reviews logged on a different branch via project-scoped JSONL", () => {
let result = run("git", ["checkout", "-b", "feature/one"], repoDir, {});
expect(result.status).toBe(0);

result = run("bash", [reviewLogBin, '{"skill":"review","timestamp":"2026-03-23T10:00:00Z","status":"clean"}'], repoDir, {
GSTACK_HOME: stateDir,
});
expect(result.status).toBe(0);

result = run("git", ["checkout", "main"], repoDir, {});
expect(result.status).toBe(0);
result = run("git", ["checkout", "-b", "feature/two"], repoDir, {});
expect(result.status).toBe(0);

result = run("bash", [reviewReadBin], repoDir, { GSTACK_HOME: stateDir });
expect(result.status).toBe(0);
expect(result.stdout).toContain('"skill":"review"');
expect(result.stdout).toContain('2026-03-23T10:00:00Z');
});

test("gstack-review-read falls back to legacy per-branch logs when project log does not exist", () => {
writeFileSync(
join(projectDir, "feature-one-reviews.jsonl"),
'{"skill":"plan-ceo-review","timestamp":"2026-03-23T09:00:00Z","status":"clean"}\n'
);
writeFileSync(
join(projectDir, "feature-two-reviews.jsonl"),
'{"skill":"review","timestamp":"2026-03-23T11:00:00Z","status":"clean"}\n'
);

const result = run("bash", [reviewReadBin], repoDir, { GSTACK_HOME: stateDir });
expect(result.status).toBe(0);
expect(result.stdout).toContain('"skill":"plan-ceo-review"');
expect(result.stdout).toContain('"skill":"review"');
});

test("gstack-find-artifact prefers the current branch design doc", () => {
run("git", ["checkout", "-b", "feature/two"], repoDir, {});
writeFileSync(
join(projectDir, "alice-feature-one-design-20260323-090000.md"),
"Branch: feature-one\nStatus: DRAFT\n"
);
writeFileSync(
join(projectDir, "alice-feature-two-design-20260323-100000.md"),
"Branch: feature-two\nStatus: DRAFT\n"
);

const result = run("bash", [findArtifactBin, "design-doc"], repoDir, { GSTACK_HOME: stateDir });
expect(result.status).toBe(0);
expect(result.stdout.trim()).toContain("feature-two-design");
});

test("gstack-find-artifact finds the active CEO plan for the current branch", () => {
run("git", ["checkout", "-b", "feature/two"], repoDir, {});
mkdirSync(join(projectDir, "ceo-plans"), { recursive: true });
writeFileSync(
join(projectDir, "ceo-plans", "2026-03-20-other-plan.md"),
"---\nstatus: ACTIVE\n---\nBranch: feature-one\n"
);
writeFileSync(
join(projectDir, "ceo-plans", "2026-03-21-current-plan.md"),
"---\nstatus: ACTIVE\n---\nBranch: feature-two\n"
);

const result = run("bash", [findArtifactBin, "ceo-plan"], repoDir, { GSTACK_HOME: stateDir });
expect(result.status).toBe(0);
expect(result.stdout.trim()).toContain("current-plan");
});
});
Loading