Merge remote-tracking branch 'origin/main' into fix/codex-auth-auq-fallback

# Conflicts:
#	SKILL.md
#	autoplan/SKILL.md
#	benchmark-models/SKILL.md
#	benchmark/SKILL.md
#	browse/SKILL.md
#	canary/SKILL.md
#	codex/SKILL.md
#	context-restore/SKILL.md
#	context-save/SKILL.md
#	cso/SKILL.md
#	design-consultation/SKILL.md
#	design-html/SKILL.md
#	design-review/SKILL.md
#	design-shotgun/SKILL.md
#	devex-review/SKILL.md
#	document-generate/SKILL.md
#	document-release/SKILL.md
#	health/SKILL.md
#	investigate/SKILL.md
#	ios-clean/SKILL.md
#	ios-design-review/SKILL.md
#	ios-fix/SKILL.md
#	ios-qa/SKILL.md
#	ios-sync/SKILL.md
#	land-and-deploy/SKILL.md
#	landing-report/SKILL.md
#	learn/SKILL.md
#	make-pdf/SKILL.md
#	office-hours/SKILL.md
#	open-gstack-browser/SKILL.md
#	pair-agent/SKILL.md
#	plan-ceo-review/SKILL.md
#	plan-design-review/SKILL.md
#	plan-devex-review/SKILL.md
#	plan-eng-review/SKILL.md
#	plan-tune/SKILL.md
#	qa-only/SKILL.md
#	qa/SKILL.md
#	retro/SKILL.md
#	review/SKILL.md
#	scrape/SKILL.md
#	scripts/resolvers/preamble/generate-ask-user-format.ts
#	scripts/resolvers/preamble/generate-completion-status.ts
#	setup-browser-cookies/SKILL.md
#	setup-deploy/SKILL.md
#	setup-gbrain/SKILL.md
#	ship/SKILL.md
#	skillify/SKILL.md
#	spec/SKILL.md
#	sync-gbrain/SKILL.md
This commit is contained in:
Byron Hsu 2026-06-11 14:13:22 -04:00
commit 4e74b27b59
194 changed files with 12360 additions and 2866 deletions

View File

@ -162,6 +162,12 @@ jobs:
permissions:
contents: read
pull-requests: write
# The comment upsert below calls the REST `/issues/{n}/comments` endpoints
# (gh api ... issues/comments). With GITHUB_TOKEN those are gated by the
# `issues` permission, not `pull-requests` — without it the GET returns 401
# on every PR that produces eval artifacts (PRs with no artifacts exit
# early and never hit it, which is why this stayed hidden). See #1802 CI fix.
issues: write
steps:
- uses: actions/checkout@v4
with:

View File

@ -1,7 +1,25 @@
name: PR Title Sync
# WHY pull_request_target (not pull_request): the default GITHUB_TOKEN is
# READ-ONLY on fork PRs under `pull_request`, so the title-sync backstop could
# never `gh pr edit` a fork/agent PR. `pull_request_target` runs in the base-repo
# context with a write token, which fixes fork coverage.
#
# WHY this is SAFE (pull_request_target is the most dangerous trigger):
# - We check out the BASE repo (no `ref:`), so the only code we execute is
# trusted base-repo infra (bin/gstack-pr-title-rewrite.sh). We NEVER check
# out or run PR-head/fork code.
# - Every attacker-controlled PR field (title, head repo, head sha) arrives via
# `env:` and is referenced as a shell-quoted "$VAR". We NEVER inline a
# `${{ github.event.pull_request.* }}` expression inside the run: script
# (that would execute a crafted title as shell).
# - The PR-head VERSION is read as DATA via the API (raw media type), from the
# head repo at the head sha — never by checking out the head.
# test/pr-title-sync-workflow-safety.test.ts is the static tripwire for all of
# the above and fails CI if any of it regresses.
on:
pull_request:
pull_request_target:
types: [opened, synchronize, edited]
paths:
- 'VERSION'
@ -19,25 +37,62 @@ jobs:
pull-requests: write
if: github.actor != 'github-actions[bot]'
steps:
- name: Checkout PR head
# Base repo only — trusted infra (the rewrite helper). No PR-head checkout.
- name: Checkout base repo (trusted)
uses: actions/checkout@v4
with:
fetch-depth: 1
ref: ${{ github.event.pull_request.head.sha }}
- name: Rewrite PR title to match VERSION
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUM: ${{ github.event.pull_request.number }}
# Attacker-controlled on fork PRs — env-only, never inlined into run:.
OLD_TITLE: ${{ github.event.pull_request.title }}
BASE_REPO: ${{ github.repository }}
HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
chmod +x ./bin/gstack-pr-title-rewrite.sh
VERSION=$(cat VERSION | tr -d '[:space:]')
NEW_TITLE=$(./bin/gstack-pr-title-rewrite.sh "$VERSION" "$OLD_TITLE")
if [ "$NEW_TITLE" = "$OLD_TITLE" ]; then
echo "Title already correct; no change."
if [ "$HEAD_REPO" = "$BASE_REPO" ]; then IS_FORK=0; else IS_FORK=1; fi
# Read the PR-head VERSION as data (raw bytes), from the head repo at
# the head sha. Guard the assignment itself: under `set -e` a bare
# `VERSION=$(...)` would abort the step before any later [ -z ] check.
if ! VERSION=$(gh api -H "Accept: application/vnd.github.raw" \
"repos/$HEAD_REPO/contents/VERSION?ref=$HEAD_SHA" 2>/dev/null | tr -d '[:space:]'); then
VERSION=""
fi
if [ -z "$VERSION" ]; then
# Same-repo read failure should never happen — fail loudly so we
# notice. A fork miss (public-contents quirk, private fork) is a
# convenience gap, not a gate — warn and skip so the check stays green.
if [ "$IS_FORK" = "0" ]; then
echo "::error::Could not read VERSION from same-repo PR head ($HEAD_SHA)."
exit 1
fi
echo "::warning::Could not read VERSION from fork $HEAD_REPO ($HEAD_SHA); skipping title sync."
exit 0
fi
# The helper rejects a malformed VERSION (exit 2). Same policy: loud for
# same-repo, soft for forks. Never echo the raw (attacker-controlled)
# title — Actions still parses ::workflow-command:: from stdout.
if ! NEW_TITLE=$(./bin/gstack-pr-title-rewrite.sh "$VERSION" "$OLD_TITLE"); then
if [ "$IS_FORK" = "0" ]; then
echo "::error::Could not compute title for VERSION '$VERSION' on PR #$PR_NUM."
exit 1
fi
echo "::warning::Could not compute title for fork PR #$PR_NUM; skipping."
exit 0
fi
if [ "$NEW_TITLE" = "$OLD_TITLE" ]; then
echo "PR #$PR_NUM title already correct; no change."
exit 0
fi
echo "Rewriting: $OLD_TITLE -> $NEW_TITLE"
gh pr edit "$PR_NUM" --title "$NEW_TITLE"
echo "PR #$PR_NUM title synced to VERSION."

4
.gitignore vendored
View File

@ -7,6 +7,7 @@ make-pdf/dist/
bin/gstack-global-discover*
.gstack/
.claude/skills/
.claude/gstack-rendered/
.claude/scheduled_tasks.lock
.claude/*.lock
.agents/
@ -37,3 +38,6 @@ supabase/.temp/
# Throughput analysis — local-only, regenerate via scripts/garry-output-comparison.ts
docs/throughput-*.json
# gbrain local source-staging dir (capability checks, source clones) — runtime artifact
.sources/

View File

@ -212,8 +212,8 @@ from `snapshot`, or `@c` refs from `snapshot -C`. Full table:
| Command | Description |
|---------|-------------|
| `js <expr>` | Run inline JavaScript expression in page context, return as string |
| `eval <file>` | Run JS from a file (path under /tmp or cwd; same sandbox as `js`) |
| `js <expr> [--out <file>] [--raw]` | Run inline JavaScript expression in page context, return as string. With `--out <file>` the result is written to disk instead of returned (a `data:*;base64,...` result is decoded to raw bytes unless `--raw`). `--out` makes the invocation a WRITE (needs `write` scope, never allowed over the tunnel). |
| `eval <file> [--out <file>] [--raw]` | Run JS from a file (path under /tmp or cwd; same sandbox as `js`). `--out`/`--raw` behave as for `js`. |
| `css <sel> <prop>` | Computed CSS value |
| `attrs <sel\|@ref>` | Element attributes as JSON |
| `is <prop> <sel\|@ref>` | State check: visible, hidden, enabled, disabled, checked, editable, focused |

View File

@ -1,5 +1,724 @@
# Changelog
## [1.57.10.0] - 2026-06-10
## **Codex review now runs by default everywhere it matters.**
## **One switch governs it, and it falls back to Claude when Codex is missing or unauthed.**
Codex cross-model review used to be inconsistent. `/review` and `/ship` ran it
automatically, but plan reviews hid it behind a "Want an outside voice?" question
you had to say yes to every time, `/document-release` never ran it at all, and every
entry point only checked whether the `codex` binary existed, not whether it was
logged in. Now `codex_reviews` is one master switch (default `enabled`) that governs
Codex review across `/review`, `/ship`, `/plan-ceo-review`, `/plan-eng-review`,
`/plan-design-review`, `/plan-devex-review`, `/document-release`, and `/autoplan`.
The plan-review outside voice runs automatically. `/document-release` gets a new
Codex pass that checks your docs against what actually shipped. Every call site now
detects install AND auth separately, and degrades to a Claude subagent with a clear
one-line reason instead of silently skipping. Turn the whole thing off with one
command: `gstack-config set codex_reviews disabled`.
### The numbers that matter
Verified by the gate-tier E2E evals that exercise these exact paths
(`codex-offered-ceo-review`, `codex-offered-eng-review`, `document-release`,
`codex-review-findings`), all green this run.
| Metric | Before | After | Δ |
|--------|--------|-------|---|
| Skills where Codex review runs by default | 2 | 8 | +6 |
| Prompts to get a plan-review outside voice | 1 (opt-in each time) | 0 (automatic) | -1 |
| Codex readiness detection | install only | install + auth | sharper |
| Master switches to disable it all | 0 (per-skill only) | 1 (`codex_reviews`) | +1 |
| `/document-release` Codex doc audit | none | doc-vs-diff pass | new |
When Codex is installed but not logged in, you used to get nothing on the paths that
checked only `command -v codex`. Now you get a named reason ("Codex installed but not
authenticated, using Claude subagent") and the review still happens. A typo on the
switch (`gstack-config set codex_reviews disabledd`) is rejected and your existing
setting is preserved, so a fat-finger can never silently turn paid Codex calls on or
off.
### What this means for you
If you run gstack day to day, you stop deciding whether to get a second model's eyes
on every plan and every release. It is just there, on by default, the way the strong
reviewers already worked on diffs. If you do not have Codex set up, nothing breaks:
you get the Claude outside voice instead, with a one-line note telling you how to add
Codex for true cross-model coverage. If you want it gone, one command turns off all
eight surfaces at once.
### Itemized changes
#### Added
- **`codex_reviews` as the master switch** for Codex review across `/review`, `/ship`,
`/document-release`, all four plan reviews, and `/autoplan` (`bin/gstack-config`).
Default `enabled`. Invalid values on `set` are rejected with the existing value
preserved, so a typo cannot flip paid Codex calls.
- **`/document-release` Codex doc audit** (`generateCodexDocReview`): reviews the
docs you touched against the release diff for stale claims, undocumented new
surface, and over/under-sold CHANGELOG entries. Informational, with an explicit
apply-fixes decision point. Never auto-edits docs.
- **`codexPreflight()` shared helper** (`scripts/resolvers/constants.ts`): one
self-contained bash block that reads the switch, sources the probe, checks install
and auth, and emits a single canonical mode (`ready` / `not_installed` /
`not_authed` / `disabled`).
#### Changed
- **Plan-review outside voice is default-on**, not opt-in. The "Want an outside
voice?" question is gone; it runs automatically and falls back to a Claude subagent
when Codex is unavailable. Incorporating its findings still requires your explicit
approval (cross-model tension is presented, never auto-applied).
- **Adversarial review detects auth, not just install** (`generateAdversarialStep`):
distinct "not installed" vs "not authenticated" guidance. The 200-line threshold
for the heavier structured `codex review` is unchanged.
- **`/autoplan` honors `codex_reviews=disabled`** in its Phase 0.5 preflight, so the
switch is truly global.
#### Fixed
- Three `gstack-config` tests asserted `get`/`list` print empty for unset keys; the
tool falls back to the documented defaults table. Assertions now match real behavior.
#### For contributors
- Size-budget guards widened for the default-on outside-voice prose, each with a
rationale comment (`test/helpers/carve-guards.ts`, `test/helpers/parity-harness.ts`).
- Static guards added: plan reviews must not carry the opt-in question and must render
the default-on voice; `/document-release` must carry the doc review; the codex host
strips all of it (`test/skill-validation.test.ts`).
## [1.57.9.0] - 2026-06-09
## **Your gstack checkout stays clean when gbrain is installed.**
## **Brain-aware skill blocks render to an untracked spot, never into tracked source.**
Before this, finishing a Conductor or dev-workspace setup with gbrain installed
rewrote 16 planning and review SKILL.md files in place, adding 326 lines of
brain-aware blocks straight into tracked source. Your working tree came back dirty,
one stray `git add` away from committing a token regression for everyone who does
not run gbrain. Now `gen-skill-docs --out-dir` renders the brain-aware variant into
an untracked per-workspace directory, and `bin/dev-setup` repoints the workspace's
skill symlinks at it. The dev workspace gets the full gbrain experience (context-load
and save-to-brain blocks live at runtime), while the tracked SKILL.md files stay
byte-for-byte canonical. To turn the blocks on across all your projects' Claude
sessions, `gstack-config gbrain-refresh` now renders them into your global install,
guarded so it never mutates a symlinked or non-gstack directory.
### The numbers that matter
Structural facts of the change, verifiable from the diff plus `bun run gen:skill-docs`
(zero drift) and the new behavioral test (`test/gen-skill-docs-out-dir.test.ts`).
| When gbrain is installed | Before | After |
|---|---|---|
| Tracked SKILL.md files dirtied by dev-setup | 16 (+326 lines) | 0 |
| Where brain-aware blocks render in a dev workspace | in-place, tracked source | `.claude/gstack-rendered/`, untracked |
| Brain-aware blocks across other projects | re-run `./setup` or hand-edit | `gstack-config gbrain-refresh` (idempotent) |
| "Is gbrain usable" check | per-caller JSON grep, can read stale state | `gstack-gbrain-detect --is-ok` (one live gate) |
The section-path rewrite is surgical: only `~/.claude/skills/gstack/<skill>/sections/`
references move to the render dir, so `bin/` and `docs/` references still resolve to
the install.
### What this means for you
If you develop gstack with gbrain on, `git status` is clean again after setup, and
you can stop fishing brain-block drift out of your commits. After a
`git reset --hard` deploy of your install, re-run `gstack-config gbrain-refresh` to
restore the machine-wide blocks (it is idempotent, and the deploy note in CLAUDE.md
spells this out).
### Itemized changes
#### Added
- `gen-skill-docs --out-dir <dir>`: render the Claude SKILL.md + sections into a
separate directory instead of in place, rewriting only the section-base path so
section reads resolve to the render. Default (no flag) output is unchanged.
- `gstack-gbrain-detect --is-ok`: live-detection exit-code gate (0 iff gbrain is
usable), so setup, dev-setup, and gstack-config share one check.
- `gstack-config gbrain-refresh` now renders brain-aware blocks into the global
install (`~/.claude/skills/gstack`), guarded against symlinked or non-gstack
targets and self-documenting about the `reset --hard` re-run cycle.
#### Changed
- `bin/dev-setup` renders the brain-aware variant into `.claude/gstack-rendered`
(gitignored) and repoints workspace skill symlinks at it; the worktree stays
canonical. `GSTACK_SKIP_GBRAIN_REGEN` is passed inline to the nested setup, never
exported.
- `setup` honors `GSTACK_SKIP_GBRAIN_REGEN` (skips the in-place brain regen on dev
trees) and writes detection state to a PID-unique tmp so concurrent workspaces
cannot clobber it.
- `scripts/dev-skill.ts` refreshes the workspace render on template change, only
when the render dir already exists.
- `bin/dev-teardown` removes the untracked render.
#### For contributors
- New tests: `test/gen-skill-docs-out-dir.test.ts` (behavioral: worktree unchanged,
blocks rendered, section paths rewritten), `test/dev-setup-render-isolation.test.ts`
and `test/gbrain-refresh-install-render.test.ts` (static tripwires), plus
`--is-ok` coverage in `test/gbrain-detect-shape.test.ts`.
## [1.57.8.0] - 2026-06-09
## **`browse` is now the one Chromium on the box, for offline rendering too.**
## **`js`/`eval --out <file>` writes a render straight to disk, so skills stop bundling their own puppeteer.**
You can now turn your own local HTML or JSON into a PNG (or any bytes) on disk
through the same headless `browse` Chromium you already run, with no second
browser install. `js "<expr>" --out out.png` and `eval script.js --out out.png`
write the evaluate result to a file instead of returning it. When the result is a
base64 data URL (the shape Excalidraw exports, og-image generators, and card
renderers hand back), `--out` decodes it to raw bytes for you; pass `--raw` to
write the literal string. Malformed base64 errors loudly instead of writing a
corrupt file, and missing parent directories are created. This closes the gap that
made local-render skills each `npm i puppeteer` and download a drifting second
Chromium.
### The numbers that matter
No synthetic benchmark — these are structural facts of the change, verifiable from
the diff and a one-line smoke (`browse load-html` → `screenshot --selector` /
`js --out`).
| For a skill that rasterizes local HTML/JSON | Before | After |
|---|---|---|
| Chromium installs per box | 2+ (browse + each skill's own puppeteer) | 1 (shared `browse`) |
| Getting a PNG from a render function | `evaluate` → multi-MB data URL over the CLI channel → hand-decode base64 → write | `js --out` decodes and writes server-side; only a short status crosses the channel |
| Render-to-file primitive | none | `js`/`eval --out [--raw]` |
The blessed offline path is documented in the browse skill: visual output goes
through `screenshot --selector` (the picture never crosses the CDP wire), and bytes
a function returns go through `js --out`.
### What this means for you
If you write skills that draw diagrams, cards, or og-images, point them at `browse`
and delete the bundled Chromium. One version to pin, one daemon to manage. `--out`
is treated as a write everywhere it matters: it needs the `write` scope, is blocked
over the pair-agent tunnel, and is gated in watch mode, so a remote agent can never
use it to write to your disk.
### Itemized changes
#### Added
- **`js` / `eval --out <file>` render-to-file** (`browse/src/read-commands.ts`).
Writes the evaluate result to disk and returns a short `... result written: <path>
(<N> bytes)` status. A `data:<type>;base64,...` result is decoded to raw bytes
(case-insensitive header parse, split on the first comma, base64-charset validated
before decode); `--raw` forces a literal write. Parent directories are created.
- **`--raw` flag** to bypass data-URL decoding and write the literal result string.
- **Offline render mode docs** in the browse skill: an explicit headless, no-proxy,
no-Xvfb path with a worked example showing visual (`screenshot --selector`) vs
bytes (`js --out`), a puppeteer→browse cheatsheet row, and a "don't bundle your
own Chromium" note (also in CONTRIBUTING.md).
#### Changed
- **`--out` is a per-invocation WRITE capability** (`browse/src/server.ts`).
`js`/`eval` stay read commands, but an `--out` invocation requires the `write`
scope, is never dispatchable over the tunnel surface (`canDispatchOverTunnel` now
consults args), and counts as a mutation for watch-mode and tab-ownership gates.
#### For contributors
- New tests: `parseOutArgs`/`hasOutArg` unit coverage (`--out`/`--out=`, `--raw`,
repeats, missing value, ordering), `--out` render-to-file integration (large
string, data-URL→PNG, `--raw`, malformed-base64, outside-safe-dir, mkdir, eval
parity, byte-for-byte null/undefined), and tunnel-gate guards proving `--out`
is never tunnel-dispatchable.
## [1.57.7.0] - 2026-06-08
## **Every plan review now ends by telling you, in one line, whether anything is still unresolved.**
## **The GSTACK REVIEW REPORT closes with the open decisions, or "NO UNRESOLVED DECISIONS" in plain sight, before you approve.**
When a plan-review skill (/plan-ceo-review, /plan-eng-review, /plan-design-review,
/plan-devex-review, and /codex) finishes and hands you the plan to approve, its report
now ends with a mandatory unresolved-decisions verdict. If decisions are still open, it
lists each one and what breaks if you ship it deferred. If nothing is open, it prints the
exact line NO UNRESOLVED DECISIONS. A token-reduction pass had made this line optional, so
a clean plan and a plan hiding an open question rendered the same. Now the line is never
omitted, it is always the last thing you read before the approval prompt, and the approval
gate refuses to let the plan through without it.
### What changed, before and after
| At plan-approval time | Before | After |
|---|---|---|
| Clean plan | usually no unresolved line | `NO UNRESOLVED DECISIONS` as the final line |
| Plan with open decisions | unresolved line optional, often dropped | `**UNRESOLVED DECISIONS:**` + one bullet per open item |
| Approval gate (ExitPlanMode) | checked the line "if applicable" | blocks unless the unresolved status is the final line |
| /plan-devex-review review log | never written, gate uncheckable | written, so the dashboard and report see its data |
The unresolved count across reviews is computed without double-counting the review that
just ran, using the same 7-day freshness window as the Review Readiness Dashboard.
### What this means for you
Every approve-plan moment now carries an explicit verdict on open questions, so a missed
ambiguity cannot slip through looking like a clean plan. If you run the plan-review skills
or /autoplan, you will see the unresolved status as the closing line of every report.
Nothing to configure. Upgrade and your next plan review shows it.
### Itemized changes
#### Added
- **Mandatory unresolved-decisions status in the GSTACK REVIEW REPORT.** Generated into
all six report consumers (/plan-ceo-review, /plan-eng-review, /plan-design-review,
/plan-devex-review, /codex, /devex-review) from `scripts/resolvers/review.ts`. The report
always ends with either the exact unbolded sentinel `NO UNRESOLVED DECISIONS` or a
`**UNRESOLVED DECISIONS:**` bullet block listing each open item; never omitted, always
the final line.
- **Blocking approval gate.** The EXIT PLAN MODE GATE now refuses ExitPlanMode unless the
report's final non-whitespace line is the unresolved status (no "if applicable" escape).
- Static and E2E tests pinning the mandatory status across every report consumer and
gate-bearing skill, so a future compression pass cannot silently drop it again.
#### Fixed
- **/plan-devex-review never logged a review entry.** It carried the approval gate but
never called `gstack-review-log`, so the gate's "review log was called" check was
structurally unsatisfiable and its data was invisible to the Review Readiness Dashboard
and the report. It now logs with the correct timestamp and DX fields.
#### For contributors
- Rebased the parity-suite size baseline v1.53.0.0 to v1.57.7.0 (captures current union
sizes; keeps the per-skill 1.05 ratio so future bloat is still caught). Regenerated the
three ship golden fixtures left stale by #1909. The frozen v1.44.1 integrity anchor and
the v1.47 size-budget baseline are untouched.
## [1.57.6.0] - 2026-06-07
## **Eight community-filed bugs fixed in one wave, four of them security guards that were quietly failing open.**
## **Your redaction gate now catches modern OpenAI keys, and `/ship`'s adversarial review stops choking on your own security tests.**
This is a fix wave. The throughline: guards that reported success while doing nothing.
The secret-redaction gate that every `/spec`, `/ship`, `/cso`, and `/document-*` run
passes through was blind to modern `sk-proj-`/`sk-svcacct-`/`sk-admin-` OpenAI keys and
silently dropped its size cap on a bad flag. The cross-project learnings trust gate was
an allowlist on paper and a denylist in code, so untrusted rows leaked between projects.
The destructive-action classifier waved through "rotate the database password." Each one
looked like it was protecting you. None of them were. All four now fail closed, with
tests that pin the exact case that used to slip by. Three more fixes clear silent
crashes and skipped reviewers, and `/ship`'s adversarial pass no longer trips Anthropic's
usage policy when it reads your repo's own attack-payload fixtures.
### The numbers that matter
Reproduce with `bun test test/redact-engine.test.ts test/gstack-learnings-search.test.ts test/one-way-doors.test.ts test/diff-scope.test.ts test/brain-cache-roundtrip.test.ts`.
| Guard / path | Before | After |
|---|---|---|
| `sk-proj-`/`sk-svcacct-`/`sk-admin-` OpenAI keys | zero findings (HIGH fails open) | blocked, with prose false-positive guards |
| `gstack-redact --max-bytes <garbage>` | NaN silently disables the size cap | rejected at the CLI; engine backstop holds |
| Cross-project learnings with no `trusted` field | imported (denylist bug) | excluded (true allowlist) |
| "rotate the database password" | classified two-way (auto-approvable) | classified one-way (always asks) |
| `.mjs/.cjs/.mts/.cts`-only PRs | backend reviewer skipped | backend reviewer runs |
| `_meta.json` missing `last_refresh` | brain-cache crashes (TypeError) | degrades to a cold cache |
| Safety-skill hooks on Claude Code 2.1.162 | every Edit/Write errored | hooks resolve and run |
| `/ship` adversarial review over security fixtures | denied by usage policy | runs, fixtures read in summary mode |
The redaction one is the sharpest: a project/service-account/admin OpenAI key pasted
into a spec or PR body used to sail straight through the gate. Now it blocks, and the
calibration is pinned so hyphenated prose like "the sk-learning-rate schedule" does not
false-positive and wedge your ship.
### What this means for you
If you rely on the redaction guard or the cross-project learnings gate, they now do what
the docs always said. If you run `/ship` on a repo that tests its own security guards,
adversarial review stops dying on contact with your fixtures. And if you are on Claude
Code 2.1.162, `/guard`, `/freeze`, and `/careful` work again instead of erroring on every
edit. Upgrade and re-run anything that touched these paths.
### Itemized changes
#### Fixed
- **Redaction misses modern OpenAI keys (#1868).** `openai.key` (HIGH/block) used a
contiguous-alphanumeric pattern that stopped at the first `-`/`_`, so base64url-bodied
`sk-proj-`/`sk-svcacct-`/`sk-admin-` keys produced no finding and failed open through
every redaction sink. Replaced with explicit bare-vs-prefixed alternation; added
positive and false-positive tests. Reported by @jbetala7.
- **Redaction size cap fails open on a bad flag (#1824).** A malformed `--max-bytes`
parsed to `NaN`, and `byteLen > NaN` is always false, silently disabling the
fail-closed oversize guard; a negative value blocked everything. The CLI now rejects
non-integer / non-positive values, and the engine falls back to the default cap as a
backstop. Reported by @jbetala7.
- **Cross-project learnings trust gate leaked (#1745).** `gstack-learnings-search
--cross-project` is documented as an allowlist but was coded as `trusted === false`,
admitting any row missing the `trusted` field. Flipped to `trusted !== true`. Reported
by @jbetala7.
- **Destructive-action classifier missed "rotate ... password" (#1839).** The `rotate`
keyword pattern omitted `password` while its `revoke`/`reset` siblings included it, so
the most common credential-rotation phrasing classified as a reversible two-way
question. Added `password` to the alternation.
- **Review Army skipped backend reviewer on ESM/CJS PRs (#1810).** `gstack-diff-scope`
matched only `*.ts|*.js`; a PR touching only `.mjs/.cjs/.mts/.cts` reported no backend
scope. Added the four module extensions. Reported by @jbetala7.
- **Brain-cache crash on a partial `_meta.json` (#1879).** `loadMeta` returned parsed
JSON verbatim; a file missing `last_refresh` crashed three consumers with a TypeError.
Added an object-shape guard and map normalization; missing schema/endpoint identity now
forces a safe rebuild rather than trusting a stale file. Reported by @jbetala7.
- **Safety-skill hooks broken on Claude Code 2.1.162 (#1871).** `guard`, `freeze`, and
`careful` frontmatter hooks used `${CLAUDE_SKILL_DIR}`, which CC 2.1.162 no longer
populates, so every Edit/Write/Bash errored. Anchored the hook commands to the
installed checkout path. Reported by @omariani-howdy.
- **`/ship` adversarial review denied on own security fixtures (#1899).** The Claude
adversarial subagent reasoned "like an attacker" over the full diff; when the diff
included the repo's own attack-payload regression fixtures, Anthropic's real-time
usage-policy safeguards denied the call. The subagent now carries authorized-defensive
-testing framing and reads fixture/test files in summary mode (no raw payload bytes),
stating so explicitly. Reported by @bmajewski.
#### For contributors
- `#1882` (skills hardcode `~/.claude/skills/gstack/`, breaking non-`gstack` install
dirs) is filed as the top item in `TODOS.md`. It was scoped out of this wave once it
proved to be a host-config/preamble change touching all 52 skills, distinct from the
`#1871` hook fix it was originally paired with.
## [1.57.5.0] - 2026-06-07
## **Your agent now keeps its decisions, not just its code.**
## **The durable calls you make, and the "why" behind them, are captured, curated, and resurfaced across sessions, with no daemon to run.**
Every session you and the agent settle real decisions: pick an architecture, cut a scope, choose a tool, reverse an earlier call. Until now that reasoning lived only in a transcript that scrolls away, so the next session re-litigates settled questions or loses the "why." This release adds an institutional decision memory. Durable decisions land in an append-only, event-sourced store, the scope-relevant ones surface automatically at session start, and you can search them any time. It is file-only and works with gbrain off; when gbrain is up you can add semantic recall on top. The planning and ship skills capture their own key calls so the high-value decisions get recorded without anyone remembering to. Separately, `/sync-gbrain` learned to build the cross-reference call graph and to heal a crashed daemon's stale lock instead of wedging every sync.
### The numbers that matter
No speed benchmark here, the win is capability and reliability. These are the real shape of the release (`git diff 1.57.0.0..HEAD`, `bun test`):
| Metric | Value |
|--------|-------|
| New commands | 2 (`gstack-decision-log`, `gstack-decision-search`) |
| Session-start read cost | O(active) bounded snapshot, not a full-history scan |
| Works with gbrain OFF | Yes, every capture/curate/resurface path is files + bins only |
| New source | ~2,550 lines across 26 files |
| New tests | 117 across the decision store + gbrain stages |
Resurfaced decision text is treated as data, not instructions (datamarked at the render boundary), secrets are blocked on write, and `redact` expunges a decision from every read path. The whole loop degrades cleanly: turn gbrain off and you still capture, curate, and resurface.
### What this means for you
Start a session tomorrow and the agent already knows what you settled and why, instead of asking again or quietly reversing it. Log a call with `gstack-decision-log`, reverse one with `--supersede`, pull the relevant history with `gstack-decision-search`. CEO, eng, spec, and ship reviews record their decisions for you. Run `/sync-gbrain` and a crashed autopilot no longer blocks your next sync.
### Itemized changes
#### Added
- **Cross-session decision memory.** An event-sourced (`decide`/`supersede`/`redact`) store at `~/.gstack/projects/<slug>/decisions.jsonl`. "Active" is computed, never a mutable flag, so the history stays honest and tolerant of dangling references.
- **`gstack-decision-log`** — capture a durable decision, reverse one (`--supersede <id>`), expunge an accidental secret (`--redact <id>`), or rewrite the log to its active set (`--compact`). Non-interactive, injection-sanitized, blocks HIGH and MEDIUM secrets on write.
- **`gstack-decision-search`** — read active decisions, scope-filtered to the current branch/issue, with `--recent N`, `--scope`, `--query`, `--all`, `--json`. Add `--semantic` (with `--query`) to append related hits from gbrain memory when it is up; it degrades silently to the reliable file results when gbrain is off.
- **Session-start resurfacing.** Context Recovery shows the scope-relevant active decisions at the top of a session, from a bounded snapshot so it stays fast as the log grows.
- **Skill capture.** `/plan-ceo-review`, `/plan-eng-review`, `/spec`, and `/ship` record their structured decisions (accepted scope, architecture verdict, filed spec, version bump) automatically.
- **A `## Cross-session decision memory` section in CLAUDE.md** documenting when and how to capture and resurface.
- **`/sync-gbrain` call-graph build (`--dream`).** Builds the symbol cross-reference graph behind a lock-free gate, with an honest outcome guard that reports a degraded no-op as WARN rather than a false success.
#### Changed
- Decision text that resurfaces into agent context is datamarked (code fences, `---` banners, `<|role|>`/`</system>` tags, chat turn-prefixes, and Unicode line terminators are neutralized) so stored text can never masquerade as instructions.
- `/sync-gbrain` pin guidance is accurate for current gbrain, and the worktree-scoped `.gbrain-source` pin routes code queries correctly.
#### Fixed
- `/sync-gbrain` no longer wedges forever on a crashed autopilot daemon's stale lock: it reads the holder pid, confirms liveness, and ignores a dead one (it stays conservative when it cannot tell).
#### For contributors
- New shared `lib/jsonl-store.ts` (injection-reject + atomic single-line append + tolerant read) backs both the learnings and decision stores, so the sanitization path is audited in one place.
- `lib/bin-context.ts` shares slug/branch/flag plumbing across the decision bins.
## [1.57.4.0] - 2026-06-08
## **The completeness principle is now Boil the Ocean, matching the post it came from.**
## **One name across the ETHOS file, every skill, and the developer-profile dial.**
The principle that tells gstack to do the complete thing was called "Boil the Lake" in
`ETHOS.md` and in every generated skill, with the ocean cast as the anti-pattern. The
developer-profile system and the completeness intro link already used "boil the ocean"
as the good, ship-the-whole-thing pole. So the same idea carried two opposite framings
depending on where you read it. This renames the principle to Boil the Ocean everywhere
and reframes the metaphor: the ocean is the complete destination, and lakes are the
boilable units you ship on the way there. The guidance is identical. Only the name and
the framing prose changed.
### The numbers that matter
Reproduce with `git diff v1.57.3.0..HEAD --stat`.
| Property | Before | After |
|---|---|---|
| Principle name in ETHOS + every skill | "Boil the Lake" | "Boil the Ocean" |
| Name vs. the `scope_appetite` dial ("boil the ocean" = complete) | split | unified |
| Files updated | — | 63 (ETHOS, CLAUDE, README, resolvers, templates, generated SKILL.md) |
| Runtime behavior change | — | none, text only |
The one number that matters is zero: no behavior changed. A reviewer reading `ETHOS.md`
no longer hits "ocean" as the thing to avoid in one section and the thing to aim for in
the next.
### What this means for you
You get the same complete-the-work recommendations, now under the name from Garry's
"Boil the Oceans" post. The metaphor reads straight through: the ocean is the goal,
lakes are how you get there one boil at a time, and only genuinely unrelated
multi-quarter migrations sit outside scope. Nothing to do on your end.
### Itemized changes
#### Changed
- `ETHOS.md` section 1 is renamed to "Boil the Ocean" and reframed so the ocean is the
complete destination and lakes are the boilable first units, not the ceiling.
- The "Completeness Principle" header injected into every tier-2+ skill now reads
"Boil the Ocean," with prose to match.
- `CLAUDE.md` and `README.md` references updated to the new name.
#### For contributors
- Source of the rename lives in the preamble resolvers
(`generate-completeness-section.ts`, the `composition.ts` skip-list, and
`generate-lake-intro.ts`); all SKILL.md files are regenerated from them.
- Unit assertions (`skill-validation`, `terse-build`) and the three ship golden
fixtures updated to the new header.
## [1.57.3.0] - 2026-06-07
## **Every PR `/ship` opens gets the version stamped into its title, fork and agent PRs included.**
## **The rule rides in the always-loaded part of the skill now, and a guard keeps it there.**
`/ship` stamps `vX.Y.Z.W` onto the title of every PR or MR it creates or updates, so
the version is the first thing you read in the PR list. That rule now lives in the
always-loaded core of the ship skill instead of an on-demand section, so the agent
applies it whether or not it opened the section that spells out the full procedure.
A CI workflow backs this up: it rewrites a title to match VERSION on every PR that
bumps the version, and it now reaches fork and agent PRs too, which a read-only token
could never touch before. Two free tests lock the behavior in so it cannot drift on
the next refactor.
### The numbers that matter
Reproduce with `bun test test/carve-section-ordering.test.ts test/pr-title-sync-workflow-safety.test.ts`
and `bun run eval:select`.
| Property | Before | After |
|---|---|---|
| Where the title rule loads | on-demand section only (since v1.54.0.0) | always-loaded skeleton + on-demand detail |
| Fork / agent PR title sync | none (read-only token under `pull_request`) | covered via hardened `pull_request_target` |
| Test proving the rule stays put | none | carve-guard registry asserts it on every PR |
| CI injection guard for the title workflow | none | static tripwire fails CI on unsafe patterns |
The title workflow now runs with a write token in the base-repo context but never
checks out or executes PR-head code, and every attacker-controlled field reaches the
script through `env:`, never inlined. A static test fails CI if either rule regresses.
### What this means for you
Ship a branch and the PR shows up titled `v1.57.3.0 fix: ...` without you touching it,
even when the PR came from a fork. The agent no longer needs to read the right section
at the right moment for the version to land in the title, and the next person who slims
the ship skill cannot quietly strand the rule again, because a free test on every PR
checks that it is still there.
### Itemized changes
#### Added
- Carve-guard coverage for the ship PR-title invariant: the registry now asserts the
`v$NEW_VERSION` rule and the title helper stay in the always-loaded skeleton, while
the full create and update procedure stays in the on-demand section.
- Static CI-safety test for the title-sync workflow that fails the build if it checks
out PR-head code or inlines an attacker-controlled PR field into a shell step.
#### Changed
- The PR/MR title-version rule is always-loaded in `/ship` again, so the version
prefix lands on every PR the workflow creates or updates.
- The PR title-sync CI workflow now covers fork and agent PRs through a hardened
`pull_request_target` trigger (base-repo checkout only, PR fields passed via `env:`,
VERSION read as data from the PR head).
#### Fixed
- A path token in the ship PR-body section that rendered literally instead of resolving
now uses the correct helper path, so the Linked Spec auto-detect step runs as written.
## [1.57.2.0] - 2026-06-08
## **When the question picker breaks mid-skill, gstack asks in plain text instead of stalling.**
## **Every skill detects a dead AskUserQuestion and falls back to a full decision brief you answer by typing a letter.**
AskUserQuestion is how every gstack skill asks you to decide. When the host's question
tool fails at runtime, which Conductor's MCP integration currently does intermittently,
skills used to stall or hard-block. Now each skill detects the failure, works out
whether a human is actually present, and if so re-renders the exact same decision as a
text message: a plain-English explanation of the issue, a completeness score on each
choice, and a recommendation with its reason, one paragraph per choice. You answer by
typing a single letter. Headless eval runs still block cleanly (no human to answer);
orchestrator sessions keep auto-choosing. This whole release was built and reviewed
through that fallback, because the Conductor tool was down the entire session.
### The numbers that matter
No production benchmark for a reliability path like this. These are the behavior and
coverage facts, verifiable with `bun test test/gstack-session-kind.test.ts
test/resolver-ask-user-format.test.ts test/auq-error-fallback-hook.test.ts`.
| When AskUserQuestion fails | Before | After |
|---|---|---|
| Interactive session (human present) | stall / hard BLOCK | full prose decision brief, answer by letter |
| Headless eval / CI | BLOCK | BLOCK (unchanged, correct) |
| Orchestrator (OpenClaw) session | undefined | auto-choose recommended (contract kept) |
| Session kinds detected | 0 | 3 (interactive / headless / spawned) |
| New tests guarding the path | 0 | 34 |
The text brief is not a degraded stub. It carries the same three things the picker
shows: a clear explanation of what is being decided, a `Completeness: X/10` on every
choice, and a recommendation with the reason it wins.
### What this means for you
If your host's question tool flakes out, a skill no longer dies on you. You get the
same decision to make, in text, and you reply with a letter. Nothing changes when the
tool works normally. If you run gstack headless, those sessions still block on a needed
question exactly as before, so eval determinism is intact.
### Itemized changes
#### Added
- `gstack-session-kind` classifies each session as interactive, headless, or spawned,
echoed as `SESSION_KIND` at skill start so any skill can branch on it.
- Plain-text fallback for AskUserQuestion: on a tool failure in an interactive session,
the skill renders the full decision brief (issue ELI10 + per-choice completeness +
recommendation) as markdown you answer by typing a letter, then stops and waits.
- A defensive hook that, when an AskUserQuestion call errors, reminds the agent to run
the fallback for the current session kind.
#### Changed
- AskUserQuestion is still sent as a normal tool call; the prose path applies only when
the tool is unavailable or erroring, and never on a `[plan-tune auto-decide]` result.
#### Fixed
- Section-loading tests use the canonical kebab test names, so the test-coverage gate
matches them.
- External-host doc-freshness checks are deterministic, no longer dependent on a prior
full regeneration.
#### For contributors
- The eval/E2E runners set `GSTACK_HEADLESS=1` so headless runs classify correctly;
interactive-path suites opt out per-run.
- Per-skill `maxSizeRatio` override in the carve-guards registry; `document-release`
gets 1.08 headroom for the cross-cutting preamble addition while every other skill
keeps the 1.05 ceiling.
## [1.57.0.0] - 2026-06-07
## **Three more heavyweight skills load lighter, and every carved skill finally has a test that proves it loads.**
## **`/cso`, `/document-release`, and `/design-consultation` shed ~49KB of always-loaded prose; CI now blocks any carve that ships without its guards.**
gstack splits its biggest skills into a small always-loaded skeleton plus on-demand
sections that load only when a step needs them. This release carves three more,
`/document-release`, `/design-consultation`, and `/cso`, so the first time you invoke
them the agent reads far less. It also closes a gap from the earlier carves: only two
of six already-carved skills had a test proving an agent actually reads the section it
was told to read. Now all nine carved skills are guarded the same way, and CI blocks
any future carve that ships without its guards. `/cso` got extra care: its mode
dispatch and false-positive-filtering rules stay always-loaded, so a security audit
can never run with a rule stranded in an unread section.
### The numbers that matter
Measured with `wc -c <skill>/SKILL.md`; the skeleton+sections union is reproduced by
`bun test test/parity-suite.test.ts test/skill-size-budget.test.ts`.
| Skill | Always-loaded before | After | Δ |
|---|---|---|---|
| /design-consultation | 80,719 B | 59,229 B | **27%** |
| /document-release | 59,256 B | 45,797 B | **23%** |
| /cso | 79,383 B | 65,117 B | **18%** |
| Carved skills with a section-load guard | 2 of 6 | 9 of 9 | **full coverage** |
Total always-loaded prose across the three skills drops about 49KB (~12K tokens) on
first invoke, with nothing lost: every line moved into an on-demand section the
skeleton points at, and the parity suite checks the union still contains it.
### What this means for you
Run `/cso`, `/document-release`, or `/design-consultation` and the agent does less
reading before it starts working, so the session stays leaner. The carve pattern is
now safe to extend: a free static test runs on every PR and a behavioral test runs
weekly to prove the agent reads each section, so future slimming can't quietly drop
behavior. Nothing about how you invoke these skills changed.
### Itemized changes
#### Added
- Canonical carved-skill guard registry (`test/helpers/carve-guards.ts`): one source of truth for which skills are carved and what each must preserve. `parity-harness.ts` and `skill-size-budget.ts` derive their carved-skill lists from it.
- Carve guard suite: data-driven static ordering test, behavioral section-loading test (periodic), a completeness meta-guard that fails CI if a carved skill lacks its guards, and negative tests proving the guards actually fire.
- `/cso`, `/document-release`, and `/design-consultation` carved into skeleton + on-demand sections.
#### Changed
- `/cso` keeps its mode dispatch (`## Arguments`, `## Mode Resolution`), always-run phases, and false-positive-filtering exceptions always-loaded; an earliest-use invariant enforces that dispatch appears before any on-demand read.
#### For contributors
- Redaction, taxonomy, and parity content tests now read the skeleton+sections union so relocated prose still counts toward coverage.
- Real-session section-read canary deferred to TODOS (the deterministic guards ship first).
## [1.56.1.0] - 2026-06-03
## **`/sync-gbrain` can no longer delete your repo. Cleanup now refuses any directory it cannot prove it created.**
A `/sync-gbrain` memory sync could recursively delete your entire working tree. A
crashed import left a checkpoint pointing at the repo root, the next sync
"resumed" into it, and the cleanup step `rm -rf`'d it, taking uncommitted and
untracked work with it. This release closes that path and fixes three more bugs
hiding in the same resume machinery: cleanup now deletes only directories it can
prove are gstack-minted staging dirs, the remote-http transcript dir is never
touched, an interrupted import actually keeps its checkpoint so the next run
resumes instead of restaging, and a resumed run no longer marks files that failed
to import as successfully ingested.
### The numbers that matter
Source: `bun test test/regression-1611-gbrain-sync-resume.test.ts` on this branch.
| Metric | Before | After | Δ |
|--------|--------|-------|---|
| Repo-root `rm -rf` reachable | yes | no | closed |
| Proof required before delete | none | 5 checks | realpath + direct-child + name + .git tripwire + minted-marker |
| Resume after a timed-out import | broken (dir deleted) | works | fixed |
| Failed files mislabeled "ingested" on resume | yes | no | fixed |
| Resume regression-test assertions | 9 | 64 | +55 |
The guard is fail-closed: anything it cannot prove it owns is left on disk (a few
seconds of re-staging next run) rather than deleted. That asymmetry is the design
- a missing marker can cost a little work, never your data.
### What this means for you
If you use `/sync-gbrain`, a crashed or timed-out import can no longer cost you
uncommitted work. Resume now does what it always claimed: a large sync that times
out picks up where it left off next run instead of starting over, and files that
failed to import get retried instead of silently skipped. Nothing to configure.
Upgrade and keep syncing.
### Itemized changes
#### Fixed
- **`/sync-gbrain` could `rm -rf` your repo root.** A poisoned resume checkpoint
(dir = the repo, written when an import was interrupted while the repo was the
working directory) was adopted as the staging dir and recursively deleted. A
single fail-closed ownership check now guards every staging delete and every
resume: a path must resolve cleanly, be a direct child of `~/.gstack` named
`.staging-ingest-*`, contain no `.git`, and carry a marker file gstack minted.
Anything else is refused. Contributed by @diazMelgarejo (cyre).
- **Remote-http syncs no longer churn (or scare you).** The persistent transcript
dir that the brain sync pushes is no longer routed through staging cleanup, so
it stops being deleted on every run and stops emitting a false "preventing data
loss" warning.
- **A timed-out import now actually resumes.** Previously the run said "checkpoint
preserved" but then deleted the staging dir, so the next run always restaged.
The staging dir is now kept when a checkpoint points at it, and the message is
honest when there is nothing to resume.
- **Resume no longer hides import failures.** A resumed run could mark files that
failed to import as ingested, so they were never retried. Failures now map back
to their source files on resume and get another pass.
#### For contributors
- New `lib/staging-guard.ts` exports `checkOwnedStagingDir()`, the single
fail-closed predicate shared by the deletion chokepoint and the resume gate. It
returns the realpath-resolved canonical path so callers delete exactly what they
validated (closes a symlink TOCTOU). `makeStagingDir()` tears down and rethrows
if its marker write fails, so a marker-less dir can never leak. The
`#1611` resume regression suite grew to 64 assertions covering the poison
matrix, the remote-http gate, timeout-preserve, and resume failure-mapping.
## [1.56.0.0] - 2026-06-03
## **Five heavy skills now load their bulk on demand, the shared question preamble slimmed corpus-wide, and a paranoid test suite proves the questions never got worse.**

View File

@ -137,7 +137,7 @@ gstack/
├── setup # One-time setup: build binary + symlink skills
├── SKILL.md # Generated from SKILL.md.tmpl (don't edit directly)
├── SKILL.md.tmpl # Template: edit this, run gen:skill-docs
├── ETHOS.md # Builder philosophy (Boil the Lake, Search Before Building)
├── ETHOS.md # Builder philosophy (Boil the Ocean, Search Before Building)
└── package.json # Build scripts for browse
```
@ -776,8 +776,10 @@ When estimating or discussing effort, always show both human-team and CC+gstack
| Research / exploration | 1 day | 3 hours | ~3x |
Completeness is cheap. Don't recommend shortcuts when the complete implementation
is a "lake" (achievable) not an "ocean" (multi-quarter migration). See the
Completeness Principle in the skill preamble for the full philosophy.
is achievable. Boil the ocean — the complete thing is the goal; only genuinely
unrelated multi-quarter migrations are separate scope, never an excuse for a
shortcut. See the Completeness Principle in the skill preamble for the full
philosophy.
## Search before building
@ -881,6 +883,12 @@ The active skill lives at `~/.claude/skills/gstack/`. After making changes:
2. Fetch and reset in the skill directory: `cd ~/.claude/skills/gstack && git fetch origin && git reset --hard origin/main`
3. Rebuild: `cd ~/.claude/skills/gstack && bun run build`
**If you use gbrain:** the `git reset --hard` in step 2 reverts the brain-aware
(`GBRAIN_CONTEXT_LOAD` / `GBRAIN_SAVE_RESULTS`) blocks that `gstack-config
gbrain-refresh` renders into the install (those generated blocks differ from
`main` by design). After deploying, re-run `gstack-config gbrain-refresh` to
restore them across all your projects' Claude sessions. It's idempotent.
Or copy the binaries directly:
- `cp browse/dist/browse ~/.claude/skills/gstack/browse/dist/browse`
- `cp design/dist/design ~/.claude/skills/gstack/design/dist/design`
@ -903,6 +911,31 @@ Key routing rules:
- Save progress → invoke /context-save
- Resume context → invoke /context-restore
## Cross-session decision memory
Durable decisions and their rationale are captured in an append-only, event-sourced
store at `~/.gstack/projects/<slug>/decisions.jsonl` so neither you nor the user
re-litigates a settled call or loses the "why" across sessions. This is the reliable,
file-only path: it works with gbrain OFF. (gbrain semantic recall is an optional
enhancement layered on top, never a dependency.)
- **Resurface** active decisions before re-deciding: `bin/gstack-decision-search`
(`--recent N`, `--scope repo|branch|issue`, `--query KW`, `--all`, `--json`).
Add `--semantic` (with `--query`) to append related hits from gbrain memory when
it's up; it degrades silently to the reliable file results when gbrain is off.
Session start already surfaces scope-relevant active decisions via Context Recovery.
If a decision is listed, treat it as settled with its rationale; if you're about to
reverse it, say so explicitly.
- **Capture** a DURABLE decision when you or the user make one:
`bin/gstack-decision-log '{"decision":"...","rationale":"...","scope":"repo|branch|issue","source":"user|skill|agent","confidence":1-10}'`.
Reverse a prior call with `--supersede <id>`; expunge an accidental secret with
`--redact <id>`; rewrite the log to the active set with `--compact`. Non-interactive
(never prompts), injection-sanitized, and HIGH-secret-blocking on write.
- **Durable means:** architecture choice, scope cut, tool/vendor choice, or a reversal
of a prior call. NOT a turn-level edit, a phrasing tweak, or anything trivially
re-derivable. Capture is curated at the source — log durable decisions only, or the
store becomes noise.
## GBrain Search Guidance (configured by /sync-gbrain)
<!-- gstack-gbrain-search-guidance:start -->

View File

@ -106,6 +106,22 @@ bun run build
bin/dev-teardown
```
### Brain-aware blocks in a dev workspace (gbrain installed)
If gbrain is installed and usable (`bin/gstack-gbrain-detect --is-ok` exits 0),
`bin/dev-setup` keeps your tracked `SKILL.md` files canonical and renders the
brain-aware variant (the `GBRAIN_CONTEXT_LOAD` / `GBRAIN_SAVE_RESULTS` blocks)
into `.claude/gstack-rendered/` (gitignored, per-workspace). It then repoints the
workspace's `SKILL.md` symlinks at that render, so your Claude sessions get the
full gbrain experience while `git status` stays clean. Under the hood, dev-setup
passes `GSTACK_SKIP_GBRAIN_REGEN=1` inline to the nested `./setup` (so it never
dirties tracked source) and runs `gen:skill-docs:user --out-dir .claude/gstack-rendered`,
which rewrites only the section-base paths to point at the render. `bin/dev-teardown`
removes the render. To make the blocks live across your *other* projects' Claude
sessions, run `gstack-config gbrain-refresh`, which renders them into the global
install (`~/.claude/skills/gstack`), guarded so it never touches a symlinked or
non-gstack directory.
## Testing & evals
### Setup
@ -232,6 +248,14 @@ For template authoring best practices (natural language over bash-isms, dynamic
To add a browse command, add it to `browse/src/commands.ts`. To add a snapshot flag, add it to `SNAPSHOT_FLAGS` in `browse/src/snapshot.ts`. Then rebuild.
**Don't bundle puppeteer/Chromium in a skill.** `browse` is the one shared
Chromium per box, including offline local-render workloads. A skill that needs to
rasterize its own HTML/JSON (diagrams, cards, og-images) should route through
`browse``screenshot --selector` for visual output, `load-html` + `js --out` for
bytes a render function returns — instead of `npm i puppeteer` and downloading a
second Chromium that drifts out of version sync. One install to pin, one daemon to
manage.
## Jargon list (V1 writing style)
gstack's Writing Style section (injected into every tier-≥2 skill's preamble)
@ -326,8 +350,8 @@ If you're using [Conductor](https://conductor.build) to run multiple Claude Code
| Hook | Script | What it does |
|------|--------|-------------|
| `setup` | `bin/dev-setup` | Copies `.env` from main worktree, installs deps, symlinks skills, runs `./setup` non-interactively |
| `archive` | `bin/dev-teardown` | Removes skill symlinks, cleans up `.claude/` directory |
| `setup` | `bin/dev-setup` | Copies `.env` from main worktree, installs deps, symlinks skills, runs `./setup` non-interactively, and (if gbrain is installed) renders brain-aware blocks into `.claude/gstack-rendered/` without dirtying tracked source |
| `archive` | `bin/dev-teardown` | Removes skill symlinks, the `.claude/gstack-rendered/` render, and cleans up `.claude/` directory |
When Conductor creates a new workspace, `bin/dev-setup` runs automatically. It detects the main worktree (via `git worktree list`), copies your `.env` so API keys carry over, and sets up dev mode — no manual steps needed.

View File

@ -31,16 +31,21 @@ The last 10% of completeness that teams used to skip? It costs seconds now.
---
## 1. Boil the Lake
## 1. Boil the Ocean
AI-assisted coding makes the marginal cost of completeness near-zero. When
the complete implementation costs minutes more than the shortcut — do the
"Don't boil the ocean" was the right advice when engineering time was the
bottleneck. That era is over. AI-assisted coding makes the marginal cost of
completeness near-zero, so the old caution has quietly turned into an excuse.
When the complete implementation costs minutes more than the shortcut — do the
complete thing. Every time.
**Lake vs. ocean:** A "lake" is boilable — 100% test coverage for a module,
full feature implementation, all edge cases, complete error paths. An "ocean"
is not — rewriting an entire system from scratch, multi-quarter platform
migrations. Boil lakes. Flag oceans as out of scope.
**Ocean, lakes first:** The ocean is the destination — 100% test coverage for a
module, full feature implementation, all edge cases, complete error paths. You
get there one lake at a time: each lake is a boilable unit, not the ceiling.
"That's boiling the ocean" is no longer a reason to ship a shortcut — boiling
the ocean is the goal. The only thing still out of scope is genuinely unrelated
work: a multi-quarter platform migration that has nothing to do with the task at
hand. Flag that as separate scope. Boil everything else.
**Completeness is cheap.** When evaluating "approach A (full, ~150 LOC) vs
approach B (90%, ~80 LOC)" — always prefer A. The 70-line delta costs
@ -144,7 +149,7 @@ think it's better, state what context you might be missing, and ask. Never act.
## How They Work Together
Boil the Lake says: **do the complete thing.**
Boil the Ocean says: **do the complete thing.**
Search Before Building says: **know what exists before you decide what to build.**
Together: search first, then build the complete version of the right thing.

View File

@ -429,7 +429,7 @@ Other references: [docs/gbrain-sync.md](docs/gbrain-sync.md) (sync-specific guid
| Doc | What it covers |
|-----|---------------|
| [Skill Deep Dives](docs/skills.md) | Philosophy, examples, and workflow for every skill (includes Greptile integration) |
| [Builder Ethos](ETHOS.md) | Builder philosophy: Boil the Lake, Search Before Building, three layers of knowledge |
| [Builder Ethos](ETHOS.md) | Builder philosophy: Boil the Ocean, Search Before Building, three layers of knowledge |
| [Using GBrain with GStack](USING_GBRAIN_WITH_GSTACK.md) | Every path, flag, bin helper, and troubleshooting step for `/setup-gbrain` |
| [GBrain Sync](docs/gbrain-sync.md) | Cross-machine memory setup, privacy modes, troubleshooting |
| [Architecture](ARCHITECTURE.md) | Design decisions and system internals |

View File

@ -45,6 +45,9 @@ echo "SKILL_PREFIX: $_SKILL_PREFIX"
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
REPO_MODE=${REPO_MODE:-unknown}
echo "REPO_MODE: $REPO_MODE"
_SESSION_KIND=$(~/.claude/skills/gstack/bin/gstack-session-kind 2>/dev/null || echo "interactive")
case "$_SESSION_KIND" in spawned|headless|interactive) ;; *) _SESSION_KIND="interactive" ;; esac
echo "SESSION_KIND: $_SESSION_KIND"
_LAKE_SEEN=$([ -f ~/.gstack/.completeness-intro-seen ] && echo "yes" || echo "no")
echo "LAKE_INTRO: $_LAKE_SEEN"
_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || true)
@ -124,7 +127,7 @@ In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`co
## Skill Invocation During Plan Mode
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If no AskUserQuestion variant is callable, use the normal chat fallback from the AskUserQuestion Format rule and stop for the user's reply. At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If AskUserQuestion is unavailable or a call fails, follow the AskUserQuestion Format failure fallback: `headless` → BLOCKED; `interactive` → the prose fallback (also satisfies end-of-turn). At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If `PROACTIVE` is `"false"`, do not auto-invoke or proactively suggest skills. If a skill seems useful, ask: "I think /skillname might help here — want me to run it?"
@ -159,7 +162,7 @@ touch ~/.gstack/.writing-style-prompted
Skip if `WRITING_STYLE_PENDING` is `no`.
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Lake** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Ocean** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
```bash
open https://garryslist.org/posts/boil-the-ocean
@ -914,10 +917,10 @@ Refs are invalidated on navigation — run `snapshot` again after `goto`.
| `cookies` | All cookies as JSON |
| `css <sel> <prop>` | Computed CSS value |
| `dialog [--clear]` | Dialog messages |
| `eval <file>` | Run JavaScript from a file in the page context and return result as string. Path must resolve under /tmp or cwd (no traversal). Use eval for multi-line scripts; use js for one-liners. |
| `eval <file> [--out <file>] [--raw]` | Run JavaScript from a file in the page context and return result as string. Path must resolve under /tmp or cwd (no traversal). Use eval for multi-line scripts; use js for one-liners. With --out <file>, the result is written to disk (base64 data URL decoded to bytes unless --raw); --out makes the invocation a WRITE (needs write scope, never allowed over the tunnel). |
| `inspect [selector] [--all] [--history]` | Deep CSS inspection via CDP — full rule cascade, box model, computed styles |
| `is <prop> <sel|@ref>` | State check on element. Valid <prop> values: visible, hidden, enabled, disabled, checked, editable, focused (case-sensitive). <sel> accepts a CSS selector OR an @ref token from a prior snapshot (e.g. @e3, @c1) — refs are interchangeable with selectors anywhere a selector is expected. |
| `js <expr>` | Run inline JavaScript expression in the page context and return result as string. Same JS sandbox as eval; the only difference is js takes an inline expr while eval reads from a file. |
| `js <expr> [--out <file>] [--raw]` | Run inline JavaScript expression in the page context and return result as string. Same JS sandbox as eval; the only difference is js takes an inline expr while eval reads from a file. With --out <file>, the result is written to disk instead of returned (a base64 data URL is decoded to raw bytes unless --raw is given) — ideal for rasterizing local renders to PNG without serializing megabytes back through the CLI. --out makes the invocation a WRITE (needs write scope, never allowed over the tunnel). |
| `network [--clear]` | Network requests |
| `perf` | Page load timings |
| `storage | storage set <key> <value>` | Read both localStorage and sessionStorage as JSON. With "set <key> <value>", write to localStorage only (sessionStorage is read-only via this command — set it with `js sessionStorage.setItem(...)`). |

View File

@ -1,5 +1,48 @@
# TODOS
## NEXT PRIORITY
### P1: #1882 — portable skill-install prefix (non-`gstack` install dirs break silently)
**What:** Every generated SKILL.md hardcodes the literal `~/.claude/skills/gstack/...`
for its `bin/`/asset calls (the per-invocation telemetry/config preamble plus ~9
resolvers). `setup` wires the top-level skill symlinks for any directory name, so
installing at `~/.claude/skills/<other>` leaves every internal `bin` reference
pointing at a non-existent `~/.claude/skills/gstack/` path — failing **silently, at
skill-invocation time**. Make the emitted references portable: resolve the install
root at runtime (the preamble already defines `GSTACK_ROOT`/`GSTACK_BIN` in
`scripts/resolvers/preamble/generate-preamble-bash.ts` but the literals don't use
them) and emit `$GSTACK_BIN`-relative paths instead of the hardcoded prefix.
**Why:** Filed as #1882. Split out of the June 2026 fix wave (decision A) once
implementation showed it is a host-config/design change, not a fix-wave patch. The
urgent half — the guard/freeze/careful frontmatter hooks broken on CC 2.1.162 — was
already fixed in that wave (#1871) with a literal `$HOME`-anchored path, because
frontmatter hooks run before any runtime variable exists and cannot use `$GSTACK_BIN`.
So #1882 is now purely the body-preamble portability work.
**Pros:** Unblocks installs at any directory name; removes a whole class of silent
invocation-time failures.
**Cons:** Touches the most load-bearing bash in the repo (every skill's preamble);
a silent mistake breaks all 52 skills. High blast radius — needs its own focused PR.
**Context / where to start:**
- Rewire `ctx.paths.binDir` (and browse/design dir paths) + the ~9 resolvers that
emit the literal (`testing.ts`, `review.ts`, `design.ts`, `browse.ts`,
`redact-doc.ts`, `tasks-section.ts`, `preamble/generate-*.ts`) to use the
preamble-defined `$GSTACK_ROOT`/`$GSTACK_BIN`.
- Ensure `GSTACK_ROOT`/`GSTACK_BIN` are defined before first use in EVERY skill's
preamble (verify the telemetry preamble's first bin call is after the definition).
- **Test conflict (verified):** `test/gen-skill-docs.test.ts:1942` and the sibling
ship assertion currently *assert* generated Claude output `.toContain('~/.claude/skills/gstack')`
as a guardrail that Codex-host paths don't leak. These must be rewritten to match
the new portable scheme.
- Regenerate all 52 SKILL.md (`bun run scripts/gen-skill-docs.ts --host all`); never
hand-edit generated files. Bisect: resolver/host-config change commit, then the
52-file regen commit.
- Smoke-test a skill invocation from a non-`gstack` install dir to prove the fix.
- Sibling of #349 (the `$CLAUDE_CONFIG_DIR` / `~/.claude` path issue).
## Test infrastructure
### ✅ DONE (v1.53.1.0): Rebaseline parity-suite (v1.44.1 → v1.53.0.0)
@ -2283,3 +2326,54 @@ into `test/helpers/fake-gbrain.ts` when the second consumer arrives
runs).
**Depends on:** None.
### P2: Real-session carve canary (E3, deferred from carve-guard plan)
**What:** Wire a real-session section-Read-miss canary on top of the
carved skills. When a real user session drives a carved skill and the
agent does NOT Read a section the skeleton's STOP directive pointed it
at, log it (salted, content-free) to
`~/.gstack/analytics/section-reads.jsonl` and surface drift via
`bun run eval:summary`. Non-blocking alert, never a merge gate
(real-session data is non-deterministic).
**Why:** The static (E2) + behavioral (T2) guards prove carves are
structurally sound and that a real agent Reads sections in a controlled
eval. They do NOT see production drift — a prompt-context change that
makes live agents start skipping a section. The canary is the only
mechanism that catches that, from real usage.
**Context:** Deferred from the carve-guard-hardening plan (D5→T2, codex
outside-voice #7). `test/helpers/transcript-section-logger.ts` exists but
is built for deterministic test transcripts + ship action fingerprints,
NOT real-session drift — it needs rework before it can back this. Ship
the deterministic guards first; add this once they've proven useful. The
carved-skill set + each skill's `requiredReads` are already declared in
`test/helpers/carve-guards.ts`, so the canary reads its expectations
from there.
**Effort:** M (human ~2d, CC ~4h).
**Depends on:** `transcript-section-logger.ts` real-session-drift rework.
### P2: Harden behavioral section-loading test hermeticity
**What:** `captureSectionReads` in `test/helpers/auq-sdk-capture.ts` accepts ANY
Read whose path matches `sections/<file>.md`. The skeleton's STOP-Read directive
points at the gstack-root install path (`scripts/resolvers/sections.ts` builds it
from `ctx.paths.skillRoot`), not the planted fixture copy. So a run can satisfy
the section-read assertion by reading the GLOBAL install's section instead of the
hermetic fixture.
**Why:** A behavioral test that passes by reading the global install doesn't prove
THIS branch's carved section loads. If the fixture's section were broken but the
global install's weren't, the test would still pass.
**Context:** Codex outside-voice finding on the carve-guard ship (v1.57.0.0).
Pre-existing in `auq-sdk-capture.ts` — affects `skill-e2e-ship-section-loading`,
`skill-e2e-plan-ceo-review-section-loading`, and the new
`carve-section-loading.test.ts`. Fix: match the fixture's ABSOLUTE sections path
(the `planDir` copy), not a bare `sections/<file>.md` regex; or rewrite the STOP
path to the fixture during the run.
**Effort:** S (human ~3h, CC ~30min). **Depends on:** None.

View File

@ -1 +1 @@
1.56.0.0
1.57.10.0

View File

@ -54,6 +54,9 @@ echo "SKILL_PREFIX: $_SKILL_PREFIX"
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
REPO_MODE=${REPO_MODE:-unknown}
echo "REPO_MODE: $REPO_MODE"
_SESSION_KIND=$(~/.claude/skills/gstack/bin/gstack-session-kind 2>/dev/null || echo "interactive")
case "$_SESSION_KIND" in spawned|headless|interactive) ;; *) _SESSION_KIND="interactive" ;; esac
echo "SESSION_KIND: $_SESSION_KIND"
_LAKE_SEEN=$([ -f ~/.gstack/.completeness-intro-seen ] && echo "yes" || echo "no")
echo "LAKE_INTRO: $_LAKE_SEEN"
_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || true)
@ -133,7 +136,7 @@ In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`co
## Skill Invocation During Plan Mode
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If no AskUserQuestion variant is callable, use the normal chat fallback from the AskUserQuestion Format rule and stop for the user's reply. At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If AskUserQuestion is unavailable or a call fails, follow the AskUserQuestion Format failure fallback: `headless` → BLOCKED; `interactive` → the prose fallback (also satisfies end-of-turn). At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If `PROACTIVE` is `"false"`, do not auto-invoke or proactively suggest skills. If a skill seems useful, ask: "I think /skillname might help here — want me to run it?"
@ -168,7 +171,7 @@ touch ~/.gstack/.writing-style-prompted
Skip if `WRITING_STYLE_PENDING` is `no`.
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Lake** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Ocean** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
```bash
open https://garryslist.org/posts/boil-the-ocean
@ -305,11 +308,31 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
**Rule:** if any `mcp__*__AskUserQuestion` variant is in your tool list, prefer it. Hosts may disable native AUQ via `--disallowedTools AskUserQuestion` (Conductor does, by default) and route through their MCP variant; calling native there silently fails. Same questions/options shape; same decision-brief format applies.
**If no AskUserQuestion variant appears in your tool list:** fall back to the host's normal user-facing chat question. Render the same decision brief in prose, stop, and wait for the user's reply. This fallback is for interactive chat hosts that can ask the user normally but do not expose an AUQ tool (for example Codex API sessions). Do not write decisions to the plan file as a substitute, and do not silently auto-decide (only `/plan-tune` AUTO_DECIDE opt-ins authorize auto-picking).
If AskUserQuestion is unavailable (no variant in your tool list) OR a call to it fails, do NOT silently auto-decide or write the decision to the plan file as a substitute. Follow the **failure fallback** below.
### When AskUserQuestion is unavailable or a call fails
Tell three outcomes apart:
1. **Auto-decide denial (NOT a failure).** The result contains `[plan-tune auto-decide] <id> → <option>` — the preference hook working as designed. Proceed with that option. Do NOT retry, do NOT fall back to prose.
2. **Genuine failure** — no variant in your tool list, OR the variant is present but the call returns an error / missing result (MCP transport error, empty result, host bug — e.g. Conductor's MCP AskUserQuestion is flaky and returns `[Tool result missing due to internal error]`).
- If it was present and **errored** (not absent), retry the SAME call **once** — but only if no answer could have surfaced (a missing-result error can arrive after the user already saw the question; retrying would double-prompt, so if it may have reached them, treat as pending, don't retry).
- Then branch on `SESSION_KIND` (echoed by the preamble; empty/absent ⇒ `interactive`):
- `spawned` → defer to the **Spawned session** block: auto-choose the recommended option. Never prose, never BLOCKED.
- `headless``BLOCKED — AskUserQuestion unavailable`; stop and wait (no human can answer).
- `interactive`**prose fallback** (below).
**Prose fallback — render the decision brief as a markdown message, not a tool call.** Same information as the tool format below, different structure (paragraphs, not ✅/❌ bullets). It MUST surface this triad:
1. **A clear ELI10 of the issue itself** — plain English on what's being decided and why it matters (the question, not per-choice), naming the stakes. Lead with it.
2. **Completeness scores per choice** — explicit `Completeness: X/10` on EACH choice (10 complete, 7 happy-path, 3 shortcut); use the kind-note when options differ in kind not coverage, but never silently drop the score.
3. **The recommendation and why** — a `Recommendation: <choice> because <reason>` line plus the `(recommended)` marker on that choice.
Layout: a `D<N>` title + a one-line note that AskUserQuestion failed and to reply with a letter; the issue ELI10; the Recommendation line; then ONE paragraph per choice carrying its `(recommended)` marker, its `Completeness: X/10`, and 2-4 sentences of reasoning — never a bare bullet list; a closing `Net:` line. Split chains / 5+ options: one prose block per per-option call, in sequence. Then STOP and wait — the user's typed answer is the decision. In plan mode this satisfies end-of-turn like a tool call.
### Format
Every AskUserQuestion is a decision brief. Send it as tool_use when an AskUserQuestion tool exists; otherwise use the normal chat fallback above and wait for the user's reply.
Every AskUserQuestion is a decision brief and must be sent as tool_use, not prose — unless the documented failure fallback above applies (interactive session + the call is unavailable/erroring), in which case the prose fallback is the correct output.
```
D<N><one-line question title>
@ -389,7 +412,7 @@ Before calling AskUserQuestion, verify:
- [ ] (recommended) label on one option (even for neutral-posture)
- [ ] Dual-scale effort labels on effort-bearing options (human / CC)
- [ ] Net line closes the decision
- [ ] You are calling the tool when available; otherwise you are using the normal chat fallback and stopping for the user's reply
- [ ] You are calling the tool, not writing prose — unless the documented failure fallback applies (then: prose with the mandatory triad — issue ELI10, per-choice Completeness, Recommendation + `(recommended)` — and a "reply with a letter" instruction, then STOP)
- [ ] Non-ASCII characters (CJK / accents) written directly, NOT \u-escaped
- [ ] If you had 5+ options, you split (or batched into ≤4-groups) — did NOT drop any
- [ ] If you split, you checked dependencies between options before firing the chain
@ -576,12 +599,19 @@ if [ -d "$_PROJ" ]; then
fi
_LATEST_CP=$(find "$_PROJ/checkpoints" -name "*.md" -type f 2>/dev/null | xargs ls -t 2>/dev/null | head -1)
[ -n "$_LATEST_CP" ] && echo "LATEST_CHECKPOINT: $_LATEST_CP"
if [ -f "$_PROJ/decisions.active.json" ]; then
echo "--- ACTIVE DECISIONS (recent, scope-relevant) ---"
~/.claude/skills/gstack/bin/gstack-decision-search --recent 5 2>/dev/null
echo "--- END DECISIONS ---"
fi
echo "--- END ARTIFACTS ---"
fi
```
If artifacts are listed, read the newest useful one. If `LAST_SESSION` or `LATEST_CHECKPOINT` appears, give a 2-sentence welcome back summary. If `RECENT_PATTERN` clearly implies a next skill, suggest it once.
**Cross-session decisions.** If `ACTIVE DECISIONS` are listed, treat them as prior settled calls with their rationale — do not silently re-litigate them; if you're about to reverse one, say so explicitly. Reach for `~/.claude/skills/gstack/bin/gstack-decision-search` whenever a question touches a past decision ("what did we decide / why / did we try"). When you or the user make a DURABLE decision (architecture, scope, tool/vendor choice, or a reversal) — NOT a turn-level or trivial choice — log it with `~/.claude/skills/gstack/bin/gstack-decision-log` (`--supersede <id>` for a reversal). Reliable and local; gbrain not required.
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format is structure; this is prose quality.
@ -596,9 +626,9 @@ Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format i
Curated jargon list lives at `~/.claude/skills/gstack/scripts/jargon-list.json` (80+ terms). On the first jargon term you encounter this session, Read that file once; treat the `terms` array as the canonical list. The list is repo-owned and may grow between releases.
## Completeness Principle — Boil the Lake
## Completeness Principle — Boil the Ocean
AI makes completeness cheap. Recommend complete lakes (tests, edge cases, error paths); flag oceans (rewrites, multi-quarter migrations).
AI makes completeness cheap, so the complete thing is the goal. Recommend full coverage (tests, edge cases, error paths) — boil the ocean one lake at a time. The only thing out of scope is genuinely unrelated work (rewrites, multi-quarter migrations); flag that as separate scope, never as an excuse for a shortcut.
When options differ in coverage, include `Completeness: X/10` (10 = all edge cases, 7 = happy path, 3 = shortcut). When options differ in kind, write: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate scores.
@ -802,7 +832,7 @@ Read the `/office-hours` skill file at `~/.claude/skills/gstack/office-hours/SKI
Follow its instructions from top to bottom, **skipping these sections** (already handled by the parent skill):
- Preamble (run first)
- AskUserQuestion Format
- Completeness Principle — Boil the Lake
- Completeness Principle — Boil the Ocean
- Search Before Building
- Contributor Mode
- Completion Status Protocol
@ -1008,7 +1038,7 @@ Read each file using the Read tool:
(they are already handled by /autoplan):**
- Preamble (run first)
- AskUserQuestion Format
- Completeness Principle — Boil the Lake
- Completeness Principle — Boil the Ocean
- Search Before Building
- Completion Status Protocol
- Telemetry (run last)
@ -1035,11 +1065,17 @@ workflow.
```bash
_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || echo off)
_CODEX_CFG=$(~/.claude/skills/gstack/bin/gstack-config get codex_reviews 2>/dev/null || echo enabled)
source ~/.claude/skills/gstack/bin/gstack-codex-probe
# Master switch first: codex_reviews=disabled turns off ALL Codex work globally,
# including autoplan's own dual-voice orchestration. Honor it before probing.
if [ "$_CODEX_CFG" = "disabled" ]; then
echo "[codex disabled by config — Claude-only voices] Re-enable: gstack-config set codex_reviews enabled"
_CODEX_AVAILABLE=false
# Check Codex binary. If missing, tag the degradation matrix and continue
# with Claude subagent only (autoplan's existing degradation fallback).
if ! command -v codex >/dev/null 2>&1; then
elif ! command -v codex >/dev/null 2>&1; then
_gstack_codex_log_event "codex_cli_missing"
echo "[codex-unavailable: binary not found] — proceeding with Claude subagent only"
_CODEX_AVAILABLE=false

View File

@ -216,7 +216,7 @@ Read each file using the Read tool:
(they are already handled by /autoplan):**
- Preamble (run first)
- AskUserQuestion Format
- Completeness Principle — Boil the Lake
- Completeness Principle — Boil the Ocean
- Search Before Building
- Completion Status Protocol
- Telemetry (run last)
@ -243,11 +243,17 @@ workflow.
```bash
_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || echo off)
_CODEX_CFG=$(~/.claude/skills/gstack/bin/gstack-config get codex_reviews 2>/dev/null || echo enabled)
source ~/.claude/skills/gstack/bin/gstack-codex-probe
# Master switch first: codex_reviews=disabled turns off ALL Codex work globally,
# including autoplan's own dual-voice orchestration. Honor it before probing.
if [ "$_CODEX_CFG" = "disabled" ]; then
echo "[codex disabled by config — Claude-only voices] Re-enable: gstack-config set codex_reviews enabled"
_CODEX_AVAILABLE=false
# Check Codex binary. If missing, tag the degradation matrix and continue
# with Claude subagent only (autoplan's existing degradation fallback).
if ! command -v codex >/dev/null 2>&1; then
elif ! command -v codex >/dev/null 2>&1; then
_gstack_codex_log_event "codex_cli_missing"
echo "[codex-unavailable: binary not found] — proceeding with Claude subagent only"
_CODEX_AVAILABLE=false

View File

@ -48,6 +48,9 @@ echo "SKILL_PREFIX: $_SKILL_PREFIX"
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
REPO_MODE=${REPO_MODE:-unknown}
echo "REPO_MODE: $REPO_MODE"
_SESSION_KIND=$(~/.claude/skills/gstack/bin/gstack-session-kind 2>/dev/null || echo "interactive")
case "$_SESSION_KIND" in spawned|headless|interactive) ;; *) _SESSION_KIND="interactive" ;; esac
echo "SESSION_KIND: $_SESSION_KIND"
_LAKE_SEEN=$([ -f ~/.gstack/.completeness-intro-seen ] && echo "yes" || echo "no")
echo "LAKE_INTRO: $_LAKE_SEEN"
_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || true)
@ -127,7 +130,7 @@ In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`co
## Skill Invocation During Plan Mode
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If no AskUserQuestion variant is callable, use the normal chat fallback from the AskUserQuestion Format rule and stop for the user's reply. At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If AskUserQuestion is unavailable or a call fails, follow the AskUserQuestion Format failure fallback: `headless` → BLOCKED; `interactive` → the prose fallback (also satisfies end-of-turn). At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If `PROACTIVE` is `"false"`, do not auto-invoke or proactively suggest skills. If a skill seems useful, ask: "I think /skillname might help here — want me to run it?"
@ -162,7 +165,7 @@ touch ~/.gstack/.writing-style-prompted
Skip if `WRITING_STYLE_PENDING` is `no`.
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Lake** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Ocean** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
```bash
open https://garryslist.org/posts/boil-the-ocean

View File

@ -48,6 +48,9 @@ echo "SKILL_PREFIX: $_SKILL_PREFIX"
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
REPO_MODE=${REPO_MODE:-unknown}
echo "REPO_MODE: $REPO_MODE"
_SESSION_KIND=$(~/.claude/skills/gstack/bin/gstack-session-kind 2>/dev/null || echo "interactive")
case "$_SESSION_KIND" in spawned|headless|interactive) ;; *) _SESSION_KIND="interactive" ;; esac
echo "SESSION_KIND: $_SESSION_KIND"
_LAKE_SEEN=$([ -f ~/.gstack/.completeness-intro-seen ] && echo "yes" || echo "no")
echo "LAKE_INTRO: $_LAKE_SEEN"
_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || true)
@ -127,7 +130,7 @@ In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`co
## Skill Invocation During Plan Mode
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If no AskUserQuestion variant is callable, use the normal chat fallback from the AskUserQuestion Format rule and stop for the user's reply. At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If AskUserQuestion is unavailable or a call fails, follow the AskUserQuestion Format failure fallback: `headless` → BLOCKED; `interactive` → the prose fallback (also satisfies end-of-turn). At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If `PROACTIVE` is `"false"`, do not auto-invoke or proactively suggest skills. If a skill seems useful, ask: "I think /skillname might help here — want me to run it?"
@ -162,7 +165,7 @@ touch ~/.gstack/.writing-style-prompted
Skip if `WRITING_STYLE_PENDING` is `no`.
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Lake** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Ocean** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
```bash
open https://garryslist.org/posts/boil-the-ocean

View File

@ -72,7 +72,48 @@ fi
# no-op skip (no install, no decline marker). A dev workspace must never mutate
# global settings.json. To install the hooks, run `./setup --plan-tune-hooks`
# directly (outside dev-setup). Saved prefix/other config preferences still apply.
"$GSTACK_LINK/setup" --plan-tune-hooks=prompt </dev/null
#
# GSTACK_SKIP_GBRAIN_REGEN=1 is passed INLINE (not exported) so it scopes to
# exactly this nested setup call and can't leak into any other setup path. It
# tells setup NOT to regenerate the gbrain :user variant into the tracked
# worktree (that would dirty checked-in source). We render it into an untracked
# per-workspace dir below instead.
GSTACK_SKIP_GBRAIN_REGEN=1 "$GSTACK_LINK/setup" --plan-tune-hooks=prompt </dev/null
# 7. Brain-aware (gbrain) blocks — render into an untracked workspace dir.
#
# The worktree's SKILL.md files stay canonical (the guard above). If gbrain is
# installed, render the :user variant (with GBRAIN_CONTEXT_LOAD +
# GBRAIN_SAVE_RESULTS) into .claude/gstack-rendered (gitignored, per-workspace)
# and repoint the workspace's SKILL.md symlinks at it. gen-skill-docs --out-dir
# also rewrites the section-base path so section reads resolve to the render, not
# the global install. Result: this workspace gets the full gbrain experience
# while git stays clean. Other projects pick up blocks via `gstack-config
# gbrain-refresh` (printed below).
GBRAIN_DETECT="$REPO_ROOT/bin/gstack-gbrain-detect"
RENDER_DIR="$REPO_ROOT/.claude/gstack-rendered"
if [ -x "$GBRAIN_DETECT" ] && "$GBRAIN_DETECT" --is-ok 2>/dev/null; then
echo ""
echo "gbrain detected — rendering brain-aware skills into .claude/gstack-rendered (workspace-only, untracked)..."
rm -rf "$RENDER_DIR"
if ( cd "$REPO_ROOT" && bun run gen:skill-docs:user --host claude --out-dir "$RENDER_DIR" >/dev/null 2>&1 ); then
# Repoint each project-local SKILL.md symlink whose worktree target has a
# rendered counterpart. The skill DIRECTORY name (basename of the symlink
# target's dir) maps to RENDER_DIR/<dir>/SKILL.md, which is robust to
# frontmatter renames and the gstack- prefix on the link name.
repointed=0
for skill_link in "$REPO_ROOT"/.claude/skills/*/SKILL.md; do
[ -L "$skill_link" ] || continue
target="$(readlink "$skill_link")"
skilldir="$(basename "$(dirname "$target")")"
rendered="$RENDER_DIR/$skilldir/SKILL.md"
if [ -f "$rendered" ]; then ln -snf "$rendered" "$skill_link"; repointed=$((repointed + 1)); fi
done
echo " $repointed workspace skills now serve brain-aware blocks (worktree stays canonical)."
else
echo " warning: brain-aware render failed — workspace uses canonical skills."
fi
fi
echo ""
echo "Dev mode active. Skills resolve from this working tree."
@ -80,4 +121,7 @@ echo " .claude/skills/gstack → $REPO_ROOT"
echo " .agents/skills/gstack → $REPO_ROOT"
echo "Edit any SKILL.md and test immediately — no copy/deploy needed."
echo ""
echo "To make brain-aware blocks live across your OTHER projects too, run:"
echo " gstack-config gbrain-refresh"
echo ""
echo "To tear down: bin/dev-teardown"

View File

@ -24,9 +24,16 @@ if [ -d "$CLAUDE_SKILLS" ]; then
fi
rmdir "$CLAUDE_SKILLS" 2>/dev/null || true
rmdir "$REPO_ROOT/.claude" 2>/dev/null || true
fi
# ─── Clean up the untracked brain-aware render (bin/dev-setup step 7) ──
RENDER_DIR="$REPO_ROOT/.claude/gstack-rendered"
if [ -d "$RENDER_DIR" ]; then
rm -rf "$RENDER_DIR"
removed+=("claude/gstack-rendered")
fi
rmdir "$REPO_ROOT/.claude" 2>/dev/null || true
# ─── Clean up .agents/skills/ ────────────────────────────────
AGENTS_SKILLS="$REPO_ROOT/.agents/skills"
if [ -d "$AGENTS_SKILLS" ]; then

View File

@ -83,7 +83,23 @@ function loadMeta(scope: 'cross-project' | 'per-project', projectSlug: string |
return { schema_version: GSTACK_SCHEMA_PACK_VERSION, endpoint_hash: detectEndpointHash(), last_refresh: {}, last_attempt: {} };
}
try {
return JSON.parse(readFileSync(path, 'utf-8')) as CacheMeta;
const parsed = JSON.parse(readFileSync(path, 'utf-8')) as unknown;
// #1879: a valid JSON file can still be the wrong shape. JSON.parse can return
// null/array/string/number, and a partial object can omit last_refresh — three
// consumers (isStale, cmdInvalidate, refreshEntity) dereference meta.last_refresh
// unguarded and crash with a TypeError.
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
return { schema_version: GSTACK_SCHEMA_PACK_VERSION, endpoint_hash: detectEndpointHash(), last_refresh: {}, last_attempt: {} };
}
const meta = parsed as CacheMeta;
// Normalize ONLY the dereferenced maps. Do NOT default schema_version /
// endpoint_hash — leaving them absent makes schemaVersionMismatch() /
// endpointSwitched() correctly force a rebuild (missing identity = mismatch =
// safe). Defaulting them to current values would suppress invalidation and
// trust a stale file of unknown provenance.
meta.last_refresh = meta.last_refresh ?? {};
meta.last_attempt = meta.last_attempt ?? {};
return meta;
} catch {
// Corrupt _meta — start fresh (entries will refresh on next access).
return { schema_version: GSTACK_SCHEMA_PACK_VERSION, endpoint_hash: detectEndpointHash(), last_refresh: {}, last_attempt: {} };

View File

@ -86,7 +86,16 @@ CONFIG_HEADER='# gstack configuration — edit freely, changes take effect on ne
# # --no-plan-tune-hooks, or env GSTACK_PLAN_TUNE_HOOKS.
#
# ─── Advanced ────────────────────────────────────────────────────────
# codex_reviews: enabled # disabled = skip Codex adversarial reviews in /ship
# codex_reviews: enabled # Master switch for Codex cross-model review. enabled =
# # Codex runs as a standard step in /review, /ship,
# # /document-release, plan reviews, and /autoplan (auto
# # falls back to a Claude subagent if Codex is missing or
# # not authenticated). disabled = skip all Codex passes.
# # Asymmetry on disabled: diff-review (/review, /ship) still
# # runs the free Claude adversarial subagent; plan-review and
# # /document-release skip the outside-voice step entirely.
# # An invalid value is REJECTED (existing value preserved) so
# # a typo cannot silently turn paid Codex calls on or off.
# gstack_contributor: false # true = file field reports when gstack misbehaves
# skip_eng_review: false # true = skip eng review gate in /ship (not recommended)
#
@ -302,6 +311,13 @@ case "${1:-}" in
echo "Warning: plan_tune_hooks '$VALUE' not recognized. Valid values: prompt, yes, no. Using prompt." >&2
VALUE="prompt"
fi
# codex_reviews controls PAID Codex calls. Unlike the warn-and-default keys above,
# an invalid value is REJECTED and the existing setting is left unchanged — a typo
# must never silently flip the switch and turn paid Codex calls on or off.
if [ "$KEY" = "codex_reviews" ] && [ "$VALUE" != "enabled" ] && [ "$VALUE" != "disabled" ]; then
echo "Error: codex_reviews '$VALUE' not recognized. Valid values: enabled, disabled. Existing value left unchanged." >&2
exit 1
fi
mkdir -p "$STATE_DIR"
# Write annotated header on first creation
if [ ! -f "$CONFIG_FILE" ]; then
@ -396,8 +412,29 @@ case "${1:-}" in
case "$STATUS" in
ok)
echo "Detected gbrain v$VERSION → brain-aware blocks will render in planning-skill SKILL.md files."
echo "Run 'bun run gen:skill-docs' in the gstack repo (or re-run ./setup) to regenerate now."
echo "Detected gbrain v$VERSION."
# Render brain-aware blocks INTO the global install so EVERY project's
# Claude sessions get them (other projects read SKILL.md + sections from
# ~/.claude/skills/gstack via absolute paths baked at gen time). Guards
# (never mutate an arbitrary directory): the target must exist, not be a
# symlink (a symlinked install points at a dev worktree — rendering there
# would dirty tracked source), and look like a real gstack clone.
INSTALL_DIR="$HOME/.claude/skills/gstack"
if [ ! -d "$INSTALL_DIR" ]; then
echo "No global install at $INSTALL_DIR — nothing to render. (Dev workspaces get blocks via bin/dev-setup.)"
elif [ -L "$INSTALL_DIR" ]; then
echo "Skip: $INSTALL_DIR is a symlink (likely a dev worktree). Rendering there would dirty tracked source — run bin/dev-setup in that worktree instead."
elif [ ! -f "$INSTALL_DIR/VERSION" ] || [ ! -f "$INSTALL_DIR/package.json" ]; then
echo "Skip: $INSTALL_DIR doesn't look like a gstack clone (missing VERSION/package.json) — refusing to modify it."
elif ! command -v bun >/dev/null 2>&1; then
echo "Skip: bun not on PATH — can't render. Install bun, then re-run 'gstack-config gbrain-refresh'."
elif ( cd "$INSTALL_DIR" && bun run gen:skill-docs:user --host claude >/dev/null 2>&1 ); then
echo "Rendered brain-aware blocks into $INSTALL_DIR — now live across all your projects' Claude sessions."
echo "Note: this dirties the install's git tree (generated blocks differ from main, by design)."
echo " A 'git reset --hard origin/main' there reverts them; re-run 'gstack-config gbrain-refresh' to restore."
else
echo "Warning: render failed. Run 'cd $INSTALL_DIR && bun run gen:skill-docs:user --host claude' manually to see the error."
fi
;;
*)
echo "gbrain not detected (local-status: $STATUS) → brain-aware blocks will be suppressed in planning-skill SKILL.md files."

89
bin/gstack-decision-log Executable file
View File

@ -0,0 +1,89 @@
#!/usr/bin/env bun
/**
* gstack-decision-log — append a durable decision (or supersede/redact/compact it).
*
* Usage:
* gstack-decision-log '{"decision":"...","rationale":"...","scope":"repo","source":"user"}'
* gstack-decision-log --supersede <decision-id>
* gstack-decision-log --redact <decision-id>
* gstack-decision-log --compact
*
* Event-sourced (lib/gstack-decision): every call appends an event and refreshes the
* bounded active snapshot. NON-INTERACTIVE — never prompts (agents/skills call this;
* a prompt would hang them). Validation + injection + HIGH-secret rejection happen in
* validateDecide; a rejected decision exits 1 with a message, nothing persisted.
*/
import { mkdirSync } from "fs";
import { dirname } from "path";
import { spawnSync } from "child_process";
import {
decisionPaths,
validateDecide,
makeRefEvent,
appendEvent,
rebuildSnapshot,
compact,
type DecisionEvent,
} from "../lib/gstack-decision";
import { resolveSlug, gitBranch, flagValue } from "../lib/bin-context";
const HERE = import.meta.dir;
const args = process.argv.slice(2);
const slug = resolveSlug(`${HERE}/gstack-slug`);
const paths = decisionPaths(slug);
mkdirSync(dirname(paths.log), { recursive: true });
function enqueue(): void {
// Fire-and-forget cross-machine sync (no-op when artifacts_sync is off).
spawnSync(`${HERE}/gstack-brain-enqueue`, [`projects/${slug}/decisions.jsonl`], { stdio: "ignore" });
}
if (args.includes("--compact")) {
const r = compact(paths);
if (r.skipped) {
console.log("compact skipped: a concurrent write/compact is in progress; log left intact — re-run");
process.exit(0);
}
console.log(`compacted: ${r.activeCount} active, ${r.archivedCount} archived, ${r.expungedCount} expunged`);
enqueue();
process.exit(0);
}
const supersedeId = flagValue(args, "--supersede");
const redactId = flagValue(args, "--redact");
if (supersedeId || redactId) {
const kind = supersedeId ? "supersede" : "redact";
const targetId = (supersedeId || redactId) as string;
appendEvent(paths, makeRefEvent(kind, targetId, { source: "agent" }));
rebuildSnapshot(paths);
enqueue();
console.log(`${kind}: ${targetId}`);
process.exit(0);
}
const jsonArg = args.find((a) => !a.startsWith("--"));
if (!jsonArg) {
process.stderr.write(
"gstack-decision-log: provide a JSON decision, or --supersede/--redact <id>, or --compact\n",
);
process.exit(1);
}
let obj: Partial<DecisionEvent>;
try {
obj = JSON.parse(jsonArg);
} catch {
process.stderr.write("gstack-decision-log: invalid JSON\n");
process.exit(1);
}
if (obj.scope === "branch" && !obj.branch) obj.branch = gitBranch();
const res = validateDecide(obj);
if (!res.ok) {
process.stderr.write(`gstack-decision-log: ${res.error}\n`);
process.exit(1);
}
appendEvent(paths, res.event);
rebuildSnapshot(paths);
enqueue();
console.log(res.event.id);

108
bin/gstack-decision-search Executable file
View File

@ -0,0 +1,108 @@
#!/usr/bin/env bun
/**
* gstack-decision-search — read active decisions (the curated "what did we decide" view).
*
* Usage:
* gstack-decision-search [--query KW] [--scope repo|branch|issue]
* [--branch B] [--issue I] [--recent N] [--all] [--json]
* [--semantic]
*
* Reads the BOUNDED active snapshot (decisions.active.json) — O(active), not a full
* history scan — and rebuilds it from the event log if missing. Scope-filtered to the
* current branch/issue context (recency != relevance). NON-INTERACTIVE. `--all` shows
* superseded decisions too (from the full log). Exit 0 silently when there are none.
*
* `--semantic` (with `--query`) appends an OPTIONAL "related from memory" block from
* gbrain semantic recall. It is a pure enhancement: when gbrain is off/unconfigured/
* empty it degrades silently to the reliable file results above. The reliable path
* never loads gbrain code (the semantic module is imported lazily only here).
*/
import { existsSync } from "fs";
import {
decisionPaths,
readSnapshot,
rebuildSnapshot,
readEvents,
filterByScope,
datamark,
type ActiveDecision,
} from "../lib/gstack-decision";
import { resolveSlug, gitBranch, flagValue } from "../lib/bin-context";
const HERE = import.meta.dir;
const args = process.argv.slice(2);
const slug = resolveSlug(`${HERE}/gstack-slug`);
const paths = decisionPaths(slug);
const queryRaw = flagValue(args, "--query");
const query = queryRaw?.toLowerCase();
const scope = flagValue(args, "--scope");
const branch = flagValue(args, "--branch") ?? gitBranch();
const issue = flagValue(args, "--issue");
const recentRaw = flagValue(args, "--recent");
const recent = recentRaw ? parseInt(recentRaw, 10) : undefined;
const showAll = args.includes("--all");
const asJson = args.includes("--json");
const semantic = args.includes("--semantic");
let rows: ActiveDecision[];
if (showAll) {
// --all includes SUPERSEDED decisions (history), but NEVER redacted ones — a redact
// is an expunge, so it must remove the text from every read path, not just active.
const events = readEvents(paths);
const redacted = new Set(
events.filter((e) => e.kind === "redact" && e.supersedes).map((e) => e.supersedes as string),
);
rows = events.filter((e): e is ActiveDecision => e.kind === "decide" && !redacted.has(e.id));
} else {
rows = readSnapshot(paths);
// Rebuild only when a snapshot is absent but a log exists (don't write a snapshot
// into a nonexistent store on an empty read — just return nothing).
if (!rows.length && existsSync(paths.log)) rows = rebuildSnapshot(paths);
}
rows = filterByScope(rows, { branch, issue });
if (scope) rows = rows.filter((d) => d.scope === scope);
if (query) {
rows = rows.filter((d) =>
[d.decision, d.rationale, d.alternatives_considered]
.filter((s): s is string => typeof s === "string")
.some((s) => s.toLowerCase().includes(query)),
);
}
rows.sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : 0)); // newest first
if (recent && recent > 0) rows = rows.slice(0, recent);
if (asJson) {
// --json stays reliable-only (semantic recall is a human-facing supplement).
console.log(JSON.stringify(rows));
process.exit(0);
}
for (const d of rows) {
// Datamark all stored free-text (decision, rationale, branch/issue) — it lands in
// agent context via Context Recovery, so treat it as DATA, not instructions.
const branchTag = d.branch ? `:${datamark(d.branch)}` : "";
const issueTag = d.issue ? `:${datamark(d.issue)}` : "";
const scopeTag = d.scope === "repo" ? "" : ` [${d.scope}${branchTag}${issueTag}]`;
console.log(`- ${datamark(d.decision ?? "")}${scopeTag} (${d.source}, ${d.date.slice(0, 10)})`);
if (d.rationale) console.log(` why: ${datamark(d.rationale)}`);
}
// OPTIONAL gbrain enhancement. Lazy import so the reliable path above never loads
// gbrain code. Degrades silently: null (gbrain off) or [] (nothing found) leaves the
// reliable results above as the answer.
if (semantic && queryRaw) {
const { semanticRecall } = await import("../lib/gstack-decision-semantic");
const hits = semanticRecall(queryRaw);
if (hits && hits.length) {
console.log("\nRelated from memory (gbrain semantic recall):");
for (const h of hits) {
// gbrain hits are EXTERNAL corpus content — datamark slug + snippet too so they
// can't spoof role markers / fences when printed into agent context.
const snip = datamark(h.snippet.length > 100 ? `${h.snippet.slice(0, 100)}…` : h.snippet);
console.log(` [${h.score.toFixed(2)}] ${datamark(h.slug)}: ${snip}`);
}
}
}

View File

@ -75,7 +75,10 @@ while IFS= read -r f; do
# Backend: everything else that's code (excluding views/components already matched)
*.rb|*.py|*.go|*.rs|*.java|*.php|*.ex|*.exs) BACKEND=true ;;
*.ts|*.js) BACKEND=true ;; # Non-component TS/JS is backend
# Non-component TS/JS is backend. Include ESM/CJS (.mjs/.cjs) and
# explicit-module TS (.mts/.cts) — #1810: these matched no category, so an
# ESM/CJS-only PR skipped the backend reviewer entirely.
*.ts|*.js|*.mjs|*.cjs|*.mts|*.cts) BACKEND=true ;;
esac
done <<< "$FILES"

View File

@ -234,4 +234,14 @@ function main(): void {
process.stdout.write(JSON.stringify(out, null, 2) + "\n");
}
// --is-ok: live engine-status gate. Exits 0 iff gbrain is usable ("ok"), 1
// otherwise. Runs detection live (never reads the possibly-stale
// gbrain-detection.json), so callers — setup, bin/dev-setup, and
// `gstack-config gbrain-refresh` — can decide whether to render the gbrain
// :user variant without duplicating the JSON grep. Prints nothing on stdout.
if (process.argv.includes("--is-ok")) {
const noCache = process.env.GSTACK_DETECT_NO_CACHE === "1";
process.exit(localEngineStatus({ noCache }) === "ok" ? 0 : 1);
}
main();

View File

@ -37,22 +37,27 @@ import { createHash } from "crypto";
import "../lib/conductor-env-shim";
import { detectEngineTier, withErrorContext, canonicalizeRemote } from "../lib/gstack-memory-helpers";
import { ensureSourceRegistered, sourcePageCount, parseSourcesList } from "../lib/gbrain-sources";
import { ensureSourceRegistered, sourcePageCount, parseSourcesList, cycleCompleted, type CycleStatus } from "../lib/gbrain-sources";
import { detectAutopilot, decideSourceRemove, decideCodeSync } from "../lib/gbrain-guards";
import { localEngineStatus, type LocalEngineStatus } from "../lib/gbrain-local-status";
import { buildGbrainEnv, spawnGbrain, execGbrainJson, NEEDS_SHELL_ON_WINDOWS } from "../lib/gbrain-exec";
import { checkOwnedStagingDir } from "../lib/staging-guard";
// ── Types ──────────────────────────────────────────────────────────────────
type Mode = "incremental" | "full" | "dry-run";
interface CliArgs {
export interface CliArgs {
mode: Mode;
quiet: boolean;
noCode: boolean;
noMemory: boolean;
noBrainSync: boolean;
codeOnly: boolean;
/** Force the source-scoped dream cycle (builds this source's call graph). Always runs. */
dream: boolean;
/** Opt out of the dream cycle that `--full` would otherwise auto-run. */
noDream: boolean;
/** #1734: opt-in to sync a URL-managed source whose code walk may auto-reclone. */
allowReclone: boolean;
}
@ -71,6 +76,13 @@ interface StageResult {
ok: boolean;
duration_ms: number;
summary: string;
/**
* Stage ran and did not error, but the outcome is a degraded no-op the user
* should know about (e.g. dream completed but the schema pack can't extract
* code symbols, so the call graph stays empty). Rendered as WARN, counts as
* ok for the exit code it's not a failure, just not the happy path.
*/
warn?: boolean;
/** Stage-specific structured detail. Code stage carries source_id + page_count. */
detail?: CodeStageDetail;
}
@ -83,6 +95,24 @@ const STATE_PATH = join(GSTACK_HOME, ".gbrain-sync-state.json");
const LOCK_PATH = join(GSTACK_HOME, ".sync-gbrain.lock");
const STALE_LOCK_MS = 5 * 60 * 1000;
// Dream (call-graph build) is brain-global and runs LOCK-FREE after the sync
// lock releases, so it can't use the sync lock to dedupe across worktrees. A
// dedicated short-TTL marker prevents two worktrees from launching duplicate
// ~35-min global jobs. TTL matches the dream timeout default so a crashed run
// can't wedge the marker longer than one cycle.
const DEFAULT_DREAM_TIMEOUT_MS = 45 * 60 * 1000; // 45min — dream is the slow stage
const DREAM_MARKER_STALE_MS = DEFAULT_DREAM_TIMEOUT_MS;
/**
* Marker path computed fresh per call (not a module const) so tests can mutate
* GSTACK_HOME at runtime same pattern as cacheFilePath() in
* lib/gbrain-local-status.ts. Avoids the ESM static-import hoist trap where a
* module-load-time const captures the real ~/.gstack before a test can redirect.
*/
export function dreamMarkerPath(): string {
return join(process.env.GSTACK_HOME || join(homedir(), ".gstack"), ".dream-in-progress");
}
// Default 35-minute timeout for code-walk + memory-ingest stages. Override via
// GSTACK_SYNC_CODE_TIMEOUT_MS / GSTACK_SYNC_MEMORY_TIMEOUT_MS. Bounds-checked
// in resolveStageTimeoutMs below so wildly-low values don't make resume
@ -99,26 +129,27 @@ const MAX_STAGE_TIMEOUT_MS = 86_400_000; // 24 hour ceiling
export function resolveStageTimeoutMs(
envValue: string | undefined,
envName: string,
defaultMs: number = DEFAULT_STAGE_TIMEOUT_MS,
): number {
if (envValue === undefined || envValue === "") return DEFAULT_STAGE_TIMEOUT_MS;
if (envValue === undefined || envValue === "") return defaultMs;
const n = Number.parseInt(envValue, 10);
if (!Number.isFinite(n) || Number.isNaN(n) || n <= 0) {
console.warn(
`[sync] ${envName}="${envValue}" is not a positive integer; falling back to ${DEFAULT_STAGE_TIMEOUT_MS}ms`,
`[sync] ${envName}="${envValue}" is not a positive integer; falling back to ${defaultMs}ms`,
);
return DEFAULT_STAGE_TIMEOUT_MS;
return defaultMs;
}
if (n < MIN_STAGE_TIMEOUT_MS) {
console.warn(
`[sync] ${envName}=${n} is below the ${MIN_STAGE_TIMEOUT_MS}ms (1min) floor; falling back to ${DEFAULT_STAGE_TIMEOUT_MS}ms`,
`[sync] ${envName}=${n} is below the ${MIN_STAGE_TIMEOUT_MS}ms (1min) floor; falling back to ${defaultMs}ms`,
);
return DEFAULT_STAGE_TIMEOUT_MS;
return defaultMs;
}
if (n > MAX_STAGE_TIMEOUT_MS) {
console.warn(
`[sync] ${envName}=${n} is above the ${MAX_STAGE_TIMEOUT_MS}ms (24h) ceiling; falling back to ${DEFAULT_STAGE_TIMEOUT_MS}ms`,
`[sync] ${envName}=${n} is above the ${MAX_STAGE_TIMEOUT_MS}ms (24h) ceiling; falling back to ${defaultMs}ms`,
);
return DEFAULT_STAGE_TIMEOUT_MS;
return defaultMs;
}
return n;
}
@ -160,7 +191,7 @@ export function readGbrainCheckpoint(): GbrainCheckpoint | null {
export type ResumeVerdict =
| { kind: "no-checkpoint" }
| { kind: "resume"; stagingDir: string; processedIndex: number; totalFiles: number }
| { kind: "stale-staging-missing"; stagingDir: string };
| { kind: "stale-staging-missing"; stagingDir: string; reason?: string };
/**
* Decide whether the next memory-ingest run should resume from gbrain's
@ -169,20 +200,20 @@ export type ResumeVerdict =
* - checkpoint + staging ok resume (gbrain picks up at processedIndex+1)
* - checkpoint + staging gone warn, fall through to fresh restage
*/
export function decideResume(): ResumeVerdict {
export function decideResume(gstackHome: string = GSTACK_HOME): ResumeVerdict {
const cp = readGbrainCheckpoint();
if (!cp || !cp.dir) return { kind: "no-checkpoint" };
const stagingDir = cp.dir;
if (!existsSync(stagingDir)) {
return { kind: "stale-staging-missing", stagingDir };
}
// Treat "non-empty" as the safe-to-resume signal. statSync on a missing
// file throws; we already handled missing above so this is dir-level shape.
try {
const st = statSync(stagingDir);
if (!st.isDirectory()) return { kind: "stale-staging-missing", stagingDir };
} catch {
return { kind: "stale-staging-missing", stagingDir };
// #1802: only resume into a path we can PROVE is a gstack-minted staging dir.
// A poisoned checkpoint (dir = repo root, written when an autopilot import was
// SIGTERM'd while CWD was the repo) would otherwise be adopted as the staging
// dir and later recursively deleted by cleanupStagingDir(). Fail-closed: any
// unprovable path restages from scratch (cost: one re-stage; never data loss).
// Pure decision: return the verdict (with reason) and let the caller log,
// so we don't double-log the same event from here and the call site.
const verdict = checkOwnedStagingDir(stagingDir, gstackHome);
if (!verdict.ok) {
return { kind: "stale-staging-missing", stagingDir, reason: verdict.reason };
}
return {
kind: "resume",
@ -208,12 +239,19 @@ Options:
--no-memory Skip the gstack-memory-ingest stage (transcripts + artifacts).
--no-brain-sync Skip the gstack-brain-sync git pipeline stage.
--code-only Only run the code-import stage (alias for --no-memory --no-brain-sync).
--dream Force the source-scoped dream cycle that builds this
source's call graph (gbrain code-callers/code-callees).
Runs lock-free AFTER the sync stages. ~minutes. Default
timeout 45min, override GSTACK_SYNC_DREAM_TIMEOUT_MS.
--no-dream Opt out of the dream cycle that --full would auto-run.
--allow-reclone Permit the code walk for URL-managed sources (remote_url set)
even though gbrain may auto-reclone the working tree (#1734).
--help This text.
Stages run in order: code memory ingest curated git push.
Each stage failure is non-fatal; subsequent stages still run.
Stages run in order: code memory ingest curated git push, then (lock-free)
the optional dream call-graph build. --full auto-runs dream ONLY when the call
graph was never built; --dream always forces it. Each stage failure is
non-fatal; subsequent stages still run.
`);
}
@ -225,6 +263,8 @@ function parseArgs(): CliArgs {
let noMemory = false;
let noBrainSync = false;
let codeOnly = false;
let dream = false;
let noDream = false;
let allowReclone = false;
for (let i = 0; i < args.length; i++) {
@ -243,6 +283,10 @@ function parseArgs(): CliArgs {
noMemory = true;
noBrainSync = true;
break;
// --dream forces the cycle; --full only chains it at the call site (so
// --no-dream can override) — do NOT set dream from --full here.
case "--dream": dream = true; break;
case "--no-dream": noDream = true; break;
case "--help":
case "-h":
printUsage();
@ -254,7 +298,7 @@ function parseArgs(): CliArgs {
}
}
return { mode, quiet, noCode, noMemory, noBrainSync, codeOnly, allowReclone };
return { mode, quiet, noCode, noMemory, noBrainSync, codeOnly, dream, noDream, allowReclone };
}
// ── Helpers ────────────────────────────────────────────────────────────────
@ -609,6 +653,58 @@ function releaseLock(): void {
}
}
/**
* Acquire the dream marker (`~/.gstack/.dream-in-progress`). Returns false when
* a FRESH marker already exists (another worktree is mid-dream) the caller
* then SKIPs rather than launching a duplicate ~35-min global job. A stale
* marker (older than DREAM_MARKER_STALE_MS, i.e. a crashed run) is taken over.
* Mirrors acquireLock but with the dream TTL and its own path.
*/
export function acquireDreamMarker(): boolean {
const path = dreamMarkerPath();
mkdirSync(dirname(path), { recursive: true });
if (existsSync(path)) {
try {
const stat = statSync(path);
if (Date.now() - stat.mtimeMs > DREAM_MARKER_STALE_MS) {
unlinkSync(path);
} else {
return false;
}
} catch {
return false;
}
}
const info: LockInfo = { pid: process.pid, started_at: new Date().toISOString() };
try {
writeFileSync(path, JSON.stringify(info), { encoding: "utf-8", flag: "wx" });
return true;
} catch {
return false;
}
}
export function releaseDreamMarker(): void {
try {
const path = dreamMarkerPath();
if (!existsSync(path)) return;
const info = JSON.parse(readFileSync(path, "utf-8")) as LockInfo;
if (info.pid === process.pid) unlinkSync(path);
} catch {
// Best-effort cleanup.
}
}
/** Read the pid recorded in a fresh dream marker, for the "already running" message. */
function dreamMarkerPid(): number | null {
try {
const info = JSON.parse(readFileSync(dreamMarkerPath(), "utf-8")) as LockInfo;
return typeof info.pid === "number" ? info.pid : null;
} catch {
return null;
}
}
// ── Stage runners ──────────────────────────────────────────────────────────
/**
@ -623,7 +719,7 @@ function releaseLock(): void {
* broken-db "config points at unreachable DB; see /setup-gbrain Step 1.5"
*/
function skipStageForLocalStatus(
stage: "code" | "memory",
stage: "code" | "memory" | "dream",
status: LocalEngineStatus,
t0: number,
): StageResult {
@ -953,8 +1049,15 @@ function runMemoryIngest(args: CliArgs): StageResult {
);
childEnv.GSTACK_INGEST_RESUME_DIR = resume.stagingDir;
} else if (resume.kind === "stale-staging-missing") {
// The reason distinguishes "actually gone" (disk cleanup / reboot) from
// "refused as unowned" (#1802 poison: the path may still exist on disk).
// Logging "gone" for a refused poison path misdirects incident diagnosis.
const why = resume.reason
? `staging dir not usable: ${resume.reason}`
: `staging dir ${resume.stagingDir} gone`;
console.error(
`[sync:memory] previous checkpoint stale (staging dir ${resume.stagingDir} gone), restaging from scratch`,
`[sync:memory] previous checkpoint stale (${why}), restaging from scratch. ` +
`Remove ~/.gbrain/import-checkpoint.json to silence.`,
);
}
@ -1039,6 +1142,240 @@ function runBrainSyncPush(args: CliArgs): StageResult {
};
}
/**
* Decide whether the dream (call-graph build) cycle should run. PURE so the
* gate matrix is unit-testable without spawning a real ~35-min dream.
*
* - explicit --dream always run (force), regardless of cycle state / --no-code.
* - --full run ONLY when the call graph was never built (cycle === "never"),
* and only when not opted out via --no-dream / --no-code. "completed" skips
* (edges already built); "unknown" skips (a flaky doctor must not trigger a
* surprise 35-min cycle see gbrain-doctor-overstrict).
* - everything else skip.
*
* `cycle` is only consulted on the --full auto path; pass null when forcing.
*/
export function shouldRunDream(args: CliArgs, cycle: CycleStatus | null): boolean {
if (args.dream) return true;
if (args.mode === "full" && !args.noDream && !args.noCode) {
return cycle === "never";
}
return false;
}
/**
* Run `gbrain dream` the brain-global maintenance cycle whose
* resolve_symbol_edges phase builds the call graph. Runs LOCK-FREE (called
* after the sync lock releases) so it never freezes sibling worktrees; the
* `.dream-in-progress` marker dedupes concurrent dreams instead.
*
* Returns a StageResult (never throws). SKIP (ran:false, ok:true) for: dry-run
* preview, local engine not ok, or a fresh marker present. ERR (ran:true,
* ok:false) for: non-zero/timeout exit, or a spawn-setup failure (missing
* binary / malformed env) a broken install must be visible, not disguised as
* optional maintenance.
*/
export async function runDream(args: CliArgs): Promise<StageResult> {
const t0 = Date.now();
if (args.mode === "dry-run") {
const root = repoRoot();
const sourceId = root ? deriveCodeSourceId(root) : null;
return {
name: "dream",
ran: false,
ok: true,
duration_ms: 0,
summary: sourceId
? `would: gbrain dream --source ${sourceId} (build this source's call graph)`
: "would: gbrain dream (call-graph build)",
};
}
const localStatus = localEngineStatus({ noCache: false });
if (localStatus !== "ok") {
return skipStageForLocalStatus("dream", localStatus, t0);
}
// Dedupe concurrent dreams across worktrees (lock-free path).
if (!acquireDreamMarker()) {
const pid = dreamMarkerPid();
return {
name: "dream",
ran: false,
ok: true,
duration_ms: Date.now() - t0,
summary: `dream already running${pid !== null ? ` (pid ${pid})` : ""} — skipped`,
};
}
try {
const dreamTimeoutMs = resolveStageTimeoutMs(
process.env.GSTACK_SYNC_DREAM_TIMEOUT_MS,
"GSTACK_SYNC_DREAM_TIMEOUT_MS",
DEFAULT_DREAM_TIMEOUT_MS,
);
// Scope the cycle to THIS worktree's code source: `gbrain dream --source <id>`.
// Verified empirically (not just from `gbrain --help`): plain `gbrain dream`
// cycles the brain's default source and never runs the source-scoped `extract`
// phase for our code source, so the call graph for the pinned source stays
// empty. `gbrain dream --source <id>` runs the per-source cycle (the form
// `gbrain doctor` recommends for stale sources) and is what actually populates
// code-callers/code-callees for this worktree. Falls back to plain `dream`
// only when we can't derive the source id (not in a git repo).
const root = repoRoot();
const sourceId = root ? deriveCodeSourceId(root) : null;
const dreamArgs = sourceId ? ["dream", "--source", sourceId] : ["dream"];
// spawnGbrain seeds DATABASE_URL from gbrain's config via buildGbrainEnv.
//
// We CAPTURE output (pipe) rather than inherit because `gbrain dream` exits 0
// even when it SKIPS the cycle — when another cycle already holds gbrain's own
// DB lock (e.g. a running `gbrain autopilot`), it prints "Skipped: another
// cycle is already running. (locked)" and exits 0. Trusting the exit code
// alone would falsely report "call graph built". Trade-off: no live streaming
// for a long cycle; we echo the captured output afterward instead.
if (!args.quiet) {
process.stderr.write("[dream] running gbrain cycle (call-graph build; this can take a few minutes)...\n");
}
let result: ReturnType<typeof spawnGbrain>;
try {
result = spawnGbrain(dreamArgs, {
stdio: ["ignore", "pipe", "pipe"],
timeout: dreamTimeoutMs,
baseEnv: process.env,
announce: !args.quiet,
});
} catch (err) {
// Spawn-setup failure (missing binary, bad env): ERR, not a benign skip.
return {
name: "dream",
ran: true,
ok: false,
duration_ms: Date.now() - t0,
summary: `gbrain dream failed to start: ${(err as Error).message}`,
};
}
if (result.error) {
const e = result.error as NodeJS.ErrnoException;
const why = e.code === "ENOENT" ? "gbrain not on PATH" : e.message;
return {
name: "dream",
ran: true,
ok: false,
duration_ms: Date.now() - t0,
summary: `gbrain dream failed to start: ${why}`,
};
}
const out = `${result.stdout || ""}${result.stderr || ""}`;
if (!args.quiet && out.trim()) {
process.stderr.write(out.endsWith("\n") ? out : `${out}\n`);
}
if (result.status !== 0) {
return {
name: "dream",
ran: true,
ok: false,
duration_ms: Date.now() - t0,
summary: `gbrain dream exited ${result.status === null ? "null (killed by signal / timeout)" : result.status}`,
};
}
// Exit 0 but the cycle was SKIPPED because gbrain's own lock is held by
// another cycle (typically `gbrain autopilot`). Report SKIP, not "built" —
// the graph builds on that other cycle, not this invocation.
if (/already running|\block(?:ed)?\b|Skipped:/i.test(out)) {
return {
name: "dream",
ran: false,
ok: true,
duration_ms: Date.now() - t0,
summary: "skipped — a gbrain cycle is already running (e.g. autopilot); the call graph builds on that cycle",
};
}
// Exit 0 and the cycle actually ran. Parse the cycle's OWN output to report
// the truth, not a flat "built": `gbrain dream` exits 0 even when the call
// graph could not be built, and a misleading "built" turns a multi-minute
// no-op into a silent dead end. gbrain only surfaces these conditions in the
// cycle log (there is no pre-flight pack-capability query as of 0.41.x), so
// string-matching the log is the available signal; an unrecognized log
// degrades to the generic success summary below.
const dreamWarn = classifyDreamOutcome(out);
if (dreamWarn) {
return {
name: "dream",
ran: true,
ok: true,
warn: true,
duration_ms: Date.now() - t0,
summary: dreamWarn,
};
}
const edges = parseResolvedEdges(out);
return {
name: "dream",
ran: true,
ok: true,
duration_ms: Date.now() - t0,
summary:
edges !== null
? `call graph built (${edges} edge${edges === 1 ? "" : "s"} resolved)`
: "call graph built (resolve_symbol_edges complete)",
};
} finally {
releaseDreamMarker();
}
}
/**
* Parse `<n>` from a `resolve_symbol_edges ... resolved <n>` cycle-log line.
* Returns null when the line is absent (older gbrain / different pack). The
* `[^\n]*?` is newline-bounded so it matches the `✓ resolve_symbol_edges ...`
* summary line, not the bracketed `[cycle.resolve_symbol_edges] start` markers.
*/
export function parseResolvedEdges(out: string): number | null {
const m = out.match(/resolve_symbol_edges\b[^\n]*?\bresolved\s+(\d+)/i);
return m ? parseInt(m[1], 10) : null;
}
/**
* Inspect a completed (exit-0) `gbrain dream` log and return a WARN summary when
* the cycle ran but could not actually build the call graph. Returns null on the
* happy path (caller emits the normal "call graph built" summary). Order matters:
* the pack-capability gap is the most actionable, so it wins over a 0-edge count
* (both appear together when the pack lacks the code-symbol phase).
*/
export function classifyDreamOutcome(out: string): string | null {
// The active schema pack doesn't declare the code-symbol extraction phase, so
// no symbols are extracted and resolve_symbol_edges has nothing to match.
if (/does not declare this phase/i.test(out)) {
return (
"dream ran, but this source's schema pack does not extract code symbols, " +
"so the call graph stays empty. Switch this source to a code-aware schema " +
"pack (`gbrain schema use <pack>`) to enable code-callers/code-callees."
);
}
// The embed phase failed for a missing key; symbols can't index without it.
if (/embed phase failed/i.test(out) || /requires\s+\S*_API_KEY/i.test(out)) {
return (
"dream ran, but the embed phase failed (missing embedding API key), so " +
"symbols won't index. Ensure the embedding provider's key is set for the " +
"gbrain process, then re-run /sync-gbrain --dream."
);
}
// Cycle ran and embedded fine, but matched zero call-graph edges.
if (parseResolvedEdges(out) === 0) {
return "dream ran but resolved 0 call-graph edges (no code symbols matched for this source yet).";
}
return null;
}
// ── State file ─────────────────────────────────────────────────────────────
interface SyncState {
@ -1077,10 +1414,28 @@ function saveSyncState(state: SyncState): void {
}
}
/**
* Persist the dream stage result with read-modify-write semantics.
*
* Dream runs AFTER the sync lock releases, so a sibling worktree may have
* written newer state in the meantime. Overwriting the whole file with our
* pre-dream snapshot + dream result would clobber that sibling's sync. Instead
* re-read the CURRENT state, replace only the `dream` entry in last_stages, and
* atomic-rename. (Atomic rename alone isn't race-safe; the re-read + targeted
* merge is what prevents the clobber.)
*/
function mergeDreamIntoState(dream: StageResult): void {
const fresh = loadSyncState();
const others = (fresh.last_stages || []).filter((s) => s.name !== "dream");
fresh.last_stages = [...others, dream];
fresh.last_sync = new Date().toISOString();
saveSyncState(fresh);
}
// ── Output ─────────────────────────────────────────────────────────────────
function formatStage(s: StageResult): string {
const status = !s.ran ? "SKIP" : s.ok ? "OK" : "ERR";
export function formatStage(s: StageResult): string {
const status = !s.ran ? "SKIP" : !s.ok ? "ERR" : s.warn ? "WARN" : "OK";
const dur = s.duration_ms > 0 ? ` (${(s.duration_ms / 1000).toFixed(1)}s)` : "";
return ` ${status.padEnd(5)} ${s.name.padEnd(12)} ${s.summary}${dur}`;
}
@ -1116,9 +1471,9 @@ async function main(): Promise<void> {
process.on("SIGTERM", () => { cleanup(); process.exit(143); });
let exitCode = 0;
const stages: StageResult[] = [];
try {
const state = loadSyncState();
const stages: StageResult[] = [];
if (!args.noCode) {
stages.push(await withErrorContext("sync:code", () => runCodeImport(args), "gstack-gbrain-sync"));
@ -1137,20 +1492,61 @@ async function main(): Promise<void> {
saveSyncState(state);
}
if (!args.quiet || args.mode === "dry-run") {
console.log(`\ngstack-gbrain-sync (${args.mode}):`);
for (const s of stages) console.log(formatStage(s));
const okCount = stages.filter((s) => s.ok).length;
const errCount = stages.filter((s) => !s.ok && s.ran).length;
console.log(`\n ${okCount} ok, ${errCount} error, ${stages.length - okCount - errCount} skipped`);
}
const anyError = stages.some((s) => s.ran && !s.ok);
exitCode = anyError ? 1 : 0;
} finally {
// Release the sync lock BEFORE the dream cycle. Dream is a source-scoped
// cycle that can run several minutes; holding the machine-wide lock that
// long would freeze every other worktree's /sync-gbrain. Dream is guarded
// by its own marker.
cleanup();
}
// ── Dream (call-graph build) — LOCK-FREE, after the sync lock releases ─────
let dreamStage: StageResult | null = null;
if (args.mode === "dry-run") {
// Preview only; never probes doctor or spawns. `--dry-run` and `--full` are
// mutually exclusive modes (last one wins in parseArgs), so the only dream
// preview that applies to a dry-run is the explicit --dream force.
if (args.dream) {
dreamStage = await runDream(args);
}
} else {
// Resolve cycle state only on the --full auto path (perf: the steady-state
// incremental sync never pays a doctor subprocess). Explicit --dream forces.
let cycle: CycleStatus | null = null;
if (!args.dream && args.mode === "full" && !args.noDream && !args.noCode) {
const root = repoRoot();
cycle = root ? cycleCompleted(deriveCodeSourceId(root), process.env) : "unknown";
}
if (shouldRunDream(args, cycle)) {
dreamStage = await runDream(args);
mergeDreamIntoState(dreamStage);
if (dreamStage.ran && !dreamStage.ok) exitCode = 1;
} else if (cycle === "unknown") {
// --full wanted to auto-build but doctor couldn't confirm the graph state.
// Surface a WARN-style SKIP so the user knows to run --dream if needed,
// rather than silently doing nothing (a flaky doctor must not trigger a
// surprise 35-min run — gbrain-doctor-overstrict).
dreamStage = {
name: "dream",
ran: false,
ok: true,
duration_ms: 0,
summary: "call-graph state unknown (doctor unavailable) — run /sync-gbrain --dream if code-callers returns 0",
};
}
}
if (!args.quiet || args.mode === "dry-run") {
const allStages = dreamStage ? [...stages, dreamStage] : stages;
console.log(`\ngstack-gbrain-sync (${args.mode}):`);
for (const s of allStages) console.log(formatStage(s));
const okCount = allStages.filter((s) => s.ok).length;
const errCount = allStages.filter((s) => !s.ok && s.ran).length;
console.log(`\n ${okCount} ok, ${errCount} error, ${allStages.length - okCount - errCount} skipped`);
}
process.exit(exitCode);
}

View File

@ -15,6 +15,7 @@ INPUT="$1"
# Validate and sanitize input
VALIDATED=$(printf '%s' "$INPUT" | bun -e "
import { hasInjection } from '$SCRIPT_DIR/../lib/jsonl-store.ts';
const raw = await Bun.stdin.text();
let j;
try { j = JSON.parse(raw); } catch { process.stderr.write('gstack-learnings-log: invalid JSON, skipping\n'); process.exit(1); }
@ -47,27 +48,11 @@ if (j.source && !ALLOWED_SOURCES.includes(j.source)) {
process.exit(1);
}
// Content sanitization: strip instruction-like patterns from insight field
// These patterns could be used for prompt injection when learnings are loaded into agent context
if (j.insight) {
const INJECTION_PATTERNS = [
/ignore\s+(all\s+)?previous\s+(instructions|context|rules)/i,
/you\s+are\s+now\s+/i,
/always\s+output\s+no\s+findings/i,
/skip\s+(all\s+)?(security|review|checks)/i,
/override[:\s]/i,
/\bsystem\s*:/i,
/\bassistant\s*:/i,
/\buser\s*:/i,
/do\s+not\s+(report|flag|mention)/i,
/approve\s+(all|every|this)/i,
];
for (const pat of INJECTION_PATTERNS) {
if (pat.test(j.insight)) {
process.stderr.write('gstack-learnings-log: insight contains suspicious instruction-like content, rejected\n');
process.exit(1);
}
}
// Content sanitization: shared injection patterns (lib/jsonl-store.ts, D2A) —
// one audited list across learnings + decisions, no drift.
if (j.insight && hasInjection(j.insight)) {
process.stderr.write('gstack-learnings-log: insight contains suspicious instruction-like content, rejected\n');
process.exit(1);
}
// Inject timestamp if not present

View File

@ -90,10 +90,16 @@ for (const taggedLine of lines) {
const isCrossProject = sourceTag === 'cross';
e._crossProject = isCrossProject;
// Trust gate: cross-project learnings only loaded if trusted (user-stated)
// Trust gate: cross-project learnings only loaded if trusted (user-stated).
// This prevents prompt injection from one project's AI-generated learnings
// silently influencing reviews in another project.
if (isCrossProject && e.trusted === false) continue;
// #1745: this is an ALLOWLIST, not a denylist. The old equals-false check
// admitted any row where trusted is missing/undefined (legacy rows written
// before the field existed, hand-edited rows, rows from other tools).
// Require trusted to be exactly true. NOTE: this whole block is a
// double-quoted bun -e string, so bash still does command substitution
// inside it. Keep backticks and dollar-paren out of these comments.
if (isCrossProject && e.trusted !== true) continue;
entries.push(e);
} catch {}

View File

@ -65,6 +65,7 @@ import {
withErrorContext,
} from "../lib/gstack-memory-helpers";
import { execGbrainText, spawnGbrainAsync } from "../lib/gbrain-exec";
import { checkOwnedStagingDir, STAGING_MARKER } from "../lib/staging-guard";
// ── Types ──────────────────────────────────────────────────────────────────
@ -907,13 +908,23 @@ interface StagingResult {
* Filename = `${slug}.md`. mkdir is recursive. Existing files overwrite.
* Errors per-file are collected; the whole batch is best-effort.
*/
/**
* Staging-relative path for a prepared page's slug. Single source of truth so
* writeStaged() (which mints the map) and the resume-path reconstruction (#1802
* C4) compute identical keys if they diverge, readNewFailures() silently stops
* mapping gbrain's failures back to sources and failed files get marked ingested.
*/
export function stagedRelPath(slug: string): string {
return `${slug}.md`;
}
function writeStaged(prepared: PreparedPage[], stagingDir: string): StagingResult {
mkdirSync(stagingDir, { recursive: true });
const stagedPathToSource = new Map<string, string>();
const errors: Array<{ slug: string; error: string }> = [];
let written = 0;
for (const p of prepared) {
const relPath = `${p.slug}.md`;
const relPath = stagedRelPath(p.slug);
const absPath = join(stagingDir, relPath);
try {
mkdirSync(dirname(absPath), { recursive: true });
@ -978,7 +989,7 @@ function parseImportJson(stdout: string): ImportJsonResult | null {
* staging-dir-relative filename gbrain saw (e.g. "transcripts/foo.md").
* stagedPathToSource maps that back to the original source file.
*/
function readNewFailures(
export function readNewFailures(
syncFailuresPath: string,
preImportOffset: number,
stagedPathToSource: Map<string, string>,
@ -1198,6 +1209,17 @@ function preparePages(
function makeStagingDir(): string {
const dir = join(GSTACK_HOME, `.staging-ingest-${process.pid}-${Date.now()}`);
mkdirSync(dir, { recursive: true });
// Mint the ownership marker (#1802) so cleanupStagingDir() and decideResume()
// can prove this dir was created by us before any recursive delete or resume.
// #1802 C5: fail hard if the marker can't be written — a marker-less dir would
// be refused by the guard forever (leaked, never cleaned). Tear down the
// partial dir and rethrow so the caller fails loudly instead of leaking.
try {
writeFileSync(join(dir, STAGING_MARKER), `${process.pid}\n${Date.now()}\n`, "utf-8");
} catch (err) {
try { rmSync(dir, { recursive: true, force: true }); } catch { /* best-effort */ }
throw err;
}
return dir;
}
@ -1259,8 +1281,21 @@ function isRemoteHttpMcpMode(): boolean {
* cleanup failure.
*/
function cleanupStagingDir(dir: string): void {
// #1802 deletion chokepoint: never recurse-delete a path we cannot PROVE we
// own. A poisoned resume could otherwise route the repo root here.
const verdict = checkOwnedStagingDir(dir, GSTACK_HOME);
if (!verdict.ok) {
console.error(
`[gbrain] staging cleanup REFUSED: "${dir}" is not an owned staging dir ` +
`(${verdict.reason}). Skipping rm -rf to prevent data loss (#1802).`,
);
return;
}
try {
rmSync(dir, { recursive: true, force: true });
// #1802 C5: delete the realpath-resolved dir the guard validated, not the
// raw input — closes the TOCTOU gap where `dir` is a symlink swapped between
// the check above and this rmSync. canonicalPath is always set when ok.
rmSync(verdict.canonicalPath ?? dir, { recursive: true, force: true });
} catch {
// best-effort
}
@ -1515,10 +1550,20 @@ async function ingestPass(args: CliArgs): Promise<BulkResult> {
// tells it where to resume.
const remoteHttpMode = isRemoteHttpMcpMode();
const resumeDir = process.env.GSTACK_INGEST_RESUME_DIR;
// #1802 second entry point: this binary is runnable directly, so it must not
// trust GSTACK_INGEST_RESUME_DIR just because it exists — a stale/poisoned env
// could make us `gbrain import` (and later clean up) an arbitrary directory.
// Prove ownership here too, independently of the orchestrator's decideResume.
const resuming = !remoteHttpMode
&& typeof resumeDir === "string"
&& resumeDir.length > 0
&& existsSync(resumeDir);
&& existsSync(resumeDir)
&& checkOwnedStagingDir(resumeDir, GSTACK_HOME).ok;
if (!remoteHttpMode && resumeDir && resumeDir.length > 0 && !resuming) {
console.error(
`[memory-ingest] ignoring GSTACK_INGEST_RESUME_DIR="${resumeDir}" — not a proven staging dir (#1802); staging fresh.`,
);
}
const stagingDir = resuming
? resumeDir!
: remoteHttpMode
@ -1531,6 +1576,11 @@ async function ingestPass(args: CliArgs): Promise<BulkResult> {
if (!remoteHttpMode) {
_activeStagingDir = stagingDir;
}
// #1802 C3: set when the import-timeout branch leaves a resumable checkpoint
// pointing at this staging dir, so the finally preserves it for the next run
// instead of deleting it (the SIGTERM forwarder's preserve branch only runs
// when the PARENT is signalled, which an internal timeout never does).
let preserveStaging = false;
try {
let staging: StagingResult;
if (resuming) {
@ -1543,7 +1593,15 @@ async function ingestPass(args: CliArgs): Promise<BulkResult> {
`[memory-ingest] resuming previous staging dir ${stagingDir} (skipping prepare phase)`,
);
}
staging = { staging_dir: stagingDir, written: prep.prepared.length, errors: [], stagedPathToSource: new Map() };
// #1802 C4: reconstruct stagedPathToSource from the prepared pages so
// readNewFailures() can still map gbrain's per-file failures back to
// sources on resume. An empty map made every failed file fall through to
// state-recording — i.e. silently marked ingested despite failing.
const stagedPathToSource = new Map<string, string>();
for (const p of prep.prepared) {
stagedPathToSource.set(stagedRelPath(p.slug), p.source_path);
}
staging = { staging_dir: stagingDir, written: prep.prepared.length, errors: [], stagedPathToSource };
} else {
staging = writeStaged(prep.prepared, stagingDir);
}
@ -1639,14 +1697,23 @@ async function ingestPass(args: CliArgs): Promise<BulkResult> {
const importJson = parseImportJson(stdout);
if (importResult.status !== 0) {
// #1611: on timeout, gbrain's import-checkpoint.json is preserved (the
// SIGTERM forwarder keeps the staging dir), so the next /sync-gbrain
// resumes rather than restarting. Tell the user instead of looking failed.
// #1611/#1802 C3: on timeout, gbrain may have written
// import-checkpoint.json so the next /sync-gbrain can resume. But an
// INTERNAL timeout (runGbrainImport kills the child and returns here)
// never signals the parent, so the SIGTERM forwarder's preserve branch
// doesn't run — and the finally would otherwise delete the staging dir
// despite a "checkpoint preserved" message. Mirror the forwarder: preserve
// only when gbrain actually checkpointed against this dir; otherwise let
// the finally clean up (nothing to resume) and say so honestly.
if (importResult.timedOut) {
const mins = Math.round(resolveImportTimeoutMs() / 60000);
const msg =
`gbrain import timed out after ${mins}min; checkpoint preserved — re-run ` +
`/sync-gbrain to resume (raise GSTACK_INGEST_TIMEOUT_MS for big brains)`;
const checkpointed = stagingDirIsCheckpointed(stagingDir);
const msg = checkpointed
? `gbrain import timed out after ${mins}min; checkpoint preserved — re-run ` +
`/sync-gbrain to resume (raise GSTACK_INGEST_TIMEOUT_MS for big brains)`
: `gbrain import timed out after ${mins}min before writing a checkpoint; ` +
`re-run /sync-gbrain to restage (raise GSTACK_INGEST_TIMEOUT_MS for big brains)`;
if (checkpointed) preserveStaging = true;
console.error(`[memory-ingest] ${msg}`);
return {
written: 0,
@ -1754,7 +1821,15 @@ async function ingestPass(args: CliArgs): Promise<BulkResult> {
);
}
} finally {
cleanupStagingDir(stagingDir);
// #1802 D1: in remote-http mode `stagingDir` is the PERSISTENT transcript
// dir (makePersistentTranscriptDir, under ~/.gstack/transcripts/) that
// gstack-brain-sync push must pick up — it is NOT a `.staging-ingest-*` dir
// and must never be deleted here. The remote-http branch above already
// documents this intent ("Skip the ... cleanupStagingDir paths"), but a
// `finally` runs on its `return`, so the gate has to live here. Gating on
// mode (rather than widening the ownership guard) keeps checkOwnedStagingDir
// strict: it only ever sees `.staging-ingest-*` dirs.
if (!remoteHttpMode && !preserveStaging) cleanupStagingDir(stagingDir);
_activeStagingDir = null;
}

View File

@ -161,12 +161,25 @@ function readLines(path: string | undefined): string[] | undefined {
function buildOpts(): ScanOptions {
const vis = (arg("--repo-visibility") as RepoVisibility) || "unknown";
const maxBytes = arg("--max-bytes");
// #1824: validate the RAW string, not the parse result. parseInt("123abc")
// is 123 and parseInt("foo") is NaN — both silently corrupt the fail-closed
// oversize guard. Require a clean positive integer or reject before scanning.
let maxBytesOpt: number | undefined;
if (maxBytes !== undefined) {
if (!/^\d+$/.test(maxBytes) || Number(maxBytes) <= 0) {
process.stderr.write(
`gstack-redact: --max-bytes must be a positive integer (got "${maxBytes}")\n`,
);
process.exit(1);
}
maxBytesOpt = Number(maxBytes);
}
return {
repoVisibility: ["public", "private", "unknown"].includes(vis) ? vis : "unknown",
allowlist: readLines(arg("--allowlist")),
selfEmail: arg("--self-email"),
repoPublicEmails: readLines(arg("--repo-public-emails")),
...(maxBytes ? { maxBytes: parseInt(maxBytes, 10) } : {}),
...(maxBytesOpt !== undefined ? { maxBytes: maxBytesOpt } : {}),
};
}

53
bin/gstack-session-kind Executable file
View File

@ -0,0 +1,53 @@
#!/usr/bin/env bash
# gstack-session-kind — classify the current agent session so skills know whether
# a human can answer an interactive prompt (AskUserQuestion).
#
# Usage: gstack-session-kind → prints one of: spawned | headless | interactive
#
# Used by the preamble (generate-preamble-bash.ts) which echoes
# SESSION_KIND: <value>
# so the AskUserQuestion-failure fallback rule can branch without a shell-out at
# failure time:
# spawned → orchestrator session (OpenClaw). Auto-choose recommended option
# per the skill's SPAWNED_SESSION block. Never prose, never BLOCKED.
# headless → no human present (claude -p evals / CI). BLOCK on AUQ failure.
# interactive → a human is present. Prose-fallback on AUQ failure.
#
# Detection is best-effort. On ANY ambiguity it prints `interactive` — BLOCK only on
# a positive headless signal, since a stray prose message in an unmarked one-shot
# `-p` run just ends the turn (harmless), whereas wrongly BLOCKING a real human is not.
#
# Why env vars and not TTY/entrypoint: an interactive Conductor session reports
# CLAUDE_CODE_ENTRYPOINT=sdk-ts with no TTY — identical to a headless SDK eval. The
# signals that actually discriminate are the host/orchestrator/CI env markers below.
set -euo pipefail
# 1. Orchestrator-spawned session (OpenClaw). Authoritative block lives in the skill;
# we only surface the classification.
if [ -n "${OPENCLAW_SESSION:-}" ]; then
echo "spawned"
exit 0
fi
# 2. Explicit headless override (set by the eval/E2E harness for determinism).
if [ -n "${GSTACK_HEADLESS:-}" ]; then
echo "headless"
exit 0
fi
# 3. Positive interactive-host signals: a human-driven host is present.
# - Conductor app sets CONDUCTOR_* workspace vars.
# - Plain interactive `claude` CLI sets CLAUDE_CODE_ENTRYPOINT=cli.
if [ -n "${CONDUCTOR_WORKSPACE_PATH:-}" ] || [ -n "${CONDUCTOR_PORT:-}" ] || [ "${CLAUDE_CODE_ENTRYPOINT:-}" = "cli" ]; then
echo "interactive"
exit 0
fi
# 4. CI / automation markers with no interactive host → headless.
if [ -n "${CI:-}" ] || [ -n "${GITHUB_ACTIONS:-}" ]; then
echo "headless"
exit 0
fi
# 5. No positive headless signal → assume a human is present (degrade-safe default).
echo "interactive"

View File

@ -46,6 +46,9 @@ echo "SKILL_PREFIX: $_SKILL_PREFIX"
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
REPO_MODE=${REPO_MODE:-unknown}
echo "REPO_MODE: $REPO_MODE"
_SESSION_KIND=$(~/.claude/skills/gstack/bin/gstack-session-kind 2>/dev/null || echo "interactive")
case "$_SESSION_KIND" in spawned|headless|interactive) ;; *) _SESSION_KIND="interactive" ;; esac
echo "SESSION_KIND: $_SESSION_KIND"
_LAKE_SEEN=$([ -f ~/.gstack/.completeness-intro-seen ] && echo "yes" || echo "no")
echo "LAKE_INTRO: $_LAKE_SEEN"
_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || true)
@ -125,7 +128,7 @@ In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`co
## Skill Invocation During Plan Mode
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If no AskUserQuestion variant is callable, use the normal chat fallback from the AskUserQuestion Format rule and stop for the user's reply. At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If AskUserQuestion is unavailable or a call fails, follow the AskUserQuestion Format failure fallback: `headless` → BLOCKED; `interactive` → the prose fallback (also satisfies end-of-turn). At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If `PROACTIVE` is `"false"`, do not auto-invoke or proactively suggest skills. If a skill seems useful, ask: "I think /skillname might help here — want me to run it?"
@ -160,7 +163,7 @@ touch ~/.gstack/.writing-style-prompted
Skip if `WRITING_STYLE_PENDING` is `no`.
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Lake** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Ocean** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
```bash
open https://garryslist.org/posts/boil-the-ocean
@ -641,6 +644,51 @@ $B screenshot /tmp/out.png --selector .tweet-card
```
Scale must be 1-3 (gstack policy cap). Changing `--scale` recreates the browser context; refs from `snapshot` are invalidated (rerun `snapshot`), but `load-html` content is replayed automatically. Not supported in headed mode.
### 14. Offline render mode (rasterize your own HTML/JSON, zero network)
This is the blessed path for "I just want to turn my own local HTML or JSON into a
PNG/PDF/bytes on disk" — Excalidraw diagrams, tweet/quote cards, og-images,
report rasterization. It is **plain headless, shared Chromium, no proxy, no Xvfb,
no anti-bot stealth**. Default `$B` is already exactly this; you do not pass
`--headed` or `--proxy`. One Chromium per box, shared by every skill — **do not
`npm i puppeteer` and ship a second browser** (see the note under the cheatsheet).
Two output shapes, pick by what you have:
**A) Visual output → `screenshot --selector` (preferred).** If the thing you want
is a picture of something on the page, screenshot it. The PNG is written from the
browser process straight to disk — the image bytes never cross the CDP wire.
```bash
echo '<div id="card" style="width:400px;height:200px;background:#1da1f2;color:#fff;padding:20px">hi</div>' > /tmp/card.html
$B viewport 480x600 --scale 2
$B load-html /tmp/card.html
$B screenshot /tmp/card.png --selector '#card' # disk path — no megabytes over CDP
```
(Use the disk path, NOT `screenshot --base64` — base64 serializes the bytes back
through the command channel, which is the cost you're trying to avoid.)
**B) Bytes a function returns → `js --out` / `eval --out`.** When a library hands
you the result as a return value (a base64 data URL, a blob, computed JSON) rather
than painting a stable element — e.g. Excalidraw's export function returns a PNG
data URL — write the evaluate result straight to disk. `--out` decodes a
`data:*;base64,...` result to raw bytes automatically (pass `--raw` to write the
literal string). The payload is written by the daemon and never serialized back
out to the CLI/stdout.
```bash
# Load the render bundle, signal readiness, then render-to-file.
$B load-html /tmp/excalidraw-export.html # bundle sets window.__render + a #done flag
$B wait '#done' # deterministic ready handshake
$B js "window.__render(SCENE_JSON)" --out /tmp/diagram.png # data URL → decoded PNG on disk
```
`--out` is a WRITE: it needs the `write` scope and is never allowed over the
pair-agent tunnel (a remote agent can't write to your disk). Parent directories
are created; malformed base64 errors instead of writing corrupt bytes. Pick A when
you can (no CDP transfer at all); reach for B only when the bytes come back as a
return value.
## Puppeteer → browse cheatsheet
Migrating from Puppeteer? Here's the 1:1 mapping for the core workflow:
@ -654,6 +702,8 @@ Migrating from Puppeteer? Here's the 1:1 mapping for the core workflow:
| `await (await page.$('.x')).screenshot({path})` | `$B screenshot <path> --selector .x` |
| `await page.screenshot({fullPage: true, path})` | `$B screenshot <path>` (full page default) |
| `await page.screenshot({clip: {x, y, w, h}, path})` | `$B screenshot <path> --clip x,y,w,h` |
| `const r = await page.evaluate(fn)` | `$B js "<expr>"` (result to stdout) |
| `fs.writeFileSync(out, Buffer.from(dataUrl.split(',')[1],'base64'))` | `$B js "<expr>" --out <file>` (data URL auto-decoded) |
Worked example (the tweet-renderer flow — Puppeteer → browse):
@ -668,6 +718,13 @@ $B screenshot /tmp/out.png --selector .tweet-card
Aliases: typing `setcontent` or `set-content` routes to `load-html` automatically. Typing a typo (`load-htm`) returns `Did you mean 'load-html'?`.
**Don't bundle your own puppeteer/Chromium.** `browse` is the one shared Chromium
per box. Skills that need to rasterize local HTML/JSON (diagrams, cards, og-images)
should route through `browse``screenshot --selector` for visual output,
`load-html` + `js --out` for bytes a function returns — instead of
`npm i puppeteer` and downloading a second Chromium that drifts out of version sync.
One install to pin, one daemon's lifecycle to manage.
## User Handoff
When you hit something you can't handle in headless mode (CAPTCHA, complex auth, multi-factor
@ -872,10 +929,10 @@ $B prettyscreenshot --cleanup --scroll-to ".pricing" --width 1440 ~/Desktop/hero
| `cookies` | All cookies as JSON |
| `css <sel> <prop>` | Computed CSS value |
| `dialog [--clear]` | Dialog messages |
| `eval <file>` | Run JavaScript from a file in the page context and return result as string. Path must resolve under /tmp or cwd (no traversal). Use eval for multi-line scripts; use js for one-liners. |
| `eval <file> [--out <file>] [--raw]` | Run JavaScript from a file in the page context and return result as string. Path must resolve under /tmp or cwd (no traversal). Use eval for multi-line scripts; use js for one-liners. With --out <file>, the result is written to disk (base64 data URL decoded to bytes unless --raw); --out makes the invocation a WRITE (needs write scope, never allowed over the tunnel). |
| `inspect [selector] [--all] [--history]` | Deep CSS inspection via CDP — full rule cascade, box model, computed styles |
| `is <prop> <sel|@ref>` | State check on element. Valid <prop> values: visible, hidden, enabled, disabled, checked, editable, focused (case-sensitive). <sel> accepts a CSS selector OR an @ref token from a prior snapshot (e.g. @e3, @c1) — refs are interchangeable with selectors anywhere a selector is expected. |
| `js <expr>` | Run inline JavaScript expression in the page context and return result as string. Same JS sandbox as eval; the only difference is js takes an inline expr while eval reads from a file. |
| `js <expr> [--out <file>] [--raw]` | Run inline JavaScript expression in the page context and return result as string. Same JS sandbox as eval; the only difference is js takes an inline expr while eval reads from a file. With --out <file>, the result is written to disk instead of returned (a base64 data URL is decoded to raw bytes unless --raw is given) — ideal for rasterizing local renders to PNG without serializing megabytes back through the CLI. --out makes the invocation a WRITE (needs write scope, never allowed over the tunnel). |
| `network [--clear]` | Network requests |
| `perf` | Page load timings |
| `storage | storage set <key> <value>` | Read both localStorage and sessionStorage as JSON. With "set <key> <value>", write to localStorage only (sessionStorage is read-only via this command — set it with `js sessionStorage.setItem(...)`). |

View File

@ -135,6 +135,51 @@ $B screenshot /tmp/out.png --selector .tweet-card
```
Scale must be 1-3 (gstack policy cap). Changing `--scale` recreates the browser context; refs from `snapshot` are invalidated (rerun `snapshot`), but `load-html` content is replayed automatically. Not supported in headed mode.
### 14. Offline render mode (rasterize your own HTML/JSON, zero network)
This is the blessed path for "I just want to turn my own local HTML or JSON into a
PNG/PDF/bytes on disk" — Excalidraw diagrams, tweet/quote cards, og-images,
report rasterization. It is **plain headless, shared Chromium, no proxy, no Xvfb,
no anti-bot stealth**. Default `$B` is already exactly this; you do not pass
`--headed` or `--proxy`. One Chromium per box, shared by every skill — **do not
`npm i puppeteer` and ship a second browser** (see the note under the cheatsheet).
Two output shapes, pick by what you have:
**A) Visual output → `screenshot --selector` (preferred).** If the thing you want
is a picture of something on the page, screenshot it. The PNG is written from the
browser process straight to disk — the image bytes never cross the CDP wire.
```bash
echo '<div id="card" style="width:400px;height:200px;background:#1da1f2;color:#fff;padding:20px">hi</div>' > /tmp/card.html
$B viewport 480x600 --scale 2
$B load-html /tmp/card.html
$B screenshot /tmp/card.png --selector '#card' # disk path — no megabytes over CDP
```
(Use the disk path, NOT `screenshot --base64` — base64 serializes the bytes back
through the command channel, which is the cost you're trying to avoid.)
**B) Bytes a function returns → `js --out` / `eval --out`.** When a library hands
you the result as a return value (a base64 data URL, a blob, computed JSON) rather
than painting a stable element — e.g. Excalidraw's export function returns a PNG
data URL — write the evaluate result straight to disk. `--out` decodes a
`data:*;base64,...` result to raw bytes automatically (pass `--raw` to write the
literal string). The payload is written by the daemon and never serialized back
out to the CLI/stdout.
```bash
# Load the render bundle, signal readiness, then render-to-file.
$B load-html /tmp/excalidraw-export.html # bundle sets window.__render + a #done flag
$B wait '#done' # deterministic ready handshake
$B js "window.__render(SCENE_JSON)" --out /tmp/diagram.png # data URL → decoded PNG on disk
```
`--out` is a WRITE: it needs the `write` scope and is never allowed over the
pair-agent tunnel (a remote agent can't write to your disk). Parent directories
are created; malformed base64 errors instead of writing corrupt bytes. Pick A when
you can (no CDP transfer at all); reach for B only when the bytes come back as a
return value.
## Puppeteer → browse cheatsheet
Migrating from Puppeteer? Here's the 1:1 mapping for the core workflow:
@ -148,6 +193,8 @@ Migrating from Puppeteer? Here's the 1:1 mapping for the core workflow:
| `await (await page.$('.x')).screenshot({path})` | `$B screenshot <path> --selector .x` |
| `await page.screenshot({fullPage: true, path})` | `$B screenshot <path>` (full page default) |
| `await page.screenshot({clip: {x, y, w, h}, path})` | `$B screenshot <path> --clip x,y,w,h` |
| `const r = await page.evaluate(fn)` | `$B js "<expr>"` (result to stdout) |
| `fs.writeFileSync(out, Buffer.from(dataUrl.split(',')[1],'base64'))` | `$B js "<expr>" --out <file>` (data URL auto-decoded) |
Worked example (the tweet-renderer flow — Puppeteer → browse):
@ -162,6 +209,13 @@ $B screenshot /tmp/out.png --selector .tweet-card
Aliases: typing `setcontent` or `set-content` routes to `load-html` automatically. Typing a typo (`load-htm`) returns `Did you mean 'load-html'?`.
**Don't bundle your own puppeteer/Chromium.** `browse` is the one shared Chromium
per box. Skills that need to rasterize local HTML/JSON (diagrams, cards, og-images)
should route through `browse` — `screenshot --selector` for visual output,
`load-html` + `js --out` for bytes a function returns — instead of
`npm i puppeteer` and downloading a second Chromium that drifts out of version sync.
One install to pin, one daemon's lifecycle to manage.
## User Handoff
When you hit something you can't handle in headless mode (CAPTCHA, complex auth, multi-factor

View File

@ -106,8 +106,8 @@ export const COMMAND_DESCRIPTIONS: Record<string, { category: string; descriptio
'media': { category: 'Reading', description: 'All media elements (images, videos, audio) with URLs, dimensions, types', usage: 'media [--images|--videos|--audio] [selector]' },
'data': { category: 'Reading', description: 'Structured data: JSON-LD, Open Graph, Twitter Cards, meta tags', usage: 'data [--jsonld|--og|--meta|--twitter]' },
// Inspection
'js': { category: 'Inspection', description: 'Run inline JavaScript expression in the page context and return result as string. Same JS sandbox as eval; the only difference is js takes an inline expr while eval reads from a file.', usage: 'js <expr>' },
'eval': { category: 'Inspection', description: 'Run JavaScript from a file in the page context and return result as string. Path must resolve under /tmp or cwd (no traversal). Use eval for multi-line scripts; use js for one-liners.', usage: 'eval <file>' },
'js': { category: 'Inspection', description: 'Run inline JavaScript expression in the page context and return result as string. Same JS sandbox as eval; the only difference is js takes an inline expr while eval reads from a file. With --out <file>, the result is written to disk instead of returned (a base64 data URL is decoded to raw bytes unless --raw is given) — ideal for rasterizing local renders to PNG without serializing megabytes back through the CLI. --out makes the invocation a WRITE (needs write scope, never allowed over the tunnel).', usage: 'js <expr> [--out <file>] [--raw]' },
'eval': { category: 'Inspection', description: 'Run JavaScript from a file in the page context and return result as string. Path must resolve under /tmp or cwd (no traversal). Use eval for multi-line scripts; use js for one-liners. With --out <file>, the result is written to disk (base64 data URL decoded to bytes unless --raw); --out makes the invocation a WRITE (needs write scope, never allowed over the tunnel).', usage: 'eval <file> [--out <file>] [--raw]' },
'css': { category: 'Inspection', description: 'Computed CSS value', usage: 'css <sel> <prop>' },
'attrs': { category: 'Inspection', description: 'Element attributes as JSON', usage: 'attrs <sel|@ref>' },
'is': { category: 'Inspection', description: 'State check on element. Valid <prop> values: visible, hidden, enabled, disabled, checked, editable, focused (case-sensitive). <sel> accepts a CSS selector OR an @ref token from a prior snapshot (e.g. @e3, @c1) — refs are interchangeable with selectors anywhere a selector is expected.', usage: 'is <prop> <sel|@ref>' },

View File

@ -13,7 +13,7 @@ import * as fs from 'fs';
import * as path from 'path';
import { TEMP_DIR } from './platform';
import { inspectElement, formatInspectorResult, getModificationHistory } from './cdp-inspector';
import { validateReadPath } from './path-security';
import { validateReadPath, validateOutputPath } from './path-security';
import { stripLoneSurrogates } from './sanitize';
// Re-export for backward compatibility (tests import from read-commands)
export { validateReadPath } from './path-security';
@ -46,6 +46,117 @@ function wrapForEvaluate(code: string): string {
: `(async()=>(${trimmed}))()`;
}
/** Flags split out of `js`/`eval` args by parseOutArgs. */
export interface OutArgs {
outPath?: string;
raw: boolean;
rest: string[];
}
/**
* Parse `--out <path>` / `--out=<path>` and `--raw` / `--raw=true|false` out of an
* arg list, returning the flags plus the remaining positional args (`rest`).
*
* Single source of truth shared by the js/eval handlers and the write-capability
* gate in server.ts, so the two never disagree on what counts as an `--out`
* invocation. Throws on malformed usage (repeated `--out`, missing value, bad
* `--raw` value) so the user gets a clear error instead of a silent misparse.
*/
export function parseOutArgs(args: string[]): OutArgs {
let outPath: string | undefined;
let raw = false;
const rest: string[] = [];
for (let i = 0; i < args.length; i++) {
const a = args[i];
if (a === '--out') {
if (outPath !== undefined) throw new Error('--out specified more than once');
const val = args[i + 1];
if (val === undefined || val.startsWith('--')) throw new Error('--out requires a file path');
outPath = val;
i++;
} else if (a.startsWith('--out=')) {
if (outPath !== undefined) throw new Error('--out specified more than once');
const val = a.slice('--out='.length);
if (val === '') throw new Error('--out requires a file path');
outPath = val;
} else if (a === '--raw') {
raw = true;
} else if (a.startsWith('--raw=')) {
const v = a.slice('--raw='.length).toLowerCase();
if (v !== 'true' && v !== 'false') throw new Error('--raw must be true or false');
raw = v === 'true';
} else {
rest.push(a);
}
}
return { outPath, raw, rest };
}
/**
* True iff an arg list contains an `--out` flag in any accepted form
* (`--out <path>` or `--out=<path>`). Used by the write-capability gate to
* decide whether an otherwise-read command (`js`/`eval`) is actually a write
* invocation. Mirrors parseOutArgs's `--out` recognition exactly. Never throws
* a malformed `--out=` still counts as an out attempt (fail safe: gate it).
*/
export function hasOutArg(args: string[]): boolean {
return args.some(a => a === '--out' || a.startsWith('--out='));
}
/**
* Convert an evaluate() result to its string form the exact conversion `js`/`eval`
* used inline before `--out` existed. Kept byte-for-byte: `typeof === 'object'`
* (which includes `null`) goes through JSON.stringify (so `null` `"null"`);
* everything else via `String(result ?? '')` (so `undefined` `''`). JSON.stringify
* still throws on circular / BigInt-bearing results, same as before.
*/
export function resultToString(result: unknown): string {
return typeof result === 'object'
? JSON.stringify(result, null, 2)
: String(result ?? '');
}
/**
* Write an evaluate result string to disk for `--out`, returning bytes written.
*
* When the result is a base64 data URL (`data:<type>;...;base64,<payload>`) and
* `raw` is false, decode the payload to raw bytes this is the Excalidraw / og-image
* path where a render function returns a PNG data URL. The header is parsed
* case-insensitively and split on the FIRST comma (data URLs can contain commas in
* the payload). The payload is validated against the base64 charset before decoding,
* because `Buffer.from(_, 'base64')` silently drops invalid characters and would
* otherwise write corrupted bytes. `--raw` forces a literal write even for data URLs.
*
* Non-base64 strings are surrogate-sanitized (matching what the stdout egress path
* did before) and written as UTF-8. Parent directories are created validateOutputPath
* gates the location but does not mkdir.
*/
export function writeEvalResult(outPath: string, str: string, opts: { raw: boolean }): number {
validateOutputPath(outPath);
fs.mkdirSync(path.dirname(path.resolve(outPath)), { recursive: true });
if (!opts.raw && str.startsWith('data:')) {
const comma = str.indexOf(',');
if (comma !== -1) {
const header = str.slice('data:'.length, comma);
const tokens = header.split(';').map(t => t.trim().toLowerCase());
if (tokens.includes('base64')) {
const payload = str.slice(comma + 1).replace(/\s+/g, '');
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(payload)) {
throw new Error('--out: malformed base64 in data URL (decode would corrupt output)');
}
const buf = Buffer.from(payload, 'base64');
fs.writeFileSync(outPath, buf);
return buf.length;
}
}
}
const buf = Buffer.from(stripLoneSurrogates(str), 'utf-8');
fs.writeFileSync(outPath, buf);
return buf.length;
}
/**
* Extract clean text from a page (strips script/style/noscript/svg).
* Exported for DRY reuse in meta-commands (diff).
@ -179,24 +290,36 @@ export async function handleReadCommand(
}
case 'js': {
const expr = args[0];
if (!expr) throw new Error('Usage: browse js <expression>');
const { outPath, raw, rest } = parseOutArgs(args);
const expr = rest[0];
if (!expr) throw new Error('Usage: browse js <expression> [--out <file>] [--raw]');
if (bm) assertJsOriginAllowed(bm, page.url());
const wrapped = wrapForEvaluate(expr);
const result = await target.evaluate(wrapped);
return typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result ?? '');
const str = resultToString(result);
if (outPath) {
const n = writeEvalResult(outPath, str, { raw });
return `JS result written: ${outPath} (${n} bytes)`;
}
return str;
}
case 'eval': {
const filePath = args[0];
if (!filePath) throw new Error('Usage: browse eval <js-file>');
const { outPath, raw, rest } = parseOutArgs(args);
const filePath = rest[0];
if (!filePath) throw new Error('Usage: browse eval <js-file> [--out <file>] [--raw]');
if (bm) assertJsOriginAllowed(bm, page.url());
validateReadPath(filePath);
if (!fs.existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
const code = fs.readFileSync(filePath, 'utf-8');
const wrapped = wrapForEvaluate(code);
const result = await target.evaluate(wrapped);
return typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result ?? '');
const str = resultToString(result);
if (outPath) {
const n = writeEvalResult(outPath, str, { raw });
return `Eval result written: ${outPath} (${n} bytes)`;
}
return str;
}
case 'css': {

View File

@ -14,7 +14,7 @@
*/
import { BrowserManager } from './browser-manager';
import { handleReadCommand } from './read-commands';
import { handleReadCommand, hasOutArg } from './read-commands';
import { handleWriteCommand } from './write-commands';
import { handleMetaCommand } from './meta-commands';
import { handleCookiePickerRoute, hasActivePicker } from './cookie-picker-routes';
@ -330,9 +330,15 @@ export const TUNNEL_COMMANDS = new Set<string>([
* without standing up an HTTP listener. Behavior is identical to the inline
* check; the function canonicalizes the command (so aliases hit the same set)
* and returns false for null/undefined input.
*
* `args` is consulted so an `--out` invocation (e.g. `eval --out <file>`) is
* NEVER tunnel-dispatchable: `--out` turns an otherwise-readable command into a
* local-disk WRITE, and the tunnel surface never grants disk-write capability to
* remote paired agents. Omitting `args` preserves the old command-only behavior.
*/
export function canDispatchOverTunnel(command: string | undefined | null): boolean {
export function canDispatchOverTunnel(command: string | undefined | null, args?: string[]): boolean {
if (typeof command !== 'string' || command.length === 0) return false;
if (Array.isArray(args) && hasOutArg(args)) return false;
const cmd = canonicalizeCommand(command);
return TUNNEL_COMMANDS.has(cmd);
}
@ -716,6 +722,19 @@ if (BROWSE_PARENT_PID > 0 && !IS_HEADED_WATCHDOG) {
import { READ_COMMANDS, WRITE_COMMANDS, META_COMMANDS } from './commands';
export { READ_COMMANDS, WRITE_COMMANDS, META_COMMANDS };
/**
* Whether an invocation should be treated as a WRITE for capability gating
* (scope, watch-mode block, tab ownership, tunnel). A command is a write if it
* mutates state (`WRITE_COMMANDS`) OR it carries an `--out` flag `js`/`eval
* --out` writes the evaluate result to local disk, so the capability is
* per-invocation, not per-command-name. This deliberately does NOT change
* dispatch routing: `js`/`eval` still route to `handleReadCommand`; only the
* security gates consult this.
*/
function isWriteInvocation(command: string, args: string[]): boolean {
return WRITE_COMMANDS.has(command) || hasOutArg(args);
}
// ─── Inspector State (in-memory) ──────────────────────────────
let inspectorData: InspectorResult | null = null;
let inspectorTimestamp: number = 0;
@ -957,6 +976,19 @@ async function handleCommandInternalImpl(
};
}
// `--out` writes the evaluate result to local disk, which is a WRITE
// capability distinct from the JS-exec (admin) capability js/eval need.
// Require write scope so an admin-but-not-write token can't write files.
if (hasOutArg(args) && !tokenInfo.scopes.includes('write')) {
return {
status: 403, json: true,
result: JSON.stringify({
error: `"--out" writes to disk and requires the "write" scope`,
hint: `Your scopes: ${tokenInfo.scopes.join(', ')}. Re-pair with write access to use --out.`,
}),
};
}
// Domain check for navigation commands
if ((command === 'goto' || command === 'newtab') && args[0]) {
if (!checkDomain(tokenInfo, args[0])) {
@ -1011,7 +1043,7 @@ async function handleCommandInternalImpl(
// Skip for `newtab` — it creates a tab rather than accessing one.
if (command !== 'newtab' && tokenInfo && tokenInfo.clientId !== 'root' && tokenInfo.tabPolicy === 'own-only') {
const targetTab = tabId ?? browserManager.getActiveTabId();
if (!browserManager.checkTabAccess(targetTab, tokenInfo.clientId, { isWrite: WRITE_COMMANDS.has(command), ownOnly: true })) {
if (!browserManager.checkTabAccess(targetTab, tokenInfo.clientId, { isWrite: isWriteInvocation(command, args), ownOnly: true })) {
return {
status: 403, json: true,
result: JSON.stringify({
@ -1035,8 +1067,9 @@ async function handleCommandInternalImpl(
};
}
// Block mutation commands while watching (read-only observation mode)
if (browserManager.isWatching() && WRITE_COMMANDS.has(command)) {
// Block mutation commands while watching (read-only observation mode).
// `--out` invocations count as mutations (they write the result to disk).
if (browserManager.isWatching() && isWriteInvocation(command, args)) {
return {
status: 400, json: true,
result: JSON.stringify({ error: 'Cannot run mutation commands while watching. Run `$B watch stop` first.' }),
@ -2650,11 +2683,11 @@ export function buildFetchHandler(cfg: ServerConfig): ServerHandle {
// Paired remote agents drive the browser but cannot configure the
// daemon, launch new browsers, import cookies, or rotate tokens.
if (surface === 'tunnel') {
if (!canDispatchOverTunnel(body?.command)) {
if (!canDispatchOverTunnel(body?.command, body?.args)) {
logTunnelDenial(req, url, `disallowed_command:${body?.command}`);
return new Response(JSON.stringify({
error: `Command '${body?.command}' is not allowed over the tunnel surface`,
hint: `Tunnel commands: ${[...TUNNEL_COMMANDS].sort().join(', ')}`,
hint: `Tunnel commands: ${[...TUNNEL_COMMANDS].sort().join(', ')}. Note: --out (disk write) is never allowed over the tunnel.`,
}), { status: 403, headers: { 'Content-Type': 'application/json' } });
}
}

View File

@ -9,7 +9,7 @@ import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
import { startTestServer } from './test-server';
import { BrowserManager } from '../src/browser-manager';
import { resolveServerScript } from '../src/cli';
import { handleReadCommand as _handleReadCommand } from '../src/read-commands';
import { handleReadCommand as _handleReadCommand, parseOutArgs, hasOutArg, resultToString } from '../src/read-commands';
import { handleWriteCommand as _handleWriteCommand } from '../src/write-commands';
import { handleMetaCommand } from '../src/meta-commands';
import { consoleBuffer, networkBuffer, dialogBuffer, addConsoleEntry, addNetworkEntry, addDialogEntry, CircularBuffer } from '../src/buffers';
@ -23,6 +23,65 @@ const handleReadCommand = (cmd: string, args: string[], b: BrowserManager) =>
const handleWriteCommand = (cmd: string, args: string[], b: BrowserManager) =>
_handleWriteCommand(cmd, args, b.getActiveSession(), b);
// ─── Pure arg-parser + result-conversion unit tests (no browser) ───
describe('parseOutArgs / hasOutArg', () => {
test('--out <path> splits the flag from the positional', () => {
expect(parseOutArgs(['expr', '--out', '/tmp/x'])).toEqual({ outPath: '/tmp/x', raw: false, rest: ['expr'] });
});
test('--out=<path> form is equivalent', () => {
expect(parseOutArgs(['expr', '--out=/tmp/x'])).toEqual({ outPath: '/tmp/x', raw: false, rest: ['expr'] });
});
test('flag ordering does not matter', () => {
expect(parseOutArgs(['--out', '/tmp/x', 'expr'])).toEqual({ outPath: '/tmp/x', raw: false, rest: ['expr'] });
});
test('--raw and --raw=true|false', () => {
expect(parseOutArgs(['e', '--out', '/tmp/x', '--raw']).raw).toBe(true);
expect(parseOutArgs(['e', '--out', '/tmp/x', '--raw=true']).raw).toBe(true);
expect(parseOutArgs(['e', '--out', '/tmp/x', '--raw=false']).raw).toBe(false);
});
test('repeated --out throws', () => {
expect(() => parseOutArgs(['e', '--out', '/a', '--out', '/b'])).toThrow(/more than once/);
});
test('--out with a missing value throws', () => {
expect(() => parseOutArgs(['e', '--out'])).toThrow(/requires a file path/);
expect(() => parseOutArgs(['e', '--out', '--raw'])).toThrow(/requires a file path/);
expect(() => parseOutArgs(['e', '--out='])).toThrow(/requires a file path/);
});
test('bad --raw value throws', () => {
expect(() => parseOutArgs(['e', '--out', '/a', '--raw=maybe'])).toThrow(/--raw must be true or false/);
});
test('hasOutArg matches --out and --out= exactly, not lookalikes', () => {
expect(hasOutArg(['a', '--out', 'b'])).toBe(true);
expect(hasOutArg(['a', '--out=b'])).toBe(true);
expect(hasOutArg(['a'])).toBe(false);
expect(hasOutArg(['a', '--output', 'b'])).toBe(false);
expect(hasOutArg(['a', '--outx'])).toBe(false);
});
});
describe('resultToString — byte-for-byte with pre-refactor behavior', () => {
test('null becomes "null" (typeof null === object → JSON.stringify)', () => {
expect(resultToString(null)).toBe('null');
});
test('undefined becomes empty string', () => {
expect(resultToString(undefined)).toBe('');
});
test('objects are pretty-printed JSON', () => {
expect(resultToString({ a: 1 })).toBe(JSON.stringify({ a: 1 }, null, 2));
});
test('primitives use String()', () => {
expect(resultToString(42)).toBe('42');
expect(resultToString(true)).toBe('true');
});
});
let testServer: ReturnType<typeof startTestServer>;
let bm: BrowserManager;
let baseUrl: string;
@ -225,6 +284,102 @@ describe('Inspection', () => {
expect(result).toBe('3');
});
// ─── js/eval --out (render-to-file) ───────────────────────────
test('js (no --out) returns a multi-MB string without truncation', async () => {
// Handler-level guarantee: the result is not sliced/capped before return.
// (Full HTTP egress path is exercised elsewhere; this pins the handler.)
const result = await handleReadCommand('js', ["'x'.repeat(3 * 1024 * 1024)"], bm);
expect(result.length).toBe(3 * 1024 * 1024);
});
test('js --out writes the result to disk and returns a short status, not the payload', async () => {
const out = `/tmp/browse-out-large-${Date.now()}.txt`;
try {
const result = await handleReadCommand('js', ["'y'.repeat(2 * 1024 * 1024)", '--out', out], bm);
expect(result).toContain('JS result written:');
expect(result).toContain(out);
expect(result).toContain(`(${2 * 1024 * 1024} bytes)`);
expect(result.length).toBeLessThan(200); // status, not the 2MB payload
expect(fs.statSync(out).size).toBe(2 * 1024 * 1024);
} finally {
fs.rmSync(out, { force: true });
}
});
test('js --out decodes a base64 PNG data URL to real bytes', async () => {
// 1x1 transparent PNG.
const b64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
const out = `/tmp/browse-out-png-${Date.now()}.png`;
try {
const result = await handleReadCommand('js', [`'data:image/png;base64,' + '${b64}'`, '--out', out], bm);
const buf = fs.readFileSync(out);
// PNG magic bytes: 89 50 4E 47
expect([buf[0], buf[1], buf[2], buf[3]]).toEqual([0x89, 0x50, 0x4e, 0x47]);
const expectedLen = Buffer.from(b64, 'base64').length;
expect(buf.length).toBe(expectedLen);
expect(result).toContain(`(${expectedLen} bytes)`);
} finally {
fs.rmSync(out, { force: true });
}
});
test('js --out --raw writes the literal data-URL string (no decode)', async () => {
const dataUrl = 'data:text/plain;base64,aGVsbG8=';
const out = `/tmp/browse-out-raw-${Date.now()}.txt`;
try {
await handleReadCommand('js', [`'${dataUrl}'`, '--out', out, '--raw'], bm);
expect(fs.readFileSync(out, 'utf-8')).toBe(dataUrl);
} finally {
fs.rmSync(out, { force: true });
}
});
test('js --out throws on a malformed base64 data URL instead of writing corrupt bytes', async () => {
const out = `/tmp/browse-out-bad-${Date.now()}.png`;
try {
await expect(
handleReadCommand('js', ["'data:image/png;base64,!!!not-base64!!!'", '--out', out], bm)
).rejects.toThrow(/malformed base64/);
expect(fs.existsSync(out)).toBe(false);
} finally {
fs.rmSync(out, { force: true });
}
});
test('js --out rejects a path outside the safe directories', async () => {
await expect(
handleReadCommand('js', ['1 + 1', '--out', '/etc/browse-should-not-write.txt'], bm)
).rejects.toThrow();
});
test('js --out creates a missing parent directory', async () => {
// validateOutputPath resolves the parent's realpath, so it permits one level
// of missing dir under a safe root (/tmp). mkdir then materializes it.
const root = `/tmp/browse-out-nested-${Date.now()}`;
const out = `${root}/result.txt`;
try {
await handleReadCommand('js', ["'nested'", '--out', out], bm);
expect(fs.readFileSync(out, 'utf-8')).toBe('nested');
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
test('eval --out writes the file result to disk (parity with js)', async () => {
const script = `/tmp/browse-eval-out-src-${Date.now()}.js`;
const out = `/tmp/browse-eval-out-${Date.now()}.txt`;
fs.writeFileSync(script, "'from eval'");
try {
const result = await handleReadCommand('eval', [script, '--out', out], bm);
expect(result).toContain('Eval result written:');
expect(fs.readFileSync(out, 'utf-8')).toBe('from eval');
} finally {
fs.rmSync(script, { force: true });
fs.rmSync(out, { force: true });
}
});
test('css returns computed property', async () => {
const result = await handleReadCommand('css', ['h1', 'color'], bm);
// Navy color

View File

@ -41,9 +41,16 @@ afterEach(() => {
describe('gstack-config', () => {
// ─── get ──────────────────────────────────────────────────
test('get on missing file returns empty, exit 0', () => {
test('get on missing file returns the default, exit 0', () => {
// auto_upgrade has a default of false; get falls back to the defaults table.
const { exitCode, stdout } = run(['get', 'auto_upgrade']);
expect(exitCode).toBe(0);
expect(stdout).toBe('false');
});
test('get unknown key on missing file returns empty, exit 0', () => {
const { exitCode, stdout } = run(['get', 'some_unknown_key']);
expect(exitCode).toBe(0);
expect(stdout).toBe('');
});
@ -110,10 +117,12 @@ describe('gstack-config', () => {
expect(stdout).toContain('update_check: false');
});
test('list on missing file returns empty, exit 0', () => {
test('list on missing file shows defaults, exit 0', () => {
// list prints the active-values block with defaults for unset keys.
const { exitCode, stdout } = run(['list']);
expect(exitCode).toBe(0);
expect(stdout).toBe('');
expect(stdout).toContain('proactive:');
expect(stdout).toContain('(default)');
});
// ─── usage ────────────────────────────────────────────────
@ -151,6 +160,29 @@ describe('gstack-config', () => {
expect(content).toContain('skip_eng_review:');
});
// ─── codex_reviews (paid-calls switch: reject-on-set, preserve existing) ──
test('codex_reviews defaults to enabled', () => {
const { exitCode, stdout } = run(['get', 'codex_reviews']);
expect(exitCode).toBe(0);
expect(stdout).toBe('enabled');
});
test('codex_reviews accepts enabled and disabled', () => {
expect(run(['set', 'codex_reviews', 'disabled']).exitCode).toBe(0);
expect(run(['get', 'codex_reviews']).stdout).toBe('disabled');
expect(run(['set', 'codex_reviews', 'enabled']).exitCode).toBe(0);
expect(run(['get', 'codex_reviews']).stdout).toBe('enabled');
});
test('codex_reviews rejects an invalid value and preserves the existing one', () => {
run(['set', 'codex_reviews', 'disabled']);
const { exitCode, stderr } = run(['set', 'codex_reviews', 'disabledd']);
expect(exitCode).not.toBe(0); // rejected, not warn-and-default
expect(stderr).toContain('not recognized');
// existing value must be untouched — a typo never silently flips paid Codex on/off
expect(run(['get', 'codex_reviews']).stdout).toBe('disabled');
});
test('header written only once, not duplicated on second set', () => {
run(['set', 'foo', 'bar']);
run(['set', 'baz', 'qux']);
@ -176,9 +208,9 @@ describe('gstack-config', () => {
});
// ─── routing_declined ──────────────────────────────────────
test('routing_declined defaults to empty (not set)', () => {
test('routing_declined defaults to false (not set)', () => {
const { stdout } = run(['get', 'routing_declined']);
expect(stdout).toBe('');
expect(stdout).toBe('false');
});
test('routing_declined can be set and read', () => {

View File

@ -95,3 +95,35 @@ describe('canDispatchOverTunnel — alias canonicalization', () => {
expect(canDispatchOverTunnel('closetab')).toBe(true);
});
});
describe('canDispatchOverTunnel — --out writes are never tunnel-dispatchable', () => {
// `--out` turns an otherwise-readable command into a local-disk WRITE. The
// tunnel surface never grants disk-write to remote paired agents, so any
// --out invocation must be 403'd even when the bare command is allowlisted.
test('bare eval dispatches, but eval --out does not', () => {
expect(canDispatchOverTunnel('eval', ['/tmp/x.js'])).toBe(true);
expect(canDispatchOverTunnel('eval', ['/tmp/x.js', '--out', '/tmp/o.png'])).toBe(false);
});
test('--out= form is rejected too (no parser-shape bypass)', () => {
expect(canDispatchOverTunnel('eval', ['/tmp/x.js', '--out=/tmp/o.png'])).toBe(false);
});
test('--out anywhere in args is caught regardless of ordering', () => {
expect(canDispatchOverTunnel('eval', ['--out', '/tmp/o.png', '/tmp/x.js'])).toBe(false);
});
test('args without --out still dispatch', () => {
expect(canDispatchOverTunnel('goto', ['https://example.com'])).toBe(true);
expect(canDispatchOverTunnel('eval', ['/tmp/x.js'])).toBe(true);
});
test('omitting args preserves the old command-only behavior', () => {
expect(canDispatchOverTunnel('eval')).toBe(true);
});
test('a lookalike flag (--output) is NOT treated as --out', () => {
// hasOutArg matches '--out' exactly or '--out='; '--output' must not trip it.
expect(canDispatchOverTunnel('eval', ['/tmp/x.js', '--output', '/tmp/o'])).toBe(true);
});
});

View File

@ -46,6 +46,9 @@ echo "SKILL_PREFIX: $_SKILL_PREFIX"
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
REPO_MODE=${REPO_MODE:-unknown}
echo "REPO_MODE: $REPO_MODE"
_SESSION_KIND=$(~/.claude/skills/gstack/bin/gstack-session-kind 2>/dev/null || echo "interactive")
case "$_SESSION_KIND" in spawned|headless|interactive) ;; *) _SESSION_KIND="interactive" ;; esac
echo "SESSION_KIND: $_SESSION_KIND"
_LAKE_SEEN=$([ -f ~/.gstack/.completeness-intro-seen ] && echo "yes" || echo "no")
echo "LAKE_INTRO: $_LAKE_SEEN"
_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || true)
@ -125,7 +128,7 @@ In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`co
## Skill Invocation During Plan Mode
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If no AskUserQuestion variant is callable, use the normal chat fallback from the AskUserQuestion Format rule and stop for the user's reply. At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If AskUserQuestion is unavailable or a call fails, follow the AskUserQuestion Format failure fallback: `headless` → BLOCKED; `interactive` → the prose fallback (also satisfies end-of-turn). At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If `PROACTIVE` is `"false"`, do not auto-invoke or proactively suggest skills. If a skill seems useful, ask: "I think /skillname might help here — want me to run it?"
@ -160,7 +163,7 @@ touch ~/.gstack/.writing-style-prompted
Skip if `WRITING_STYLE_PENDING` is `no`.
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Lake** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Ocean** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
```bash
open https://garryslist.org/posts/boil-the-ocean
@ -297,11 +300,31 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
**Rule:** if any `mcp__*__AskUserQuestion` variant is in your tool list, prefer it. Hosts may disable native AUQ via `--disallowedTools AskUserQuestion` (Conductor does, by default) and route through their MCP variant; calling native there silently fails. Same questions/options shape; same decision-brief format applies.
**If no AskUserQuestion variant appears in your tool list:** fall back to the host's normal user-facing chat question. Render the same decision brief in prose, stop, and wait for the user's reply. This fallback is for interactive chat hosts that can ask the user normally but do not expose an AUQ tool (for example Codex API sessions). Do not write decisions to the plan file as a substitute, and do not silently auto-decide (only `/plan-tune` AUTO_DECIDE opt-ins authorize auto-picking).
If AskUserQuestion is unavailable (no variant in your tool list) OR a call to it fails, do NOT silently auto-decide or write the decision to the plan file as a substitute. Follow the **failure fallback** below.
### When AskUserQuestion is unavailable or a call fails
Tell three outcomes apart:
1. **Auto-decide denial (NOT a failure).** The result contains `[plan-tune auto-decide] <id> → <option>` — the preference hook working as designed. Proceed with that option. Do NOT retry, do NOT fall back to prose.
2. **Genuine failure** — no variant in your tool list, OR the variant is present but the call returns an error / missing result (MCP transport error, empty result, host bug — e.g. Conductor's MCP AskUserQuestion is flaky and returns `[Tool result missing due to internal error]`).
- If it was present and **errored** (not absent), retry the SAME call **once** — but only if no answer could have surfaced (a missing-result error can arrive after the user already saw the question; retrying would double-prompt, so if it may have reached them, treat as pending, don't retry).
- Then branch on `SESSION_KIND` (echoed by the preamble; empty/absent ⇒ `interactive`):
- `spawned` → defer to the **Spawned session** block: auto-choose the recommended option. Never prose, never BLOCKED.
- `headless``BLOCKED — AskUserQuestion unavailable`; stop and wait (no human can answer).
- `interactive`**prose fallback** (below).
**Prose fallback — render the decision brief as a markdown message, not a tool call.** Same information as the tool format below, different structure (paragraphs, not ✅/❌ bullets). It MUST surface this triad:
1. **A clear ELI10 of the issue itself** — plain English on what's being decided and why it matters (the question, not per-choice), naming the stakes. Lead with it.
2. **Completeness scores per choice** — explicit `Completeness: X/10` on EACH choice (10 complete, 7 happy-path, 3 shortcut); use the kind-note when options differ in kind not coverage, but never silently drop the score.
3. **The recommendation and why** — a `Recommendation: <choice> because <reason>` line plus the `(recommended)` marker on that choice.
Layout: a `D<N>` title + a one-line note that AskUserQuestion failed and to reply with a letter; the issue ELI10; the Recommendation line; then ONE paragraph per choice carrying its `(recommended)` marker, its `Completeness: X/10`, and 2-4 sentences of reasoning — never a bare bullet list; a closing `Net:` line. Split chains / 5+ options: one prose block per per-option call, in sequence. Then STOP and wait — the user's typed answer is the decision. In plan mode this satisfies end-of-turn like a tool call.
### Format
Every AskUserQuestion is a decision brief. Send it as tool_use when an AskUserQuestion tool exists; otherwise use the normal chat fallback above and wait for the user's reply.
Every AskUserQuestion is a decision brief and must be sent as tool_use, not prose — unless the documented failure fallback above applies (interactive session + the call is unavailable/erroring), in which case the prose fallback is the correct output.
```
D<N><one-line question title>
@ -381,7 +404,7 @@ Before calling AskUserQuestion, verify:
- [ ] (recommended) label on one option (even for neutral-posture)
- [ ] Dual-scale effort labels on effort-bearing options (human / CC)
- [ ] Net line closes the decision
- [ ] You are calling the tool when available; otherwise you are using the normal chat fallback and stopping for the user's reply
- [ ] You are calling the tool, not writing prose — unless the documented failure fallback applies (then: prose with the mandatory triad — issue ELI10, per-choice Completeness, Recommendation + `(recommended)` — and a "reply with a letter" instruction, then STOP)
- [ ] Non-ASCII characters (CJK / accents) written directly, NOT \u-escaped
- [ ] If you had 5+ options, you split (or batched into ≤4-groups) — did NOT drop any
- [ ] If you split, you checked dependencies between options before firing the chain
@ -568,12 +591,19 @@ if [ -d "$_PROJ" ]; then
fi
_LATEST_CP=$(find "$_PROJ/checkpoints" -name "*.md" -type f 2>/dev/null | xargs ls -t 2>/dev/null | head -1)
[ -n "$_LATEST_CP" ] && echo "LATEST_CHECKPOINT: $_LATEST_CP"
if [ -f "$_PROJ/decisions.active.json" ]; then
echo "--- ACTIVE DECISIONS (recent, scope-relevant) ---"
~/.claude/skills/gstack/bin/gstack-decision-search --recent 5 2>/dev/null
echo "--- END DECISIONS ---"
fi
echo "--- END ARTIFACTS ---"
fi
```
If artifacts are listed, read the newest useful one. If `LAST_SESSION` or `LATEST_CHECKPOINT` appears, give a 2-sentence welcome back summary. If `RECENT_PATTERN` clearly implies a next skill, suggest it once.
**Cross-session decisions.** If `ACTIVE DECISIONS` are listed, treat them as prior settled calls with their rationale — do not silently re-litigate them; if you're about to reverse one, say so explicitly. Reach for `~/.claude/skills/gstack/bin/gstack-decision-search` whenever a question touches a past decision ("what did we decide / why / did we try"). When you or the user make a DURABLE decision (architecture, scope, tool/vendor choice, or a reversal) — NOT a turn-level or trivial choice — log it with `~/.claude/skills/gstack/bin/gstack-decision-log` (`--supersede <id>` for a reversal). Reliable and local; gbrain not required.
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format is structure; this is prose quality.
@ -588,9 +618,9 @@ Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format i
Curated jargon list lives at `~/.claude/skills/gstack/scripts/jargon-list.json` (80+ terms). On the first jargon term you encounter this session, Read that file once; treat the `terms` array as the canonical list. The list is repo-owned and may grow between releases.
## Completeness Principle — Boil the Lake
## Completeness Principle — Boil the Ocean
AI makes completeness cheap. Recommend complete lakes (tests, edge cases, error paths); flag oceans (rewrites, multi-quarter migrations).
AI makes completeness cheap, so the complete thing is the goal. Recommend full coverage (tests, edge cases, error paths) — boil the ocean one lake at a time. The only thing out of scope is genuinely unrelated work (rewrites, multi-quarter migrations); flag that as separate scope, never as an excuse for a shortcut.
When options differ in coverage, include `Completeness: X/10` (10 = all edge cases, 7 = happy path, 3 = shortcut). When options differ in kind, write: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate scores.

View File

@ -14,7 +14,7 @@ hooks:
- matcher: "Bash"
hooks:
- type: command
command: "bash ${CLAUDE_SKILL_DIR}/bin/check-careful.sh"
command: "bash $HOME/.claude/skills/gstack/careful/bin/check-careful.sh"
statusMessage: "Checking for destructive commands..."
---
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->

View File

@ -19,7 +19,7 @@ hooks:
- matcher: "Bash"
hooks:
- type: command
command: "bash ${CLAUDE_SKILL_DIR}/bin/check-careful.sh"
command: "bash $HOME/.claude/skills/gstack/careful/bin/check-careful.sh"
statusMessage: "Checking for destructive commands..."
sensitive: true
---

View File

@ -49,6 +49,9 @@ echo "SKILL_PREFIX: $_SKILL_PREFIX"
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
REPO_MODE=${REPO_MODE:-unknown}
echo "REPO_MODE: $REPO_MODE"
_SESSION_KIND=$(~/.claude/skills/gstack/bin/gstack-session-kind 2>/dev/null || echo "interactive")
case "$_SESSION_KIND" in spawned|headless|interactive) ;; *) _SESSION_KIND="interactive" ;; esac
echo "SESSION_KIND: $_SESSION_KIND"
_LAKE_SEEN=$([ -f ~/.gstack/.completeness-intro-seen ] && echo "yes" || echo "no")
echo "LAKE_INTRO: $_LAKE_SEEN"
_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || true)
@ -128,7 +131,7 @@ In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`co
## Skill Invocation During Plan Mode
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If no AskUserQuestion variant is callable, use the normal chat fallback from the AskUserQuestion Format rule and stop for the user's reply. At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If AskUserQuestion is unavailable or a call fails, follow the AskUserQuestion Format failure fallback: `headless` → BLOCKED; `interactive` → the prose fallback (also satisfies end-of-turn). At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If `PROACTIVE` is `"false"`, do not auto-invoke or proactively suggest skills. If a skill seems useful, ask: "I think /skillname might help here — want me to run it?"
@ -163,7 +166,7 @@ touch ~/.gstack/.writing-style-prompted
Skip if `WRITING_STYLE_PENDING` is `no`.
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Lake** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Ocean** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
```bash
open https://garryslist.org/posts/boil-the-ocean
@ -300,11 +303,31 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
**Rule:** if any `mcp__*__AskUserQuestion` variant is in your tool list, prefer it. Hosts may disable native AUQ via `--disallowedTools AskUserQuestion` (Conductor does, by default) and route through their MCP variant; calling native there silently fails. Same questions/options shape; same decision-brief format applies.
**If no AskUserQuestion variant appears in your tool list:** fall back to the host's normal user-facing chat question. Render the same decision brief in prose, stop, and wait for the user's reply. This fallback is for interactive chat hosts that can ask the user normally but do not expose an AUQ tool (for example Codex API sessions). Do not write decisions to the plan file as a substitute, and do not silently auto-decide (only `/plan-tune` AUTO_DECIDE opt-ins authorize auto-picking).
If AskUserQuestion is unavailable (no variant in your tool list) OR a call to it fails, do NOT silently auto-decide or write the decision to the plan file as a substitute. Follow the **failure fallback** below.
### When AskUserQuestion is unavailable or a call fails
Tell three outcomes apart:
1. **Auto-decide denial (NOT a failure).** The result contains `[plan-tune auto-decide] <id> → <option>` — the preference hook working as designed. Proceed with that option. Do NOT retry, do NOT fall back to prose.
2. **Genuine failure** — no variant in your tool list, OR the variant is present but the call returns an error / missing result (MCP transport error, empty result, host bug — e.g. Conductor's MCP AskUserQuestion is flaky and returns `[Tool result missing due to internal error]`).
- If it was present and **errored** (not absent), retry the SAME call **once** — but only if no answer could have surfaced (a missing-result error can arrive after the user already saw the question; retrying would double-prompt, so if it may have reached them, treat as pending, don't retry).
- Then branch on `SESSION_KIND` (echoed by the preamble; empty/absent ⇒ `interactive`):
- `spawned` → defer to the **Spawned session** block: auto-choose the recommended option. Never prose, never BLOCKED.
- `headless``BLOCKED — AskUserQuestion unavailable`; stop and wait (no human can answer).
- `interactive`**prose fallback** (below).
**Prose fallback — render the decision brief as a markdown message, not a tool call.** Same information as the tool format below, different structure (paragraphs, not ✅/❌ bullets). It MUST surface this triad:
1. **A clear ELI10 of the issue itself** — plain English on what's being decided and why it matters (the question, not per-choice), naming the stakes. Lead with it.
2. **Completeness scores per choice** — explicit `Completeness: X/10` on EACH choice (10 complete, 7 happy-path, 3 shortcut); use the kind-note when options differ in kind not coverage, but never silently drop the score.
3. **The recommendation and why** — a `Recommendation: <choice> because <reason>` line plus the `(recommended)` marker on that choice.
Layout: a `D<N>` title + a one-line note that AskUserQuestion failed and to reply with a letter; the issue ELI10; the Recommendation line; then ONE paragraph per choice carrying its `(recommended)` marker, its `Completeness: X/10`, and 2-4 sentences of reasoning — never a bare bullet list; a closing `Net:` line. Split chains / 5+ options: one prose block per per-option call, in sequence. Then STOP and wait — the user's typed answer is the decision. In plan mode this satisfies end-of-turn like a tool call.
### Format
Every AskUserQuestion is a decision brief. Send it as tool_use when an AskUserQuestion tool exists; otherwise use the normal chat fallback above and wait for the user's reply.
Every AskUserQuestion is a decision brief and must be sent as tool_use, not prose — unless the documented failure fallback above applies (interactive session + the call is unavailable/erroring), in which case the prose fallback is the correct output.
```
D<N><one-line question title>
@ -384,7 +407,7 @@ Before calling AskUserQuestion, verify:
- [ ] (recommended) label on one option (even for neutral-posture)
- [ ] Dual-scale effort labels on effort-bearing options (human / CC)
- [ ] Net line closes the decision
- [ ] You are calling the tool when available; otherwise you are using the normal chat fallback and stopping for the user's reply
- [ ] You are calling the tool, not writing prose — unless the documented failure fallback applies (then: prose with the mandatory triad — issue ELI10, per-choice Completeness, Recommendation + `(recommended)` — and a "reply with a letter" instruction, then STOP)
- [ ] Non-ASCII characters (CJK / accents) written directly, NOT \u-escaped
- [ ] If you had 5+ options, you split (or batched into ≤4-groups) — did NOT drop any
- [ ] If you split, you checked dependencies between options before firing the chain
@ -571,12 +594,19 @@ if [ -d "$_PROJ" ]; then
fi
_LATEST_CP=$(find "$_PROJ/checkpoints" -name "*.md" -type f 2>/dev/null | xargs ls -t 2>/dev/null | head -1)
[ -n "$_LATEST_CP" ] && echo "LATEST_CHECKPOINT: $_LATEST_CP"
if [ -f "$_PROJ/decisions.active.json" ]; then
echo "--- ACTIVE DECISIONS (recent, scope-relevant) ---"
~/.claude/skills/gstack/bin/gstack-decision-search --recent 5 2>/dev/null
echo "--- END DECISIONS ---"
fi
echo "--- END ARTIFACTS ---"
fi
```
If artifacts are listed, read the newest useful one. If `LAST_SESSION` or `LATEST_CHECKPOINT` appears, give a 2-sentence welcome back summary. If `RECENT_PATTERN` clearly implies a next skill, suggest it once.
**Cross-session decisions.** If `ACTIVE DECISIONS` are listed, treat them as prior settled calls with their rationale — do not silently re-litigate them; if you're about to reverse one, say so explicitly. Reach for `~/.claude/skills/gstack/bin/gstack-decision-search` whenever a question touches a past decision ("what did we decide / why / did we try"). When you or the user make a DURABLE decision (architecture, scope, tool/vendor choice, or a reversal) — NOT a turn-level or trivial choice — log it with `~/.claude/skills/gstack/bin/gstack-decision-log` (`--supersede <id>` for a reversal). Reliable and local; gbrain not required.
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format is structure; this is prose quality.
@ -591,9 +621,9 @@ Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format i
Curated jargon list lives at `~/.claude/skills/gstack/scripts/jargon-list.json` (80+ terms). On the first jargon term you encounter this session, Read that file once; treat the `terms` array as the canonical list. The list is repo-owned and may grow between releases.
## Completeness Principle — Boil the Lake
## Completeness Principle — Boil the Ocean
AI makes completeness cheap. Recommend complete lakes (tests, edge cases, error paths); flag oceans (rewrites, multi-quarter migrations).
AI makes completeness cheap, so the complete thing is the goal. Recommend full coverage (tests, edge cases, error paths) — boil the ocean one lake at a time. The only thing out of scope is genuinely unrelated work (rewrites, multi-quarter migrations); flag that as separate scope, never as an excuse for a shortcut.
When options differ in coverage, include `Completeness: X/10` (10 = all edge cases, 7 = happy path, 3 = shortcut). When options differ in kind, write: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate scores.
@ -1085,14 +1115,24 @@ Produce this markdown table:
| DX Review | \`/plan-devex-review\` | Developer experience gaps | {runs} | {status} | {findings} |
\`\`\`
Below the table, add these lines (omit any that are empty/not applicable):
Below the table, add these lines. **CODEX** and **CROSS-MODEL** are optional (omit when
empty); **VERDICT** is always present:
- **CODEX:** (only if codex-review ran) — one-line summary of codex fixes
- **CROSS-MODEL:** (only if both Claude and Codex reviews exist) — overlap analysis
- **UNRESOLVED:** total unresolved decisions across all reviews
- **VERDICT:** list reviews that are CLEAR (e.g., "CEO + ENG CLEARED — ready to implement").
If Eng Review is not CLEAR and not skipped globally, append "eng review required".
**Unresolved-decisions status (MANDATORY — never omitted; the report's final non-whitespace
line).** After VERDICT, end the report (content under the \`## GSTACK REVIEW REPORT\`
heading — a bold label, never a new \`## \` heading; exempt from the "omit when empty"
rule) with exactly one: the exact unbolded line \`NO UNRESOLVED DECISIONS\` (a bolded one
does NOT count), OR a \`**UNRESOLVED DECISIONS:**\` header + one bullet per open item
(last bullet = final line; add \`+ N unresolved from prior reviews\` only when N > 0).
This avoids double-counting: list THIS review's open items from context; for prior reviews
sum \`unresolved\` over the latest fresh row per skill (dashboard 7-day window) after you
DROP the current skill's row; emit the sentinel only when both are zero.
### Write to the plan file
**PLAN MODE EXCEPTION — ALWAYS RUN:** This writes to the plan file, which is the one
@ -1133,12 +1173,17 @@ missing work — do NOT call ExitPlanMode:
In-body prose that mentions "outside voice", "codex findings", or similar
does NOT count — only the structured `## GSTACK REVIEW REPORT` section
satisfies this check.
3. Confirm the report contains: a Runs / Status / Findings table, a VERDICT
line, and absorbs CODEX / CROSS-MODEL / UNRESOLVED lines if applicable.
4. If a plan file is in context for this skill invocation: confirm
3. Confirm the report has a Runs / Status / Findings table and a VERDICT line
(CODEX / CROSS-MODEL absorbed if applicable).
4. Confirm the report's FINAL non-whitespace line is the unresolved-decisions
status: the exact unbolded `NO UNRESOLVED DECISIONS`, or a bullet of a final
`**UNRESOLVED DECISIONS:**` block. BLOCKING, no "if applicable" escape — a
bolded sentinel, any trailing CODEX/CROSS-MODEL/VERDICT/prose, or a missing
status each FAILS the gate.
5. If a plan file is in context for this skill invocation: confirm
`gstack-review-log` was called and `gstack-review-read` was run at least
once. If no plan file is in context (e.g. `/codex consult` against a
diff with no plan), this check short-circuits — checks 1-3 already
diff with no plan), this check short-circuits — checks 1-4 already
short-circuit when no plan file exists.
Failing this gate and calling ExitPlanMode anyway is a contract violation —

View File

@ -50,6 +50,9 @@ echo "SKILL_PREFIX: $_SKILL_PREFIX"
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
REPO_MODE=${REPO_MODE:-unknown}
echo "REPO_MODE: $REPO_MODE"
_SESSION_KIND=$(~/.claude/skills/gstack/bin/gstack-session-kind 2>/dev/null || echo "interactive")
case "$_SESSION_KIND" in spawned|headless|interactive) ;; *) _SESSION_KIND="interactive" ;; esac
echo "SESSION_KIND: $_SESSION_KIND"
_LAKE_SEEN=$([ -f ~/.gstack/.completeness-intro-seen ] && echo "yes" || echo "no")
echo "LAKE_INTRO: $_LAKE_SEEN"
_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || true)
@ -129,7 +132,7 @@ In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`co
## Skill Invocation During Plan Mode
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If no AskUserQuestion variant is callable, use the normal chat fallback from the AskUserQuestion Format rule and stop for the user's reply. At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If AskUserQuestion is unavailable or a call fails, follow the AskUserQuestion Format failure fallback: `headless` → BLOCKED; `interactive` → the prose fallback (also satisfies end-of-turn). At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If `PROACTIVE` is `"false"`, do not auto-invoke or proactively suggest skills. If a skill seems useful, ask: "I think /skillname might help here — want me to run it?"
@ -164,7 +167,7 @@ touch ~/.gstack/.writing-style-prompted
Skip if `WRITING_STYLE_PENDING` is `no`.
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Lake** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Ocean** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
```bash
open https://garryslist.org/posts/boil-the-ocean
@ -301,11 +304,31 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
**Rule:** if any `mcp__*__AskUserQuestion` variant is in your tool list, prefer it. Hosts may disable native AUQ via `--disallowedTools AskUserQuestion` (Conductor does, by default) and route through their MCP variant; calling native there silently fails. Same questions/options shape; same decision-brief format applies.
**If no AskUserQuestion variant appears in your tool list:** fall back to the host's normal user-facing chat question. Render the same decision brief in prose, stop, and wait for the user's reply. This fallback is for interactive chat hosts that can ask the user normally but do not expose an AUQ tool (for example Codex API sessions). Do not write decisions to the plan file as a substitute, and do not silently auto-decide (only `/plan-tune` AUTO_DECIDE opt-ins authorize auto-picking).
If AskUserQuestion is unavailable (no variant in your tool list) OR a call to it fails, do NOT silently auto-decide or write the decision to the plan file as a substitute. Follow the **failure fallback** below.
### When AskUserQuestion is unavailable or a call fails
Tell three outcomes apart:
1. **Auto-decide denial (NOT a failure).** The result contains `[plan-tune auto-decide] <id> → <option>` — the preference hook working as designed. Proceed with that option. Do NOT retry, do NOT fall back to prose.
2. **Genuine failure** — no variant in your tool list, OR the variant is present but the call returns an error / missing result (MCP transport error, empty result, host bug — e.g. Conductor's MCP AskUserQuestion is flaky and returns `[Tool result missing due to internal error]`).
- If it was present and **errored** (not absent), retry the SAME call **once** — but only if no answer could have surfaced (a missing-result error can arrive after the user already saw the question; retrying would double-prompt, so if it may have reached them, treat as pending, don't retry).
- Then branch on `SESSION_KIND` (echoed by the preamble; empty/absent ⇒ `interactive`):
- `spawned` → defer to the **Spawned session** block: auto-choose the recommended option. Never prose, never BLOCKED.
- `headless``BLOCKED — AskUserQuestion unavailable`; stop and wait (no human can answer).
- `interactive`**prose fallback** (below).
**Prose fallback — render the decision brief as a markdown message, not a tool call.** Same information as the tool format below, different structure (paragraphs, not ✅/❌ bullets). It MUST surface this triad:
1. **A clear ELI10 of the issue itself** — plain English on what's being decided and why it matters (the question, not per-choice), naming the stakes. Lead with it.
2. **Completeness scores per choice** — explicit `Completeness: X/10` on EACH choice (10 complete, 7 happy-path, 3 shortcut); use the kind-note when options differ in kind not coverage, but never silently drop the score.
3. **The recommendation and why** — a `Recommendation: <choice> because <reason>` line plus the `(recommended)` marker on that choice.
Layout: a `D<N>` title + a one-line note that AskUserQuestion failed and to reply with a letter; the issue ELI10; the Recommendation line; then ONE paragraph per choice carrying its `(recommended)` marker, its `Completeness: X/10`, and 2-4 sentences of reasoning — never a bare bullet list; a closing `Net:` line. Split chains / 5+ options: one prose block per per-option call, in sequence. Then STOP and wait — the user's typed answer is the decision. In plan mode this satisfies end-of-turn like a tool call.
### Format
Every AskUserQuestion is a decision brief. Send it as tool_use when an AskUserQuestion tool exists; otherwise use the normal chat fallback above and wait for the user's reply.
Every AskUserQuestion is a decision brief and must be sent as tool_use, not prose — unless the documented failure fallback above applies (interactive session + the call is unavailable/erroring), in which case the prose fallback is the correct output.
```
D<N><one-line question title>
@ -385,7 +408,7 @@ Before calling AskUserQuestion, verify:
- [ ] (recommended) label on one option (even for neutral-posture)
- [ ] Dual-scale effort labels on effort-bearing options (human / CC)
- [ ] Net line closes the decision
- [ ] You are calling the tool when available; otherwise you are using the normal chat fallback and stopping for the user's reply
- [ ] You are calling the tool, not writing prose — unless the documented failure fallback applies (then: prose with the mandatory triad — issue ELI10, per-choice Completeness, Recommendation + `(recommended)` — and a "reply with a letter" instruction, then STOP)
- [ ] Non-ASCII characters (CJK / accents) written directly, NOT \u-escaped
- [ ] If you had 5+ options, you split (or batched into ≤4-groups) — did NOT drop any
- [ ] If you split, you checked dependencies between options before firing the chain
@ -572,12 +595,19 @@ if [ -d "$_PROJ" ]; then
fi
_LATEST_CP=$(find "$_PROJ/checkpoints" -name "*.md" -type f 2>/dev/null | xargs ls -t 2>/dev/null | head -1)
[ -n "$_LATEST_CP" ] && echo "LATEST_CHECKPOINT: $_LATEST_CP"
if [ -f "$_PROJ/decisions.active.json" ]; then
echo "--- ACTIVE DECISIONS (recent, scope-relevant) ---"
~/.claude/skills/gstack/bin/gstack-decision-search --recent 5 2>/dev/null
echo "--- END DECISIONS ---"
fi
echo "--- END ARTIFACTS ---"
fi
```
If artifacts are listed, read the newest useful one. If `LAST_SESSION` or `LATEST_CHECKPOINT` appears, give a 2-sentence welcome back summary. If `RECENT_PATTERN` clearly implies a next skill, suggest it once.
**Cross-session decisions.** If `ACTIVE DECISIONS` are listed, treat them as prior settled calls with their rationale — do not silently re-litigate them; if you're about to reverse one, say so explicitly. Reach for `~/.claude/skills/gstack/bin/gstack-decision-search` whenever a question touches a past decision ("what did we decide / why / did we try"). When you or the user make a DURABLE decision (architecture, scope, tool/vendor choice, or a reversal) — NOT a turn-level or trivial choice — log it with `~/.claude/skills/gstack/bin/gstack-decision-log` (`--supersede <id>` for a reversal). Reliable and local; gbrain not required.
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format is structure; this is prose quality.
@ -592,9 +622,9 @@ Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format i
Curated jargon list lives at `~/.claude/skills/gstack/scripts/jargon-list.json` (80+ terms). On the first jargon term you encounter this session, Read that file once; treat the `terms` array as the canonical list. The list is repo-owned and may grow between releases.
## Completeness Principle — Boil the Lake
## Completeness Principle — Boil the Ocean
AI makes completeness cheap. Recommend complete lakes (tests, edge cases, error paths); flag oceans (rewrites, multi-quarter migrations).
AI makes completeness cheap, so the complete thing is the goal. Recommend full coverage (tests, edge cases, error paths) — boil the ocean one lake at a time. The only thing out of scope is genuinely unrelated work (rewrites, multi-quarter migrations); flag that as separate scope, never as an excuse for a shortcut.
When options differ in coverage, include `Completeness: X/10` (10 = all edge cases, 7 = happy path, 3 = shortcut). When options differ in kind, write: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate scores.

View File

@ -49,6 +49,9 @@ echo "SKILL_PREFIX: $_SKILL_PREFIX"
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
REPO_MODE=${REPO_MODE:-unknown}
echo "REPO_MODE: $REPO_MODE"
_SESSION_KIND=$(~/.claude/skills/gstack/bin/gstack-session-kind 2>/dev/null || echo "interactive")
case "$_SESSION_KIND" in spawned|headless|interactive) ;; *) _SESSION_KIND="interactive" ;; esac
echo "SESSION_KIND: $_SESSION_KIND"
_LAKE_SEEN=$([ -f ~/.gstack/.completeness-intro-seen ] && echo "yes" || echo "no")
echo "LAKE_INTRO: $_LAKE_SEEN"
_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || true)
@ -128,7 +131,7 @@ In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`co
## Skill Invocation During Plan Mode
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If no AskUserQuestion variant is callable, use the normal chat fallback from the AskUserQuestion Format rule and stop for the user's reply. At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If AskUserQuestion is unavailable or a call fails, follow the AskUserQuestion Format failure fallback: `headless` → BLOCKED; `interactive` → the prose fallback (also satisfies end-of-turn). At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If `PROACTIVE` is `"false"`, do not auto-invoke or proactively suggest skills. If a skill seems useful, ask: "I think /skillname might help here — want me to run it?"
@ -163,7 +166,7 @@ touch ~/.gstack/.writing-style-prompted
Skip if `WRITING_STYLE_PENDING` is `no`.
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Lake** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Ocean** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
```bash
open https://garryslist.org/posts/boil-the-ocean
@ -300,11 +303,31 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
**Rule:** if any `mcp__*__AskUserQuestion` variant is in your tool list, prefer it. Hosts may disable native AUQ via `--disallowedTools AskUserQuestion` (Conductor does, by default) and route through their MCP variant; calling native there silently fails. Same questions/options shape; same decision-brief format applies.
**If no AskUserQuestion variant appears in your tool list:** fall back to the host's normal user-facing chat question. Render the same decision brief in prose, stop, and wait for the user's reply. This fallback is for interactive chat hosts that can ask the user normally but do not expose an AUQ tool (for example Codex API sessions). Do not write decisions to the plan file as a substitute, and do not silently auto-decide (only `/plan-tune` AUTO_DECIDE opt-ins authorize auto-picking).
If AskUserQuestion is unavailable (no variant in your tool list) OR a call to it fails, do NOT silently auto-decide or write the decision to the plan file as a substitute. Follow the **failure fallback** below.
### When AskUserQuestion is unavailable or a call fails
Tell three outcomes apart:
1. **Auto-decide denial (NOT a failure).** The result contains `[plan-tune auto-decide] <id> → <option>` — the preference hook working as designed. Proceed with that option. Do NOT retry, do NOT fall back to prose.
2. **Genuine failure** — no variant in your tool list, OR the variant is present but the call returns an error / missing result (MCP transport error, empty result, host bug — e.g. Conductor's MCP AskUserQuestion is flaky and returns `[Tool result missing due to internal error]`).
- If it was present and **errored** (not absent), retry the SAME call **once** — but only if no answer could have surfaced (a missing-result error can arrive after the user already saw the question; retrying would double-prompt, so if it may have reached them, treat as pending, don't retry).
- Then branch on `SESSION_KIND` (echoed by the preamble; empty/absent ⇒ `interactive`):
- `spawned` → defer to the **Spawned session** block: auto-choose the recommended option. Never prose, never BLOCKED.
- `headless``BLOCKED — AskUserQuestion unavailable`; stop and wait (no human can answer).
- `interactive`**prose fallback** (below).
**Prose fallback — render the decision brief as a markdown message, not a tool call.** Same information as the tool format below, different structure (paragraphs, not ✅/❌ bullets). It MUST surface this triad:
1. **A clear ELI10 of the issue itself** — plain English on what's being decided and why it matters (the question, not per-choice), naming the stakes. Lead with it.
2. **Completeness scores per choice** — explicit `Completeness: X/10` on EACH choice (10 complete, 7 happy-path, 3 shortcut); use the kind-note when options differ in kind not coverage, but never silently drop the score.
3. **The recommendation and why** — a `Recommendation: <choice> because <reason>` line plus the `(recommended)` marker on that choice.
Layout: a `D<N>` title + a one-line note that AskUserQuestion failed and to reply with a letter; the issue ELI10; the Recommendation line; then ONE paragraph per choice carrying its `(recommended)` marker, its `Completeness: X/10`, and 2-4 sentences of reasoning — never a bare bullet list; a closing `Net:` line. Split chains / 5+ options: one prose block per per-option call, in sequence. Then STOP and wait — the user's typed answer is the decision. In plan mode this satisfies end-of-turn like a tool call.
### Format
Every AskUserQuestion is a decision brief. Send it as tool_use when an AskUserQuestion tool exists; otherwise use the normal chat fallback above and wait for the user's reply.
Every AskUserQuestion is a decision brief and must be sent as tool_use, not prose — unless the documented failure fallback above applies (interactive session + the call is unavailable/erroring), in which case the prose fallback is the correct output.
```
D<N><one-line question title>
@ -384,7 +407,7 @@ Before calling AskUserQuestion, verify:
- [ ] (recommended) label on one option (even for neutral-posture)
- [ ] Dual-scale effort labels on effort-bearing options (human / CC)
- [ ] Net line closes the decision
- [ ] You are calling the tool when available; otherwise you are using the normal chat fallback and stopping for the user's reply
- [ ] You are calling the tool, not writing prose — unless the documented failure fallback applies (then: prose with the mandatory triad — issue ELI10, per-choice Completeness, Recommendation + `(recommended)` — and a "reply with a letter" instruction, then STOP)
- [ ] Non-ASCII characters (CJK / accents) written directly, NOT \u-escaped
- [ ] If you had 5+ options, you split (or batched into ≤4-groups) — did NOT drop any
- [ ] If you split, you checked dependencies between options before firing the chain
@ -571,12 +594,19 @@ if [ -d "$_PROJ" ]; then
fi
_LATEST_CP=$(find "$_PROJ/checkpoints" -name "*.md" -type f 2>/dev/null | xargs ls -t 2>/dev/null | head -1)
[ -n "$_LATEST_CP" ] && echo "LATEST_CHECKPOINT: $_LATEST_CP"
if [ -f "$_PROJ/decisions.active.json" ]; then
echo "--- ACTIVE DECISIONS (recent, scope-relevant) ---"
~/.claude/skills/gstack/bin/gstack-decision-search --recent 5 2>/dev/null
echo "--- END DECISIONS ---"
fi
echo "--- END ARTIFACTS ---"
fi
```
If artifacts are listed, read the newest useful one. If `LAST_SESSION` or `LATEST_CHECKPOINT` appears, give a 2-sentence welcome back summary. If `RECENT_PATTERN` clearly implies a next skill, suggest it once.
**Cross-session decisions.** If `ACTIVE DECISIONS` are listed, treat them as prior settled calls with their rationale — do not silently re-litigate them; if you're about to reverse one, say so explicitly. Reach for `~/.claude/skills/gstack/bin/gstack-decision-search` whenever a question touches a past decision ("what did we decide / why / did we try"). When you or the user make a DURABLE decision (architecture, scope, tool/vendor choice, or a reversal) — NOT a turn-level or trivial choice — log it with `~/.claude/skills/gstack/bin/gstack-decision-log` (`--supersede <id>` for a reversal). Reliable and local; gbrain not required.
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format is structure; this is prose quality.
@ -591,9 +621,9 @@ Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format i
Curated jargon list lives at `~/.claude/skills/gstack/scripts/jargon-list.json` (80+ terms). On the first jargon term you encounter this session, Read that file once; treat the `terms` array as the canonical list. The list is repo-owned and may grow between releases.
## Completeness Principle — Boil the Lake
## Completeness Principle — Boil the Ocean
AI makes completeness cheap. Recommend complete lakes (tests, edge cases, error paths); flag oceans (rewrites, multi-quarter migrations).
AI makes completeness cheap, so the complete thing is the goal. Recommend full coverage (tests, edge cases, error paths) — boil the ocean one lake at a time. The only thing out of scope is genuinely unrelated work (rewrites, multi-quarter migrations); flag that as separate scope, never as an excuse for a shortcut.
When options differ in coverage, include `Completeness: X/10` (10 = all edge cases, 7 = happy path, 3 = shortcut). When options differ in kind, write: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate scores.

View File

@ -52,6 +52,9 @@ echo "SKILL_PREFIX: $_SKILL_PREFIX"
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
REPO_MODE=${REPO_MODE:-unknown}
echo "REPO_MODE: $REPO_MODE"
_SESSION_KIND=$(~/.claude/skills/gstack/bin/gstack-session-kind 2>/dev/null || echo "interactive")
case "$_SESSION_KIND" in spawned|headless|interactive) ;; *) _SESSION_KIND="interactive" ;; esac
echo "SESSION_KIND: $_SESSION_KIND"
_LAKE_SEEN=$([ -f ~/.gstack/.completeness-intro-seen ] && echo "yes" || echo "no")
echo "LAKE_INTRO: $_LAKE_SEEN"
_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || true)
@ -131,7 +134,7 @@ In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`co
## Skill Invocation During Plan Mode
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If no AskUserQuestion variant is callable, use the normal chat fallback from the AskUserQuestion Format rule and stop for the user's reply. At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If AskUserQuestion is unavailable or a call fails, follow the AskUserQuestion Format failure fallback: `headless` → BLOCKED; `interactive` → the prose fallback (also satisfies end-of-turn). At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If `PROACTIVE` is `"false"`, do not auto-invoke or proactively suggest skills. If a skill seems useful, ask: "I think /skillname might help here — want me to run it?"
@ -166,7 +169,7 @@ touch ~/.gstack/.writing-style-prompted
Skip if `WRITING_STYLE_PENDING` is `no`.
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Lake** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Ocean** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
```bash
open https://garryslist.org/posts/boil-the-ocean
@ -303,11 +306,31 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
**Rule:** if any `mcp__*__AskUserQuestion` variant is in your tool list, prefer it. Hosts may disable native AUQ via `--disallowedTools AskUserQuestion` (Conductor does, by default) and route through their MCP variant; calling native there silently fails. Same questions/options shape; same decision-brief format applies.
**If no AskUserQuestion variant appears in your tool list:** fall back to the host's normal user-facing chat question. Render the same decision brief in prose, stop, and wait for the user's reply. This fallback is for interactive chat hosts that can ask the user normally but do not expose an AUQ tool (for example Codex API sessions). Do not write decisions to the plan file as a substitute, and do not silently auto-decide (only `/plan-tune` AUTO_DECIDE opt-ins authorize auto-picking).
If AskUserQuestion is unavailable (no variant in your tool list) OR a call to it fails, do NOT silently auto-decide or write the decision to the plan file as a substitute. Follow the **failure fallback** below.
### When AskUserQuestion is unavailable or a call fails
Tell three outcomes apart:
1. **Auto-decide denial (NOT a failure).** The result contains `[plan-tune auto-decide] <id> → <option>` — the preference hook working as designed. Proceed with that option. Do NOT retry, do NOT fall back to prose.
2. **Genuine failure** — no variant in your tool list, OR the variant is present but the call returns an error / missing result (MCP transport error, empty result, host bug — e.g. Conductor's MCP AskUserQuestion is flaky and returns `[Tool result missing due to internal error]`).
- If it was present and **errored** (not absent), retry the SAME call **once** — but only if no answer could have surfaced (a missing-result error can arrive after the user already saw the question; retrying would double-prompt, so if it may have reached them, treat as pending, don't retry).
- Then branch on `SESSION_KIND` (echoed by the preamble; empty/absent ⇒ `interactive`):
- `spawned` → defer to the **Spawned session** block: auto-choose the recommended option. Never prose, never BLOCKED.
- `headless``BLOCKED — AskUserQuestion unavailable`; stop and wait (no human can answer).
- `interactive`**prose fallback** (below).
**Prose fallback — render the decision brief as a markdown message, not a tool call.** Same information as the tool format below, different structure (paragraphs, not ✅/❌ bullets). It MUST surface this triad:
1. **A clear ELI10 of the issue itself** — plain English on what's being decided and why it matters (the question, not per-choice), naming the stakes. Lead with it.
2. **Completeness scores per choice** — explicit `Completeness: X/10` on EACH choice (10 complete, 7 happy-path, 3 shortcut); use the kind-note when options differ in kind not coverage, but never silently drop the score.
3. **The recommendation and why** — a `Recommendation: <choice> because <reason>` line plus the `(recommended)` marker on that choice.
Layout: a `D<N>` title + a one-line note that AskUserQuestion failed and to reply with a letter; the issue ELI10; the Recommendation line; then ONE paragraph per choice carrying its `(recommended)` marker, its `Completeness: X/10`, and 2-4 sentences of reasoning — never a bare bullet list; a closing `Net:` line. Split chains / 5+ options: one prose block per per-option call, in sequence. Then STOP and wait — the user's typed answer is the decision. In plan mode this satisfies end-of-turn like a tool call.
### Format
Every AskUserQuestion is a decision brief. Send it as tool_use when an AskUserQuestion tool exists; otherwise use the normal chat fallback above and wait for the user's reply.
Every AskUserQuestion is a decision brief and must be sent as tool_use, not prose — unless the documented failure fallback above applies (interactive session + the call is unavailable/erroring), in which case the prose fallback is the correct output.
```
D<N><one-line question title>
@ -387,7 +410,7 @@ Before calling AskUserQuestion, verify:
- [ ] (recommended) label on one option (even for neutral-posture)
- [ ] Dual-scale effort labels on effort-bearing options (human / CC)
- [ ] Net line closes the decision
- [ ] You are calling the tool when available; otherwise you are using the normal chat fallback and stopping for the user's reply
- [ ] You are calling the tool, not writing prose — unless the documented failure fallback applies (then: prose with the mandatory triad — issue ELI10, per-choice Completeness, Recommendation + `(recommended)` — and a "reply with a letter" instruction, then STOP)
- [ ] Non-ASCII characters (CJK / accents) written directly, NOT \u-escaped
- [ ] If you had 5+ options, you split (or batched into ≤4-groups) — did NOT drop any
- [ ] If you split, you checked dependencies between options before firing the chain
@ -574,12 +597,19 @@ if [ -d "$_PROJ" ]; then
fi
_LATEST_CP=$(find "$_PROJ/checkpoints" -name "*.md" -type f 2>/dev/null | xargs ls -t 2>/dev/null | head -1)
[ -n "$_LATEST_CP" ] && echo "LATEST_CHECKPOINT: $_LATEST_CP"
if [ -f "$_PROJ/decisions.active.json" ]; then
echo "--- ACTIVE DECISIONS (recent, scope-relevant) ---"
~/.claude/skills/gstack/bin/gstack-decision-search --recent 5 2>/dev/null
echo "--- END DECISIONS ---"
fi
echo "--- END ARTIFACTS ---"
fi
```
If artifacts are listed, read the newest useful one. If `LAST_SESSION` or `LATEST_CHECKPOINT` appears, give a 2-sentence welcome back summary. If `RECENT_PATTERN` clearly implies a next skill, suggest it once.
**Cross-session decisions.** If `ACTIVE DECISIONS` are listed, treat them as prior settled calls with their rationale — do not silently re-litigate them; if you're about to reverse one, say so explicitly. Reach for `~/.claude/skills/gstack/bin/gstack-decision-search` whenever a question touches a past decision ("what did we decide / why / did we try"). When you or the user make a DURABLE decision (architecture, scope, tool/vendor choice, or a reversal) — NOT a turn-level or trivial choice — log it with `~/.claude/skills/gstack/bin/gstack-decision-log` (`--supersede <id>` for a reversal). Reliable and local; gbrain not required.
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format is structure; this is prose quality.
@ -594,9 +624,9 @@ Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format i
Curated jargon list lives at `~/.claude/skills/gstack/scripts/jargon-list.json` (80+ terms). On the first jargon term you encounter this session, Read that file once; treat the `terms` array as the canonical list. The list is repo-owned and may grow between releases.
## Completeness Principle — Boil the Lake
## Completeness Principle — Boil the Ocean
AI makes completeness cheap. Recommend complete lakes (tests, edge cases, error paths); flag oceans (rewrites, multi-quarter migrations).
AI makes completeness cheap, so the complete thing is the goal. Recommend full coverage (tests, edge cases, error paths) — boil the ocean one lake at a time. The only thing out of scope is genuinely unrelated work (rewrites, multi-quarter migrations); flag that as separate scope, never as an excuse for a shortcut.
When options differ in coverage, include `Completeness: X/10` (10 = all edge cases, 7 = happy path, 3 = shortcut). When options differ in kind, write: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate scores.
@ -746,6 +776,18 @@ When the user types `/cso`, run this skill.
6. Phases 0, 1, 12, 13, 14 ALWAYS run regardless of scope flag.
7. If WebSearch is unavailable, skip checks that require it and note: "WebSearch unavailable — proceeding with local-only analysis."
---
## Section index — Read each section when its situation applies
This skill is a decision-tree skeleton. The steps below point to on-demand
sections. Read a section in full before doing its step; do not work from memory.
| When | Read this section |
|------|-------------------|
| running the scope-dependent audit phases (Phases 2-11) selected by the resolved mode, after the Phase 0 stack detection and Phase 1 attack-surface census | `sections/audit-phases.md` |
---
## Important: Use the Grep tool for all code searches
The bash blocks throughout this skill show WHAT patterns to search for, not HOW to run them. Use Claude Code's Grep tool (which handles permissions and access correctly) rather than raw bash grep. The bash blocks are illustrative examples — do NOT copy-paste them into a terminal. Do NOT use `| head` to truncate results.
@ -870,255 +912,8 @@ INFRASTRUCTURE SURFACE
Secret management: [env vars | KMS | vault | unknown]
```
### Phase 2: Secrets Archaeology
Scan git history for leaked credentials, check tracked `.env` files, find CI configs with inline secrets.
**Canonical pattern catalog.** The HIGH-tier credential prefixes the archaeology
greps below target (AKIA, ghp_, sk-ant-, sk_live_, xoxb-, `-----BEGIN ... PRIVATE
KEY-----`, etc.) are the same set `/spec`'s in-flight redaction blocks on. The full
3-tier taxonomy (HIGH credentials, MEDIUM PII/legal/internal, LOW) is generated from
and lives in `lib/redact-patterns.ts` — the single source of truth shared by the
`gstack-redact` engine, `/spec`, `/ship`, and the `/document-*` skills.
**Git history — known secret prefixes:**
```bash
git log -p --all -S "AKIA" --diff-filter=A -- "*.env" "*.yml" "*.yaml" "*.json" "*.toml" 2>/dev/null
git log -p --all -S "sk-" --diff-filter=A -- "*.env" "*.yml" "*.json" "*.ts" "*.js" "*.py" 2>/dev/null
git log -p --all -G "ghp_|gho_|github_pat_" 2>/dev/null
git log -p --all -G "xoxb-|xoxp-|xapp-" 2>/dev/null
git log -p --all -G "password|secret|token|api_key" -- "*.env" "*.yml" "*.json" "*.conf" 2>/dev/null
```
**.env files tracked by git:**
```bash
git ls-files '*.env' '.env.*' 2>/dev/null | grep -v '.example\|.sample\|.template'
grep -q "^\.env$\|^\.env\.\*" .gitignore 2>/dev/null && echo ".env IS gitignored" || echo "WARNING: .env NOT in .gitignore"
```
**CI configs with inline secrets (not using secret stores):**
```bash
for f in $(find .github/workflows -maxdepth 1 \( -name '*.yml' -o -name '*.yaml' \) 2>/dev/null) .gitlab-ci.yml .circleci/config.yml; do
[ -f "$f" ] && grep -n "password:\|token:\|secret:\|api_key:" "$f" | grep -v '\${{' | grep -v 'secrets\.'
done 2>/dev/null
```
**Severity:** CRITICAL for active secret patterns in git history (AKIA, sk_live_, ghp_, xoxb-). HIGH for .env tracked by git, CI configs with inline credentials. MEDIUM for suspicious .env.example values.
**FP rules:** Placeholders ("your_", "changeme", "TODO") excluded. Test fixtures excluded unless same value in non-test code. Rotated secrets still flagged (they were exposed). `.env.local` in `.gitignore` is expected.
**Diff mode:** Replace `git log -p --all` with `git log -p <base>..HEAD`.
### Phase 3: Dependency Supply Chain
Goes beyond `npm audit`. Checks actual supply chain risk.
**Package manager detection:**
```bash
[ -f package.json ] && echo "DETECTED: npm/yarn/bun"
[ -f Gemfile ] && echo "DETECTED: bundler"
[ -f requirements.txt ] || [ -f pyproject.toml ] && echo "DETECTED: pip"
[ -f Cargo.toml ] && echo "DETECTED: cargo"
[ -f go.mod ] && echo "DETECTED: go"
```
**Standard vulnerability scan:** Run whichever package manager's audit tool is available. Each tool is optional — if not installed, note it in the report as "SKIPPED — tool not installed" with install instructions. This is informational, NOT a finding. The audit continues with whatever tools ARE available.
**Install scripts in production deps (supply chain attack vector):** For Node.js projects with hydrated `node_modules`, check production dependencies for `preinstall`, `postinstall`, or `install` scripts.
**Lockfile integrity:** Check that lockfiles exist AND are tracked by git.
**Severity:** CRITICAL for known CVEs (high/critical) in direct deps. HIGH for install scripts in prod deps / missing lockfile. MEDIUM for abandoned packages / medium CVEs / lockfile not tracked.
**FP rules:** devDependency CVEs are MEDIUM max. `node-gyp`/`cmake` install scripts expected (MEDIUM not HIGH). No-fix-available advisories without known exploits excluded. Missing lockfile for library repos (not apps) is NOT a finding.
### Phase 4: CI/CD Pipeline Security
Check who can modify workflows and what secrets they can access.
**GitHub Actions analysis:** For each workflow file, check for:
- Unpinned third-party actions (not SHA-pinned) — use Grep for `uses:` lines missing `@[sha]`
- `pull_request_target` (dangerous: fork PRs get write access)
- Script injection via `${{ github.event.* }}` in `run:` steps
- Secrets as env vars (could leak in logs)
- CODEOWNERS protection on workflow files
**Severity:** CRITICAL for `pull_request_target` + checkout of PR code / script injection via `${{ github.event.*.body }}` in `run:` steps. HIGH for unpinned third-party actions / secrets as env vars without masking. MEDIUM for missing CODEOWNERS on workflow files.
**FP rules:** First-party `actions/*` unpinned = MEDIUM not HIGH. `pull_request_target` without PR ref checkout is safe (precedent #11). Secrets in `with:` blocks (not `env:`/`run:`) are handled by runtime.
### Phase 5: Infrastructure Shadow Surface
Find shadow infrastructure with excessive access.
**Dockerfiles:** For each Dockerfile, check for missing `USER` directive (runs as root), secrets passed as `ARG`, `.env` files copied into images, exposed ports.
**Config files with prod credentials:** Use Grep to search for database connection strings (postgres://, mysql://, mongodb://, redis://) in config files, excluding localhost/127.0.0.1/example.com. Check for staging/dev configs referencing prod.
**IaC security:** For Terraform files, check for `"*"` in IAM actions/resources, hardcoded secrets in `.tf`/`.tfvars`. For K8s manifests, check for privileged containers, hostNetwork, hostPID.
**Severity:** CRITICAL for prod DB URLs with credentials in committed config / `"*"` IAM on sensitive resources / secrets baked into Docker images. HIGH for root containers in prod / staging with prod DB access / privileged K8s. MEDIUM for missing USER directive / exposed ports without documented purpose.
**FP rules:** `docker-compose.yml` for local dev with localhost = not a finding (precedent #12). Terraform `"*"` in `data` sources (read-only) excluded. K8s manifests in `test/`/`dev/`/`local/` with localhost networking excluded.
### Phase 6: Webhook & Integration Audit
Find inbound endpoints that accept anything.
**Webhook routes:** Use Grep to find files containing webhook/hook/callback route patterns. For each file, check whether it also contains signature verification (signature, hmac, verify, digest, x-hub-signature, stripe-signature, svix). Files with webhook routes but NO signature verification are findings.
**TLS verification disabled:** Use Grep to search for patterns like `verify.*false`, `VERIFY_NONE`, `InsecureSkipVerify`, `NODE_TLS_REJECT_UNAUTHORIZED.*0`.
**OAuth scope analysis:** Use Grep to find OAuth configurations and check for overly broad scopes.
**Verification approach (code-tracing only — NO live requests):** For webhook findings, trace the handler code to determine if signature verification exists anywhere in the middleware chain (parent router, middleware stack, API gateway config). Do NOT make actual HTTP requests to webhook endpoints.
**Severity:** CRITICAL for webhooks without any signature verification. HIGH for TLS verification disabled in prod code / overly broad OAuth scopes. MEDIUM for undocumented outbound data flows to third parties.
**FP rules:** TLS disabled in test code excluded. Internal service-to-service webhooks on private networks = MEDIUM max. Webhook endpoints behind API gateway that handles signature verification upstream are NOT findings — but require evidence.
### Phase 7: LLM & AI Security
Check for AI/LLM-specific vulnerabilities. This is a new attack class.
Use Grep to search for these patterns:
- **Prompt injection vectors:** User input flowing into system prompts or tool schemas — look for string interpolation near system prompt construction
- **Unsanitized LLM output:** `dangerouslySetInnerHTML`, `v-html`, `innerHTML`, `.html()`, `raw()` rendering LLM responses
- **Tool/function calling without validation:** `tool_choice`, `function_call`, `tools=`, `functions=`
- **AI API keys in code (not env vars):** `sk-` patterns, hardcoded API key assignments
- **Eval/exec of LLM output:** `eval()`, `exec()`, `Function()`, `new Function` processing AI responses
**Key checks (beyond grep):**
- Trace user content flow — does it enter system prompts or tool schemas?
- RAG poisoning: can external documents influence AI behavior via retrieval?
- Tool calling permissions: are LLM tool calls validated before execution?
- Output sanitization: is LLM output treated as trusted (rendered as HTML, executed as code)?
- Cost/resource attacks: can a user trigger unbounded LLM calls?
**Severity:** CRITICAL for user input in system prompts / unsanitized LLM output rendered as HTML / eval of LLM output. HIGH for missing tool call validation / exposed AI API keys. MEDIUM for unbounded LLM calls / RAG without input validation.
**FP rules:** User content in the user-message position of an AI conversation is NOT prompt injection (precedent #13). Only flag when user content enters system prompts, tool schemas, or function-calling contexts.
### Phase 8: Skill Supply Chain
Scan installed Claude Code skills for malicious patterns. 36% of published skills have security flaws, 13.4% are outright malicious (Snyk ToxicSkills research).
**Tier 1 — repo-local (automatic):** Scan the repo's local skills directory for suspicious patterns:
```bash
ls -la .claude/skills/ 2>/dev/null
```
Use Grep to search all local skill SKILL.md files for suspicious patterns:
- `curl`, `wget`, `fetch`, `http`, `exfiltrat` (network exfiltration)
- `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `env.`, `process.env` (credential access)
- `IGNORE PREVIOUS`, `system override`, `disregard`, `forget your instructions` (prompt injection)
**Tier 2 — global skills (requires permission):** Before scanning globally installed skills or user settings, use AskUserQuestion:
"Phase 8 can scan your globally installed AI coding agent skills and hooks for malicious patterns. This reads files outside the repo. Want to include this?"
Options: A) Yes — scan global skills too B) No — repo-local only
If approved, run the same Grep patterns on globally installed skill files and check hooks in user settings.
**Severity:** CRITICAL for credential exfiltration attempts / prompt injection in skill files. HIGH for suspicious network calls / overly broad tool permissions. MEDIUM for skills from unverified sources without review.
**FP rules:** gstack's own skills are trusted (check if skill path resolves to a known repo). Skills that use `curl` for legitimate purposes (downloading tools, health checks) need context — only flag when the target URL is suspicious or when the command includes credential variables.
### Phase 9: OWASP Top 10 Assessment
For each OWASP category, perform targeted analysis. Use the Grep tool for all searches — scope file extensions to detected stacks from Phase 0.
#### A01: Broken Access Control
- Check for missing auth on controllers/routes (skip_before_action, skip_authorization, public, no_auth)
- Check for direct object reference patterns (params[:id], req.params.id, request.args.get)
- Can user A access user B's resources by changing IDs?
- Is there horizontal/vertical privilege escalation?
#### A02: Cryptographic Failures
- Weak crypto (MD5, SHA1, DES, ECB) or hardcoded secrets
- Is sensitive data encrypted at rest and in transit?
- Are keys/secrets properly managed (env vars, not hardcoded)?
#### A03: Injection
- SQL injection: raw queries, string interpolation in SQL
- Command injection: system(), exec(), spawn(), popen
- Template injection: render with params, eval(), html_safe, raw()
- LLM prompt injection: see Phase 7 for comprehensive coverage
#### A04: Insecure Design
- Rate limits on authentication endpoints?
- Account lockout after failed attempts?
- Business logic validated server-side?
#### A05: Security Misconfiguration
- CORS configuration (wildcard origins in production?)
- CSP headers present?
- Debug mode / verbose errors in production?
#### A06: Vulnerable and Outdated Components
See **Phase 3 (Dependency Supply Chain)** for comprehensive component analysis.
#### A07: Identification and Authentication Failures
- Session management: creation, storage, invalidation
- Password policy: complexity, rotation, breach checking
- MFA: available? enforced for admin?
- Token management: JWT expiration, refresh rotation
#### A08: Software and Data Integrity Failures
See **Phase 4 (CI/CD Pipeline Security)** for pipeline protection analysis.
- Deserialization inputs validated?
- Integrity checking on external data?
#### A09: Security Logging and Monitoring Failures
- Authentication events logged?
- Authorization failures logged?
- Admin actions audit-trailed?
- Logs protected from tampering?
#### A10: Server-Side Request Forgery (SSRF)
- URL construction from user input?
- Internal service reachability from user-controlled URLs?
- Allowlist/blocklist enforcement on outbound requests?
### Phase 10: STRIDE Threat Model
For each major component identified in Phase 0, evaluate:
```
COMPONENT: [Name]
Spoofing: Can an attacker impersonate a user/service?
Tampering: Can data be modified in transit/at rest?
Repudiation: Can actions be denied? Is there an audit trail?
Information Disclosure: Can sensitive data leak?
Denial of Service: Can the component be overwhelmed?
Elevation of Privilege: Can a user gain unauthorized access?
```
### Phase 11: Data Classification
Classify all data handled by the application:
```
DATA CLASSIFICATION
═══════════════════
RESTRICTED (breach = legal liability):
- Passwords/credentials: [where stored, how protected]
- Payment data: [where stored, PCI compliance status]
- PII: [what types, where stored, retention policy]
CONFIDENTIAL (breach = business damage):
- API keys: [where stored, rotation policy]
- Business logic: [trade secrets in code?]
- User behavior data: [analytics, tracking]
INTERNAL (breach = embarrassment):
- System logs: [what they contain, who can access]
- Configuration: [what's exposed in error messages]
PUBLIC:
- Marketing content, documentation, public APIs
```
> **STOP.** Before running the scope-dependent audit phases (Phases 2-11) selected by the resolved mode, after the Phase 0 stack detection and Phase 1 attack-surface census, Read `~/.claude/skills/gstack/cso/sections/audit-phases.md` and execute it
> in full. Do not work from memory — that section is the source of truth for this step.
### Phase 12: False Positive Filtering + Active Verification
Before producing findings, run every candidate through this filter.

View File

@ -67,6 +67,11 @@ When the user types `/cso`, run this skill.
6. Phases 0, 1, 12, 13, 14 ALWAYS run regardless of scope flag.
7. If WebSearch is unavailable, skip checks that require it and note: "WebSearch unavailable — proceeding with local-only analysis."
---
{{SECTION_INDEX:cso}}
---
## Important: Use the Grep tool for all code searches
The bash blocks throughout this skill show WHAT patterns to search for, not HOW to run them. Use Claude Code's Grep tool (which handles permissions and access correctly) rather than raw bash grep. The bash blocks are illustrative examples — do NOT copy-paste them into a terminal. Do NOT use `| head` to truncate results.
@ -155,255 +160,7 @@ INFRASTRUCTURE SURFACE
Secret management: [env vars | KMS | vault | unknown]
```
### Phase 2: Secrets Archaeology
Scan git history for leaked credentials, check tracked `.env` files, find CI configs with inline secrets.
**Canonical pattern catalog.** The HIGH-tier credential prefixes the archaeology
greps below target (AKIA, ghp_, sk-ant-, sk_live_, xoxb-, `-----BEGIN ... PRIVATE
KEY-----`, etc.) are the same set `/spec`'s in-flight redaction blocks on. The full
3-tier taxonomy (HIGH credentials, MEDIUM PII/legal/internal, LOW) is generated from
and lives in `lib/redact-patterns.ts` — the single source of truth shared by the
`gstack-redact` engine, `/spec`, `/ship`, and the `/document-*` skills.
**Git history — known secret prefixes:**
```bash
git log -p --all -S "AKIA" --diff-filter=A -- "*.env" "*.yml" "*.yaml" "*.json" "*.toml" 2>/dev/null
git log -p --all -S "sk-" --diff-filter=A -- "*.env" "*.yml" "*.json" "*.ts" "*.js" "*.py" 2>/dev/null
git log -p --all -G "ghp_|gho_|github_pat_" 2>/dev/null
git log -p --all -G "xoxb-|xoxp-|xapp-" 2>/dev/null
git log -p --all -G "password|secret|token|api_key" -- "*.env" "*.yml" "*.json" "*.conf" 2>/dev/null
```
**.env files tracked by git:**
```bash
git ls-files '*.env' '.env.*' 2>/dev/null | grep -v '.example\|.sample\|.template'
grep -q "^\.env$\|^\.env\.\*" .gitignore 2>/dev/null && echo ".env IS gitignored" || echo "WARNING: .env NOT in .gitignore"
```
**CI configs with inline secrets (not using secret stores):**
```bash
for f in $(find .github/workflows -maxdepth 1 \( -name '*.yml' -o -name '*.yaml' \) 2>/dev/null) .gitlab-ci.yml .circleci/config.yml; do
[ -f "$f" ] && grep -n "password:\|token:\|secret:\|api_key:" "$f" | grep -v '\${{' | grep -v 'secrets\.'
done 2>/dev/null
```
**Severity:** CRITICAL for active secret patterns in git history (AKIA, sk_live_, ghp_, xoxb-). HIGH for .env tracked by git, CI configs with inline credentials. MEDIUM for suspicious .env.example values.
**FP rules:** Placeholders ("your_", "changeme", "TODO") excluded. Test fixtures excluded unless same value in non-test code. Rotated secrets still flagged (they were exposed). `.env.local` in `.gitignore` is expected.
**Diff mode:** Replace `git log -p --all` with `git log -p <base>..HEAD`.
### Phase 3: Dependency Supply Chain
Goes beyond `npm audit`. Checks actual supply chain risk.
**Package manager detection:**
```bash
[ -f package.json ] && echo "DETECTED: npm/yarn/bun"
[ -f Gemfile ] && echo "DETECTED: bundler"
[ -f requirements.txt ] || [ -f pyproject.toml ] && echo "DETECTED: pip"
[ -f Cargo.toml ] && echo "DETECTED: cargo"
[ -f go.mod ] && echo "DETECTED: go"
```
**Standard vulnerability scan:** Run whichever package manager's audit tool is available. Each tool is optional — if not installed, note it in the report as "SKIPPED — tool not installed" with install instructions. This is informational, NOT a finding. The audit continues with whatever tools ARE available.
**Install scripts in production deps (supply chain attack vector):** For Node.js projects with hydrated `node_modules`, check production dependencies for `preinstall`, `postinstall`, or `install` scripts.
**Lockfile integrity:** Check that lockfiles exist AND are tracked by git.
**Severity:** CRITICAL for known CVEs (high/critical) in direct deps. HIGH for install scripts in prod deps / missing lockfile. MEDIUM for abandoned packages / medium CVEs / lockfile not tracked.
**FP rules:** devDependency CVEs are MEDIUM max. `node-gyp`/`cmake` install scripts expected (MEDIUM not HIGH). No-fix-available advisories without known exploits excluded. Missing lockfile for library repos (not apps) is NOT a finding.
### Phase 4: CI/CD Pipeline Security
Check who can modify workflows and what secrets they can access.
**GitHub Actions analysis:** For each workflow file, check for:
- Unpinned third-party actions (not SHA-pinned) — use Grep for `uses:` lines missing `@[sha]`
- `pull_request_target` (dangerous: fork PRs get write access)
- Script injection via `${{ github.event.* }}` in `run:` steps
- Secrets as env vars (could leak in logs)
- CODEOWNERS protection on workflow files
**Severity:** CRITICAL for `pull_request_target` + checkout of PR code / script injection via `${{ github.event.*.body }}` in `run:` steps. HIGH for unpinned third-party actions / secrets as env vars without masking. MEDIUM for missing CODEOWNERS on workflow files.
**FP rules:** First-party `actions/*` unpinned = MEDIUM not HIGH. `pull_request_target` without PR ref checkout is safe (precedent #11). Secrets in `with:` blocks (not `env:`/`run:`) are handled by runtime.
### Phase 5: Infrastructure Shadow Surface
Find shadow infrastructure with excessive access.
**Dockerfiles:** For each Dockerfile, check for missing `USER` directive (runs as root), secrets passed as `ARG`, `.env` files copied into images, exposed ports.
**Config files with prod credentials:** Use Grep to search for database connection strings (postgres://, mysql://, mongodb://, redis://) in config files, excluding localhost/127.0.0.1/example.com. Check for staging/dev configs referencing prod.
**IaC security:** For Terraform files, check for `"*"` in IAM actions/resources, hardcoded secrets in `.tf`/`.tfvars`. For K8s manifests, check for privileged containers, hostNetwork, hostPID.
**Severity:** CRITICAL for prod DB URLs with credentials in committed config / `"*"` IAM on sensitive resources / secrets baked into Docker images. HIGH for root containers in prod / staging with prod DB access / privileged K8s. MEDIUM for missing USER directive / exposed ports without documented purpose.
**FP rules:** `docker-compose.yml` for local dev with localhost = not a finding (precedent #12). Terraform `"*"` in `data` sources (read-only) excluded. K8s manifests in `test/`/`dev/`/`local/` with localhost networking excluded.
### Phase 6: Webhook & Integration Audit
Find inbound endpoints that accept anything.
**Webhook routes:** Use Grep to find files containing webhook/hook/callback route patterns. For each file, check whether it also contains signature verification (signature, hmac, verify, digest, x-hub-signature, stripe-signature, svix). Files with webhook routes but NO signature verification are findings.
**TLS verification disabled:** Use Grep to search for patterns like `verify.*false`, `VERIFY_NONE`, `InsecureSkipVerify`, `NODE_TLS_REJECT_UNAUTHORIZED.*0`.
**OAuth scope analysis:** Use Grep to find OAuth configurations and check for overly broad scopes.
**Verification approach (code-tracing only — NO live requests):** For webhook findings, trace the handler code to determine if signature verification exists anywhere in the middleware chain (parent router, middleware stack, API gateway config). Do NOT make actual HTTP requests to webhook endpoints.
**Severity:** CRITICAL for webhooks without any signature verification. HIGH for TLS verification disabled in prod code / overly broad OAuth scopes. MEDIUM for undocumented outbound data flows to third parties.
**FP rules:** TLS disabled in test code excluded. Internal service-to-service webhooks on private networks = MEDIUM max. Webhook endpoints behind API gateway that handles signature verification upstream are NOT findings — but require evidence.
### Phase 7: LLM & AI Security
Check for AI/LLM-specific vulnerabilities. This is a new attack class.
Use Grep to search for these patterns:
- **Prompt injection vectors:** User input flowing into system prompts or tool schemas — look for string interpolation near system prompt construction
- **Unsanitized LLM output:** `dangerouslySetInnerHTML`, `v-html`, `innerHTML`, `.html()`, `raw()` rendering LLM responses
- **Tool/function calling without validation:** `tool_choice`, `function_call`, `tools=`, `functions=`
- **AI API keys in code (not env vars):** `sk-` patterns, hardcoded API key assignments
- **Eval/exec of LLM output:** `eval()`, `exec()`, `Function()`, `new Function` processing AI responses
**Key checks (beyond grep):**
- Trace user content flow — does it enter system prompts or tool schemas?
- RAG poisoning: can external documents influence AI behavior via retrieval?
- Tool calling permissions: are LLM tool calls validated before execution?
- Output sanitization: is LLM output treated as trusted (rendered as HTML, executed as code)?
- Cost/resource attacks: can a user trigger unbounded LLM calls?
**Severity:** CRITICAL for user input in system prompts / unsanitized LLM output rendered as HTML / eval of LLM output. HIGH for missing tool call validation / exposed AI API keys. MEDIUM for unbounded LLM calls / RAG without input validation.
**FP rules:** User content in the user-message position of an AI conversation is NOT prompt injection (precedent #13). Only flag when user content enters system prompts, tool schemas, or function-calling contexts.
### Phase 8: Skill Supply Chain
Scan installed Claude Code skills for malicious patterns. 36% of published skills have security flaws, 13.4% are outright malicious (Snyk ToxicSkills research).
**Tier 1 — repo-local (automatic):** Scan the repo's local skills directory for suspicious patterns:
```bash
ls -la .claude/skills/ 2>/dev/null
```
Use Grep to search all local skill SKILL.md files for suspicious patterns:
- `curl`, `wget`, `fetch`, `http`, `exfiltrat` (network exfiltration)
- `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `env.`, `process.env` (credential access)
- `IGNORE PREVIOUS`, `system override`, `disregard`, `forget your instructions` (prompt injection)
**Tier 2 — global skills (requires permission):** Before scanning globally installed skills or user settings, use AskUserQuestion:
"Phase 8 can scan your globally installed AI coding agent skills and hooks for malicious patterns. This reads files outside the repo. Want to include this?"
Options: A) Yes — scan global skills too B) No — repo-local only
If approved, run the same Grep patterns on globally installed skill files and check hooks in user settings.
**Severity:** CRITICAL for credential exfiltration attempts / prompt injection in skill files. HIGH for suspicious network calls / overly broad tool permissions. MEDIUM for skills from unverified sources without review.
**FP rules:** gstack's own skills are trusted (check if skill path resolves to a known repo). Skills that use `curl` for legitimate purposes (downloading tools, health checks) need context — only flag when the target URL is suspicious or when the command includes credential variables.
### Phase 9: OWASP Top 10 Assessment
For each OWASP category, perform targeted analysis. Use the Grep tool for all searches — scope file extensions to detected stacks from Phase 0.
#### A01: Broken Access Control
- Check for missing auth on controllers/routes (skip_before_action, skip_authorization, public, no_auth)
- Check for direct object reference patterns (params[:id], req.params.id, request.args.get)
- Can user A access user B's resources by changing IDs?
- Is there horizontal/vertical privilege escalation?
#### A02: Cryptographic Failures
- Weak crypto (MD5, SHA1, DES, ECB) or hardcoded secrets
- Is sensitive data encrypted at rest and in transit?
- Are keys/secrets properly managed (env vars, not hardcoded)?
#### A03: Injection
- SQL injection: raw queries, string interpolation in SQL
- Command injection: system(), exec(), spawn(), popen
- Template injection: render with params, eval(), html_safe, raw()
- LLM prompt injection: see Phase 7 for comprehensive coverage
#### A04: Insecure Design
- Rate limits on authentication endpoints?
- Account lockout after failed attempts?
- Business logic validated server-side?
#### A05: Security Misconfiguration
- CORS configuration (wildcard origins in production?)
- CSP headers present?
- Debug mode / verbose errors in production?
#### A06: Vulnerable and Outdated Components
See **Phase 3 (Dependency Supply Chain)** for comprehensive component analysis.
#### A07: Identification and Authentication Failures
- Session management: creation, storage, invalidation
- Password policy: complexity, rotation, breach checking
- MFA: available? enforced for admin?
- Token management: JWT expiration, refresh rotation
#### A08: Software and Data Integrity Failures
See **Phase 4 (CI/CD Pipeline Security)** for pipeline protection analysis.
- Deserialization inputs validated?
- Integrity checking on external data?
#### A09: Security Logging and Monitoring Failures
- Authentication events logged?
- Authorization failures logged?
- Admin actions audit-trailed?
- Logs protected from tampering?
#### A10: Server-Side Request Forgery (SSRF)
- URL construction from user input?
- Internal service reachability from user-controlled URLs?
- Allowlist/blocklist enforcement on outbound requests?
### Phase 10: STRIDE Threat Model
For each major component identified in Phase 0, evaluate:
```
COMPONENT: [Name]
Spoofing: Can an attacker impersonate a user/service?
Tampering: Can data be modified in transit/at rest?
Repudiation: Can actions be denied? Is there an audit trail?
Information Disclosure: Can sensitive data leak?
Denial of Service: Can the component be overwhelmed?
Elevation of Privilege: Can a user gain unauthorized access?
```
### Phase 11: Data Classification
Classify all data handled by the application:
```
DATA CLASSIFICATION
═══════════════════
RESTRICTED (breach = legal liability):
- Passwords/credentials: [where stored, how protected]
- Payment data: [where stored, PCI compliance status]
- PII: [what types, where stored, retention policy]
CONFIDENTIAL (breach = business damage):
- API keys: [where stored, rotation policy]
- Business logic: [trade secrets in code?]
- User behavior data: [analytics, tracking]
INTERNAL (breach = embarrassment):
- System logs: [what they contain, who can access]
- Configuration: [what's exposed in error messages]
PUBLIC:
- Marketing content, documentation, public APIs
```
{{SECTION:audit-phases}}
### Phase 12: False Positive Filtering + Active Verification
Before producing findings, run every candidate through this filter.

View File

@ -0,0 +1,253 @@
<!-- AUTO-GENERATED from audit-phases.md.tmpl — do not edit directly -->
<!-- Regenerate: bun run gen:skill-docs -->
**Scope gate (read first).** This section holds every scope-dependent phase (2-11), but you run ONLY the phases your resolved mode selected back in `## Mode Resolution` (always-loaded in the skeleton). Phases 0, 1, 12, 13, 14 always run; Phases 2-11 are scope-gated. "Execute in full" means work through this section applying that selection, NOT run a phase your mode did not select just because its prose lives here. Example: `--owasp` runs Phase 9 from this section, not Phases 2-8/10/11.
### Phase 2: Secrets Archaeology
Scan git history for leaked credentials, check tracked `.env` files, find CI configs with inline secrets.
**Canonical pattern catalog.** The HIGH-tier credential prefixes the archaeology
greps below target (AKIA, ghp_, sk-ant-, sk_live_, xoxb-, `-----BEGIN ... PRIVATE
KEY-----`, etc.) are the same set `/spec`'s in-flight redaction blocks on. The full
3-tier taxonomy (HIGH credentials, MEDIUM PII/legal/internal, LOW) is generated from
and lives in `lib/redact-patterns.ts` — the single source of truth shared by the
`gstack-redact` engine, `/spec`, `/ship`, and the `/document-*` skills.
**Git history — known secret prefixes:**
```bash
git log -p --all -S "AKIA" --diff-filter=A -- "*.env" "*.yml" "*.yaml" "*.json" "*.toml" 2>/dev/null
git log -p --all -S "sk-" --diff-filter=A -- "*.env" "*.yml" "*.json" "*.ts" "*.js" "*.py" 2>/dev/null
git log -p --all -G "ghp_|gho_|github_pat_" 2>/dev/null
git log -p --all -G "xoxb-|xoxp-|xapp-" 2>/dev/null
git log -p --all -G "password|secret|token|api_key" -- "*.env" "*.yml" "*.json" "*.conf" 2>/dev/null
```
**.env files tracked by git:**
```bash
git ls-files '*.env' '.env.*' 2>/dev/null | grep -v '.example\|.sample\|.template'
grep -q "^\.env$\|^\.env\.\*" .gitignore 2>/dev/null && echo ".env IS gitignored" || echo "WARNING: .env NOT in .gitignore"
```
**CI configs with inline secrets (not using secret stores):**
```bash
for f in $(find .github/workflows -maxdepth 1 \( -name '*.yml' -o -name '*.yaml' \) 2>/dev/null) .gitlab-ci.yml .circleci/config.yml; do
[ -f "$f" ] && grep -n "password:\|token:\|secret:\|api_key:" "$f" | grep -v '\${{' | grep -v 'secrets\.'
done 2>/dev/null
```
**Severity:** CRITICAL for active secret patterns in git history (AKIA, sk_live_, ghp_, xoxb-). HIGH for .env tracked by git, CI configs with inline credentials. MEDIUM for suspicious .env.example values.
**FP rules:** Placeholders ("your_", "changeme", "TODO") excluded. Test fixtures excluded unless same value in non-test code. Rotated secrets still flagged (they were exposed). `.env.local` in `.gitignore` is expected.
**Diff mode:** Replace `git log -p --all` with `git log -p <base>..HEAD`.
### Phase 3: Dependency Supply Chain
Goes beyond `npm audit`. Checks actual supply chain risk.
**Package manager detection:**
```bash
[ -f package.json ] && echo "DETECTED: npm/yarn/bun"
[ -f Gemfile ] && echo "DETECTED: bundler"
[ -f requirements.txt ] || [ -f pyproject.toml ] && echo "DETECTED: pip"
[ -f Cargo.toml ] && echo "DETECTED: cargo"
[ -f go.mod ] && echo "DETECTED: go"
```
**Standard vulnerability scan:** Run whichever package manager's audit tool is available. Each tool is optional — if not installed, note it in the report as "SKIPPED — tool not installed" with install instructions. This is informational, NOT a finding. The audit continues with whatever tools ARE available.
**Install scripts in production deps (supply chain attack vector):** For Node.js projects with hydrated `node_modules`, check production dependencies for `preinstall`, `postinstall`, or `install` scripts.
**Lockfile integrity:** Check that lockfiles exist AND are tracked by git.
**Severity:** CRITICAL for known CVEs (high/critical) in direct deps. HIGH for install scripts in prod deps / missing lockfile. MEDIUM for abandoned packages / medium CVEs / lockfile not tracked.
**FP rules:** devDependency CVEs are MEDIUM max. `node-gyp`/`cmake` install scripts expected (MEDIUM not HIGH). No-fix-available advisories without known exploits excluded. Missing lockfile for library repos (not apps) is NOT a finding.
### Phase 4: CI/CD Pipeline Security
Check who can modify workflows and what secrets they can access.
**GitHub Actions analysis:** For each workflow file, check for:
- Unpinned third-party actions (not SHA-pinned) — use Grep for `uses:` lines missing `@[sha]`
- `pull_request_target` (dangerous: fork PRs get write access)
- Script injection via `${{ github.event.* }}` in `run:` steps
- Secrets as env vars (could leak in logs)
- CODEOWNERS protection on workflow files
**Severity:** CRITICAL for `pull_request_target` + checkout of PR code / script injection via `${{ github.event.*.body }}` in `run:` steps. HIGH for unpinned third-party actions / secrets as env vars without masking. MEDIUM for missing CODEOWNERS on workflow files.
**FP rules:** First-party `actions/*` unpinned = MEDIUM not HIGH. `pull_request_target` without PR ref checkout is safe (precedent #11). Secrets in `with:` blocks (not `env:`/`run:`) are handled by runtime.
### Phase 5: Infrastructure Shadow Surface
Find shadow infrastructure with excessive access.
**Dockerfiles:** For each Dockerfile, check for missing `USER` directive (runs as root), secrets passed as `ARG`, `.env` files copied into images, exposed ports.
**Config files with prod credentials:** Use Grep to search for database connection strings (postgres://, mysql://, mongodb://, redis://) in config files, excluding localhost/127.0.0.1/example.com. Check for staging/dev configs referencing prod.
**IaC security:** For Terraform files, check for `"*"` in IAM actions/resources, hardcoded secrets in `.tf`/`.tfvars`. For K8s manifests, check for privileged containers, hostNetwork, hostPID.
**Severity:** CRITICAL for prod DB URLs with credentials in committed config / `"*"` IAM on sensitive resources / secrets baked into Docker images. HIGH for root containers in prod / staging with prod DB access / privileged K8s. MEDIUM for missing USER directive / exposed ports without documented purpose.
**FP rules:** `docker-compose.yml` for local dev with localhost = not a finding (precedent #12). Terraform `"*"` in `data` sources (read-only) excluded. K8s manifests in `test/`/`dev/`/`local/` with localhost networking excluded.
### Phase 6: Webhook & Integration Audit
Find inbound endpoints that accept anything.
**Webhook routes:** Use Grep to find files containing webhook/hook/callback route patterns. For each file, check whether it also contains signature verification (signature, hmac, verify, digest, x-hub-signature, stripe-signature, svix). Files with webhook routes but NO signature verification are findings.
**TLS verification disabled:** Use Grep to search for patterns like `verify.*false`, `VERIFY_NONE`, `InsecureSkipVerify`, `NODE_TLS_REJECT_UNAUTHORIZED.*0`.
**OAuth scope analysis:** Use Grep to find OAuth configurations and check for overly broad scopes.
**Verification approach (code-tracing only — NO live requests):** For webhook findings, trace the handler code to determine if signature verification exists anywhere in the middleware chain (parent router, middleware stack, API gateway config). Do NOT make actual HTTP requests to webhook endpoints.
**Severity:** CRITICAL for webhooks without any signature verification. HIGH for TLS verification disabled in prod code / overly broad OAuth scopes. MEDIUM for undocumented outbound data flows to third parties.
**FP rules:** TLS disabled in test code excluded. Internal service-to-service webhooks on private networks = MEDIUM max. Webhook endpoints behind API gateway that handles signature verification upstream are NOT findings — but require evidence.
### Phase 7: LLM & AI Security
Check for AI/LLM-specific vulnerabilities. This is a new attack class.
Use Grep to search for these patterns:
- **Prompt injection vectors:** User input flowing into system prompts or tool schemas — look for string interpolation near system prompt construction
- **Unsanitized LLM output:** `dangerouslySetInnerHTML`, `v-html`, `innerHTML`, `.html()`, `raw()` rendering LLM responses
- **Tool/function calling without validation:** `tool_choice`, `function_call`, `tools=`, `functions=`
- **AI API keys in code (not env vars):** `sk-` patterns, hardcoded API key assignments
- **Eval/exec of LLM output:** `eval()`, `exec()`, `Function()`, `new Function` processing AI responses
**Key checks (beyond grep):**
- Trace user content flow — does it enter system prompts or tool schemas?
- RAG poisoning: can external documents influence AI behavior via retrieval?
- Tool calling permissions: are LLM tool calls validated before execution?
- Output sanitization: is LLM output treated as trusted (rendered as HTML, executed as code)?
- Cost/resource attacks: can a user trigger unbounded LLM calls?
**Severity:** CRITICAL for user input in system prompts / unsanitized LLM output rendered as HTML / eval of LLM output. HIGH for missing tool call validation / exposed AI API keys. MEDIUM for unbounded LLM calls / RAG without input validation.
**FP rules:** User content in the user-message position of an AI conversation is NOT prompt injection (precedent #13). Only flag when user content enters system prompts, tool schemas, or function-calling contexts.
### Phase 8: Skill Supply Chain
Scan installed Claude Code skills for malicious patterns. 36% of published skills have security flaws, 13.4% are outright malicious (Snyk ToxicSkills research).
**Tier 1 — repo-local (automatic):** Scan the repo's local skills directory for suspicious patterns:
```bash
ls -la .claude/skills/ 2>/dev/null
```
Use Grep to search all local skill SKILL.md files for suspicious patterns:
- `curl`, `wget`, `fetch`, `http`, `exfiltrat` (network exfiltration)
- `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `env.`, `process.env` (credential access)
- `IGNORE PREVIOUS`, `system override`, `disregard`, `forget your instructions` (prompt injection)
**Tier 2 — global skills (requires permission):** Before scanning globally installed skills or user settings, use AskUserQuestion:
"Phase 8 can scan your globally installed AI coding agent skills and hooks for malicious patterns. This reads files outside the repo. Want to include this?"
Options: A) Yes — scan global skills too B) No — repo-local only
If approved, run the same Grep patterns on globally installed skill files and check hooks in user settings.
**Severity:** CRITICAL for credential exfiltration attempts / prompt injection in skill files. HIGH for suspicious network calls / overly broad tool permissions. MEDIUM for skills from unverified sources without review.
**FP rules:** gstack's own skills are trusted (check if skill path resolves to a known repo). Skills that use `curl` for legitimate purposes (downloading tools, health checks) need context — only flag when the target URL is suspicious or when the command includes credential variables.
### Phase 9: OWASP Top 10 Assessment
For each OWASP category, perform targeted analysis. Use the Grep tool for all searches — scope file extensions to detected stacks from Phase 0.
#### A01: Broken Access Control
- Check for missing auth on controllers/routes (skip_before_action, skip_authorization, public, no_auth)
- Check for direct object reference patterns (params[:id], req.params.id, request.args.get)
- Can user A access user B's resources by changing IDs?
- Is there horizontal/vertical privilege escalation?
#### A02: Cryptographic Failures
- Weak crypto (MD5, SHA1, DES, ECB) or hardcoded secrets
- Is sensitive data encrypted at rest and in transit?
- Are keys/secrets properly managed (env vars, not hardcoded)?
#### A03: Injection
- SQL injection: raw queries, string interpolation in SQL
- Command injection: system(), exec(), spawn(), popen
- Template injection: render with params, eval(), html_safe, raw()
- LLM prompt injection: see Phase 7 for comprehensive coverage
#### A04: Insecure Design
- Rate limits on authentication endpoints?
- Account lockout after failed attempts?
- Business logic validated server-side?
#### A05: Security Misconfiguration
- CORS configuration (wildcard origins in production?)
- CSP headers present?
- Debug mode / verbose errors in production?
#### A06: Vulnerable and Outdated Components
See **Phase 3 (Dependency Supply Chain)** for comprehensive component analysis.
#### A07: Identification and Authentication Failures
- Session management: creation, storage, invalidation
- Password policy: complexity, rotation, breach checking
- MFA: available? enforced for admin?
- Token management: JWT expiration, refresh rotation
#### A08: Software and Data Integrity Failures
See **Phase 4 (CI/CD Pipeline Security)** for pipeline protection analysis.
- Deserialization inputs validated?
- Integrity checking on external data?
#### A09: Security Logging and Monitoring Failures
- Authentication events logged?
- Authorization failures logged?
- Admin actions audit-trailed?
- Logs protected from tampering?
#### A10: Server-Side Request Forgery (SSRF)
- URL construction from user input?
- Internal service reachability from user-controlled URLs?
- Allowlist/blocklist enforcement on outbound requests?
### Phase 10: STRIDE Threat Model
For each major component identified in Phase 0, evaluate:
```
COMPONENT: [Name]
Spoofing: Can an attacker impersonate a user/service?
Tampering: Can data be modified in transit/at rest?
Repudiation: Can actions be denied? Is there an audit trail?
Information Disclosure: Can sensitive data leak?
Denial of Service: Can the component be overwhelmed?
Elevation of Privilege: Can a user gain unauthorized access?
```
### Phase 11: Data Classification
Classify all data handled by the application:
```
DATA CLASSIFICATION
═══════════════════
RESTRICTED (breach = legal liability):
- Passwords/credentials: [where stored, how protected]
- Payment data: [where stored, PCI compliance status]
- PII: [what types, where stored, retention policy]
CONFIDENTIAL (breach = business damage):
- API keys: [where stored, rotation policy]
- Business logic: [trade secrets in code?]
- User behavior data: [analytics, tracking]
INTERNAL (breach = embarrassment):
- System logs: [what they contain, who can access]
- Configuration: [what's exposed in error messages]
PUBLIC:
- Marketing content, documentation, public APIs
```

View File

@ -0,0 +1,251 @@
**Scope gate (read first).** This section holds every scope-dependent phase (2-11), but you run ONLY the phases your resolved mode selected back in `## Mode Resolution` (always-loaded in the skeleton). Phases 0, 1, 12, 13, 14 always run; Phases 2-11 are scope-gated. "Execute in full" means work through this section applying that selection, NOT run a phase your mode did not select just because its prose lives here. Example: `--owasp` runs Phase 9 from this section, not Phases 2-8/10/11.
### Phase 2: Secrets Archaeology
Scan git history for leaked credentials, check tracked `.env` files, find CI configs with inline secrets.
**Canonical pattern catalog.** The HIGH-tier credential prefixes the archaeology
greps below target (AKIA, ghp_, sk-ant-, sk_live_, xoxb-, `-----BEGIN ... PRIVATE
KEY-----`, etc.) are the same set `/spec`'s in-flight redaction blocks on. The full
3-tier taxonomy (HIGH credentials, MEDIUM PII/legal/internal, LOW) is generated from
and lives in `lib/redact-patterns.ts` — the single source of truth shared by the
`gstack-redact` engine, `/spec`, `/ship`, and the `/document-*` skills.
**Git history — known secret prefixes:**
```bash
git log -p --all -S "AKIA" --diff-filter=A -- "*.env" "*.yml" "*.yaml" "*.json" "*.toml" 2>/dev/null
git log -p --all -S "sk-" --diff-filter=A -- "*.env" "*.yml" "*.json" "*.ts" "*.js" "*.py" 2>/dev/null
git log -p --all -G "ghp_|gho_|github_pat_" 2>/dev/null
git log -p --all -G "xoxb-|xoxp-|xapp-" 2>/dev/null
git log -p --all -G "password|secret|token|api_key" -- "*.env" "*.yml" "*.json" "*.conf" 2>/dev/null
```
**.env files tracked by git:**
```bash
git ls-files '*.env' '.env.*' 2>/dev/null | grep -v '.example\|.sample\|.template'
grep -q "^\.env$\|^\.env\.\*" .gitignore 2>/dev/null && echo ".env IS gitignored" || echo "WARNING: .env NOT in .gitignore"
```
**CI configs with inline secrets (not using secret stores):**
```bash
for f in $(find .github/workflows -maxdepth 1 \( -name '*.yml' -o -name '*.yaml' \) 2>/dev/null) .gitlab-ci.yml .circleci/config.yml; do
[ -f "$f" ] && grep -n "password:\|token:\|secret:\|api_key:" "$f" | grep -v '\${{' | grep -v 'secrets\.'
done 2>/dev/null
```
**Severity:** CRITICAL for active secret patterns in git history (AKIA, sk_live_, ghp_, xoxb-). HIGH for .env tracked by git, CI configs with inline credentials. MEDIUM for suspicious .env.example values.
**FP rules:** Placeholders ("your_", "changeme", "TODO") excluded. Test fixtures excluded unless same value in non-test code. Rotated secrets still flagged (they were exposed). `.env.local` in `.gitignore` is expected.
**Diff mode:** Replace `git log -p --all` with `git log -p <base>..HEAD`.
### Phase 3: Dependency Supply Chain
Goes beyond `npm audit`. Checks actual supply chain risk.
**Package manager detection:**
```bash
[ -f package.json ] && echo "DETECTED: npm/yarn/bun"
[ -f Gemfile ] && echo "DETECTED: bundler"
[ -f requirements.txt ] || [ -f pyproject.toml ] && echo "DETECTED: pip"
[ -f Cargo.toml ] && echo "DETECTED: cargo"
[ -f go.mod ] && echo "DETECTED: go"
```
**Standard vulnerability scan:** Run whichever package manager's audit tool is available. Each tool is optional — if not installed, note it in the report as "SKIPPED — tool not installed" with install instructions. This is informational, NOT a finding. The audit continues with whatever tools ARE available.
**Install scripts in production deps (supply chain attack vector):** For Node.js projects with hydrated `node_modules`, check production dependencies for `preinstall`, `postinstall`, or `install` scripts.
**Lockfile integrity:** Check that lockfiles exist AND are tracked by git.
**Severity:** CRITICAL for known CVEs (high/critical) in direct deps. HIGH for install scripts in prod deps / missing lockfile. MEDIUM for abandoned packages / medium CVEs / lockfile not tracked.
**FP rules:** devDependency CVEs are MEDIUM max. `node-gyp`/`cmake` install scripts expected (MEDIUM not HIGH). No-fix-available advisories without known exploits excluded. Missing lockfile for library repos (not apps) is NOT a finding.
### Phase 4: CI/CD Pipeline Security
Check who can modify workflows and what secrets they can access.
**GitHub Actions analysis:** For each workflow file, check for:
- Unpinned third-party actions (not SHA-pinned) — use Grep for `uses:` lines missing `@[sha]`
- `pull_request_target` (dangerous: fork PRs get write access)
- Script injection via `${{ github.event.* }}` in `run:` steps
- Secrets as env vars (could leak in logs)
- CODEOWNERS protection on workflow files
**Severity:** CRITICAL for `pull_request_target` + checkout of PR code / script injection via `${{ github.event.*.body }}` in `run:` steps. HIGH for unpinned third-party actions / secrets as env vars without masking. MEDIUM for missing CODEOWNERS on workflow files.
**FP rules:** First-party `actions/*` unpinned = MEDIUM not HIGH. `pull_request_target` without PR ref checkout is safe (precedent #11). Secrets in `with:` blocks (not `env:`/`run:`) are handled by runtime.
### Phase 5: Infrastructure Shadow Surface
Find shadow infrastructure with excessive access.
**Dockerfiles:** For each Dockerfile, check for missing `USER` directive (runs as root), secrets passed as `ARG`, `.env` files copied into images, exposed ports.
**Config files with prod credentials:** Use Grep to search for database connection strings (postgres://, mysql://, mongodb://, redis://) in config files, excluding localhost/127.0.0.1/example.com. Check for staging/dev configs referencing prod.
**IaC security:** For Terraform files, check for `"*"` in IAM actions/resources, hardcoded secrets in `.tf`/`.tfvars`. For K8s manifests, check for privileged containers, hostNetwork, hostPID.
**Severity:** CRITICAL for prod DB URLs with credentials in committed config / `"*"` IAM on sensitive resources / secrets baked into Docker images. HIGH for root containers in prod / staging with prod DB access / privileged K8s. MEDIUM for missing USER directive / exposed ports without documented purpose.
**FP rules:** `docker-compose.yml` for local dev with localhost = not a finding (precedent #12). Terraform `"*"` in `data` sources (read-only) excluded. K8s manifests in `test/`/`dev/`/`local/` with localhost networking excluded.
### Phase 6: Webhook & Integration Audit
Find inbound endpoints that accept anything.
**Webhook routes:** Use Grep to find files containing webhook/hook/callback route patterns. For each file, check whether it also contains signature verification (signature, hmac, verify, digest, x-hub-signature, stripe-signature, svix). Files with webhook routes but NO signature verification are findings.
**TLS verification disabled:** Use Grep to search for patterns like `verify.*false`, `VERIFY_NONE`, `InsecureSkipVerify`, `NODE_TLS_REJECT_UNAUTHORIZED.*0`.
**OAuth scope analysis:** Use Grep to find OAuth configurations and check for overly broad scopes.
**Verification approach (code-tracing only — NO live requests):** For webhook findings, trace the handler code to determine if signature verification exists anywhere in the middleware chain (parent router, middleware stack, API gateway config). Do NOT make actual HTTP requests to webhook endpoints.
**Severity:** CRITICAL for webhooks without any signature verification. HIGH for TLS verification disabled in prod code / overly broad OAuth scopes. MEDIUM for undocumented outbound data flows to third parties.
**FP rules:** TLS disabled in test code excluded. Internal service-to-service webhooks on private networks = MEDIUM max. Webhook endpoints behind API gateway that handles signature verification upstream are NOT findings — but require evidence.
### Phase 7: LLM & AI Security
Check for AI/LLM-specific vulnerabilities. This is a new attack class.
Use Grep to search for these patterns:
- **Prompt injection vectors:** User input flowing into system prompts or tool schemas — look for string interpolation near system prompt construction
- **Unsanitized LLM output:** `dangerouslySetInnerHTML`, `v-html`, `innerHTML`, `.html()`, `raw()` rendering LLM responses
- **Tool/function calling without validation:** `tool_choice`, `function_call`, `tools=`, `functions=`
- **AI API keys in code (not env vars):** `sk-` patterns, hardcoded API key assignments
- **Eval/exec of LLM output:** `eval()`, `exec()`, `Function()`, `new Function` processing AI responses
**Key checks (beyond grep):**
- Trace user content flow — does it enter system prompts or tool schemas?
- RAG poisoning: can external documents influence AI behavior via retrieval?
- Tool calling permissions: are LLM tool calls validated before execution?
- Output sanitization: is LLM output treated as trusted (rendered as HTML, executed as code)?
- Cost/resource attacks: can a user trigger unbounded LLM calls?
**Severity:** CRITICAL for user input in system prompts / unsanitized LLM output rendered as HTML / eval of LLM output. HIGH for missing tool call validation / exposed AI API keys. MEDIUM for unbounded LLM calls / RAG without input validation.
**FP rules:** User content in the user-message position of an AI conversation is NOT prompt injection (precedent #13). Only flag when user content enters system prompts, tool schemas, or function-calling contexts.
### Phase 8: Skill Supply Chain
Scan installed Claude Code skills for malicious patterns. 36% of published skills have security flaws, 13.4% are outright malicious (Snyk ToxicSkills research).
**Tier 1 — repo-local (automatic):** Scan the repo's local skills directory for suspicious patterns:
```bash
ls -la .claude/skills/ 2>/dev/null
```
Use Grep to search all local skill SKILL.md files for suspicious patterns:
- `curl`, `wget`, `fetch`, `http`, `exfiltrat` (network exfiltration)
- `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `env.`, `process.env` (credential access)
- `IGNORE PREVIOUS`, `system override`, `disregard`, `forget your instructions` (prompt injection)
**Tier 2 — global skills (requires permission):** Before scanning globally installed skills or user settings, use AskUserQuestion:
"Phase 8 can scan your globally installed AI coding agent skills and hooks for malicious patterns. This reads files outside the repo. Want to include this?"
Options: A) Yes — scan global skills too B) No — repo-local only
If approved, run the same Grep patterns on globally installed skill files and check hooks in user settings.
**Severity:** CRITICAL for credential exfiltration attempts / prompt injection in skill files. HIGH for suspicious network calls / overly broad tool permissions. MEDIUM for skills from unverified sources without review.
**FP rules:** gstack's own skills are trusted (check if skill path resolves to a known repo). Skills that use `curl` for legitimate purposes (downloading tools, health checks) need context — only flag when the target URL is suspicious or when the command includes credential variables.
### Phase 9: OWASP Top 10 Assessment
For each OWASP category, perform targeted analysis. Use the Grep tool for all searches — scope file extensions to detected stacks from Phase 0.
#### A01: Broken Access Control
- Check for missing auth on controllers/routes (skip_before_action, skip_authorization, public, no_auth)
- Check for direct object reference patterns (params[:id], req.params.id, request.args.get)
- Can user A access user B's resources by changing IDs?
- Is there horizontal/vertical privilege escalation?
#### A02: Cryptographic Failures
- Weak crypto (MD5, SHA1, DES, ECB) or hardcoded secrets
- Is sensitive data encrypted at rest and in transit?
- Are keys/secrets properly managed (env vars, not hardcoded)?
#### A03: Injection
- SQL injection: raw queries, string interpolation in SQL
- Command injection: system(), exec(), spawn(), popen
- Template injection: render with params, eval(), html_safe, raw()
- LLM prompt injection: see Phase 7 for comprehensive coverage
#### A04: Insecure Design
- Rate limits on authentication endpoints?
- Account lockout after failed attempts?
- Business logic validated server-side?
#### A05: Security Misconfiguration
- CORS configuration (wildcard origins in production?)
- CSP headers present?
- Debug mode / verbose errors in production?
#### A06: Vulnerable and Outdated Components
See **Phase 3 (Dependency Supply Chain)** for comprehensive component analysis.
#### A07: Identification and Authentication Failures
- Session management: creation, storage, invalidation
- Password policy: complexity, rotation, breach checking
- MFA: available? enforced for admin?
- Token management: JWT expiration, refresh rotation
#### A08: Software and Data Integrity Failures
See **Phase 4 (CI/CD Pipeline Security)** for pipeline protection analysis.
- Deserialization inputs validated?
- Integrity checking on external data?
#### A09: Security Logging and Monitoring Failures
- Authentication events logged?
- Authorization failures logged?
- Admin actions audit-trailed?
- Logs protected from tampering?
#### A10: Server-Side Request Forgery (SSRF)
- URL construction from user input?
- Internal service reachability from user-controlled URLs?
- Allowlist/blocklist enforcement on outbound requests?
### Phase 10: STRIDE Threat Model
For each major component identified in Phase 0, evaluate:
```
COMPONENT: [Name]
Spoofing: Can an attacker impersonate a user/service?
Tampering: Can data be modified in transit/at rest?
Repudiation: Can actions be denied? Is there an audit trail?
Information Disclosure: Can sensitive data leak?
Denial of Service: Can the component be overwhelmed?
Elevation of Privilege: Can a user gain unauthorized access?
```
### Phase 11: Data Classification
Classify all data handled by the application:
```
DATA CLASSIFICATION
═══════════════════
RESTRICTED (breach = legal liability):
- Passwords/credentials: [where stored, how protected]
- Payment data: [where stored, PCI compliance status]
- PII: [what types, where stored, retention policy]
CONFIDENTIAL (breach = business damage):
- API keys: [where stored, rotation policy]
- Business logic: [trade secrets in code?]
- User behavior data: [analytics, tracking]
INTERNAL (breach = embarrassment):
- System logs: [what they contain, who can access]
- Configuration: [what's exposed in error messages]
PUBLIC:
- Marketing content, documentation, public APIs
```

View File

@ -0,0 +1,14 @@
{
"$schema": "https://gstack.dev/schemas/section-manifest.json",
"skill": "cso",
"version": 1,
"note": "PASSIVE registry (v2 plan T9 / CM2). id/file/title/trigger text ONLY. Mode dispatch (## Arguments, ## Mode Resolution), always-run phases (0,1), and FP-filtering exceptions (Phase 12) stay in the always-loaded skeleton; only the scope-dependent audit phases are on demand.",
"sections": [
{
"id": "audit-phases",
"file": "audit-phases.md",
"title": "Scope-dependent audit phases: secrets, dependencies, CI/CD, infra, webhooks, LLM/AI, skill supply chain, OWASP Top 10, STRIDE, data classification (Phases 2-11)",
"trigger": "running the scope-dependent audit phases (Phases 2-11) selected by the resolved mode, after the Phase 0 stack detection and Phase 1 attack-surface census"
}
]
}

View File

@ -72,6 +72,9 @@ echo "SKILL_PREFIX: $_SKILL_PREFIX"
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
REPO_MODE=${REPO_MODE:-unknown}
echo "REPO_MODE: $REPO_MODE"
_SESSION_KIND=$(~/.claude/skills/gstack/bin/gstack-session-kind 2>/dev/null || echo "interactive")
case "$_SESSION_KIND" in spawned|headless|interactive) ;; *) _SESSION_KIND="interactive" ;; esac
echo "SESSION_KIND: $_SESSION_KIND"
_LAKE_SEEN=$([ -f ~/.gstack/.completeness-intro-seen ] && echo "yes" || echo "no")
echo "LAKE_INTRO: $_LAKE_SEEN"
_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || true)
@ -151,7 +154,7 @@ In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`co
## Skill Invocation During Plan Mode
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If no AskUserQuestion variant is callable, use the normal chat fallback from the AskUserQuestion Format rule and stop for the user's reply. At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If AskUserQuestion is unavailable or a call fails, follow the AskUserQuestion Format failure fallback: `headless` → BLOCKED; `interactive` → the prose fallback (also satisfies end-of-turn). At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If `PROACTIVE` is `"false"`, do not auto-invoke or proactively suggest skills. If a skill seems useful, ask: "I think /skillname might help here — want me to run it?"
@ -186,7 +189,7 @@ touch ~/.gstack/.writing-style-prompted
Skip if `WRITING_STYLE_PENDING` is `no`.
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Lake** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Ocean** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
```bash
open https://garryslist.org/posts/boil-the-ocean
@ -323,11 +326,31 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
**Rule:** if any `mcp__*__AskUserQuestion` variant is in your tool list, prefer it. Hosts may disable native AUQ via `--disallowedTools AskUserQuestion` (Conductor does, by default) and route through their MCP variant; calling native there silently fails. Same questions/options shape; same decision-brief format applies.
**If no AskUserQuestion variant appears in your tool list:** fall back to the host's normal user-facing chat question. Render the same decision brief in prose, stop, and wait for the user's reply. This fallback is for interactive chat hosts that can ask the user normally but do not expose an AUQ tool (for example Codex API sessions). Do not write decisions to the plan file as a substitute, and do not silently auto-decide (only `/plan-tune` AUTO_DECIDE opt-ins authorize auto-picking).
If AskUserQuestion is unavailable (no variant in your tool list) OR a call to it fails, do NOT silently auto-decide or write the decision to the plan file as a substitute. Follow the **failure fallback** below.
### When AskUserQuestion is unavailable or a call fails
Tell three outcomes apart:
1. **Auto-decide denial (NOT a failure).** The result contains `[plan-tune auto-decide] <id> → <option>` — the preference hook working as designed. Proceed with that option. Do NOT retry, do NOT fall back to prose.
2. **Genuine failure** — no variant in your tool list, OR the variant is present but the call returns an error / missing result (MCP transport error, empty result, host bug — e.g. Conductor's MCP AskUserQuestion is flaky and returns `[Tool result missing due to internal error]`).
- If it was present and **errored** (not absent), retry the SAME call **once** — but only if no answer could have surfaced (a missing-result error can arrive after the user already saw the question; retrying would double-prompt, so if it may have reached them, treat as pending, don't retry).
- Then branch on `SESSION_KIND` (echoed by the preamble; empty/absent ⇒ `interactive`):
- `spawned` → defer to the **Spawned session** block: auto-choose the recommended option. Never prose, never BLOCKED.
- `headless``BLOCKED — AskUserQuestion unavailable`; stop and wait (no human can answer).
- `interactive`**prose fallback** (below).
**Prose fallback — render the decision brief as a markdown message, not a tool call.** Same information as the tool format below, different structure (paragraphs, not ✅/❌ bullets). It MUST surface this triad:
1. **A clear ELI10 of the issue itself** — plain English on what's being decided and why it matters (the question, not per-choice), naming the stakes. Lead with it.
2. **Completeness scores per choice** — explicit `Completeness: X/10` on EACH choice (10 complete, 7 happy-path, 3 shortcut); use the kind-note when options differ in kind not coverage, but never silently drop the score.
3. **The recommendation and why** — a `Recommendation: <choice> because <reason>` line plus the `(recommended)` marker on that choice.
Layout: a `D<N>` title + a one-line note that AskUserQuestion failed and to reply with a letter; the issue ELI10; the Recommendation line; then ONE paragraph per choice carrying its `(recommended)` marker, its `Completeness: X/10`, and 2-4 sentences of reasoning — never a bare bullet list; a closing `Net:` line. Split chains / 5+ options: one prose block per per-option call, in sequence. Then STOP and wait — the user's typed answer is the decision. In plan mode this satisfies end-of-turn like a tool call.
### Format
Every AskUserQuestion is a decision brief. Send it as tool_use when an AskUserQuestion tool exists; otherwise use the normal chat fallback above and wait for the user's reply.
Every AskUserQuestion is a decision brief and must be sent as tool_use, not prose — unless the documented failure fallback above applies (interactive session + the call is unavailable/erroring), in which case the prose fallback is the correct output.
```
D<N><one-line question title>
@ -407,7 +430,7 @@ Before calling AskUserQuestion, verify:
- [ ] (recommended) label on one option (even for neutral-posture)
- [ ] Dual-scale effort labels on effort-bearing options (human / CC)
- [ ] Net line closes the decision
- [ ] You are calling the tool when available; otherwise you are using the normal chat fallback and stopping for the user's reply
- [ ] You are calling the tool, not writing prose — unless the documented failure fallback applies (then: prose with the mandatory triad — issue ELI10, per-choice Completeness, Recommendation + `(recommended)` — and a "reply with a letter" instruction, then STOP)
- [ ] Non-ASCII characters (CJK / accents) written directly, NOT \u-escaped
- [ ] If you had 5+ options, you split (or batched into ≤4-groups) — did NOT drop any
- [ ] If you split, you checked dependencies between options before firing the chain
@ -594,12 +617,19 @@ if [ -d "$_PROJ" ]; then
fi
_LATEST_CP=$(find "$_PROJ/checkpoints" -name "*.md" -type f 2>/dev/null | xargs ls -t 2>/dev/null | head -1)
[ -n "$_LATEST_CP" ] && echo "LATEST_CHECKPOINT: $_LATEST_CP"
if [ -f "$_PROJ/decisions.active.json" ]; then
echo "--- ACTIVE DECISIONS (recent, scope-relevant) ---"
~/.claude/skills/gstack/bin/gstack-decision-search --recent 5 2>/dev/null
echo "--- END DECISIONS ---"
fi
echo "--- END ARTIFACTS ---"
fi
```
If artifacts are listed, read the newest useful one. If `LAST_SESSION` or `LATEST_CHECKPOINT` appears, give a 2-sentence welcome back summary. If `RECENT_PATTERN` clearly implies a next skill, suggest it once.
**Cross-session decisions.** If `ACTIVE DECISIONS` are listed, treat them as prior settled calls with their rationale — do not silently re-litigate them; if you're about to reverse one, say so explicitly. Reach for `~/.claude/skills/gstack/bin/gstack-decision-search` whenever a question touches a past decision ("what did we decide / why / did we try"). When you or the user make a DURABLE decision (architecture, scope, tool/vendor choice, or a reversal) — NOT a turn-level or trivial choice — log it with `~/.claude/skills/gstack/bin/gstack-decision-log` (`--supersede <id>` for a reversal). Reliable and local; gbrain not required.
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format is structure; this is prose quality.
@ -614,9 +644,9 @@ Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format i
Curated jargon list lives at `~/.claude/skills/gstack/scripts/jargon-list.json` (80+ terms). On the first jargon term you encounter this session, Read that file once; treat the `terms` array as the canonical list. The list is repo-owned and may grow between releases.
## Completeness Principle — Boil the Lake
## Completeness Principle — Boil the Ocean
AI makes completeness cheap. Recommend complete lakes (tests, edge cases, error paths); flag oceans (rewrites, multi-quarter migrations).
AI makes completeness cheap, so the complete thing is the goal. Recommend full coverage (tests, edge cases, error paths) — boil the ocean one lake at a time. The only thing out of scope is genuinely unrelated work (rewrites, multi-quarter migrations); flag that as separate scope, never as an excuse for a shortcut.
When options differ in coverage, include `Completeness: X/10` (10 = all edge cases, 7 = happy path, 3 = shortcut). When options differ in kind, write: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate scores.
@ -921,6 +951,17 @@ matches a past learning, display:
This makes the compounding visible. The user should see that gstack is getting
smarter on their codebase over time.
## Section index — Read each section when its situation applies
This skill is a decision-tree skeleton. The steps below point to on-demand
sections. Read a section in full before doing its step; do not work from memory.
| When | Read this section |
|------|-------------------|
| building the complete design-system proposal, drill-downs, the design preview, and writing DESIGN.md (Phases 3-6, after product context and research) | `sections/proposal-and-preview.md` |
---
## Phase 1: Product Context
Ask the user a single question that covers everything you need to know. Pre-fill what you can infer from the codebase.
@ -1106,412 +1147,8 @@ Present subagent output under a `CLAUDE SUBAGENT (design direction):` header.
```
Replace STATUS with "clean" or "issues_found", SOURCE with "codex+subagent", "codex-only", "subagent-only", or "unavailable".
## Phase 3: The Complete Proposal
This is the soul of the skill. Propose EVERYTHING as one coherent package.
**AskUserQuestion Q2 — present the full proposal with SAFE/RISK breakdown:**
```
Based on [product context] and [research findings / my design knowledge]:
AESTHETIC: [direction] — [one-line rationale]
DECORATION: [level] — [why this pairs with the aesthetic]
LAYOUT: [approach] — [why this fits the product type]
COLOR: [approach] + proposed palette (hex values) — [rationale]
TYPOGRAPHY: [3 font recommendations with roles] — [why these fonts]
SPACING: [base unit + density] — [rationale]
MOTION: [approach] — [rationale]
This system is coherent because [explain how choices reinforce each other].
SAFE CHOICES (category baseline — your users expect these):
- [2-3 decisions that match category conventions, with rationale for playing safe]
RISKS (where your product gets its own face):
- [2-3 deliberate departures from convention]
- For each risk: what it is, why it works, what you gain, what it costs
The safe choices keep you literate in your category. The risks are where
your product becomes memorable. Which risks appeal to you? Want to see
different ones? Or adjust anything else?
```
The SAFE/RISK breakdown is critical. Design coherence is table stakes — every product in a category can be coherent and still look identical. The real question is: where do you take creative risks? The agent should always propose at least 2 risks, each with a clear rationale for why the risk is worth taking and what the user gives up. Risks might include: an unexpected typeface for the category, a bold accent color nobody else uses, tighter or looser spacing than the norm, a layout approach that breaks from convention, motion choices that add personality.
**Options:** A) Looks great — generate the preview page. B) I want to adjust [section]. C) I want different risks — show me wilder options. D) Start over with a different direction. E) Skip the preview, just write DESIGN.md.
### Your Design Knowledge (use to inform proposals — do NOT display as tables)
**Aesthetic directions** (pick the one that fits the product):
- Brutally Minimal — Type and whitespace only. No decoration. Modernist.
- Maximalist Chaos — Dense, layered, pattern-heavy. Y2K meets contemporary.
- Retro-Futuristic — Vintage tech nostalgia. CRT glow, pixel grids, warm monospace.
- Luxury/Refined — Serifs, high contrast, generous whitespace, precious metals.
- Playful/Toy-like — Rounded, bouncy, bold primaries. Approachable and fun.
- Editorial/Magazine — Strong typographic hierarchy, asymmetric grids, pull quotes.
- Brutalist/Raw — Exposed structure, system fonts, visible grid, no polish.
- Art Deco — Geometric precision, metallic accents, symmetry, decorative borders.
- Organic/Natural — Earth tones, rounded forms, hand-drawn texture, grain.
- Industrial/Utilitarian — Function-first, data-dense, monospace accents, muted palette.
**Decoration levels:** minimal (typography does all the work) / intentional (subtle texture, grain, or background treatment) / expressive (full creative direction, layered depth, patterns)
**Layout approaches:** grid-disciplined (strict columns, predictable alignment) / creative-editorial (asymmetry, overlap, grid-breaking) / hybrid (grid for app, creative for marketing)
**Color approaches:** restrained (1 accent + neutrals, color is rare and meaningful) / balanced (primary + secondary, semantic colors for hierarchy) / expressive (color as a primary design tool, bold palettes)
**Motion approaches:** minimal-functional (only transitions that aid comprehension) / intentional (subtle entrance animations, meaningful state transitions) / expressive (full choreography, scroll-driven, playful)
**Font recommendations by purpose:**
- Display/Hero: Satoshi, General Sans, Instrument Serif, Fraunces, Clash Grotesk, Cabinet Grotesk
- Body: Instrument Sans, DM Sans, Source Sans 3, Geist, Plus Jakarta Sans, Outfit
- Data/Tables: Geist (tabular-nums), DM Sans (tabular-nums), JetBrains Mono, IBM Plex Mono
- Code: JetBrains Mono, Fira Code, Berkeley Mono, Geist Mono
**Font blacklist** (never recommend):
Papyrus, Comic Sans, Lobster, Impact, Jokerman, Bleeding Cowboys, Permanent Marker, Bradley Hand, Brush Script, Hobo, Trajan, Raleway, Clash Display, Courier New (for body)
**Overused fonts** (never recommend as primary — use only if user specifically requests):
Inter, Roboto, Arial, Helvetica, Open Sans, Lato, Montserrat, Poppins, Space Grotesk.
Space Grotesk is on the list specifically because every AI design tool converges on it
as "the safe alternative to Inter." That's the convergence trap. Treat it the same as
Inter: only use if the user asks for it by name.
**Anti-convergence directive:** Across multiple generations in the same project, VARY
light/dark, fonts, and aesthetic directions. Never propose the same choices twice
without explicit justification. If the user's prior session used Geist + dark + editorial,
propose something different this time (or explicitly acknowledge you're doubling down
because it fits the brief). Convergence across generations is slop.
**AI slop anti-patterns** (never include in your recommendations):
- Purple/violet gradients as default accent
- 3-column feature grid with icons in colored circles
- Centered everything with uniform spacing
- Uniform bubbly border-radius on all elements
- Gradient buttons as the primary CTA pattern
- Generic stock-photo-style hero sections
- system-ui / -apple-system as the primary display or body font (the "I gave up on typography" signal)
- "Built for X" / "Designed for Y" marketing copy patterns
### Coherence Validation
When the user overrides one section, check if the rest still coheres. Flag mismatches with a gentle nudge — never block:
- Brutalist/Minimal aesthetic + expressive motion → "Heads up: brutalist aesthetics usually pair with minimal motion. Your combo is unusual — which is fine if intentional. Want me to suggest motion that fits, or keep it?"
- Expressive color + restrained decoration → "Bold palette with minimal decoration can work, but the colors will carry a lot of weight. Want me to suggest decoration that supports the palette?"
- Creative-editorial layout + data-heavy product → "Editorial layouts are gorgeous but can fight data density. Want me to show how a hybrid approach keeps both?"
- Always accept the user's final choice. Never refuse to proceed.
---
## Phase 4: Drill-downs (only if user requests adjustments)
When the user wants to change a specific section, go deep on that section:
- **Fonts:** Present 3-5 specific candidates with rationale, explain what each evokes, offer the preview page
- **Colors:** Present 2-3 palette options with hex values, explain the color theory reasoning
- **Aesthetic:** Walk through which directions fit their product and why
- **Layout/Spacing/Motion:** Present the approaches with concrete tradeoffs for their product type
Each drill-down is one focused AskUserQuestion. After the user decides, re-check coherence with the rest of the system.
---
## Phase 5: Design System Preview (default ON)
This phase generates visual previews of the proposed design system. Two paths depending on whether the gstack designer is available.
### Path A: AI Mockups (if DESIGN_READY)
Generate AI-rendered mockups showing the proposed design system applied to realistic screens for this product. This is far more powerful than an HTML preview — the user sees what their product could actually look like.
```bash
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)"
_DESIGN_DIR="$HOME/.gstack/projects/$SLUG/designs/design-system-$(date +%Y%m%d)"
mkdir -p "$_DESIGN_DIR"
echo "DESIGN_DIR: $_DESIGN_DIR"
```
Construct a design brief from the Phase 3 proposal (aesthetic, colors, typography, spacing, layout) and the product context from Phase 1:
```bash
$D variants --brief "<product name: [name]. Product type: [type]. Aesthetic: [direction]. Colors: primary [hex], secondary [hex], neutrals [range]. Typography: display [font], body [font]. Layout: [approach]. Show a realistic [page type] screen with [specific content for this product].>" --count 3 --output-dir "$_DESIGN_DIR/"
```
Run quality check on each variant:
```bash
$D check --image "$_DESIGN_DIR/variant-A.png" --brief "<the original brief>"
```
Show each variant inline (Read tool on each PNG) for instant preview.
**Before presenting to the user, self-gate:** For each variant, ask yourself: *"Would
a human designer be embarrassed to put their name on this?"* If yes, discard the
variant and regenerate. This is a hard gate. A mediocre AI mockup is worse than no
mockup. Embarrassment triggers include: purple gradient hero, 3-column SaaS grid,
centered-everything, Inter body text, generic stock-photo vibe, system-ui font,
gradient CTA button, bubble-radius everything. Any of those = reject and regenerate.
Tell the user: "I've generated 3 visual directions applying your design system to a realistic [product type] screen. Pick your favorite in the comparison board that just opened in your browser. You can also remix elements across variants."
### Comparison Board + Feedback Loop
Create the comparison board and serve it over HTTP:
```bash
$D compare --images "$_DESIGN_DIR/variant-A.png,$_DESIGN_DIR/variant-B.png,$_DESIGN_DIR/variant-C.png" --output "$_DESIGN_DIR/design-board.html" --serve
```
This command generates the board HTML, starts an HTTP server on a random port,
and opens it in the user's default browser. **Run it in the background** with `&`
because the server needs to stay running while the user interacts with the board.
Parse the board URL from stderr output. Default daemon path:
`BOARD_URL: http://127.0.0.1:N/boards/<id>/` (already includes the per-board
path; use this for the AskUserQuestion URL AND as the base for the reload
endpoint). Legacy `--no-daemon` path emits `SERVE_STARTED: port=XXXXX` and
serves a single board at `/`, with reload at `/api/reload` — only relevant
when an external caller explicitly passes `--no-daemon`.
**PRIMARY WAIT: AskUserQuestion with board URL**
After the board is serving, use AskUserQuestion to wait for the user. Include the
board URL so they can click it if they lost the browser tab:
"I've opened a comparison board with the design variants:
<BOARD_URL> — Rate them, leave comments, remix
elements you like, and click Submit when you're done. Let me know when you've
submitted your feedback (or paste your preferences here). If you clicked
Regenerate or Remix on the board, tell me and I'll generate new variants."
Substitute `<BOARD_URL>` with the URL parsed from stderr (the daemon path
emits `BOARD_URL: http://127.0.0.1:N/boards/<id>/`).
**Do NOT use AskUserQuestion to ask which variant the user prefers.** The comparison
board IS the chooser. AskUserQuestion is just the blocking wait mechanism.
**After the user responds to AskUserQuestion:**
Check for feedback files next to the board HTML:
- `$_DESIGN_DIR/feedback.json` — written when user clicks Submit (final choice)
- `$_DESIGN_DIR/feedback-pending.json` — written when user clicks Regenerate/Remix/More Like This
```bash
if [ -f "$_DESIGN_DIR/feedback.json" ]; then
echo "SUBMIT_RECEIVED"
cat "$_DESIGN_DIR/feedback.json"
elif [ -f "$_DESIGN_DIR/feedback-pending.json" ]; then
echo "REGENERATE_RECEIVED"
cat "$_DESIGN_DIR/feedback-pending.json"
rm "$_DESIGN_DIR/feedback-pending.json"
else
echo "NO_FEEDBACK_FILE"
fi
```
The feedback JSON has this shape:
```json
{
"preferred": "A",
"ratings": { "A": 4, "B": 3, "C": 2 },
"comments": { "A": "Love the spacing" },
"overall": "Go with A, bigger CTA",
"regenerated": false
}
```
**If `feedback.json` found:** The user clicked Submit on the board.
Read `preferred`, `ratings`, `comments`, `overall` from the JSON. Proceed with
the approved variant.
**If `feedback-pending.json` found:** The user clicked Regenerate/Remix on the board.
1. Read `regenerateAction` from the JSON (`"different"`, `"match"`, `"more_like_B"`,
`"remix"`, or custom text)
2. If `regenerateAction` is `"remix"`, read `remixSpec` (e.g. `{"layout":"A","colors":"B"}`)
3. Generate new variants with `$D iterate` or `$D variants` using updated brief
4. Create new board: `$D compare --images "..." --output "$_DESIGN_DIR/design-board.html"`
5. Reload the board in the user's browser (same tab) — the URL is per-board
under daemon mode, so use `<BOARD_URL>` (from the `BOARD_URL:` stderr
line) as the base:
`curl -s -X POST "${BOARD_URL}api/reload" -H 'Content-Type: application/json' -d '{"html":"$_DESIGN_DIR/design-board.html"}'`
Under `--no-daemon` the reload endpoint is `/api/reload` at the legacy
port; this path only matters if the caller explicitly opted out of the
daemon.
6. The board auto-refreshes. **AskUserQuestion again** with the same board URL to
wait for the next round of feedback. Repeat until `feedback.json` appears.
**If `NO_FEEDBACK_FILE`:** The user typed their preferences directly in the
AskUserQuestion response instead of using the board. Use their text response
as the feedback.
**POLLING FALLBACK:** Only use polling if `$D serve` fails (no port available).
In that case, show each variant inline using the Read tool (so the user can see them),
then use AskUserQuestion:
"The comparison board server failed to start. I've shown the variants above.
Which do you prefer? Any feedback?"
**After receiving feedback (any path):** Output a clear summary confirming
what was understood:
"Here's what I understood from your feedback:
PREFERRED: Variant [X]
RATINGS: [list]
YOUR NOTES: [comments]
DIRECTION: [overall]
Is this right?"
Use AskUserQuestion to verify before proceeding.
**Save the approved choice:**
```bash
echo '{"approved_variant":"<V>","feedback":"<FB>","date":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","screen":"<SCREEN>","branch":"'$(git branch --show-current 2>/dev/null)'"}' > "$_DESIGN_DIR/approved.json"
```
After the user picks a direction:
- Use `$D extract --image "$_DESIGN_DIR/variant-<CHOSEN>.png"` to analyze the approved mockup and extract design tokens (colors, typography, spacing) that will populate DESIGN.md in Phase 6. This grounds the design system in what was actually approved visually, not just what was described in text.
- If the user wants to iterate further: `$D iterate --feedback "<user's feedback>" --output "$_DESIGN_DIR/refined.png"`
**Plan mode vs. implementation mode:**
- **If in plan mode:** Add the approved mockup path (the full `$_DESIGN_DIR` path) and extracted tokens to the plan file under an "## Approved Design Direction" section. The design system gets written to DESIGN.md when the plan is implemented.
- **If NOT in plan mode:** Proceed directly to Phase 6 and write DESIGN.md with the extracted tokens.
### Path B: HTML Preview Page (fallback if DESIGN_NOT_AVAILABLE)
Generate a polished HTML preview page and open it in the user's browser. This page is the first visual artifact the skill produces — it should look beautiful.
```bash
PREVIEW_FILE="/tmp/design-consultation-preview-$(date +%s).html"
```
Write the preview HTML to `$PREVIEW_FILE`, then open it:
```bash
open "$PREVIEW_FILE"
```
### Preview Page Requirements (Path B only)
The agent writes a **single, self-contained HTML file** (no framework dependencies) that:
1. **Loads proposed fonts** from Google Fonts (or Bunny Fonts) via `<link>` tags
2. **Uses the proposed color palette** throughout — dogfood the design system
3. **Shows the product name** (not "Lorem Ipsum") as the hero heading
4. **Font specimen section:**
- Each font candidate shown in its proposed role (hero heading, body paragraph, button label, data table row)
- Side-by-side comparison if multiple candidates for one role
- Real content that matches the product (e.g., civic tech → government data examples)
5. **Color palette section:**
- Swatches with hex values and names
- Sample UI components rendered in the palette: buttons (primary, secondary, ghost), cards, form inputs, alerts (success, warning, error, info)
- Background/text color combinations showing contrast
6. **Realistic product mockups** — this is what makes the preview page powerful. Based on the project type from Phase 1, render 2-3 realistic page layouts using the full design system:
- **Dashboard / web app:** sample data table with metrics, sidebar nav, header with user avatar, stat cards
- **Marketing site:** hero section with real copy, feature highlights, testimonial block, CTA
- **Settings / admin:** form with labeled inputs, toggle switches, dropdowns, save button
- **Auth / onboarding:** login form with social buttons, branding, input validation states
- Use the product name, realistic content for the domain, and the proposed spacing/layout/border-radius. The user should see their product (roughly) before writing any code.
7. **Light/dark mode toggle** using CSS custom properties and a JS toggle button
8. **Clean, professional layout** — the preview page IS a taste signal for the skill
9. **Responsive** — looks good on any screen width
The page should make the user think "oh nice, they thought of this." It's selling the design system by showing what the product could feel like, not just listing hex codes and font names.
If `open` fails (headless environment), tell the user: *"I wrote the preview to [path] — open it in your browser to see the fonts and colors rendered."*
If the user says skip the preview, go directly to Phase 6.
---
## Phase 6: Write DESIGN.md & Confirm
If `$D extract` was used in Phase 5 (Path A), use the extracted tokens as the primary source for DESIGN.md values — colors, typography, and spacing grounded in the approved mockup rather than text descriptions alone. Merge extracted tokens with the Phase 3 proposal (the proposal provides rationale and context; the extraction provides exact values).
**If in plan mode:** Write the DESIGN.md content into the plan file as a "## Proposed DESIGN.md" section. Do NOT write the actual file — that happens at implementation time.
**If NOT in plan mode:** Write `DESIGN.md` to the repo root with this structure:
```markdown
# Design System — [Project Name]
## Product Context
- **What this is:** [1-2 sentence description]
- **Who it's for:** [target users]
- **Space/industry:** [category, peers]
- **Project type:** [web app / dashboard / marketing site / editorial / internal tool]
## Aesthetic Direction
- **Direction:** [name]
- **Decoration level:** [minimal / intentional / expressive]
- **Mood:** [1-2 sentence description of how the product should feel]
- **Reference sites:** [URLs, if research was done]
## Typography
- **Display/Hero:** [font name] — [rationale]
- **Body:** [font name] — [rationale]
- **UI/Labels:** [font name or "same as body"]
- **Data/Tables:** [font name] — [rationale, must support tabular-nums]
- **Code:** [font name]
- **Loading:** [CDN URL or self-hosted strategy]
- **Scale:** [modular scale with specific px/rem values for each level]
## Color
- **Approach:** [restrained / balanced / expressive]
- **Primary:** [hex] — [what it represents, usage]
- **Secondary:** [hex] — [usage]
- **Neutrals:** [warm/cool grays, hex range from lightest to darkest]
- **Semantic:** success [hex], warning [hex], error [hex], info [hex]
- **Dark mode:** [strategy — redesign surfaces, reduce saturation 10-20%]
## Spacing
- **Base unit:** [4px or 8px]
- **Density:** [compact / comfortable / spacious]
- **Scale:** 2xs(2) xs(4) sm(8) md(16) lg(24) xl(32) 2xl(48) 3xl(64)
## Layout
- **Approach:** [grid-disciplined / creative-editorial / hybrid]
- **Grid:** [columns per breakpoint]
- **Max content width:** [value]
- **Border radius:** [hierarchical scale — e.g., sm:4px, md:8px, lg:12px, full:9999px]
## Motion
- **Approach:** [minimal-functional / intentional / expressive]
- **Easing:** enter(ease-out) exit(ease-in) move(ease-in-out)
- **Duration:** micro(50-100ms) short(150-250ms) medium(250-400ms) long(400-700ms)
## Decisions Log
| Date | Decision | Rationale |
|------|----------|-----------|
| [today] | Initial design system created | Created by /design-consultation based on [product context / research] |
```
**Update CLAUDE.md** (or create it if it doesn't exist) — append this section:
```markdown
## Design System
Always read DESIGN.md before making any visual or UI decisions.
All font choices, colors, spacing, and aesthetic direction are defined there.
Do not deviate without explicit user approval.
In QA mode, flag any code that doesn't match DESIGN.md.
```
**AskUserQuestion Q-final — show summary and confirm:**
List all decisions. Flag any that used agent defaults without explicit user confirmation (the user should know what they're shipping). Options:
- A) Ship it — write DESIGN.md and CLAUDE.md
- B) I want to change something (specify what)
- C) Start over
After shipping DESIGN.md, if the session produced screen-level mockups or page layouts
(not just system-level tokens), suggest:
"Want to see this design system as working Pretext-native HTML? Run /design-html."
---
> **STOP.** Before building the complete design-system proposal, drill-downs, the design preview, and writing DESIGN.md (Phases 3-6, after product context and research), Read `~/.claude/skills/gstack/design-consultation/sections/proposal-and-preview.md` and execute it
> in full. Do not work from memory — that section is the source of truth for this step.
## Capture Learnings
If you discovered a non-obvious pattern, pitfall, or architectural insight during

View File

@ -110,6 +110,10 @@ If `DESIGN_NOT_AVAILABLE`: Phase 5 falls back to the HTML preview page (still go
{{LEARNINGS_SEARCH}}
{{SECTION_INDEX:design-consultation}}
---
## Phase 1: Product Context
Ask the user a single question that covers everything you need to know. Pre-fill what you can infer from the codebase.
@ -193,300 +197,7 @@ If the user said no research, skip entirely and proceed to Phase 3 using your bu
{{DESIGN_OUTSIDE_VOICES}}
## Phase 3: The Complete Proposal
This is the soul of the skill. Propose EVERYTHING as one coherent package.
**AskUserQuestion Q2 — present the full proposal with SAFE/RISK breakdown:**
```
Based on [product context] and [research findings / my design knowledge]:
AESTHETIC: [direction] — [one-line rationale]
DECORATION: [level] — [why this pairs with the aesthetic]
LAYOUT: [approach] — [why this fits the product type]
COLOR: [approach] + proposed palette (hex values) — [rationale]
TYPOGRAPHY: [3 font recommendations with roles] — [why these fonts]
SPACING: [base unit + density] — [rationale]
MOTION: [approach] — [rationale]
This system is coherent because [explain how choices reinforce each other].
SAFE CHOICES (category baseline — your users expect these):
- [2-3 decisions that match category conventions, with rationale for playing safe]
RISKS (where your product gets its own face):
- [2-3 deliberate departures from convention]
- For each risk: what it is, why it works, what you gain, what it costs
The safe choices keep you literate in your category. The risks are where
your product becomes memorable. Which risks appeal to you? Want to see
different ones? Or adjust anything else?
```
The SAFE/RISK breakdown is critical. Design coherence is table stakes — every product in a category can be coherent and still look identical. The real question is: where do you take creative risks? The agent should always propose at least 2 risks, each with a clear rationale for why the risk is worth taking and what the user gives up. Risks might include: an unexpected typeface for the category, a bold accent color nobody else uses, tighter or looser spacing than the norm, a layout approach that breaks from convention, motion choices that add personality.
**Options:** A) Looks great — generate the preview page. B) I want to adjust [section]. C) I want different risks — show me wilder options. D) Start over with a different direction. E) Skip the preview, just write DESIGN.md.
### Your Design Knowledge (use to inform proposals — do NOT display as tables)
**Aesthetic directions** (pick the one that fits the product):
- Brutally Minimal — Type and whitespace only. No decoration. Modernist.
- Maximalist Chaos — Dense, layered, pattern-heavy. Y2K meets contemporary.
- Retro-Futuristic — Vintage tech nostalgia. CRT glow, pixel grids, warm monospace.
- Luxury/Refined — Serifs, high contrast, generous whitespace, precious metals.
- Playful/Toy-like — Rounded, bouncy, bold primaries. Approachable and fun.
- Editorial/Magazine — Strong typographic hierarchy, asymmetric grids, pull quotes.
- Brutalist/Raw — Exposed structure, system fonts, visible grid, no polish.
- Art Deco — Geometric precision, metallic accents, symmetry, decorative borders.
- Organic/Natural — Earth tones, rounded forms, hand-drawn texture, grain.
- Industrial/Utilitarian — Function-first, data-dense, monospace accents, muted palette.
**Decoration levels:** minimal (typography does all the work) / intentional (subtle texture, grain, or background treatment) / expressive (full creative direction, layered depth, patterns)
**Layout approaches:** grid-disciplined (strict columns, predictable alignment) / creative-editorial (asymmetry, overlap, grid-breaking) / hybrid (grid for app, creative for marketing)
**Color approaches:** restrained (1 accent + neutrals, color is rare and meaningful) / balanced (primary + secondary, semantic colors for hierarchy) / expressive (color as a primary design tool, bold palettes)
**Motion approaches:** minimal-functional (only transitions that aid comprehension) / intentional (subtle entrance animations, meaningful state transitions) / expressive (full choreography, scroll-driven, playful)
**Font recommendations by purpose:**
- Display/Hero: Satoshi, General Sans, Instrument Serif, Fraunces, Clash Grotesk, Cabinet Grotesk
- Body: Instrument Sans, DM Sans, Source Sans 3, Geist, Plus Jakarta Sans, Outfit
- Data/Tables: Geist (tabular-nums), DM Sans (tabular-nums), JetBrains Mono, IBM Plex Mono
- Code: JetBrains Mono, Fira Code, Berkeley Mono, Geist Mono
**Font blacklist** (never recommend):
Papyrus, Comic Sans, Lobster, Impact, Jokerman, Bleeding Cowboys, Permanent Marker, Bradley Hand, Brush Script, Hobo, Trajan, Raleway, Clash Display, Courier New (for body)
**Overused fonts** (never recommend as primary — use only if user specifically requests):
Inter, Roboto, Arial, Helvetica, Open Sans, Lato, Montserrat, Poppins, Space Grotesk.
Space Grotesk is on the list specifically because every AI design tool converges on it
as "the safe alternative to Inter." That's the convergence trap. Treat it the same as
Inter: only use if the user asks for it by name.
**Anti-convergence directive:** Across multiple generations in the same project, VARY
light/dark, fonts, and aesthetic directions. Never propose the same choices twice
without explicit justification. If the user's prior session used Geist + dark + editorial,
propose something different this time (or explicitly acknowledge you're doubling down
because it fits the brief). Convergence across generations is slop.
**AI slop anti-patterns** (never include in your recommendations):
- Purple/violet gradients as default accent
- 3-column feature grid with icons in colored circles
- Centered everything with uniform spacing
- Uniform bubbly border-radius on all elements
- Gradient buttons as the primary CTA pattern
- Generic stock-photo-style hero sections
- system-ui / -apple-system as the primary display or body font (the "I gave up on typography" signal)
- "Built for X" / "Designed for Y" marketing copy patterns
### Coherence Validation
When the user overrides one section, check if the rest still coheres. Flag mismatches with a gentle nudge — never block:
- Brutalist/Minimal aesthetic + expressive motion → "Heads up: brutalist aesthetics usually pair with minimal motion. Your combo is unusual — which is fine if intentional. Want me to suggest motion that fits, or keep it?"
- Expressive color + restrained decoration → "Bold palette with minimal decoration can work, but the colors will carry a lot of weight. Want me to suggest decoration that supports the palette?"
- Creative-editorial layout + data-heavy product → "Editorial layouts are gorgeous but can fight data density. Want me to show how a hybrid approach keeps both?"
- Always accept the user's final choice. Never refuse to proceed.
---
## Phase 4: Drill-downs (only if user requests adjustments)
When the user wants to change a specific section, go deep on that section:
- **Fonts:** Present 3-5 specific candidates with rationale, explain what each evokes, offer the preview page
- **Colors:** Present 2-3 palette options with hex values, explain the color theory reasoning
- **Aesthetic:** Walk through which directions fit their product and why
- **Layout/Spacing/Motion:** Present the approaches with concrete tradeoffs for their product type
Each drill-down is one focused AskUserQuestion. After the user decides, re-check coherence with the rest of the system.
---
## Phase 5: Design System Preview (default ON)
This phase generates visual previews of the proposed design system. Two paths depending on whether the gstack designer is available.
### Path A: AI Mockups (if DESIGN_READY)
Generate AI-rendered mockups showing the proposed design system applied to realistic screens for this product. This is far more powerful than an HTML preview — the user sees what their product could actually look like.
```bash
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)"
_DESIGN_DIR="$HOME/.gstack/projects/$SLUG/designs/design-system-$(date +%Y%m%d)"
mkdir -p "$_DESIGN_DIR"
echo "DESIGN_DIR: $_DESIGN_DIR"
```
Construct a design brief from the Phase 3 proposal (aesthetic, colors, typography, spacing, layout) and the product context from Phase 1:
```bash
$D variants --brief "<product name: [name]. Product type: [type]. Aesthetic: [direction]. Colors: primary [hex], secondary [hex], neutrals [range]. Typography: display [font], body [font]. Layout: [approach]. Show a realistic [page type] screen with [specific content for this product].>" --count 3 --output-dir "$_DESIGN_DIR/"
```
Run quality check on each variant:
```bash
$D check --image "$_DESIGN_DIR/variant-A.png" --brief "<the original brief>"
```
Show each variant inline (Read tool on each PNG) for instant preview.
**Before presenting to the user, self-gate:** For each variant, ask yourself: *"Would
a human designer be embarrassed to put their name on this?"* If yes, discard the
variant and regenerate. This is a hard gate. A mediocre AI mockup is worse than no
mockup. Embarrassment triggers include: purple gradient hero, 3-column SaaS grid,
centered-everything, Inter body text, generic stock-photo vibe, system-ui font,
gradient CTA button, bubble-radius everything. Any of those = reject and regenerate.
Tell the user: "I've generated 3 visual directions applying your design system to a realistic [product type] screen. Pick your favorite in the comparison board that just opened in your browser. You can also remix elements across variants."
{{DESIGN_SHOTGUN_LOOP}}
After the user picks a direction:
- Use `$D extract --image "$_DESIGN_DIR/variant-<CHOSEN>.png"` to analyze the approved mockup and extract design tokens (colors, typography, spacing) that will populate DESIGN.md in Phase 6. This grounds the design system in what was actually approved visually, not just what was described in text.
- If the user wants to iterate further: `$D iterate --feedback "<user's feedback>" --output "$_DESIGN_DIR/refined.png"`
**Plan mode vs. implementation mode:**
- **If in plan mode:** Add the approved mockup path (the full `$_DESIGN_DIR` path) and extracted tokens to the plan file under an "## Approved Design Direction" section. The design system gets written to DESIGN.md when the plan is implemented.
- **If NOT in plan mode:** Proceed directly to Phase 6 and write DESIGN.md with the extracted tokens.
### Path B: HTML Preview Page (fallback if DESIGN_NOT_AVAILABLE)
Generate a polished HTML preview page and open it in the user's browser. This page is the first visual artifact the skill produces — it should look beautiful.
```bash
PREVIEW_FILE="/tmp/design-consultation-preview-$(date +%s).html"
```
Write the preview HTML to `$PREVIEW_FILE`, then open it:
```bash
open "$PREVIEW_FILE"
```
### Preview Page Requirements (Path B only)
The agent writes a **single, self-contained HTML file** (no framework dependencies) that:
1. **Loads proposed fonts** from Google Fonts (or Bunny Fonts) via `<link>` tags
2. **Uses the proposed color palette** throughout — dogfood the design system
3. **Shows the product name** (not "Lorem Ipsum") as the hero heading
4. **Font specimen section:**
- Each font candidate shown in its proposed role (hero heading, body paragraph, button label, data table row)
- Side-by-side comparison if multiple candidates for one role
- Real content that matches the product (e.g., civic tech → government data examples)
5. **Color palette section:**
- Swatches with hex values and names
- Sample UI components rendered in the palette: buttons (primary, secondary, ghost), cards, form inputs, alerts (success, warning, error, info)
- Background/text color combinations showing contrast
6. **Realistic product mockups** — this is what makes the preview page powerful. Based on the project type from Phase 1, render 2-3 realistic page layouts using the full design system:
- **Dashboard / web app:** sample data table with metrics, sidebar nav, header with user avatar, stat cards
- **Marketing site:** hero section with real copy, feature highlights, testimonial block, CTA
- **Settings / admin:** form with labeled inputs, toggle switches, dropdowns, save button
- **Auth / onboarding:** login form with social buttons, branding, input validation states
- Use the product name, realistic content for the domain, and the proposed spacing/layout/border-radius. The user should see their product (roughly) before writing any code.
7. **Light/dark mode toggle** using CSS custom properties and a JS toggle button
8. **Clean, professional layout** — the preview page IS a taste signal for the skill
9. **Responsive** — looks good on any screen width
The page should make the user think "oh nice, they thought of this." It's selling the design system by showing what the product could feel like, not just listing hex codes and font names.
If `open` fails (headless environment), tell the user: *"I wrote the preview to [path] — open it in your browser to see the fonts and colors rendered."*
If the user says skip the preview, go directly to Phase 6.
---
## Phase 6: Write DESIGN.md & Confirm
If `$D extract` was used in Phase 5 (Path A), use the extracted tokens as the primary source for DESIGN.md values — colors, typography, and spacing grounded in the approved mockup rather than text descriptions alone. Merge extracted tokens with the Phase 3 proposal (the proposal provides rationale and context; the extraction provides exact values).
**If in plan mode:** Write the DESIGN.md content into the plan file as a "## Proposed DESIGN.md" section. Do NOT write the actual file — that happens at implementation time.
**If NOT in plan mode:** Write `DESIGN.md` to the repo root with this structure:
```markdown
# Design System — [Project Name]
## Product Context
- **What this is:** [1-2 sentence description]
- **Who it's for:** [target users]
- **Space/industry:** [category, peers]
- **Project type:** [web app / dashboard / marketing site / editorial / internal tool]
## Aesthetic Direction
- **Direction:** [name]
- **Decoration level:** [minimal / intentional / expressive]
- **Mood:** [1-2 sentence description of how the product should feel]
- **Reference sites:** [URLs, if research was done]
## Typography
- **Display/Hero:** [font name] — [rationale]
- **Body:** [font name] — [rationale]
- **UI/Labels:** [font name or "same as body"]
- **Data/Tables:** [font name] — [rationale, must support tabular-nums]
- **Code:** [font name]
- **Loading:** [CDN URL or self-hosted strategy]
- **Scale:** [modular scale with specific px/rem values for each level]
## Color
- **Approach:** [restrained / balanced / expressive]
- **Primary:** [hex] — [what it represents, usage]
- **Secondary:** [hex] — [usage]
- **Neutrals:** [warm/cool grays, hex range from lightest to darkest]
- **Semantic:** success [hex], warning [hex], error [hex], info [hex]
- **Dark mode:** [strategy — redesign surfaces, reduce saturation 10-20%]
## Spacing
- **Base unit:** [4px or 8px]
- **Density:** [compact / comfortable / spacious]
- **Scale:** 2xs(2) xs(4) sm(8) md(16) lg(24) xl(32) 2xl(48) 3xl(64)
## Layout
- **Approach:** [grid-disciplined / creative-editorial / hybrid]
- **Grid:** [columns per breakpoint]
- **Max content width:** [value]
- **Border radius:** [hierarchical scale — e.g., sm:4px, md:8px, lg:12px, full:9999px]
## Motion
- **Approach:** [minimal-functional / intentional / expressive]
- **Easing:** enter(ease-out) exit(ease-in) move(ease-in-out)
- **Duration:** micro(50-100ms) short(150-250ms) medium(250-400ms) long(400-700ms)
## Decisions Log
| Date | Decision | Rationale |
|------|----------|-----------|
| [today] | Initial design system created | Created by /design-consultation based on [product context / research] |
```
**Update CLAUDE.md** (or create it if it doesn't exist) — append this section:
```markdown
## Design System
Always read DESIGN.md before making any visual or UI decisions.
All font choices, colors, spacing, and aesthetic direction are defined there.
Do not deviate without explicit user approval.
In QA mode, flag any code that doesn't match DESIGN.md.
```
**AskUserQuestion Q-final — show summary and confirm:**
List all decisions. Flag any that used agent defaults without explicit user confirmation (the user should know what they're shipping). Options:
- A) Ship it — write DESIGN.md and CLAUDE.md
- B) I want to change something (specify what)
- C) Start over
After shipping DESIGN.md, if the session produced screen-level mockups or page layouts
(not just system-level tokens), suggest:
"Want to see this design system as working Pretext-native HTML? Run /design-html."
---
{{SECTION:proposal-and-preview}}
{{LEARNINGS_LOG}}
{{GBRAIN_SAVE_RESULTS}}

View File

@ -0,0 +1,14 @@
{
"$schema": "https://gstack.dev/schemas/section-manifest.json",
"skill": "design-consultation",
"version": 1,
"note": "PASSIVE registry (v2 plan T9 / CM2). id/file/title/trigger text ONLY. The skeleton's decision-tree prose decides WHEN to read.",
"sections": [
{
"id": "proposal-and-preview",
"file": "proposal-and-preview.md",
"title": "The complete design-system proposal, drill-downs, design preview (AI mockups / HTML), and writing DESIGN.md (Phases 3-6)",
"trigger": "building the complete design-system proposal, drill-downs, the design preview, and writing DESIGN.md (Phases 3-6, after product context and research)"
}
]
}

View File

@ -0,0 +1,408 @@
<!-- AUTO-GENERATED from proposal-and-preview.md.tmpl — do not edit directly -->
<!-- Regenerate: bun run gen:skill-docs -->
## Phase 3: The Complete Proposal
This is the soul of the skill. Propose EVERYTHING as one coherent package.
**AskUserQuestion Q2 — present the full proposal with SAFE/RISK breakdown:**
```
Based on [product context] and [research findings / my design knowledge]:
AESTHETIC: [direction] — [one-line rationale]
DECORATION: [level] — [why this pairs with the aesthetic]
LAYOUT: [approach] — [why this fits the product type]
COLOR: [approach] + proposed palette (hex values) — [rationale]
TYPOGRAPHY: [3 font recommendations with roles] — [why these fonts]
SPACING: [base unit + density] — [rationale]
MOTION: [approach] — [rationale]
This system is coherent because [explain how choices reinforce each other].
SAFE CHOICES (category baseline — your users expect these):
- [2-3 decisions that match category conventions, with rationale for playing safe]
RISKS (where your product gets its own face):
- [2-3 deliberate departures from convention]
- For each risk: what it is, why it works, what you gain, what it costs
The safe choices keep you literate in your category. The risks are where
your product becomes memorable. Which risks appeal to you? Want to see
different ones? Or adjust anything else?
```
The SAFE/RISK breakdown is critical. Design coherence is table stakes — every product in a category can be coherent and still look identical. The real question is: where do you take creative risks? The agent should always propose at least 2 risks, each with a clear rationale for why the risk is worth taking and what the user gives up. Risks might include: an unexpected typeface for the category, a bold accent color nobody else uses, tighter or looser spacing than the norm, a layout approach that breaks from convention, motion choices that add personality.
**Options:** A) Looks great — generate the preview page. B) I want to adjust [section]. C) I want different risks — show me wilder options. D) Start over with a different direction. E) Skip the preview, just write DESIGN.md.
### Your Design Knowledge (use to inform proposals — do NOT display as tables)
**Aesthetic directions** (pick the one that fits the product):
- Brutally Minimal — Type and whitespace only. No decoration. Modernist.
- Maximalist Chaos — Dense, layered, pattern-heavy. Y2K meets contemporary.
- Retro-Futuristic — Vintage tech nostalgia. CRT glow, pixel grids, warm monospace.
- Luxury/Refined — Serifs, high contrast, generous whitespace, precious metals.
- Playful/Toy-like — Rounded, bouncy, bold primaries. Approachable and fun.
- Editorial/Magazine — Strong typographic hierarchy, asymmetric grids, pull quotes.
- Brutalist/Raw — Exposed structure, system fonts, visible grid, no polish.
- Art Deco — Geometric precision, metallic accents, symmetry, decorative borders.
- Organic/Natural — Earth tones, rounded forms, hand-drawn texture, grain.
- Industrial/Utilitarian — Function-first, data-dense, monospace accents, muted palette.
**Decoration levels:** minimal (typography does all the work) / intentional (subtle texture, grain, or background treatment) / expressive (full creative direction, layered depth, patterns)
**Layout approaches:** grid-disciplined (strict columns, predictable alignment) / creative-editorial (asymmetry, overlap, grid-breaking) / hybrid (grid for app, creative for marketing)
**Color approaches:** restrained (1 accent + neutrals, color is rare and meaningful) / balanced (primary + secondary, semantic colors for hierarchy) / expressive (color as a primary design tool, bold palettes)
**Motion approaches:** minimal-functional (only transitions that aid comprehension) / intentional (subtle entrance animations, meaningful state transitions) / expressive (full choreography, scroll-driven, playful)
**Font recommendations by purpose:**
- Display/Hero: Satoshi, General Sans, Instrument Serif, Fraunces, Clash Grotesk, Cabinet Grotesk
- Body: Instrument Sans, DM Sans, Source Sans 3, Geist, Plus Jakarta Sans, Outfit
- Data/Tables: Geist (tabular-nums), DM Sans (tabular-nums), JetBrains Mono, IBM Plex Mono
- Code: JetBrains Mono, Fira Code, Berkeley Mono, Geist Mono
**Font blacklist** (never recommend):
Papyrus, Comic Sans, Lobster, Impact, Jokerman, Bleeding Cowboys, Permanent Marker, Bradley Hand, Brush Script, Hobo, Trajan, Raleway, Clash Display, Courier New (for body)
**Overused fonts** (never recommend as primary — use only if user specifically requests):
Inter, Roboto, Arial, Helvetica, Open Sans, Lato, Montserrat, Poppins, Space Grotesk.
Space Grotesk is on the list specifically because every AI design tool converges on it
as "the safe alternative to Inter." That's the convergence trap. Treat it the same as
Inter: only use if the user asks for it by name.
**Anti-convergence directive:** Across multiple generations in the same project, VARY
light/dark, fonts, and aesthetic directions. Never propose the same choices twice
without explicit justification. If the user's prior session used Geist + dark + editorial,
propose something different this time (or explicitly acknowledge you're doubling down
because it fits the brief). Convergence across generations is slop.
**AI slop anti-patterns** (never include in your recommendations):
- Purple/violet gradients as default accent
- 3-column feature grid with icons in colored circles
- Centered everything with uniform spacing
- Uniform bubbly border-radius on all elements
- Gradient buttons as the primary CTA pattern
- Generic stock-photo-style hero sections
- system-ui / -apple-system as the primary display or body font (the "I gave up on typography" signal)
- "Built for X" / "Designed for Y" marketing copy patterns
### Coherence Validation
When the user overrides one section, check if the rest still coheres. Flag mismatches with a gentle nudge — never block:
- Brutalist/Minimal aesthetic + expressive motion → "Heads up: brutalist aesthetics usually pair with minimal motion. Your combo is unusual — which is fine if intentional. Want me to suggest motion that fits, or keep it?"
- Expressive color + restrained decoration → "Bold palette with minimal decoration can work, but the colors will carry a lot of weight. Want me to suggest decoration that supports the palette?"
- Creative-editorial layout + data-heavy product → "Editorial layouts are gorgeous but can fight data density. Want me to show how a hybrid approach keeps both?"
- Always accept the user's final choice. Never refuse to proceed.
---
## Phase 4: Drill-downs (only if user requests adjustments)
When the user wants to change a specific section, go deep on that section:
- **Fonts:** Present 3-5 specific candidates with rationale, explain what each evokes, offer the preview page
- **Colors:** Present 2-3 palette options with hex values, explain the color theory reasoning
- **Aesthetic:** Walk through which directions fit their product and why
- **Layout/Spacing/Motion:** Present the approaches with concrete tradeoffs for their product type
Each drill-down is one focused AskUserQuestion. After the user decides, re-check coherence with the rest of the system.
---
## Phase 5: Design System Preview (default ON)
This phase generates visual previews of the proposed design system. Two paths depending on whether the gstack designer is available.
### Path A: AI Mockups (if DESIGN_READY)
Generate AI-rendered mockups showing the proposed design system applied to realistic screens for this product. This is far more powerful than an HTML preview — the user sees what their product could actually look like.
```bash
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)"
_DESIGN_DIR="$HOME/.gstack/projects/$SLUG/designs/design-system-$(date +%Y%m%d)"
mkdir -p "$_DESIGN_DIR"
echo "DESIGN_DIR: $_DESIGN_DIR"
```
Construct a design brief from the Phase 3 proposal (aesthetic, colors, typography, spacing, layout) and the product context from Phase 1:
```bash
$D variants --brief "<product name: [name]. Product type: [type]. Aesthetic: [direction]. Colors: primary [hex], secondary [hex], neutrals [range]. Typography: display [font], body [font]. Layout: [approach]. Show a realistic [page type] screen with [specific content for this product].>" --count 3 --output-dir "$_DESIGN_DIR/"
```
Run quality check on each variant:
```bash
$D check --image "$_DESIGN_DIR/variant-A.png" --brief "<the original brief>"
```
Show each variant inline (Read tool on each PNG) for instant preview.
**Before presenting to the user, self-gate:** For each variant, ask yourself: *"Would
a human designer be embarrassed to put their name on this?"* If yes, discard the
variant and regenerate. This is a hard gate. A mediocre AI mockup is worse than no
mockup. Embarrassment triggers include: purple gradient hero, 3-column SaaS grid,
centered-everything, Inter body text, generic stock-photo vibe, system-ui font,
gradient CTA button, bubble-radius everything. Any of those = reject and regenerate.
Tell the user: "I've generated 3 visual directions applying your design system to a realistic [product type] screen. Pick your favorite in the comparison board that just opened in your browser. You can also remix elements across variants."
### Comparison Board + Feedback Loop
Create the comparison board and serve it over HTTP:
```bash
$D compare --images "$_DESIGN_DIR/variant-A.png,$_DESIGN_DIR/variant-B.png,$_DESIGN_DIR/variant-C.png" --output "$_DESIGN_DIR/design-board.html" --serve
```
This command generates the board HTML, starts an HTTP server on a random port,
and opens it in the user's default browser. **Run it in the background** with `&`
because the server needs to stay running while the user interacts with the board.
Parse the board URL from stderr output. Default daemon path:
`BOARD_URL: http://127.0.0.1:N/boards/<id>/` (already includes the per-board
path; use this for the AskUserQuestion URL AND as the base for the reload
endpoint). Legacy `--no-daemon` path emits `SERVE_STARTED: port=XXXXX` and
serves a single board at `/`, with reload at `/api/reload` — only relevant
when an external caller explicitly passes `--no-daemon`.
**PRIMARY WAIT: AskUserQuestion with board URL**
After the board is serving, use AskUserQuestion to wait for the user. Include the
board URL so they can click it if they lost the browser tab:
"I've opened a comparison board with the design variants:
<BOARD_URL> — Rate them, leave comments, remix
elements you like, and click Submit when you're done. Let me know when you've
submitted your feedback (or paste your preferences here). If you clicked
Regenerate or Remix on the board, tell me and I'll generate new variants."
Substitute `<BOARD_URL>` with the URL parsed from stderr (the daemon path
emits `BOARD_URL: http://127.0.0.1:N/boards/<id>/`).
**Do NOT use AskUserQuestion to ask which variant the user prefers.** The comparison
board IS the chooser. AskUserQuestion is just the blocking wait mechanism.
**After the user responds to AskUserQuestion:**
Check for feedback files next to the board HTML:
- `$_DESIGN_DIR/feedback.json` — written when user clicks Submit (final choice)
- `$_DESIGN_DIR/feedback-pending.json` — written when user clicks Regenerate/Remix/More Like This
```bash
if [ -f "$_DESIGN_DIR/feedback.json" ]; then
echo "SUBMIT_RECEIVED"
cat "$_DESIGN_DIR/feedback.json"
elif [ -f "$_DESIGN_DIR/feedback-pending.json" ]; then
echo "REGENERATE_RECEIVED"
cat "$_DESIGN_DIR/feedback-pending.json"
rm "$_DESIGN_DIR/feedback-pending.json"
else
echo "NO_FEEDBACK_FILE"
fi
```
The feedback JSON has this shape:
```json
{
"preferred": "A",
"ratings": { "A": 4, "B": 3, "C": 2 },
"comments": { "A": "Love the spacing" },
"overall": "Go with A, bigger CTA",
"regenerated": false
}
```
**If `feedback.json` found:** The user clicked Submit on the board.
Read `preferred`, `ratings`, `comments`, `overall` from the JSON. Proceed with
the approved variant.
**If `feedback-pending.json` found:** The user clicked Regenerate/Remix on the board.
1. Read `regenerateAction` from the JSON (`"different"`, `"match"`, `"more_like_B"`,
`"remix"`, or custom text)
2. If `regenerateAction` is `"remix"`, read `remixSpec` (e.g. `{"layout":"A","colors":"B"}`)
3. Generate new variants with `$D iterate` or `$D variants` using updated brief
4. Create new board: `$D compare --images "..." --output "$_DESIGN_DIR/design-board.html"`
5. Reload the board in the user's browser (same tab) — the URL is per-board
under daemon mode, so use `<BOARD_URL>` (from the `BOARD_URL:` stderr
line) as the base:
`curl -s -X POST "${BOARD_URL}api/reload" -H 'Content-Type: application/json' -d '{"html":"$_DESIGN_DIR/design-board.html"}'`
Under `--no-daemon` the reload endpoint is `/api/reload` at the legacy
port; this path only matters if the caller explicitly opted out of the
daemon.
6. The board auto-refreshes. **AskUserQuestion again** with the same board URL to
wait for the next round of feedback. Repeat until `feedback.json` appears.
**If `NO_FEEDBACK_FILE`:** The user typed their preferences directly in the
AskUserQuestion response instead of using the board. Use their text response
as the feedback.
**POLLING FALLBACK:** Only use polling if `$D serve` fails (no port available).
In that case, show each variant inline using the Read tool (so the user can see them),
then use AskUserQuestion:
"The comparison board server failed to start. I've shown the variants above.
Which do you prefer? Any feedback?"
**After receiving feedback (any path):** Output a clear summary confirming
what was understood:
"Here's what I understood from your feedback:
PREFERRED: Variant [X]
RATINGS: [list]
YOUR NOTES: [comments]
DIRECTION: [overall]
Is this right?"
Use AskUserQuestion to verify before proceeding.
**Save the approved choice:**
```bash
echo '{"approved_variant":"<V>","feedback":"<FB>","date":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","screen":"<SCREEN>","branch":"'$(git branch --show-current 2>/dev/null)'"}' > "$_DESIGN_DIR/approved.json"
```
After the user picks a direction:
- Use `$D extract --image "$_DESIGN_DIR/variant-<CHOSEN>.png"` to analyze the approved mockup and extract design tokens (colors, typography, spacing) that will populate DESIGN.md in Phase 6. This grounds the design system in what was actually approved visually, not just what was described in text.
- If the user wants to iterate further: `$D iterate --feedback "<user's feedback>" --output "$_DESIGN_DIR/refined.png"`
**Plan mode vs. implementation mode:**
- **If in plan mode:** Add the approved mockup path (the full `$_DESIGN_DIR` path) and extracted tokens to the plan file under an "## Approved Design Direction" section. The design system gets written to DESIGN.md when the plan is implemented.
- **If NOT in plan mode:** Proceed directly to Phase 6 and write DESIGN.md with the extracted tokens.
### Path B: HTML Preview Page (fallback if DESIGN_NOT_AVAILABLE)
Generate a polished HTML preview page and open it in the user's browser. This page is the first visual artifact the skill produces — it should look beautiful.
```bash
PREVIEW_FILE="/tmp/design-consultation-preview-$(date +%s).html"
```
Write the preview HTML to `$PREVIEW_FILE`, then open it:
```bash
open "$PREVIEW_FILE"
```
### Preview Page Requirements (Path B only)
The agent writes a **single, self-contained HTML file** (no framework dependencies) that:
1. **Loads proposed fonts** from Google Fonts (or Bunny Fonts) via `<link>` tags
2. **Uses the proposed color palette** throughout — dogfood the design system
3. **Shows the product name** (not "Lorem Ipsum") as the hero heading
4. **Font specimen section:**
- Each font candidate shown in its proposed role (hero heading, body paragraph, button label, data table row)
- Side-by-side comparison if multiple candidates for one role
- Real content that matches the product (e.g., civic tech → government data examples)
5. **Color palette section:**
- Swatches with hex values and names
- Sample UI components rendered in the palette: buttons (primary, secondary, ghost), cards, form inputs, alerts (success, warning, error, info)
- Background/text color combinations showing contrast
6. **Realistic product mockups** — this is what makes the preview page powerful. Based on the project type from Phase 1, render 2-3 realistic page layouts using the full design system:
- **Dashboard / web app:** sample data table with metrics, sidebar nav, header with user avatar, stat cards
- **Marketing site:** hero section with real copy, feature highlights, testimonial block, CTA
- **Settings / admin:** form with labeled inputs, toggle switches, dropdowns, save button
- **Auth / onboarding:** login form with social buttons, branding, input validation states
- Use the product name, realistic content for the domain, and the proposed spacing/layout/border-radius. The user should see their product (roughly) before writing any code.
7. **Light/dark mode toggle** using CSS custom properties and a JS toggle button
8. **Clean, professional layout** — the preview page IS a taste signal for the skill
9. **Responsive** — looks good on any screen width
The page should make the user think "oh nice, they thought of this." It's selling the design system by showing what the product could feel like, not just listing hex codes and font names.
If `open` fails (headless environment), tell the user: *"I wrote the preview to [path] — open it in your browser to see the fonts and colors rendered."*
If the user says skip the preview, go directly to Phase 6.
---
## Phase 6: Write DESIGN.md & Confirm
If `$D extract` was used in Phase 5 (Path A), use the extracted tokens as the primary source for DESIGN.md values — colors, typography, and spacing grounded in the approved mockup rather than text descriptions alone. Merge extracted tokens with the Phase 3 proposal (the proposal provides rationale and context; the extraction provides exact values).
**If in plan mode:** Write the DESIGN.md content into the plan file as a "## Proposed DESIGN.md" section. Do NOT write the actual file — that happens at implementation time.
**If NOT in plan mode:** Write `DESIGN.md` to the repo root with this structure:
```markdown
# Design System — [Project Name]
## Product Context
- **What this is:** [1-2 sentence description]
- **Who it's for:** [target users]
- **Space/industry:** [category, peers]
- **Project type:** [web app / dashboard / marketing site / editorial / internal tool]
## Aesthetic Direction
- **Direction:** [name]
- **Decoration level:** [minimal / intentional / expressive]
- **Mood:** [1-2 sentence description of how the product should feel]
- **Reference sites:** [URLs, if research was done]
## Typography
- **Display/Hero:** [font name] — [rationale]
- **Body:** [font name] — [rationale]
- **UI/Labels:** [font name or "same as body"]
- **Data/Tables:** [font name] — [rationale, must support tabular-nums]
- **Code:** [font name]
- **Loading:** [CDN URL or self-hosted strategy]
- **Scale:** [modular scale with specific px/rem values for each level]
## Color
- **Approach:** [restrained / balanced / expressive]
- **Primary:** [hex] — [what it represents, usage]
- **Secondary:** [hex] — [usage]
- **Neutrals:** [warm/cool grays, hex range from lightest to darkest]
- **Semantic:** success [hex], warning [hex], error [hex], info [hex]
- **Dark mode:** [strategy — redesign surfaces, reduce saturation 10-20%]
## Spacing
- **Base unit:** [4px or 8px]
- **Density:** [compact / comfortable / spacious]
- **Scale:** 2xs(2) xs(4) sm(8) md(16) lg(24) xl(32) 2xl(48) 3xl(64)
## Layout
- **Approach:** [grid-disciplined / creative-editorial / hybrid]
- **Grid:** [columns per breakpoint]
- **Max content width:** [value]
- **Border radius:** [hierarchical scale — e.g., sm:4px, md:8px, lg:12px, full:9999px]
## Motion
- **Approach:** [minimal-functional / intentional / expressive]
- **Easing:** enter(ease-out) exit(ease-in) move(ease-in-out)
- **Duration:** micro(50-100ms) short(150-250ms) medium(250-400ms) long(400-700ms)
## Decisions Log
| Date | Decision | Rationale |
|------|----------|-----------|
| [today] | Initial design system created | Created by /design-consultation based on [product context / research] |
```
**Update CLAUDE.md** (or create it if it doesn't exist) — append this section:
```markdown
## Design System
Always read DESIGN.md before making any visual or UI decisions.
All font choices, colors, spacing, and aesthetic direction are defined there.
Do not deviate without explicit user approval.
In QA mode, flag any code that doesn't match DESIGN.md.
```
**AskUserQuestion Q-final — show summary and confirm:**
List all decisions. Flag any that used agent defaults without explicit user confirmation (the user should know what they're shipping). Options:
- A) Ship it — write DESIGN.md and CLAUDE.md
- B) I want to change something (specify what)
- C) Start over
After shipping DESIGN.md, if the session produced screen-level mockups or page layouts
(not just system-level tokens), suggest:
"Want to see this design system as working Pretext-native HTML? Run /design-html."
---

View File

@ -0,0 +1,294 @@
## Phase 3: The Complete Proposal
This is the soul of the skill. Propose EVERYTHING as one coherent package.
**AskUserQuestion Q2 — present the full proposal with SAFE/RISK breakdown:**
```
Based on [product context] and [research findings / my design knowledge]:
AESTHETIC: [direction] — [one-line rationale]
DECORATION: [level] — [why this pairs with the aesthetic]
LAYOUT: [approach] — [why this fits the product type]
COLOR: [approach] + proposed palette (hex values) — [rationale]
TYPOGRAPHY: [3 font recommendations with roles] — [why these fonts]
SPACING: [base unit + density] — [rationale]
MOTION: [approach] — [rationale]
This system is coherent because [explain how choices reinforce each other].
SAFE CHOICES (category baseline — your users expect these):
- [2-3 decisions that match category conventions, with rationale for playing safe]
RISKS (where your product gets its own face):
- [2-3 deliberate departures from convention]
- For each risk: what it is, why it works, what you gain, what it costs
The safe choices keep you literate in your category. The risks are where
your product becomes memorable. Which risks appeal to you? Want to see
different ones? Or adjust anything else?
```
The SAFE/RISK breakdown is critical. Design coherence is table stakes — every product in a category can be coherent and still look identical. The real question is: where do you take creative risks? The agent should always propose at least 2 risks, each with a clear rationale for why the risk is worth taking and what the user gives up. Risks might include: an unexpected typeface for the category, a bold accent color nobody else uses, tighter or looser spacing than the norm, a layout approach that breaks from convention, motion choices that add personality.
**Options:** A) Looks great — generate the preview page. B) I want to adjust [section]. C) I want different risks — show me wilder options. D) Start over with a different direction. E) Skip the preview, just write DESIGN.md.
### Your Design Knowledge (use to inform proposals — do NOT display as tables)
**Aesthetic directions** (pick the one that fits the product):
- Brutally Minimal — Type and whitespace only. No decoration. Modernist.
- Maximalist Chaos — Dense, layered, pattern-heavy. Y2K meets contemporary.
- Retro-Futuristic — Vintage tech nostalgia. CRT glow, pixel grids, warm monospace.
- Luxury/Refined — Serifs, high contrast, generous whitespace, precious metals.
- Playful/Toy-like — Rounded, bouncy, bold primaries. Approachable and fun.
- Editorial/Magazine — Strong typographic hierarchy, asymmetric grids, pull quotes.
- Brutalist/Raw — Exposed structure, system fonts, visible grid, no polish.
- Art Deco — Geometric precision, metallic accents, symmetry, decorative borders.
- Organic/Natural — Earth tones, rounded forms, hand-drawn texture, grain.
- Industrial/Utilitarian — Function-first, data-dense, monospace accents, muted palette.
**Decoration levels:** minimal (typography does all the work) / intentional (subtle texture, grain, or background treatment) / expressive (full creative direction, layered depth, patterns)
**Layout approaches:** grid-disciplined (strict columns, predictable alignment) / creative-editorial (asymmetry, overlap, grid-breaking) / hybrid (grid for app, creative for marketing)
**Color approaches:** restrained (1 accent + neutrals, color is rare and meaningful) / balanced (primary + secondary, semantic colors for hierarchy) / expressive (color as a primary design tool, bold palettes)
**Motion approaches:** minimal-functional (only transitions that aid comprehension) / intentional (subtle entrance animations, meaningful state transitions) / expressive (full choreography, scroll-driven, playful)
**Font recommendations by purpose:**
- Display/Hero: Satoshi, General Sans, Instrument Serif, Fraunces, Clash Grotesk, Cabinet Grotesk
- Body: Instrument Sans, DM Sans, Source Sans 3, Geist, Plus Jakarta Sans, Outfit
- Data/Tables: Geist (tabular-nums), DM Sans (tabular-nums), JetBrains Mono, IBM Plex Mono
- Code: JetBrains Mono, Fira Code, Berkeley Mono, Geist Mono
**Font blacklist** (never recommend):
Papyrus, Comic Sans, Lobster, Impact, Jokerman, Bleeding Cowboys, Permanent Marker, Bradley Hand, Brush Script, Hobo, Trajan, Raleway, Clash Display, Courier New (for body)
**Overused fonts** (never recommend as primary — use only if user specifically requests):
Inter, Roboto, Arial, Helvetica, Open Sans, Lato, Montserrat, Poppins, Space Grotesk.
Space Grotesk is on the list specifically because every AI design tool converges on it
as "the safe alternative to Inter." That's the convergence trap. Treat it the same as
Inter: only use if the user asks for it by name.
**Anti-convergence directive:** Across multiple generations in the same project, VARY
light/dark, fonts, and aesthetic directions. Never propose the same choices twice
without explicit justification. If the user's prior session used Geist + dark + editorial,
propose something different this time (or explicitly acknowledge you're doubling down
because it fits the brief). Convergence across generations is slop.
**AI slop anti-patterns** (never include in your recommendations):
- Purple/violet gradients as default accent
- 3-column feature grid with icons in colored circles
- Centered everything with uniform spacing
- Uniform bubbly border-radius on all elements
- Gradient buttons as the primary CTA pattern
- Generic stock-photo-style hero sections
- system-ui / -apple-system as the primary display or body font (the "I gave up on typography" signal)
- "Built for X" / "Designed for Y" marketing copy patterns
### Coherence Validation
When the user overrides one section, check if the rest still coheres. Flag mismatches with a gentle nudge — never block:
- Brutalist/Minimal aesthetic + expressive motion → "Heads up: brutalist aesthetics usually pair with minimal motion. Your combo is unusual — which is fine if intentional. Want me to suggest motion that fits, or keep it?"
- Expressive color + restrained decoration → "Bold palette with minimal decoration can work, but the colors will carry a lot of weight. Want me to suggest decoration that supports the palette?"
- Creative-editorial layout + data-heavy product → "Editorial layouts are gorgeous but can fight data density. Want me to show how a hybrid approach keeps both?"
- Always accept the user's final choice. Never refuse to proceed.
---
## Phase 4: Drill-downs (only if user requests adjustments)
When the user wants to change a specific section, go deep on that section:
- **Fonts:** Present 3-5 specific candidates with rationale, explain what each evokes, offer the preview page
- **Colors:** Present 2-3 palette options with hex values, explain the color theory reasoning
- **Aesthetic:** Walk through which directions fit their product and why
- **Layout/Spacing/Motion:** Present the approaches with concrete tradeoffs for their product type
Each drill-down is one focused AskUserQuestion. After the user decides, re-check coherence with the rest of the system.
---
## Phase 5: Design System Preview (default ON)
This phase generates visual previews of the proposed design system. Two paths depending on whether the gstack designer is available.
### Path A: AI Mockups (if DESIGN_READY)
Generate AI-rendered mockups showing the proposed design system applied to realistic screens for this product. This is far more powerful than an HTML preview — the user sees what their product could actually look like.
```bash
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)"
_DESIGN_DIR="$HOME/.gstack/projects/$SLUG/designs/design-system-$(date +%Y%m%d)"
mkdir -p "$_DESIGN_DIR"
echo "DESIGN_DIR: $_DESIGN_DIR"
```
Construct a design brief from the Phase 3 proposal (aesthetic, colors, typography, spacing, layout) and the product context from Phase 1:
```bash
$D variants --brief "<product name: [name]. Product type: [type]. Aesthetic: [direction]. Colors: primary [hex], secondary [hex], neutrals [range]. Typography: display [font], body [font]. Layout: [approach]. Show a realistic [page type] screen with [specific content for this product].>" --count 3 --output-dir "$_DESIGN_DIR/"
```
Run quality check on each variant:
```bash
$D check --image "$_DESIGN_DIR/variant-A.png" --brief "<the original brief>"
```
Show each variant inline (Read tool on each PNG) for instant preview.
**Before presenting to the user, self-gate:** For each variant, ask yourself: *"Would
a human designer be embarrassed to put their name on this?"* If yes, discard the
variant and regenerate. This is a hard gate. A mediocre AI mockup is worse than no
mockup. Embarrassment triggers include: purple gradient hero, 3-column SaaS grid,
centered-everything, Inter body text, generic stock-photo vibe, system-ui font,
gradient CTA button, bubble-radius everything. Any of those = reject and regenerate.
Tell the user: "I've generated 3 visual directions applying your design system to a realistic [product type] screen. Pick your favorite in the comparison board that just opened in your browser. You can also remix elements across variants."
{{DESIGN_SHOTGUN_LOOP}}
After the user picks a direction:
- Use `$D extract --image "$_DESIGN_DIR/variant-<CHOSEN>.png"` to analyze the approved mockup and extract design tokens (colors, typography, spacing) that will populate DESIGN.md in Phase 6. This grounds the design system in what was actually approved visually, not just what was described in text.
- If the user wants to iterate further: `$D iterate --feedback "<user's feedback>" --output "$_DESIGN_DIR/refined.png"`
**Plan mode vs. implementation mode:**
- **If in plan mode:** Add the approved mockup path (the full `$_DESIGN_DIR` path) and extracted tokens to the plan file under an "## Approved Design Direction" section. The design system gets written to DESIGN.md when the plan is implemented.
- **If NOT in plan mode:** Proceed directly to Phase 6 and write DESIGN.md with the extracted tokens.
### Path B: HTML Preview Page (fallback if DESIGN_NOT_AVAILABLE)
Generate a polished HTML preview page and open it in the user's browser. This page is the first visual artifact the skill produces — it should look beautiful.
```bash
PREVIEW_FILE="/tmp/design-consultation-preview-$(date +%s).html"
```
Write the preview HTML to `$PREVIEW_FILE`, then open it:
```bash
open "$PREVIEW_FILE"
```
### Preview Page Requirements (Path B only)
The agent writes a **single, self-contained HTML file** (no framework dependencies) that:
1. **Loads proposed fonts** from Google Fonts (or Bunny Fonts) via `<link>` tags
2. **Uses the proposed color palette** throughout — dogfood the design system
3. **Shows the product name** (not "Lorem Ipsum") as the hero heading
4. **Font specimen section:**
- Each font candidate shown in its proposed role (hero heading, body paragraph, button label, data table row)
- Side-by-side comparison if multiple candidates for one role
- Real content that matches the product (e.g., civic tech → government data examples)
5. **Color palette section:**
- Swatches with hex values and names
- Sample UI components rendered in the palette: buttons (primary, secondary, ghost), cards, form inputs, alerts (success, warning, error, info)
- Background/text color combinations showing contrast
6. **Realistic product mockups** — this is what makes the preview page powerful. Based on the project type from Phase 1, render 2-3 realistic page layouts using the full design system:
- **Dashboard / web app:** sample data table with metrics, sidebar nav, header with user avatar, stat cards
- **Marketing site:** hero section with real copy, feature highlights, testimonial block, CTA
- **Settings / admin:** form with labeled inputs, toggle switches, dropdowns, save button
- **Auth / onboarding:** login form with social buttons, branding, input validation states
- Use the product name, realistic content for the domain, and the proposed spacing/layout/border-radius. The user should see their product (roughly) before writing any code.
7. **Light/dark mode toggle** using CSS custom properties and a JS toggle button
8. **Clean, professional layout** — the preview page IS a taste signal for the skill
9. **Responsive** — looks good on any screen width
The page should make the user think "oh nice, they thought of this." It's selling the design system by showing what the product could feel like, not just listing hex codes and font names.
If `open` fails (headless environment), tell the user: *"I wrote the preview to [path] — open it in your browser to see the fonts and colors rendered."*
If the user says skip the preview, go directly to Phase 6.
---
## Phase 6: Write DESIGN.md & Confirm
If `$D extract` was used in Phase 5 (Path A), use the extracted tokens as the primary source for DESIGN.md values — colors, typography, and spacing grounded in the approved mockup rather than text descriptions alone. Merge extracted tokens with the Phase 3 proposal (the proposal provides rationale and context; the extraction provides exact values).
**If in plan mode:** Write the DESIGN.md content into the plan file as a "## Proposed DESIGN.md" section. Do NOT write the actual file — that happens at implementation time.
**If NOT in plan mode:** Write `DESIGN.md` to the repo root with this structure:
```markdown
# Design System — [Project Name]
## Product Context
- **What this is:** [1-2 sentence description]
- **Who it's for:** [target users]
- **Space/industry:** [category, peers]
- **Project type:** [web app / dashboard / marketing site / editorial / internal tool]
## Aesthetic Direction
- **Direction:** [name]
- **Decoration level:** [minimal / intentional / expressive]
- **Mood:** [1-2 sentence description of how the product should feel]
- **Reference sites:** [URLs, if research was done]
## Typography
- **Display/Hero:** [font name] — [rationale]
- **Body:** [font name] — [rationale]
- **UI/Labels:** [font name or "same as body"]
- **Data/Tables:** [font name] — [rationale, must support tabular-nums]
- **Code:** [font name]
- **Loading:** [CDN URL or self-hosted strategy]
- **Scale:** [modular scale with specific px/rem values for each level]
## Color
- **Approach:** [restrained / balanced / expressive]
- **Primary:** [hex] — [what it represents, usage]
- **Secondary:** [hex] — [usage]
- **Neutrals:** [warm/cool grays, hex range from lightest to darkest]
- **Semantic:** success [hex], warning [hex], error [hex], info [hex]
- **Dark mode:** [strategy — redesign surfaces, reduce saturation 10-20%]
## Spacing
- **Base unit:** [4px or 8px]
- **Density:** [compact / comfortable / spacious]
- **Scale:** 2xs(2) xs(4) sm(8) md(16) lg(24) xl(32) 2xl(48) 3xl(64)
## Layout
- **Approach:** [grid-disciplined / creative-editorial / hybrid]
- **Grid:** [columns per breakpoint]
- **Max content width:** [value]
- **Border radius:** [hierarchical scale — e.g., sm:4px, md:8px, lg:12px, full:9999px]
## Motion
- **Approach:** [minimal-functional / intentional / expressive]
- **Easing:** enter(ease-out) exit(ease-in) move(ease-in-out)
- **Duration:** micro(50-100ms) short(150-250ms) medium(250-400ms) long(400-700ms)
## Decisions Log
| Date | Decision | Rationale |
|------|----------|-----------|
| [today] | Initial design system created | Created by /design-consultation based on [product context / research] |
```
**Update CLAUDE.md** (or create it if it doesn't exist) — append this section:
```markdown
## Design System
Always read DESIGN.md before making any visual or UI decisions.
All font choices, colors, spacing, and aesthetic direction are defined there.
Do not deviate without explicit user approval.
In QA mode, flag any code that doesn't match DESIGN.md.
```
**AskUserQuestion Q-final — show summary and confirm:**
List all decisions. Flag any that used agent defaults without explicit user confirmation (the user should know what they're shipping). Options:
- A) Ship it — write DESIGN.md and CLAUDE.md
- B) I want to change something (specify what)
- C) Start over
After shipping DESIGN.md, if the session produced screen-level mockups or page layouts
(not just system-level tokens), suggest:
"Want to see this design system as working Pretext-native HTML? Run /design-html."
---

View File

@ -53,6 +53,9 @@ echo "SKILL_PREFIX: $_SKILL_PREFIX"
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
REPO_MODE=${REPO_MODE:-unknown}
echo "REPO_MODE: $REPO_MODE"
_SESSION_KIND=$(~/.claude/skills/gstack/bin/gstack-session-kind 2>/dev/null || echo "interactive")
case "$_SESSION_KIND" in spawned|headless|interactive) ;; *) _SESSION_KIND="interactive" ;; esac
echo "SESSION_KIND: $_SESSION_KIND"
_LAKE_SEEN=$([ -f ~/.gstack/.completeness-intro-seen ] && echo "yes" || echo "no")
echo "LAKE_INTRO: $_LAKE_SEEN"
_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || true)
@ -132,7 +135,7 @@ In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`co
## Skill Invocation During Plan Mode
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If no AskUserQuestion variant is callable, use the normal chat fallback from the AskUserQuestion Format rule and stop for the user's reply. At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If AskUserQuestion is unavailable or a call fails, follow the AskUserQuestion Format failure fallback: `headless` → BLOCKED; `interactive` → the prose fallback (also satisfies end-of-turn). At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If `PROACTIVE` is `"false"`, do not auto-invoke or proactively suggest skills. If a skill seems useful, ask: "I think /skillname might help here — want me to run it?"
@ -167,7 +170,7 @@ touch ~/.gstack/.writing-style-prompted
Skip if `WRITING_STYLE_PENDING` is `no`.
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Lake** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Ocean** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
```bash
open https://garryslist.org/posts/boil-the-ocean
@ -304,11 +307,31 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
**Rule:** if any `mcp__*__AskUserQuestion` variant is in your tool list, prefer it. Hosts may disable native AUQ via `--disallowedTools AskUserQuestion` (Conductor does, by default) and route through their MCP variant; calling native there silently fails. Same questions/options shape; same decision-brief format applies.
**If no AskUserQuestion variant appears in your tool list:** fall back to the host's normal user-facing chat question. Render the same decision brief in prose, stop, and wait for the user's reply. This fallback is for interactive chat hosts that can ask the user normally but do not expose an AUQ tool (for example Codex API sessions). Do not write decisions to the plan file as a substitute, and do not silently auto-decide (only `/plan-tune` AUTO_DECIDE opt-ins authorize auto-picking).
If AskUserQuestion is unavailable (no variant in your tool list) OR a call to it fails, do NOT silently auto-decide or write the decision to the plan file as a substitute. Follow the **failure fallback** below.
### When AskUserQuestion is unavailable or a call fails
Tell three outcomes apart:
1. **Auto-decide denial (NOT a failure).** The result contains `[plan-tune auto-decide] <id> → <option>` — the preference hook working as designed. Proceed with that option. Do NOT retry, do NOT fall back to prose.
2. **Genuine failure** — no variant in your tool list, OR the variant is present but the call returns an error / missing result (MCP transport error, empty result, host bug — e.g. Conductor's MCP AskUserQuestion is flaky and returns `[Tool result missing due to internal error]`).
- If it was present and **errored** (not absent), retry the SAME call **once** — but only if no answer could have surfaced (a missing-result error can arrive after the user already saw the question; retrying would double-prompt, so if it may have reached them, treat as pending, don't retry).
- Then branch on `SESSION_KIND` (echoed by the preamble; empty/absent ⇒ `interactive`):
- `spawned` → defer to the **Spawned session** block: auto-choose the recommended option. Never prose, never BLOCKED.
- `headless``BLOCKED — AskUserQuestion unavailable`; stop and wait (no human can answer).
- `interactive`**prose fallback** (below).
**Prose fallback — render the decision brief as a markdown message, not a tool call.** Same information as the tool format below, different structure (paragraphs, not ✅/❌ bullets). It MUST surface this triad:
1. **A clear ELI10 of the issue itself** — plain English on what's being decided and why it matters (the question, not per-choice), naming the stakes. Lead with it.
2. **Completeness scores per choice** — explicit `Completeness: X/10` on EACH choice (10 complete, 7 happy-path, 3 shortcut); use the kind-note when options differ in kind not coverage, but never silently drop the score.
3. **The recommendation and why** — a `Recommendation: <choice> because <reason>` line plus the `(recommended)` marker on that choice.
Layout: a `D<N>` title + a one-line note that AskUserQuestion failed and to reply with a letter; the issue ELI10; the Recommendation line; then ONE paragraph per choice carrying its `(recommended)` marker, its `Completeness: X/10`, and 2-4 sentences of reasoning — never a bare bullet list; a closing `Net:` line. Split chains / 5+ options: one prose block per per-option call, in sequence. Then STOP and wait — the user's typed answer is the decision. In plan mode this satisfies end-of-turn like a tool call.
### Format
Every AskUserQuestion is a decision brief. Send it as tool_use when an AskUserQuestion tool exists; otherwise use the normal chat fallback above and wait for the user's reply.
Every AskUserQuestion is a decision brief and must be sent as tool_use, not prose — unless the documented failure fallback above applies (interactive session + the call is unavailable/erroring), in which case the prose fallback is the correct output.
```
D<N><one-line question title>
@ -388,7 +411,7 @@ Before calling AskUserQuestion, verify:
- [ ] (recommended) label on one option (even for neutral-posture)
- [ ] Dual-scale effort labels on effort-bearing options (human / CC)
- [ ] Net line closes the decision
- [ ] You are calling the tool when available; otherwise you are using the normal chat fallback and stopping for the user's reply
- [ ] You are calling the tool, not writing prose — unless the documented failure fallback applies (then: prose with the mandatory triad — issue ELI10, per-choice Completeness, Recommendation + `(recommended)` — and a "reply with a letter" instruction, then STOP)
- [ ] Non-ASCII characters (CJK / accents) written directly, NOT \u-escaped
- [ ] If you had 5+ options, you split (or batched into ≤4-groups) — did NOT drop any
- [ ] If you split, you checked dependencies between options before firing the chain
@ -575,12 +598,19 @@ if [ -d "$_PROJ" ]; then
fi
_LATEST_CP=$(find "$_PROJ/checkpoints" -name "*.md" -type f 2>/dev/null | xargs ls -t 2>/dev/null | head -1)
[ -n "$_LATEST_CP" ] && echo "LATEST_CHECKPOINT: $_LATEST_CP"
if [ -f "$_PROJ/decisions.active.json" ]; then
echo "--- ACTIVE DECISIONS (recent, scope-relevant) ---"
~/.claude/skills/gstack/bin/gstack-decision-search --recent 5 2>/dev/null
echo "--- END DECISIONS ---"
fi
echo "--- END ARTIFACTS ---"
fi
```
If artifacts are listed, read the newest useful one. If `LAST_SESSION` or `LATEST_CHECKPOINT` appears, give a 2-sentence welcome back summary. If `RECENT_PATTERN` clearly implies a next skill, suggest it once.
**Cross-session decisions.** If `ACTIVE DECISIONS` are listed, treat them as prior settled calls with their rationale — do not silently re-litigate them; if you're about to reverse one, say so explicitly. Reach for `~/.claude/skills/gstack/bin/gstack-decision-search` whenever a question touches a past decision ("what did we decide / why / did we try"). When you or the user make a DURABLE decision (architecture, scope, tool/vendor choice, or a reversal) — NOT a turn-level or trivial choice — log it with `~/.claude/skills/gstack/bin/gstack-decision-log` (`--supersede <id>` for a reversal). Reliable and local; gbrain not required.
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format is structure; this is prose quality.
@ -595,9 +625,9 @@ Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format i
Curated jargon list lives at `~/.claude/skills/gstack/scripts/jargon-list.json` (80+ terms). On the first jargon term you encounter this session, Read that file once; treat the `terms` array as the canonical list. The list is repo-owned and may grow between releases.
## Completeness Principle — Boil the Lake
## Completeness Principle — Boil the Ocean
AI makes completeness cheap. Recommend complete lakes (tests, edge cases, error paths); flag oceans (rewrites, multi-quarter migrations).
AI makes completeness cheap, so the complete thing is the goal. Recommend full coverage (tests, edge cases, error paths) — boil the ocean one lake at a time. The only thing out of scope is genuinely unrelated work (rewrites, multi-quarter migrations); flag that as separate scope, never as an excuse for a shortcut.
When options differ in coverage, include `Completeness: X/10` (10 = all edge cases, 7 = happy path, 3 = shortcut). When options differ in kind, write: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate scores.

View File

@ -50,6 +50,9 @@ echo "SKILL_PREFIX: $_SKILL_PREFIX"
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
REPO_MODE=${REPO_MODE:-unknown}
echo "REPO_MODE: $REPO_MODE"
_SESSION_KIND=$(~/.claude/skills/gstack/bin/gstack-session-kind 2>/dev/null || echo "interactive")
case "$_SESSION_KIND" in spawned|headless|interactive) ;; *) _SESSION_KIND="interactive" ;; esac
echo "SESSION_KIND: $_SESSION_KIND"
_LAKE_SEEN=$([ -f ~/.gstack/.completeness-intro-seen ] && echo "yes" || echo "no")
echo "LAKE_INTRO: $_LAKE_SEEN"
_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || true)
@ -129,7 +132,7 @@ In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`co
## Skill Invocation During Plan Mode
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If no AskUserQuestion variant is callable, use the normal chat fallback from the AskUserQuestion Format rule and stop for the user's reply. At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If AskUserQuestion is unavailable or a call fails, follow the AskUserQuestion Format failure fallback: `headless` → BLOCKED; `interactive` → the prose fallback (also satisfies end-of-turn). At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If `PROACTIVE` is `"false"`, do not auto-invoke or proactively suggest skills. If a skill seems useful, ask: "I think /skillname might help here — want me to run it?"
@ -164,7 +167,7 @@ touch ~/.gstack/.writing-style-prompted
Skip if `WRITING_STYLE_PENDING` is `no`.
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Lake** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Ocean** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
```bash
open https://garryslist.org/posts/boil-the-ocean
@ -301,11 +304,31 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
**Rule:** if any `mcp__*__AskUserQuestion` variant is in your tool list, prefer it. Hosts may disable native AUQ via `--disallowedTools AskUserQuestion` (Conductor does, by default) and route through their MCP variant; calling native there silently fails. Same questions/options shape; same decision-brief format applies.
**If no AskUserQuestion variant appears in your tool list:** fall back to the host's normal user-facing chat question. Render the same decision brief in prose, stop, and wait for the user's reply. This fallback is for interactive chat hosts that can ask the user normally but do not expose an AUQ tool (for example Codex API sessions). Do not write decisions to the plan file as a substitute, and do not silently auto-decide (only `/plan-tune` AUTO_DECIDE opt-ins authorize auto-picking).
If AskUserQuestion is unavailable (no variant in your tool list) OR a call to it fails, do NOT silently auto-decide or write the decision to the plan file as a substitute. Follow the **failure fallback** below.
### When AskUserQuestion is unavailable or a call fails
Tell three outcomes apart:
1. **Auto-decide denial (NOT a failure).** The result contains `[plan-tune auto-decide] <id> → <option>` — the preference hook working as designed. Proceed with that option. Do NOT retry, do NOT fall back to prose.
2. **Genuine failure** — no variant in your tool list, OR the variant is present but the call returns an error / missing result (MCP transport error, empty result, host bug — e.g. Conductor's MCP AskUserQuestion is flaky and returns `[Tool result missing due to internal error]`).
- If it was present and **errored** (not absent), retry the SAME call **once** — but only if no answer could have surfaced (a missing-result error can arrive after the user already saw the question; retrying would double-prompt, so if it may have reached them, treat as pending, don't retry).
- Then branch on `SESSION_KIND` (echoed by the preamble; empty/absent ⇒ `interactive`):
- `spawned` → defer to the **Spawned session** block: auto-choose the recommended option. Never prose, never BLOCKED.
- `headless``BLOCKED — AskUserQuestion unavailable`; stop and wait (no human can answer).
- `interactive`**prose fallback** (below).
**Prose fallback — render the decision brief as a markdown message, not a tool call.** Same information as the tool format below, different structure (paragraphs, not ✅/❌ bullets). It MUST surface this triad:
1. **A clear ELI10 of the issue itself** — plain English on what's being decided and why it matters (the question, not per-choice), naming the stakes. Lead with it.
2. **Completeness scores per choice** — explicit `Completeness: X/10` on EACH choice (10 complete, 7 happy-path, 3 shortcut); use the kind-note when options differ in kind not coverage, but never silently drop the score.
3. **The recommendation and why** — a `Recommendation: <choice> because <reason>` line plus the `(recommended)` marker on that choice.
Layout: a `D<N>` title + a one-line note that AskUserQuestion failed and to reply with a letter; the issue ELI10; the Recommendation line; then ONE paragraph per choice carrying its `(recommended)` marker, its `Completeness: X/10`, and 2-4 sentences of reasoning — never a bare bullet list; a closing `Net:` line. Split chains / 5+ options: one prose block per per-option call, in sequence. Then STOP and wait — the user's typed answer is the decision. In plan mode this satisfies end-of-turn like a tool call.
### Format
Every AskUserQuestion is a decision brief. Send it as tool_use when an AskUserQuestion tool exists; otherwise use the normal chat fallback above and wait for the user's reply.
Every AskUserQuestion is a decision brief and must be sent as tool_use, not prose — unless the documented failure fallback above applies (interactive session + the call is unavailable/erroring), in which case the prose fallback is the correct output.
```
D<N><one-line question title>
@ -385,7 +408,7 @@ Before calling AskUserQuestion, verify:
- [ ] (recommended) label on one option (even for neutral-posture)
- [ ] Dual-scale effort labels on effort-bearing options (human / CC)
- [ ] Net line closes the decision
- [ ] You are calling the tool when available; otherwise you are using the normal chat fallback and stopping for the user's reply
- [ ] You are calling the tool, not writing prose — unless the documented failure fallback applies (then: prose with the mandatory triad — issue ELI10, per-choice Completeness, Recommendation + `(recommended)` — and a "reply with a letter" instruction, then STOP)
- [ ] Non-ASCII characters (CJK / accents) written directly, NOT \u-escaped
- [ ] If you had 5+ options, you split (or batched into ≤4-groups) — did NOT drop any
- [ ] If you split, you checked dependencies between options before firing the chain
@ -572,12 +595,19 @@ if [ -d "$_PROJ" ]; then
fi
_LATEST_CP=$(find "$_PROJ/checkpoints" -name "*.md" -type f 2>/dev/null | xargs ls -t 2>/dev/null | head -1)
[ -n "$_LATEST_CP" ] && echo "LATEST_CHECKPOINT: $_LATEST_CP"
if [ -f "$_PROJ/decisions.active.json" ]; then
echo "--- ACTIVE DECISIONS (recent, scope-relevant) ---"
~/.claude/skills/gstack/bin/gstack-decision-search --recent 5 2>/dev/null
echo "--- END DECISIONS ---"
fi
echo "--- END ARTIFACTS ---"
fi
```
If artifacts are listed, read the newest useful one. If `LAST_SESSION` or `LATEST_CHECKPOINT` appears, give a 2-sentence welcome back summary. If `RECENT_PATTERN` clearly implies a next skill, suggest it once.
**Cross-session decisions.** If `ACTIVE DECISIONS` are listed, treat them as prior settled calls with their rationale — do not silently re-litigate them; if you're about to reverse one, say so explicitly. Reach for `~/.claude/skills/gstack/bin/gstack-decision-search` whenever a question touches a past decision ("what did we decide / why / did we try"). When you or the user make a DURABLE decision (architecture, scope, tool/vendor choice, or a reversal) — NOT a turn-level or trivial choice — log it with `~/.claude/skills/gstack/bin/gstack-decision-log` (`--supersede <id>` for a reversal). Reliable and local; gbrain not required.
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format is structure; this is prose quality.
@ -592,9 +622,9 @@ Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format i
Curated jargon list lives at `~/.claude/skills/gstack/scripts/jargon-list.json` (80+ terms). On the first jargon term you encounter this session, Read that file once; treat the `terms` array as the canonical list. The list is repo-owned and may grow between releases.
## Completeness Principle — Boil the Lake
## Completeness Principle — Boil the Ocean
AI makes completeness cheap. Recommend complete lakes (tests, edge cases, error paths); flag oceans (rewrites, multi-quarter migrations).
AI makes completeness cheap, so the complete thing is the goal. Recommend full coverage (tests, edge cases, error paths) — boil the ocean one lake at a time. The only thing out of scope is genuinely unrelated work (rewrites, multi-quarter migrations); flag that as separate scope, never as an excuse for a shortcut.
When options differ in coverage, include `Completeness: X/10` (10 = all edge cases, 7 = happy path, 3 = shortcut). When options differ in kind, write: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate scores.

View File

@ -67,6 +67,9 @@ echo "SKILL_PREFIX: $_SKILL_PREFIX"
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
REPO_MODE=${REPO_MODE:-unknown}
echo "REPO_MODE: $REPO_MODE"
_SESSION_KIND=$(~/.claude/skills/gstack/bin/gstack-session-kind 2>/dev/null || echo "interactive")
case "$_SESSION_KIND" in spawned|headless|interactive) ;; *) _SESSION_KIND="interactive" ;; esac
echo "SESSION_KIND: $_SESSION_KIND"
_LAKE_SEEN=$([ -f ~/.gstack/.completeness-intro-seen ] && echo "yes" || echo "no")
echo "LAKE_INTRO: $_LAKE_SEEN"
_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || true)
@ -146,7 +149,7 @@ In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`co
## Skill Invocation During Plan Mode
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If no AskUserQuestion variant is callable, use the normal chat fallback from the AskUserQuestion Format rule and stop for the user's reply. At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If AskUserQuestion is unavailable or a call fails, follow the AskUserQuestion Format failure fallback: `headless` → BLOCKED; `interactive` → the prose fallback (also satisfies end-of-turn). At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If `PROACTIVE` is `"false"`, do not auto-invoke or proactively suggest skills. If a skill seems useful, ask: "I think /skillname might help here — want me to run it?"
@ -181,7 +184,7 @@ touch ~/.gstack/.writing-style-prompted
Skip if `WRITING_STYLE_PENDING` is `no`.
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Lake** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Ocean** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
```bash
open https://garryslist.org/posts/boil-the-ocean
@ -318,11 +321,31 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
**Rule:** if any `mcp__*__AskUserQuestion` variant is in your tool list, prefer it. Hosts may disable native AUQ via `--disallowedTools AskUserQuestion` (Conductor does, by default) and route through their MCP variant; calling native there silently fails. Same questions/options shape; same decision-brief format applies.
**If no AskUserQuestion variant appears in your tool list:** fall back to the host's normal user-facing chat question. Render the same decision brief in prose, stop, and wait for the user's reply. This fallback is for interactive chat hosts that can ask the user normally but do not expose an AUQ tool (for example Codex API sessions). Do not write decisions to the plan file as a substitute, and do not silently auto-decide (only `/plan-tune` AUTO_DECIDE opt-ins authorize auto-picking).
If AskUserQuestion is unavailable (no variant in your tool list) OR a call to it fails, do NOT silently auto-decide or write the decision to the plan file as a substitute. Follow the **failure fallback** below.
### When AskUserQuestion is unavailable or a call fails
Tell three outcomes apart:
1. **Auto-decide denial (NOT a failure).** The result contains `[plan-tune auto-decide] <id> → <option>` — the preference hook working as designed. Proceed with that option. Do NOT retry, do NOT fall back to prose.
2. **Genuine failure** — no variant in your tool list, OR the variant is present but the call returns an error / missing result (MCP transport error, empty result, host bug — e.g. Conductor's MCP AskUserQuestion is flaky and returns `[Tool result missing due to internal error]`).
- If it was present and **errored** (not absent), retry the SAME call **once** — but only if no answer could have surfaced (a missing-result error can arrive after the user already saw the question; retrying would double-prompt, so if it may have reached them, treat as pending, don't retry).
- Then branch on `SESSION_KIND` (echoed by the preamble; empty/absent ⇒ `interactive`):
- `spawned` → defer to the **Spawned session** block: auto-choose the recommended option. Never prose, never BLOCKED.
- `headless``BLOCKED — AskUserQuestion unavailable`; stop and wait (no human can answer).
- `interactive`**prose fallback** (below).
**Prose fallback — render the decision brief as a markdown message, not a tool call.** Same information as the tool format below, different structure (paragraphs, not ✅/❌ bullets). It MUST surface this triad:
1. **A clear ELI10 of the issue itself** — plain English on what's being decided and why it matters (the question, not per-choice), naming the stakes. Lead with it.
2. **Completeness scores per choice** — explicit `Completeness: X/10` on EACH choice (10 complete, 7 happy-path, 3 shortcut); use the kind-note when options differ in kind not coverage, but never silently drop the score.
3. **The recommendation and why** — a `Recommendation: <choice> because <reason>` line plus the `(recommended)` marker on that choice.
Layout: a `D<N>` title + a one-line note that AskUserQuestion failed and to reply with a letter; the issue ELI10; the Recommendation line; then ONE paragraph per choice carrying its `(recommended)` marker, its `Completeness: X/10`, and 2-4 sentences of reasoning — never a bare bullet list; a closing `Net:` line. Split chains / 5+ options: one prose block per per-option call, in sequence. Then STOP and wait — the user's typed answer is the decision. In plan mode this satisfies end-of-turn like a tool call.
### Format
Every AskUserQuestion is a decision brief. Send it as tool_use when an AskUserQuestion tool exists; otherwise use the normal chat fallback above and wait for the user's reply.
Every AskUserQuestion is a decision brief and must be sent as tool_use, not prose — unless the documented failure fallback above applies (interactive session + the call is unavailable/erroring), in which case the prose fallback is the correct output.
```
D<N><one-line question title>
@ -402,7 +425,7 @@ Before calling AskUserQuestion, verify:
- [ ] (recommended) label on one option (even for neutral-posture)
- [ ] Dual-scale effort labels on effort-bearing options (human / CC)
- [ ] Net line closes the decision
- [ ] You are calling the tool when available; otherwise you are using the normal chat fallback and stopping for the user's reply
- [ ] You are calling the tool, not writing prose — unless the documented failure fallback applies (then: prose with the mandatory triad — issue ELI10, per-choice Completeness, Recommendation + `(recommended)` — and a "reply with a letter" instruction, then STOP)
- [ ] Non-ASCII characters (CJK / accents) written directly, NOT \u-escaped
- [ ] If you had 5+ options, you split (or batched into ≤4-groups) — did NOT drop any
- [ ] If you split, you checked dependencies between options before firing the chain
@ -589,12 +612,19 @@ if [ -d "$_PROJ" ]; then
fi
_LATEST_CP=$(find "$_PROJ/checkpoints" -name "*.md" -type f 2>/dev/null | xargs ls -t 2>/dev/null | head -1)
[ -n "$_LATEST_CP" ] && echo "LATEST_CHECKPOINT: $_LATEST_CP"
if [ -f "$_PROJ/decisions.active.json" ]; then
echo "--- ACTIVE DECISIONS (recent, scope-relevant) ---"
~/.claude/skills/gstack/bin/gstack-decision-search --recent 5 2>/dev/null
echo "--- END DECISIONS ---"
fi
echo "--- END ARTIFACTS ---"
fi
```
If artifacts are listed, read the newest useful one. If `LAST_SESSION` or `LATEST_CHECKPOINT` appears, give a 2-sentence welcome back summary. If `RECENT_PATTERN` clearly implies a next skill, suggest it once.
**Cross-session decisions.** If `ACTIVE DECISIONS` are listed, treat them as prior settled calls with their rationale — do not silently re-litigate them; if you're about to reverse one, say so explicitly. Reach for `~/.claude/skills/gstack/bin/gstack-decision-search` whenever a question touches a past decision ("what did we decide / why / did we try"). When you or the user make a DURABLE decision (architecture, scope, tool/vendor choice, or a reversal) — NOT a turn-level or trivial choice — log it with `~/.claude/skills/gstack/bin/gstack-decision-log` (`--supersede <id>` for a reversal). Reliable and local; gbrain not required.
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format is structure; this is prose quality.
@ -609,9 +639,9 @@ Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format i
Curated jargon list lives at `~/.claude/skills/gstack/scripts/jargon-list.json` (80+ terms). On the first jargon term you encounter this session, Read that file once; treat the `terms` array as the canonical list. The list is repo-owned and may grow between releases.
## Completeness Principle — Boil the Lake
## Completeness Principle — Boil the Ocean
AI makes completeness cheap. Recommend complete lakes (tests, edge cases, error paths); flag oceans (rewrites, multi-quarter migrations).
AI makes completeness cheap, so the complete thing is the goal. Recommend full coverage (tests, edge cases, error paths) — boil the ocean one lake at a time. The only thing out of scope is genuinely unrelated work (rewrites, multi-quarter migrations); flag that as separate scope, never as an excuse for a shortcut.
When options differ in coverage, include `Completeness: X/10` (10 = all edge cases, 7 = happy path, 3 = shortcut). When options differ in kind, write: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate scores.

View File

@ -52,6 +52,9 @@ echo "SKILL_PREFIX: $_SKILL_PREFIX"
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
REPO_MODE=${REPO_MODE:-unknown}
echo "REPO_MODE: $REPO_MODE"
_SESSION_KIND=$(~/.claude/skills/gstack/bin/gstack-session-kind 2>/dev/null || echo "interactive")
case "$_SESSION_KIND" in spawned|headless|interactive) ;; *) _SESSION_KIND="interactive" ;; esac
echo "SESSION_KIND: $_SESSION_KIND"
_LAKE_SEEN=$([ -f ~/.gstack/.completeness-intro-seen ] && echo "yes" || echo "no")
echo "LAKE_INTRO: $_LAKE_SEEN"
_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || true)
@ -131,7 +134,7 @@ In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`co
## Skill Invocation During Plan Mode
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If no AskUserQuestion variant is callable, use the normal chat fallback from the AskUserQuestion Format rule and stop for the user's reply. At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If AskUserQuestion is unavailable or a call fails, follow the AskUserQuestion Format failure fallback: `headless` → BLOCKED; `interactive` → the prose fallback (also satisfies end-of-turn). At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If `PROACTIVE` is `"false"`, do not auto-invoke or proactively suggest skills. If a skill seems useful, ask: "I think /skillname might help here — want me to run it?"
@ -166,7 +169,7 @@ touch ~/.gstack/.writing-style-prompted
Skip if `WRITING_STYLE_PENDING` is `no`.
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Lake** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Ocean** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
```bash
open https://garryslist.org/posts/boil-the-ocean
@ -303,11 +306,31 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
**Rule:** if any `mcp__*__AskUserQuestion` variant is in your tool list, prefer it. Hosts may disable native AUQ via `--disallowedTools AskUserQuestion` (Conductor does, by default) and route through their MCP variant; calling native there silently fails. Same questions/options shape; same decision-brief format applies.
**If no AskUserQuestion variant appears in your tool list:** fall back to the host's normal user-facing chat question. Render the same decision brief in prose, stop, and wait for the user's reply. This fallback is for interactive chat hosts that can ask the user normally but do not expose an AUQ tool (for example Codex API sessions). Do not write decisions to the plan file as a substitute, and do not silently auto-decide (only `/plan-tune` AUTO_DECIDE opt-ins authorize auto-picking).
If AskUserQuestion is unavailable (no variant in your tool list) OR a call to it fails, do NOT silently auto-decide or write the decision to the plan file as a substitute. Follow the **failure fallback** below.
### When AskUserQuestion is unavailable or a call fails
Tell three outcomes apart:
1. **Auto-decide denial (NOT a failure).** The result contains `[plan-tune auto-decide] <id> → <option>` — the preference hook working as designed. Proceed with that option. Do NOT retry, do NOT fall back to prose.
2. **Genuine failure** — no variant in your tool list, OR the variant is present but the call returns an error / missing result (MCP transport error, empty result, host bug — e.g. Conductor's MCP AskUserQuestion is flaky and returns `[Tool result missing due to internal error]`).
- If it was present and **errored** (not absent), retry the SAME call **once** — but only if no answer could have surfaced (a missing-result error can arrive after the user already saw the question; retrying would double-prompt, so if it may have reached them, treat as pending, don't retry).
- Then branch on `SESSION_KIND` (echoed by the preamble; empty/absent ⇒ `interactive`):
- `spawned` → defer to the **Spawned session** block: auto-choose the recommended option. Never prose, never BLOCKED.
- `headless``BLOCKED — AskUserQuestion unavailable`; stop and wait (no human can answer).
- `interactive`**prose fallback** (below).
**Prose fallback — render the decision brief as a markdown message, not a tool call.** Same information as the tool format below, different structure (paragraphs, not ✅/❌ bullets). It MUST surface this triad:
1. **A clear ELI10 of the issue itself** — plain English on what's being decided and why it matters (the question, not per-choice), naming the stakes. Lead with it.
2. **Completeness scores per choice** — explicit `Completeness: X/10` on EACH choice (10 complete, 7 happy-path, 3 shortcut); use the kind-note when options differ in kind not coverage, but never silently drop the score.
3. **The recommendation and why** — a `Recommendation: <choice> because <reason>` line plus the `(recommended)` marker on that choice.
Layout: a `D<N>` title + a one-line note that AskUserQuestion failed and to reply with a letter; the issue ELI10; the Recommendation line; then ONE paragraph per choice carrying its `(recommended)` marker, its `Completeness: X/10`, and 2-4 sentences of reasoning — never a bare bullet list; a closing `Net:` line. Split chains / 5+ options: one prose block per per-option call, in sequence. Then STOP and wait — the user's typed answer is the decision. In plan mode this satisfies end-of-turn like a tool call.
### Format
Every AskUserQuestion is a decision brief. Send it as tool_use when an AskUserQuestion tool exists; otherwise use the normal chat fallback above and wait for the user's reply.
Every AskUserQuestion is a decision brief and must be sent as tool_use, not prose — unless the documented failure fallback above applies (interactive session + the call is unavailable/erroring), in which case the prose fallback is the correct output.
```
D<N><one-line question title>
@ -387,7 +410,7 @@ Before calling AskUserQuestion, verify:
- [ ] (recommended) label on one option (even for neutral-posture)
- [ ] Dual-scale effort labels on effort-bearing options (human / CC)
- [ ] Net line closes the decision
- [ ] You are calling the tool when available; otherwise you are using the normal chat fallback and stopping for the user's reply
- [ ] You are calling the tool, not writing prose — unless the documented failure fallback applies (then: prose with the mandatory triad — issue ELI10, per-choice Completeness, Recommendation + `(recommended)` — and a "reply with a letter" instruction, then STOP)
- [ ] Non-ASCII characters (CJK / accents) written directly, NOT \u-escaped
- [ ] If you had 5+ options, you split (or batched into ≤4-groups) — did NOT drop any
- [ ] If you split, you checked dependencies between options before firing the chain
@ -574,12 +597,19 @@ if [ -d "$_PROJ" ]; then
fi
_LATEST_CP=$(find "$_PROJ/checkpoints" -name "*.md" -type f 2>/dev/null | xargs ls -t 2>/dev/null | head -1)
[ -n "$_LATEST_CP" ] && echo "LATEST_CHECKPOINT: $_LATEST_CP"
if [ -f "$_PROJ/decisions.active.json" ]; then
echo "--- ACTIVE DECISIONS (recent, scope-relevant) ---"
~/.claude/skills/gstack/bin/gstack-decision-search --recent 5 2>/dev/null
echo "--- END DECISIONS ---"
fi
echo "--- END ARTIFACTS ---"
fi
```
If artifacts are listed, read the newest useful one. If `LAST_SESSION` or `LATEST_CHECKPOINT` appears, give a 2-sentence welcome back summary. If `RECENT_PATTERN` clearly implies a next skill, suggest it once.
**Cross-session decisions.** If `ACTIVE DECISIONS` are listed, treat them as prior settled calls with their rationale — do not silently re-litigate them; if you're about to reverse one, say so explicitly. Reach for `~/.claude/skills/gstack/bin/gstack-decision-search` whenever a question touches a past decision ("what did we decide / why / did we try"). When you or the user make a DURABLE decision (architecture, scope, tool/vendor choice, or a reversal) — NOT a turn-level or trivial choice — log it with `~/.claude/skills/gstack/bin/gstack-decision-log` (`--supersede <id>` for a reversal). Reliable and local; gbrain not required.
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format is structure; this is prose quality.
@ -594,9 +624,9 @@ Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format i
Curated jargon list lives at `~/.claude/skills/gstack/scripts/jargon-list.json` (80+ terms). On the first jargon term you encounter this session, Read that file once; treat the `terms` array as the canonical list. The list is repo-owned and may grow between releases.
## Completeness Principle — Boil the Lake
## Completeness Principle — Boil the Ocean
AI makes completeness cheap. Recommend complete lakes (tests, edge cases, error paths); flag oceans (rewrites, multi-quarter migrations).
AI makes completeness cheap, so the complete thing is the goal. Recommend full coverage (tests, edge cases, error paths) — boil the ocean one lake at a time. The only thing out of scope is genuinely unrelated work (rewrites, multi-quarter migrations); flag that as separate scope, never as an excuse for a shortcut.
When options differ in coverage, include `Completeness: X/10` (10 = all edge cases, 7 = happy path, 3 = shortcut). When options differ in kind, write: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate scores.
@ -1146,14 +1176,24 @@ Produce this markdown table:
| DX Review | \`/plan-devex-review\` | Developer experience gaps | {runs} | {status} | {findings} |
\`\`\`
Below the table, add these lines (omit any that are empty/not applicable):
Below the table, add these lines. **CODEX** and **CROSS-MODEL** are optional (omit when
empty); **VERDICT** is always present:
- **CODEX:** (only if codex-review ran) — one-line summary of codex fixes
- **CROSS-MODEL:** (only if both Claude and Codex reviews exist) — overlap analysis
- **UNRESOLVED:** total unresolved decisions across all reviews
- **VERDICT:** list reviews that are CLEAR (e.g., "CEO + ENG CLEARED — ready to implement").
If Eng Review is not CLEAR and not skipped globally, append "eng review required".
**Unresolved-decisions status (MANDATORY — never omitted; the report's final non-whitespace
line).** After VERDICT, end the report (content under the \`## GSTACK REVIEW REPORT\`
heading — a bold label, never a new \`## \` heading; exempt from the "omit when empty"
rule) with exactly one: the exact unbolded line \`NO UNRESOLVED DECISIONS\` (a bolded one
does NOT count), OR a \`**UNRESOLVED DECISIONS:**\` header + one bullet per open item
(last bullet = final line; add \`+ N unresolved from prior reviews\` only when N > 0).
This avoids double-counting: list THIS review's open items from context; for prior reviews
sum \`unresolved\` over the latest fresh row per skill (dashboard 7-day window) after you
DROP the current skill's row; emit the sentinel only when both are zero.
### Write to the plan file
**PLAN MODE EXCEPTION — ALWAYS RUN:** This writes to the plan file, which is the one

View File

@ -127,6 +127,38 @@ required for our hook to fire there.
(PostToolUse hooks can also set `additionalContext` to append to the tool
result; we don't need this for v1 capture.)
## PostToolUse on tool error (AUQ-failure fallback, OV3:B) — UNVERIFIED
The AUQ-failure prose fallback adds a defensive PostToolUse hook
(`hosts/claude/hooks/auq-error-fallback-hook.ts`) that, when an AskUserQuestion
call returns an error / missing result, injects `additionalContext` reminding the
model to run the prose fallback per `SESSION_KIND`. It uses the same
`additionalContext` mechanism documented above.
**Open question we could NOT settle in a harness:** does Claude Code invoke
PostToolUse hooks when an MCP tool call returns a transport/missing-result error
(the Conductor bug surfaces `[Tool result missing due to internal error]`)? The
docs above cover PostToolUse on *success*. We could not force the Conductor
internal MCP failure on demand to observe it.
**Decision (OV3:B = A):** build the hook defensively anyway.
- It is **inert on success** (only fires when `isErrorResponse(tool_response)` is
true) and **inert if the platform never invokes it** on the error path.
- The prompt-level fallback in `generate-ask-user-format.ts` covers the case
regardless — the hook is a reliability *layer*, not the mechanism.
- Its decision logic is unit-tested deterministically
(`test/auq-error-fallback-hook.test.ts`): given a synthetic error `tool_response`
+ each `SESSION_KIND`, it emits the correct directive; given a real answer it
defers.
**Recommended manual / partial spike (to close the gap later):** register a
throwaway PostToolUse hook that logs on fire, then (a) trigger a normal tool
error (e.g. a failing `Bash` call) to confirm PostToolUse fires on tool errors at
all, and (b) reproduce the Conductor MCP AUQ failure and check the log. If (b)
confirms a fire, promote the hook from "defensive/inert" to "verified". Until
then, treat the runtime layer as best-effort and the prompt-level fallback as the
guaranteed path.
## Settings.json snippet for T8 hook installer
```json

View File

@ -52,6 +52,9 @@ echo "SKILL_PREFIX: $_SKILL_PREFIX"
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
REPO_MODE=${REPO_MODE:-unknown}
echo "REPO_MODE: $REPO_MODE"
_SESSION_KIND=$(~/.claude/skills/gstack/bin/gstack-session-kind 2>/dev/null || echo "interactive")
case "$_SESSION_KIND" in spawned|headless|interactive) ;; *) _SESSION_KIND="interactive" ;; esac
echo "SESSION_KIND: $_SESSION_KIND"
_LAKE_SEEN=$([ -f ~/.gstack/.completeness-intro-seen ] && echo "yes" || echo "no")
echo "LAKE_INTRO: $_LAKE_SEEN"
_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || true)
@ -131,7 +134,7 @@ In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`co
## Skill Invocation During Plan Mode
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If no AskUserQuestion variant is callable, use the normal chat fallback from the AskUserQuestion Format rule and stop for the user's reply. At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If AskUserQuestion is unavailable or a call fails, follow the AskUserQuestion Format failure fallback: `headless` → BLOCKED; `interactive` → the prose fallback (also satisfies end-of-turn). At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If `PROACTIVE` is `"false"`, do not auto-invoke or proactively suggest skills. If a skill seems useful, ask: "I think /skillname might help here — want me to run it?"
@ -166,7 +169,7 @@ touch ~/.gstack/.writing-style-prompted
Skip if `WRITING_STYLE_PENDING` is `no`.
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Lake** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Ocean** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
```bash
open https://garryslist.org/posts/boil-the-ocean
@ -303,11 +306,31 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
**Rule:** if any `mcp__*__AskUserQuestion` variant is in your tool list, prefer it. Hosts may disable native AUQ via `--disallowedTools AskUserQuestion` (Conductor does, by default) and route through their MCP variant; calling native there silently fails. Same questions/options shape; same decision-brief format applies.
**If no AskUserQuestion variant appears in your tool list:** fall back to the host's normal user-facing chat question. Render the same decision brief in prose, stop, and wait for the user's reply. This fallback is for interactive chat hosts that can ask the user normally but do not expose an AUQ tool (for example Codex API sessions). Do not write decisions to the plan file as a substitute, and do not silently auto-decide (only `/plan-tune` AUTO_DECIDE opt-ins authorize auto-picking).
If AskUserQuestion is unavailable (no variant in your tool list) OR a call to it fails, do NOT silently auto-decide or write the decision to the plan file as a substitute. Follow the **failure fallback** below.
### When AskUserQuestion is unavailable or a call fails
Tell three outcomes apart:
1. **Auto-decide denial (NOT a failure).** The result contains `[plan-tune auto-decide] <id> → <option>` — the preference hook working as designed. Proceed with that option. Do NOT retry, do NOT fall back to prose.
2. **Genuine failure** — no variant in your tool list, OR the variant is present but the call returns an error / missing result (MCP transport error, empty result, host bug — e.g. Conductor's MCP AskUserQuestion is flaky and returns `[Tool result missing due to internal error]`).
- If it was present and **errored** (not absent), retry the SAME call **once** — but only if no answer could have surfaced (a missing-result error can arrive after the user already saw the question; retrying would double-prompt, so if it may have reached them, treat as pending, don't retry).
- Then branch on `SESSION_KIND` (echoed by the preamble; empty/absent ⇒ `interactive`):
- `spawned` → defer to the **Spawned session** block: auto-choose the recommended option. Never prose, never BLOCKED.
- `headless``BLOCKED — AskUserQuestion unavailable`; stop and wait (no human can answer).
- `interactive`**prose fallback** (below).
**Prose fallback — render the decision brief as a markdown message, not a tool call.** Same information as the tool format below, different structure (paragraphs, not ✅/❌ bullets). It MUST surface this triad:
1. **A clear ELI10 of the issue itself** — plain English on what's being decided and why it matters (the question, not per-choice), naming the stakes. Lead with it.
2. **Completeness scores per choice** — explicit `Completeness: X/10` on EACH choice (10 complete, 7 happy-path, 3 shortcut); use the kind-note when options differ in kind not coverage, but never silently drop the score.
3. **The recommendation and why** — a `Recommendation: <choice> because <reason>` line plus the `(recommended)` marker on that choice.
Layout: a `D<N>` title + a one-line note that AskUserQuestion failed and to reply with a letter; the issue ELI10; the Recommendation line; then ONE paragraph per choice carrying its `(recommended)` marker, its `Completeness: X/10`, and 2-4 sentences of reasoning — never a bare bullet list; a closing `Net:` line. Split chains / 5+ options: one prose block per per-option call, in sequence. Then STOP and wait — the user's typed answer is the decision. In plan mode this satisfies end-of-turn like a tool call.
### Format
Every AskUserQuestion is a decision brief. Send it as tool_use when an AskUserQuestion tool exists; otherwise use the normal chat fallback above and wait for the user's reply.
Every AskUserQuestion is a decision brief and must be sent as tool_use, not prose — unless the documented failure fallback above applies (interactive session + the call is unavailable/erroring), in which case the prose fallback is the correct output.
```
D<N><one-line question title>
@ -387,7 +410,7 @@ Before calling AskUserQuestion, verify:
- [ ] (recommended) label on one option (even for neutral-posture)
- [ ] Dual-scale effort labels on effort-bearing options (human / CC)
- [ ] Net line closes the decision
- [ ] You are calling the tool when available; otherwise you are using the normal chat fallback and stopping for the user's reply
- [ ] You are calling the tool, not writing prose — unless the documented failure fallback applies (then: prose with the mandatory triad — issue ELI10, per-choice Completeness, Recommendation + `(recommended)` — and a "reply with a letter" instruction, then STOP)
- [ ] Non-ASCII characters (CJK / accents) written directly, NOT \u-escaped
- [ ] If you had 5+ options, you split (or batched into ≤4-groups) — did NOT drop any
- [ ] If you split, you checked dependencies between options before firing the chain
@ -574,12 +597,19 @@ if [ -d "$_PROJ" ]; then
fi
_LATEST_CP=$(find "$_PROJ/checkpoints" -name "*.md" -type f 2>/dev/null | xargs ls -t 2>/dev/null | head -1)
[ -n "$_LATEST_CP" ] && echo "LATEST_CHECKPOINT: $_LATEST_CP"
if [ -f "$_PROJ/decisions.active.json" ]; then
echo "--- ACTIVE DECISIONS (recent, scope-relevant) ---"
~/.claude/skills/gstack/bin/gstack-decision-search --recent 5 2>/dev/null
echo "--- END DECISIONS ---"
fi
echo "--- END ARTIFACTS ---"
fi
```
If artifacts are listed, read the newest useful one. If `LAST_SESSION` or `LATEST_CHECKPOINT` appears, give a 2-sentence welcome back summary. If `RECENT_PATTERN` clearly implies a next skill, suggest it once.
**Cross-session decisions.** If `ACTIVE DECISIONS` are listed, treat them as prior settled calls with their rationale — do not silently re-litigate them; if you're about to reverse one, say so explicitly. Reach for `~/.claude/skills/gstack/bin/gstack-decision-search` whenever a question touches a past decision ("what did we decide / why / did we try"). When you or the user make a DURABLE decision (architecture, scope, tool/vendor choice, or a reversal) — NOT a turn-level or trivial choice — log it with `~/.claude/skills/gstack/bin/gstack-decision-log` (`--supersede <id>` for a reversal). Reliable and local; gbrain not required.
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format is structure; this is prose quality.
@ -594,9 +624,9 @@ Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format i
Curated jargon list lives at `~/.claude/skills/gstack/scripts/jargon-list.json` (80+ terms). On the first jargon term you encounter this session, Read that file once; treat the `terms` array as the canonical list. The list is repo-owned and may grow between releases.
## Completeness Principle — Boil the Lake
## Completeness Principle — Boil the Ocean
AI makes completeness cheap. Recommend complete lakes (tests, edge cases, error paths); flag oceans (rewrites, multi-quarter migrations).
AI makes completeness cheap, so the complete thing is the goal. Recommend full coverage (tests, edge cases, error paths) — boil the ocean one lake at a time. The only thing out of scope is genuinely unrelated work (rewrites, multi-quarter migrations); flag that as separate scope, never as an excuse for a shortcut.
When options differ in coverage, include `Completeness: X/10` (10 = all edge cases, 7 = happy path, 3 = shortcut). When options differ in kind, write: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate scores.
@ -1177,4 +1207,4 @@ Documentation generated:
- **Voice: friendly, concrete, user-forward.** Write like you're explaining to a smart person
who hasn't seen the code. Never corporate, never academic.
- **Completeness over minimalism.** AI makes comprehensive documentation cheap. Don't write
"minimal viable docs" — write complete docs. Boil the lake.
"minimal viable docs" — write complete docs. Boil the ocean.

View File

@ -457,4 +457,4 @@ Documentation generated:
- **Voice: friendly, concrete, user-forward.** Write like you're explaining to a smart person
who hasn't seen the code. Never corporate, never academic.
- **Completeness over minimalism.** AI makes comprehensive documentation cheap. Don't write
"minimal viable docs" — write complete docs. Boil the lake.
"minimal viable docs" — write complete docs. Boil the ocean.

View File

@ -50,6 +50,9 @@ echo "SKILL_PREFIX: $_SKILL_PREFIX"
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
REPO_MODE=${REPO_MODE:-unknown}
echo "REPO_MODE: $REPO_MODE"
_SESSION_KIND=$(~/.claude/skills/gstack/bin/gstack-session-kind 2>/dev/null || echo "interactive")
case "$_SESSION_KIND" in spawned|headless|interactive) ;; *) _SESSION_KIND="interactive" ;; esac
echo "SESSION_KIND: $_SESSION_KIND"
_LAKE_SEEN=$([ -f ~/.gstack/.completeness-intro-seen ] && echo "yes" || echo "no")
echo "LAKE_INTRO: $_LAKE_SEEN"
_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || true)
@ -129,7 +132,7 @@ In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`co
## Skill Invocation During Plan Mode
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If no AskUserQuestion variant is callable, use the normal chat fallback from the AskUserQuestion Format rule and stop for the user's reply. At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If AskUserQuestion is unavailable or a call fails, follow the AskUserQuestion Format failure fallback: `headless` → BLOCKED; `interactive` → the prose fallback (also satisfies end-of-turn). At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If `PROACTIVE` is `"false"`, do not auto-invoke or proactively suggest skills. If a skill seems useful, ask: "I think /skillname might help here — want me to run it?"
@ -164,7 +167,7 @@ touch ~/.gstack/.writing-style-prompted
Skip if `WRITING_STYLE_PENDING` is `no`.
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Lake** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Ocean** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
```bash
open https://garryslist.org/posts/boil-the-ocean
@ -301,11 +304,31 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
**Rule:** if any `mcp__*__AskUserQuestion` variant is in your tool list, prefer it. Hosts may disable native AUQ via `--disallowedTools AskUserQuestion` (Conductor does, by default) and route through their MCP variant; calling native there silently fails. Same questions/options shape; same decision-brief format applies.
**If no AskUserQuestion variant appears in your tool list:** fall back to the host's normal user-facing chat question. Render the same decision brief in prose, stop, and wait for the user's reply. This fallback is for interactive chat hosts that can ask the user normally but do not expose an AUQ tool (for example Codex API sessions). Do not write decisions to the plan file as a substitute, and do not silently auto-decide (only `/plan-tune` AUTO_DECIDE opt-ins authorize auto-picking).
If AskUserQuestion is unavailable (no variant in your tool list) OR a call to it fails, do NOT silently auto-decide or write the decision to the plan file as a substitute. Follow the **failure fallback** below.
### When AskUserQuestion is unavailable or a call fails
Tell three outcomes apart:
1. **Auto-decide denial (NOT a failure).** The result contains `[plan-tune auto-decide] <id> → <option>` — the preference hook working as designed. Proceed with that option. Do NOT retry, do NOT fall back to prose.
2. **Genuine failure** — no variant in your tool list, OR the variant is present but the call returns an error / missing result (MCP transport error, empty result, host bug — e.g. Conductor's MCP AskUserQuestion is flaky and returns `[Tool result missing due to internal error]`).
- If it was present and **errored** (not absent), retry the SAME call **once** — but only if no answer could have surfaced (a missing-result error can arrive after the user already saw the question; retrying would double-prompt, so if it may have reached them, treat as pending, don't retry).
- Then branch on `SESSION_KIND` (echoed by the preamble; empty/absent ⇒ `interactive`):
- `spawned` → defer to the **Spawned session** block: auto-choose the recommended option. Never prose, never BLOCKED.
- `headless``BLOCKED — AskUserQuestion unavailable`; stop and wait (no human can answer).
- `interactive`**prose fallback** (below).
**Prose fallback — render the decision brief as a markdown message, not a tool call.** Same information as the tool format below, different structure (paragraphs, not ✅/❌ bullets). It MUST surface this triad:
1. **A clear ELI10 of the issue itself** — plain English on what's being decided and why it matters (the question, not per-choice), naming the stakes. Lead with it.
2. **Completeness scores per choice** — explicit `Completeness: X/10` on EACH choice (10 complete, 7 happy-path, 3 shortcut); use the kind-note when options differ in kind not coverage, but never silently drop the score.
3. **The recommendation and why** — a `Recommendation: <choice> because <reason>` line plus the `(recommended)` marker on that choice.
Layout: a `D<N>` title + a one-line note that AskUserQuestion failed and to reply with a letter; the issue ELI10; the Recommendation line; then ONE paragraph per choice carrying its `(recommended)` marker, its `Completeness: X/10`, and 2-4 sentences of reasoning — never a bare bullet list; a closing `Net:` line. Split chains / 5+ options: one prose block per per-option call, in sequence. Then STOP and wait — the user's typed answer is the decision. In plan mode this satisfies end-of-turn like a tool call.
### Format
Every AskUserQuestion is a decision brief. Send it as tool_use when an AskUserQuestion tool exists; otherwise use the normal chat fallback above and wait for the user's reply.
Every AskUserQuestion is a decision brief and must be sent as tool_use, not prose — unless the documented failure fallback above applies (interactive session + the call is unavailable/erroring), in which case the prose fallback is the correct output.
```
D<N><one-line question title>
@ -385,7 +408,7 @@ Before calling AskUserQuestion, verify:
- [ ] (recommended) label on one option (even for neutral-posture)
- [ ] Dual-scale effort labels on effort-bearing options (human / CC)
- [ ] Net line closes the decision
- [ ] You are calling the tool when available; otherwise you are using the normal chat fallback and stopping for the user's reply
- [ ] You are calling the tool, not writing prose — unless the documented failure fallback applies (then: prose with the mandatory triad — issue ELI10, per-choice Completeness, Recommendation + `(recommended)` — and a "reply with a letter" instruction, then STOP)
- [ ] Non-ASCII characters (CJK / accents) written directly, NOT \u-escaped
- [ ] If you had 5+ options, you split (or batched into ≤4-groups) — did NOT drop any
- [ ] If you split, you checked dependencies between options before firing the chain
@ -572,12 +595,19 @@ if [ -d "$_PROJ" ]; then
fi
_LATEST_CP=$(find "$_PROJ/checkpoints" -name "*.md" -type f 2>/dev/null | xargs ls -t 2>/dev/null | head -1)
[ -n "$_LATEST_CP" ] && echo "LATEST_CHECKPOINT: $_LATEST_CP"
if [ -f "$_PROJ/decisions.active.json" ]; then
echo "--- ACTIVE DECISIONS (recent, scope-relevant) ---"
~/.claude/skills/gstack/bin/gstack-decision-search --recent 5 2>/dev/null
echo "--- END DECISIONS ---"
fi
echo "--- END ARTIFACTS ---"
fi
```
If artifacts are listed, read the newest useful one. If `LAST_SESSION` or `LATEST_CHECKPOINT` appears, give a 2-sentence welcome back summary. If `RECENT_PATTERN` clearly implies a next skill, suggest it once.
**Cross-session decisions.** If `ACTIVE DECISIONS` are listed, treat them as prior settled calls with their rationale — do not silently re-litigate them; if you're about to reverse one, say so explicitly. Reach for `~/.claude/skills/gstack/bin/gstack-decision-search` whenever a question touches a past decision ("what did we decide / why / did we try"). When you or the user make a DURABLE decision (architecture, scope, tool/vendor choice, or a reversal) — NOT a turn-level or trivial choice — log it with `~/.claude/skills/gstack/bin/gstack-decision-log` (`--supersede <id>` for a reversal). Reliable and local; gbrain not required.
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format is structure; this is prose quality.
@ -592,9 +622,9 @@ Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format i
Curated jargon list lives at `~/.claude/skills/gstack/scripts/jargon-list.json` (80+ terms). On the first jargon term you encounter this session, Read that file once; treat the `terms` array as the canonical list. The list is repo-owned and may grow between releases.
## Completeness Principle — Boil the Lake
## Completeness Principle — Boil the Ocean
AI makes completeness cheap. Recommend complete lakes (tests, edge cases, error paths); flag oceans (rewrites, multi-quarter migrations).
AI makes completeness cheap, so the complete thing is the goal. Recommend full coverage (tests, edge cases, error paths) — boil the ocean one lake at a time. The only thing out of scope is genuinely unrelated work (rewrites, multi-quarter migrations); flag that as separate scope, never as an excuse for a shortcut.
When options differ in coverage, include `Completeness: X/10` (10 = all edge cases, 7 = happy path, 3 = shortcut). When options differ in kind, write: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate scores.
@ -780,6 +810,17 @@ subjective decisions.
---
## Section index — Read each section when its situation applies
This skill is a decision-tree skeleton. The steps below point to on-demand
sections. Read a section in full before doing its step; do not work from memory.
| When | Read this section |
|------|-------------------|
| auditing each doc file and applying updates, polishing CHANGELOG voice, checking cross-doc consistency, cleaning up TODOS, the VERSION bump, and committing (Steps 2-9, after the coverage map in Step 1.5) | `sections/release-body.md` |
---
## Step 1: Pre-flight & Diff Analysis
1. Check the current branch. If on the base branch, **abort**: "You're on the base branch. Run from a feature branch."
@ -856,364 +897,8 @@ When significant gaps are found, suggest running `/document-generate` to fill th
---
## Step 2: Per-File Documentation Audit
Read each documentation file and cross-reference it against the diff. Use these generic heuristics
(adapt to whatever project you're in — these are not gstack-specific):
**README.md:**
- Does it describe all features and capabilities visible in the diff?
- Are install/setup instructions consistent with the changes?
- Are examples, demos, and usage descriptions still valid?
- Are troubleshooting steps still accurate?
**ARCHITECTURE.md:**
- Do ASCII diagrams and component descriptions match the current code?
- Are design decisions and "why" explanations still accurate?
- Be conservative — only update things clearly contradicted by the diff. Architecture docs
describe things unlikely to change frequently.
**CONTRIBUTING.md — New contributor smoke test:**
- Walk through the setup instructions as if you are a brand new contributor.
- Are the listed commands accurate? Would each step succeed?
- Do test tier descriptions match the current test infrastructure?
- Are workflow descriptions (dev setup, operational learnings, etc.) current?
- Flag anything that would fail or confuse a first-time contributor.
**CLAUDE.md / project instructions:**
- Does the project structure section match the actual file tree?
- Are listed commands and scripts accurate?
- Do build/test instructions match what's in package.json (or equivalent)?
**Any other .md files:**
- Read the file, determine its purpose and audience.
- Cross-reference against the diff to check if it contradicts anything the file says.
For each file, classify needed updates as:
- **Auto-update** — Factual corrections clearly warranted by the diff: adding an item to a
table, updating a file path, fixing a count, updating a project structure tree.
- **Ask user** — Narrative changes, section removal, security model changes, large rewrites
(more than ~10 lines in one section), ambiguous relevance, adding entirely new sections.
---
## Step 3: Apply Auto-Updates
Make all clear, factual updates directly using the Edit tool.
For each file modified, output a one-line summary describing **what specifically changed** — not
just "Updated README.md" but "README.md: added /new-skill to skills table, updated skill count
from 9 to 10."
**Never auto-update:**
- README introduction or project positioning
- ARCHITECTURE philosophy or design rationale
- Security model descriptions
- Do not remove entire sections from any document
---
## Step 4: Ask About Risky/Questionable Changes
For each risky or questionable update identified in Step 2, use AskUserQuestion with:
- Context: project name, branch, which doc file, what we're reviewing
- The specific documentation decision
- `RECOMMENDATION: Choose [X] because [one-line reason]`
- Options including C) Skip — leave as-is
Apply approved changes immediately after each answer.
---
## Step 5: CHANGELOG Voice Polish
**CRITICAL — NEVER CLOBBER CHANGELOG ENTRIES.**
This step polishes voice. It does NOT rewrite, replace, or regenerate CHANGELOG content.
A real incident occurred where an agent replaced existing CHANGELOG entries when it should have
preserved them. This skill must NEVER do that.
**Rules:**
1. Read the entire CHANGELOG.md first. Understand what is already there.
2. Only modify wording within existing entries. Never delete, reorder, or replace entries.
3. Never regenerate a CHANGELOG entry from scratch. The entry was written by `/ship` from the
actual diff and commit history. It is the source of truth. You are polishing prose, not
rewriting history.
4. If an entry looks wrong or incomplete, use AskUserQuestion — do NOT silently fix it.
5. Use Edit tool with exact `old_string` matches — never use Write to overwrite CHANGELOG.md.
**If CHANGELOG was not modified in this branch:** skip this step.
**If CHANGELOG was modified in this branch**, review the entry for voice:
- **Sell test (Diataxis rubric):** Score each CHANGELOG entry 0-3:
- **1 point** — answers "What changed?" (reference: names the feature/fix)
- **1 point** — answers "Why should I care?" (explanation: user impact, pain removed)
- **1 point** — answers "How do I use it?" (how-to: command, flag, or link to docs)
- Entries scoring <2 need a rewrite. Entries scoring 3 are gold.
- Lead with what the user can now **do** — not implementation details.
- "You can now..." not "Refactored the..."
- Flag and rewrite any entry that reads like a commit message.
- Internal/contributor changes belong in a separate "### For contributors" subsection.
- Auto-fix minor voice adjustments. Use AskUserQuestion if a rewrite would alter meaning.
---
## Step 6: Cross-Doc Consistency & Discoverability Check
After auditing each file individually, do a cross-doc consistency pass:
1. Does the README's feature/capability list match what CLAUDE.md (or project instructions) describes?
2. Does ARCHITECTURE's component list match CONTRIBUTING's project structure description?
3. Does CHANGELOG's latest version match the VERSION file?
4. **Discoverability:** Is every documentation file reachable from README.md or CLAUDE.md? If
ARCHITECTURE.md exists but neither README nor CLAUDE.md links to it, flag it. Every doc
should be discoverable from one of the two entry-point files.
5. Flag any contradictions between documents. Auto-fix clear factual inconsistencies (e.g., a
version mismatch). Use AskUserQuestion for narrative contradictions.
---
## Step 7: TODOS.md Cleanup
This is a second pass that complements `/ship`'s Step 5.5. Read `review/TODOS-format.md` (if
available) for the canonical TODO item format.
If TODOS.md does not exist, skip this step.
1. **Completed items not yet marked:** Cross-reference the diff against open TODO items. If a
TODO is clearly completed by the changes in this branch, move it to the Completed section
with `**Completed:** vX.Y.Z.W (YYYY-MM-DD)`. Be conservative — only mark items with clear
evidence in the diff.
2. **Items needing description updates:** If a TODO references files or components that were
significantly changed, its description may be stale. Use AskUserQuestion to confirm whether
the TODO should be updated, completed, or left as-is.
3. **New deferred work:** Check the diff for `TODO`, `FIXME`, `HACK`, and `XXX` comments. For
each one that represents meaningful deferred work (not a trivial inline note), use
AskUserQuestion to ask whether it should be captured in TODOS.md.
---
## Step 8: VERSION Bump Question
**CRITICAL — NEVER BUMP VERSION WITHOUT ASKING.**
1. **If VERSION does not exist:** Skip silently.
2. Check if VERSION was already modified on this branch:
```bash
git diff <base>...HEAD -- VERSION
```
3. **If VERSION was NOT bumped:** Use AskUserQuestion:
- RECOMMENDATION: Choose C (Skip) because docs-only changes rarely warrant a version bump
- A) Bump PATCH (X.Y.Z+1) — if doc changes ship alongside code changes
- B) Bump MINOR (X.Y+1.0) — if this is a significant standalone release
- C) Skip — no version bump needed
4. **If VERSION was already bumped:** Do NOT skip silently. Instead, check whether the bump
still covers the full scope of changes on this branch:
a. Read the CHANGELOG entry for the current VERSION. What features does it describe?
b. Read the full diff (`git diff <base>...HEAD --stat` and `git diff <base>...HEAD --name-only`).
Are there significant changes (new features, new skills, new commands, major refactors)
that are NOT mentioned in the CHANGELOG entry for the current version?
c. **If the CHANGELOG entry covers everything:** Skip — output "VERSION: Already bumped to
vX.Y.Z, covers all changes."
d. **If there are significant uncovered changes:** Use AskUserQuestion explaining what the
current version covers vs what's new, and ask:
- RECOMMENDATION: Choose A because the new changes warrant their own version
- A) Bump to next patch (X.Y.Z+1) — give the new changes their own version
- B) Keep current version — add new changes to the existing CHANGELOG entry
- C) Skip — leave version as-is, handle later
The key insight: a VERSION bump set for "feature A" should not silently absorb "feature B"
if feature B is substantial enough to deserve its own version entry.
---
## Step 9: Commit & Output
**Empty check first:** Run `git status` (never use `-uall`). If no documentation files were
modified by any previous step, output "All documentation is up to date." and exit without
committing.
**Commit:**
1. Stage modified documentation files by name (never `git add -A` or `git add .`).
2. Create a single commit:
```bash
git commit -m "$(cat <<'EOF'
docs: update project documentation for vX.Y.Z.W
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
EOF
)"
```
3. Push to the current branch:
```bash
git push
```
**PR/MR body update (idempotent, race-safe):**
1. Read the existing PR/MR body into a PID-unique tempfile (use the platform detected in Step 0):
**If GitHub:**
```bash
gh pr view --json body -q .body > /tmp/gstack-pr-body-$$.md
```
**If GitLab:**
```bash
glab mr view -F json 2>/dev/null | python3 -c "import sys,json; print(json.load(sys.stdin).get('description',''))" > /tmp/gstack-pr-body-$$.md
```
2. If the tempfile already contains a `## Documentation` section, replace that section with the
updated content. If it does not contain one, append a `## Documentation` section at the end.
3. The Documentation section should include:
a. **Doc diff preview** — for each file modified, describe what specifically changed (e.g.,
"README.md: added /document-release to skills table, updated skill count from 9 to 10").
b. **Documentation debt** — if the coverage map from Step 1.5 found gaps, append a
`### Documentation Debt` subsection listing:
- Critical gaps: new public surface with zero documentation coverage
- Common gaps: features with reference-only coverage (no how-to or tutorial)
- Stale diagrams: architecture diagrams with entity names that drifted from the code
- Each item should include a one-line description of what's missing and which Diataxis
quadrant would fill it (e.g., "⚠️ `/new-skill` — has reference in AGENTS.md but no
how-to example in README")
If there are any documentation debt items, suggest adding a `docs-debt` label to the PR.
4. Redaction scan-at-sink, then write the updated body back. The body is already
in a temp file (`/tmp/gstack-pr-body-$$.md`); scan THAT file before editing so
the bytes scanned are the bytes sent:
```bash
REDACT_VIS=$(~/.claude/skills/gstack/bin/gstack-config get redact_repo_visibility 2>/dev/null)
[ -z "$REDACT_VIS" ] && REDACT_VIS=$(gh repo view --json visibility -q .visibility 2>/dev/null | tr 'A-Z' 'a-z')
~/.claude/skills/gstack/bin/gstack-redact --from-file /tmp/gstack-pr-body-$$.md --repo-visibility "${REDACT_VIS:-unknown}" --json
# exit 3 (HIGH) → do NOT edit, rotate+redact; exit 2 (MEDIUM) → confirm per finding.
```
**If GitHub:**
```bash
gh pr edit --body-file /tmp/gstack-pr-body-$$.md
```
**If GitLab:**
Read the contents of `/tmp/gstack-pr-body-$$.md` using the Read tool, then pass it to `glab mr update` using a heredoc to avoid shell metacharacter issues:
```bash
glab mr update -d "$(cat <<'MRBODY'
<paste the file contents here>
MRBODY
)"
```
5. Clean up the tempfile:
```bash
rm -f /tmp/gstack-pr-body-$$.md
```
6. If `gh pr view` / `glab mr view` fails (no PR/MR exists): skip with message "No PR/MR found — skipping body update."
7. If `gh pr edit` / `glab mr update` fails: warn "Could not update PR/MR body — documentation changes are in the
commit." and continue.
**PR/MR title sync (idempotent, always-on):**
PR titles must always start with `v<VERSION>` — same rule as `/ship`. If Step 8 bumped VERSION after `/ship` had already created the PR, the title is now stale. This sub-step fixes it.
1. Read the current VERSION:
```bash
V=$(cat VERSION 2>/dev/null | tr -d '[:space:]')
```
If `VERSION` does not exist or is empty, skip this sub-step entirely.
2. Read the current PR/MR title:
**If GitHub:**
```bash
CURRENT_TITLE=$(gh pr view --json title -q .title 2>/dev/null || true)
```
**If GitLab:**
```bash
CURRENT_TITLE=$(glab mr view -F json 2>/dev/null | jq -r .title 2>/dev/null || true)
```
If `CURRENT_TITLE` is empty (no open PR/MR), skip with message "No PR/MR found — skipping title sync."
3. Compute the corrected title using the shared helper (single source of truth — same one `/ship` uses):
```bash
NEW_TITLE=$(~/.claude/skills/gstack/bin/gstack-pr-title-rewrite.sh "$V" "$CURRENT_TITLE")
```
The helper handles three cases: title already correct (no-op), title has a different `v<X.Y.Z.W>` prefix (replace it), or title has no version prefix (prepend one).
4. If `NEW_TITLE` differs from `CURRENT_TITLE`, update it:
**If GitHub:**
```bash
gh pr edit --title "$NEW_TITLE"
```
**If GitLab:**
```bash
glab mr update -t "$NEW_TITLE"
```
5. If the edit command fails: warn "Could not update PR/MR title — documentation changes are still in the commit." and continue. Do not block on title sync failure.
**Structured doc health summary (final output):**
Output a scannable summary showing every documentation file's status:
```
Documentation health:
README.md [status] ([details])
ARCHITECTURE.md [status] ([details])
CONTRIBUTING.md [status] ([details])
CHANGELOG.md [status] ([details])
TODOS.md [status] ([details])
VERSION [status] ([details])
```
Where status is one of:
- Updated — with description of what changed
- Current — no changes needed
- Voice polished — wording adjusted
- Not bumped — user chose to skip
- Already bumped — version was set by /ship
- Skipped — file does not exist
If the coverage map from Step 1.5 identified any gaps, append:
```
Documentation coverage:
[entity] [reference] [how-to] [tutorial] [explanation]
/new-skill ✅ ❌ ❌ ❌
--new-flag ✅ ✅ ❌ ❌
Diagram drift:
ARCHITECTURE.md: "FooProcessor" renamed to "BarProcessor" in code — diagram may be stale
```
If all coverage is complete and no diagrams drifted, output: "Coverage: all shipped features have adequate documentation."
> **STOP.** Before auditing each doc file and applying updates, polishing CHANGELOG voice, checking cross-doc consistency, cleaning up TODOS, the VERSION bump, and committing (Steps 2-9, after the coverage map in Step 1.5), Read `~/.claude/skills/gstack/document-release/sections/release-body.md` and execute it
> in full. Do not work from memory — that section is the source of truth for this step.
---

View File

@ -59,6 +59,10 @@ subjective decisions.
---
{{SECTION_INDEX:document-release}}
---
## Step 1: Pre-flight & Diff Analysis
1. Check the current branch. If on the base branch, **abort**: "You're on the base branch. Run from a feature branch."
@ -135,364 +139,7 @@ When significant gaps are found, suggest running `/document-generate` to fill th
---
## Step 2: Per-File Documentation Audit
Read each documentation file and cross-reference it against the diff. Use these generic heuristics
(adapt to whatever project you're in — these are not gstack-specific):
**README.md:**
- Does it describe all features and capabilities visible in the diff?
- Are install/setup instructions consistent with the changes?
- Are examples, demos, and usage descriptions still valid?
- Are troubleshooting steps still accurate?
**ARCHITECTURE.md:**
- Do ASCII diagrams and component descriptions match the current code?
- Are design decisions and "why" explanations still accurate?
- Be conservative — only update things clearly contradicted by the diff. Architecture docs
describe things unlikely to change frequently.
**CONTRIBUTING.md — New contributor smoke test:**
- Walk through the setup instructions as if you are a brand new contributor.
- Are the listed commands accurate? Would each step succeed?
- Do test tier descriptions match the current test infrastructure?
- Are workflow descriptions (dev setup, operational learnings, etc.) current?
- Flag anything that would fail or confuse a first-time contributor.
**CLAUDE.md / project instructions:**
- Does the project structure section match the actual file tree?
- Are listed commands and scripts accurate?
- Do build/test instructions match what's in package.json (or equivalent)?
**Any other .md files:**
- Read the file, determine its purpose and audience.
- Cross-reference against the diff to check if it contradicts anything the file says.
For each file, classify needed updates as:
- **Auto-update** — Factual corrections clearly warranted by the diff: adding an item to a
table, updating a file path, fixing a count, updating a project structure tree.
- **Ask user** — Narrative changes, section removal, security model changes, large rewrites
(more than ~10 lines in one section), ambiguous relevance, adding entirely new sections.
---
## Step 3: Apply Auto-Updates
Make all clear, factual updates directly using the Edit tool.
For each file modified, output a one-line summary describing **what specifically changed** — not
just "Updated README.md" but "README.md: added /new-skill to skills table, updated skill count
from 9 to 10."
**Never auto-update:**
- README introduction or project positioning
- ARCHITECTURE philosophy or design rationale
- Security model descriptions
- Do not remove entire sections from any document
---
## Step 4: Ask About Risky/Questionable Changes
For each risky or questionable update identified in Step 2, use AskUserQuestion with:
- Context: project name, branch, which doc file, what we're reviewing
- The specific documentation decision
- `RECOMMENDATION: Choose [X] because [one-line reason]`
- Options including C) Skip — leave as-is
Apply approved changes immediately after each answer.
---
## Step 5: CHANGELOG Voice Polish
**CRITICAL — NEVER CLOBBER CHANGELOG ENTRIES.**
This step polishes voice. It does NOT rewrite, replace, or regenerate CHANGELOG content.
A real incident occurred where an agent replaced existing CHANGELOG entries when it should have
preserved them. This skill must NEVER do that.
**Rules:**
1. Read the entire CHANGELOG.md first. Understand what is already there.
2. Only modify wording within existing entries. Never delete, reorder, or replace entries.
3. Never regenerate a CHANGELOG entry from scratch. The entry was written by `/ship` from the
actual diff and commit history. It is the source of truth. You are polishing prose, not
rewriting history.
4. If an entry looks wrong or incomplete, use AskUserQuestion — do NOT silently fix it.
5. Use Edit tool with exact `old_string` matches — never use Write to overwrite CHANGELOG.md.
**If CHANGELOG was not modified in this branch:** skip this step.
**If CHANGELOG was modified in this branch**, review the entry for voice:
- **Sell test (Diataxis rubric):** Score each CHANGELOG entry 0-3:
- **1 point** — answers "What changed?" (reference: names the feature/fix)
- **1 point** — answers "Why should I care?" (explanation: user impact, pain removed)
- **1 point** — answers "How do I use it?" (how-to: command, flag, or link to docs)
- Entries scoring <2 need a rewrite. Entries scoring 3 are gold.
- Lead with what the user can now **do** — not implementation details.
- "You can now..." not "Refactored the..."
- Flag and rewrite any entry that reads like a commit message.
- Internal/contributor changes belong in a separate "### For contributors" subsection.
- Auto-fix minor voice adjustments. Use AskUserQuestion if a rewrite would alter meaning.
---
## Step 6: Cross-Doc Consistency & Discoverability Check
After auditing each file individually, do a cross-doc consistency pass:
1. Does the README's feature/capability list match what CLAUDE.md (or project instructions) describes?
2. Does ARCHITECTURE's component list match CONTRIBUTING's project structure description?
3. Does CHANGELOG's latest version match the VERSION file?
4. **Discoverability:** Is every documentation file reachable from README.md or CLAUDE.md? If
ARCHITECTURE.md exists but neither README nor CLAUDE.md links to it, flag it. Every doc
should be discoverable from one of the two entry-point files.
5. Flag any contradictions between documents. Auto-fix clear factual inconsistencies (e.g., a
version mismatch). Use AskUserQuestion for narrative contradictions.
---
## Step 7: TODOS.md Cleanup
This is a second pass that complements `/ship`'s Step 5.5. Read `review/TODOS-format.md` (if
available) for the canonical TODO item format.
If TODOS.md does not exist, skip this step.
1. **Completed items not yet marked:** Cross-reference the diff against open TODO items. If a
TODO is clearly completed by the changes in this branch, move it to the Completed section
with `**Completed:** vX.Y.Z.W (YYYY-MM-DD)`. Be conservative — only mark items with clear
evidence in the diff.
2. **Items needing description updates:** If a TODO references files or components that were
significantly changed, its description may be stale. Use AskUserQuestion to confirm whether
the TODO should be updated, completed, or left as-is.
3. **New deferred work:** Check the diff for `TODO`, `FIXME`, `HACK`, and `XXX` comments. For
each one that represents meaningful deferred work (not a trivial inline note), use
AskUserQuestion to ask whether it should be captured in TODOS.md.
---
## Step 8: VERSION Bump Question
**CRITICAL — NEVER BUMP VERSION WITHOUT ASKING.**
1. **If VERSION does not exist:** Skip silently.
2. Check if VERSION was already modified on this branch:
```bash
git diff <base>...HEAD -- VERSION
```
3. **If VERSION was NOT bumped:** Use AskUserQuestion:
- RECOMMENDATION: Choose C (Skip) because docs-only changes rarely warrant a version bump
- A) Bump PATCH (X.Y.Z+1) — if doc changes ship alongside code changes
- B) Bump MINOR (X.Y+1.0) — if this is a significant standalone release
- C) Skip — no version bump needed
4. **If VERSION was already bumped:** Do NOT skip silently. Instead, check whether the bump
still covers the full scope of changes on this branch:
a. Read the CHANGELOG entry for the current VERSION. What features does it describe?
b. Read the full diff (`git diff <base>...HEAD --stat` and `git diff <base>...HEAD --name-only`).
Are there significant changes (new features, new skills, new commands, major refactors)
that are NOT mentioned in the CHANGELOG entry for the current version?
c. **If the CHANGELOG entry covers everything:** Skip — output "VERSION: Already bumped to
vX.Y.Z, covers all changes."
d. **If there are significant uncovered changes:** Use AskUserQuestion explaining what the
current version covers vs what's new, and ask:
- RECOMMENDATION: Choose A because the new changes warrant their own version
- A) Bump to next patch (X.Y.Z+1) — give the new changes their own version
- B) Keep current version — add new changes to the existing CHANGELOG entry
- C) Skip — leave version as-is, handle later
The key insight: a VERSION bump set for "feature A" should not silently absorb "feature B"
if feature B is substantial enough to deserve its own version entry.
---
## Step 9: Commit & Output
**Empty check first:** Run `git status` (never use `-uall`). If no documentation files were
modified by any previous step, output "All documentation is up to date." and exit without
committing.
**Commit:**
1. Stage modified documentation files by name (never `git add -A` or `git add .`).
2. Create a single commit:
```bash
git commit -m "$(cat <<'EOF'
docs: update project documentation for vX.Y.Z.W
{{CO_AUTHOR_TRAILER}}
EOF
)"
```
3. Push to the current branch:
```bash
git push
```
**PR/MR body update (idempotent, race-safe):**
1. Read the existing PR/MR body into a PID-unique tempfile (use the platform detected in Step 0):
**If GitHub:**
```bash
gh pr view --json body -q .body > /tmp/gstack-pr-body-$$.md
```
**If GitLab:**
```bash
glab mr view -F json 2>/dev/null | python3 -c "import sys,json; print(json.load(sys.stdin).get('description',''))" > /tmp/gstack-pr-body-$$.md
```
2. If the tempfile already contains a `## Documentation` section, replace that section with the
updated content. If it does not contain one, append a `## Documentation` section at the end.
3. The Documentation section should include:
a. **Doc diff preview** — for each file modified, describe what specifically changed (e.g.,
"README.md: added /document-release to skills table, updated skill count from 9 to 10").
b. **Documentation debt** — if the coverage map from Step 1.5 found gaps, append a
`### Documentation Debt` subsection listing:
- Critical gaps: new public surface with zero documentation coverage
- Common gaps: features with reference-only coverage (no how-to or tutorial)
- Stale diagrams: architecture diagrams with entity names that drifted from the code
- Each item should include a one-line description of what's missing and which Diataxis
quadrant would fill it (e.g., "⚠️ `/new-skill` — has reference in AGENTS.md but no
how-to example in README")
If there are any documentation debt items, suggest adding a `docs-debt` label to the PR.
4. Redaction scan-at-sink, then write the updated body back. The body is already
in a temp file (`/tmp/gstack-pr-body-$$.md`); scan THAT file before editing so
the bytes scanned are the bytes sent:
```bash
REDACT_VIS=$(~/.claude/skills/gstack/bin/gstack-config get redact_repo_visibility 2>/dev/null)
[ -z "$REDACT_VIS" ] && REDACT_VIS=$(gh repo view --json visibility -q .visibility 2>/dev/null | tr 'A-Z' 'a-z')
~/.claude/skills/gstack/bin/gstack-redact --from-file /tmp/gstack-pr-body-$$.md --repo-visibility "${REDACT_VIS:-unknown}" --json
# exit 3 (HIGH) → do NOT edit, rotate+redact; exit 2 (MEDIUM) → confirm per finding.
```
**If GitHub:**
```bash
gh pr edit --body-file /tmp/gstack-pr-body-$$.md
```
**If GitLab:**
Read the contents of `/tmp/gstack-pr-body-$$.md` using the Read tool, then pass it to `glab mr update` using a heredoc to avoid shell metacharacter issues:
```bash
glab mr update -d "$(cat <<'MRBODY'
<paste the file contents here>
MRBODY
)"
```
5. Clean up the tempfile:
```bash
rm -f /tmp/gstack-pr-body-$$.md
```
6. If `gh pr view` / `glab mr view` fails (no PR/MR exists): skip with message "No PR/MR found — skipping body update."
7. If `gh pr edit` / `glab mr update` fails: warn "Could not update PR/MR body — documentation changes are in the
commit." and continue.
**PR/MR title sync (idempotent, always-on):**
PR titles must always start with `v<VERSION>` — same rule as `/ship`. If Step 8 bumped VERSION after `/ship` had already created the PR, the title is now stale. This sub-step fixes it.
1. Read the current VERSION:
```bash
V=$(cat VERSION 2>/dev/null | tr -d '[:space:]')
```
If `VERSION` does not exist or is empty, skip this sub-step entirely.
2. Read the current PR/MR title:
**If GitHub:**
```bash
CURRENT_TITLE=$(gh pr view --json title -q .title 2>/dev/null || true)
```
**If GitLab:**
```bash
CURRENT_TITLE=$(glab mr view -F json 2>/dev/null | jq -r .title 2>/dev/null || true)
```
If `CURRENT_TITLE` is empty (no open PR/MR), skip with message "No PR/MR found — skipping title sync."
3. Compute the corrected title using the shared helper (single source of truth — same one `/ship` uses):
```bash
NEW_TITLE=$(~/.claude/skills/gstack/bin/gstack-pr-title-rewrite.sh "$V" "$CURRENT_TITLE")
```
The helper handles three cases: title already correct (no-op), title has a different `v<X.Y.Z.W>` prefix (replace it), or title has no version prefix (prepend one).
4. If `NEW_TITLE` differs from `CURRENT_TITLE`, update it:
**If GitHub:**
```bash
gh pr edit --title "$NEW_TITLE"
```
**If GitLab:**
```bash
glab mr update -t "$NEW_TITLE"
```
5. If the edit command fails: warn "Could not update PR/MR title — documentation changes are still in the commit." and continue. Do not block on title sync failure.
**Structured doc health summary (final output):**
Output a scannable summary showing every documentation file's status:
```
Documentation health:
README.md [status] ([details])
ARCHITECTURE.md [status] ([details])
CONTRIBUTING.md [status] ([details])
CHANGELOG.md [status] ([details])
TODOS.md [status] ([details])
VERSION [status] ([details])
```
Where status is one of:
- Updated — with description of what changed
- Current — no changes needed
- Voice polished — wording adjusted
- Not bumped — user chose to skip
- Already bumped — version was set by /ship
- Skipped — file does not exist
If the coverage map from Step 1.5 identified any gaps, append:
```
Documentation coverage:
[entity] [reference] [how-to] [tutorial] [explanation]
/new-skill ✅ ❌ ❌ ❌
--new-flag ✅ ✅ ❌ ❌
Diagram drift:
ARCHITECTURE.md: "FooProcessor" renamed to "BarProcessor" in code — diagram may be stale
```
If all coverage is complete and no diagrams drifted, output: "Coverage: all shipped features have adequate documentation."
{{SECTION:release-body}}
---

View File

@ -0,0 +1,14 @@
{
"$schema": "https://gstack.dev/schemas/section-manifest.json",
"skill": "document-release",
"version": 1,
"note": "PASSIVE registry (v2 plan T9 / CM2). id/file/title/trigger text ONLY. The skeleton's decision-tree prose decides WHEN to read. No machine predicate here.",
"sections": [
{
"id": "release-body",
"file": "release-body.md",
"title": "Per-file audit, auto-updates, risky-change asks, CHANGELOG voice polish, cross-doc consistency, TODOS cleanup, VERSION bump, commit + PR body (Steps 2-9)",
"trigger": "auditing each doc file and applying updates, polishing CHANGELOG voice, checking cross-doc consistency, cleaning up TODOS, the VERSION bump, and committing (Steps 2-9, after the coverage map in Step 1.5)"
}
]
}

View File

@ -0,0 +1,477 @@
<!-- AUTO-GENERATED from release-body.md.tmpl — do not edit directly -->
<!-- Regenerate: bun run gen:skill-docs -->
## Step 2: Per-File Documentation Audit
Read each documentation file and cross-reference it against the diff. Use these generic heuristics
(adapt to whatever project you're in — these are not gstack-specific):
**README.md:**
- Does it describe all features and capabilities visible in the diff?
- Are install/setup instructions consistent with the changes?
- Are examples, demos, and usage descriptions still valid?
- Are troubleshooting steps still accurate?
**ARCHITECTURE.md:**
- Do ASCII diagrams and component descriptions match the current code?
- Are design decisions and "why" explanations still accurate?
- Be conservative — only update things clearly contradicted by the diff. Architecture docs
describe things unlikely to change frequently.
**CONTRIBUTING.md — New contributor smoke test:**
- Walk through the setup instructions as if you are a brand new contributor.
- Are the listed commands accurate? Would each step succeed?
- Do test tier descriptions match the current test infrastructure?
- Are workflow descriptions (dev setup, operational learnings, etc.) current?
- Flag anything that would fail or confuse a first-time contributor.
**CLAUDE.md / project instructions:**
- Does the project structure section match the actual file tree?
- Are listed commands and scripts accurate?
- Do build/test instructions match what's in package.json (or equivalent)?
**Any other .md files:**
- Read the file, determine its purpose and audience.
- Cross-reference against the diff to check if it contradicts anything the file says.
For each file, classify needed updates as:
- **Auto-update** — Factual corrections clearly warranted by the diff: adding an item to a
table, updating a file path, fixing a count, updating a project structure tree.
- **Ask user** — Narrative changes, section removal, security model changes, large rewrites
(more than ~10 lines in one section), ambiguous relevance, adding entirely new sections.
---
## Step 3: Apply Auto-Updates
Make all clear, factual updates directly using the Edit tool.
For each file modified, output a one-line summary describing **what specifically changed** — not
just "Updated README.md" but "README.md: added /new-skill to skills table, updated skill count
from 9 to 10."
**Never auto-update:**
- README introduction or project positioning
- ARCHITECTURE philosophy or design rationale
- Security model descriptions
- Do not remove entire sections from any document
---
## Step 4: Ask About Risky/Questionable Changes
For each risky or questionable update identified in Step 2, use AskUserQuestion with:
- Context: project name, branch, which doc file, what we're reviewing
- The specific documentation decision
- `RECOMMENDATION: Choose [X] because [one-line reason]`
- Options including C) Skip — leave as-is
Apply approved changes immediately after each answer.
---
## Step 5: CHANGELOG Voice Polish
**CRITICAL — NEVER CLOBBER CHANGELOG ENTRIES.**
This step polishes voice. It does NOT rewrite, replace, or regenerate CHANGELOG content.
A real incident occurred where an agent replaced existing CHANGELOG entries when it should have
preserved them. This skill must NEVER do that.
**Rules:**
1. Read the entire CHANGELOG.md first. Understand what is already there.
2. Only modify wording within existing entries. Never delete, reorder, or replace entries.
3. Never regenerate a CHANGELOG entry from scratch. The entry was written by `/ship` from the
actual diff and commit history. It is the source of truth. You are polishing prose, not
rewriting history.
4. If an entry looks wrong or incomplete, use AskUserQuestion — do NOT silently fix it.
5. Use Edit tool with exact `old_string` matches — never use Write to overwrite CHANGELOG.md.
**If CHANGELOG was not modified in this branch:** skip this step.
**If CHANGELOG was modified in this branch**, review the entry for voice:
- **Sell test (Diataxis rubric):** Score each CHANGELOG entry 0-3:
- **1 point** — answers "What changed?" (reference: names the feature/fix)
- **1 point** — answers "Why should I care?" (explanation: user impact, pain removed)
- **1 point** — answers "How do I use it?" (how-to: command, flag, or link to docs)
- Entries scoring <2 need a rewrite. Entries scoring 3 are gold.
- Lead with what the user can now **do** — not implementation details.
- "You can now..." not "Refactored the..."
- Flag and rewrite any entry that reads like a commit message.
- Internal/contributor changes belong in a separate "### For contributors" subsection.
- Auto-fix minor voice adjustments. Use AskUserQuestion if a rewrite would alter meaning.
---
## Step 6: Cross-Doc Consistency & Discoverability Check
After auditing each file individually, do a cross-doc consistency pass:
1. Does the README's feature/capability list match what CLAUDE.md (or project instructions) describes?
2. Does ARCHITECTURE's component list match CONTRIBUTING's project structure description?
3. Does CHANGELOG's latest version match the VERSION file?
4. **Discoverability:** Is every documentation file reachable from README.md or CLAUDE.md? If
ARCHITECTURE.md exists but neither README nor CLAUDE.md links to it, flag it. Every doc
should be discoverable from one of the two entry-point files.
5. Flag any contradictions between documents. Auto-fix clear factual inconsistencies (e.g., a
version mismatch). Use AskUserQuestion for narrative contradictions.
---
## Step 7: TODOS.md Cleanup
This is a second pass that complements `/ship`'s Step 5.5. Read `review/TODOS-format.md` (if
available) for the canonical TODO item format.
If TODOS.md does not exist, skip this step.
1. **Completed items not yet marked:** Cross-reference the diff against open TODO items. If a
TODO is clearly completed by the changes in this branch, move it to the Completed section
with `**Completed:** vX.Y.Z.W (YYYY-MM-DD)`. Be conservative — only mark items with clear
evidence in the diff.
2. **Items needing description updates:** If a TODO references files or components that were
significantly changed, its description may be stale. Use AskUserQuestion to confirm whether
the TODO should be updated, completed, or left as-is.
3. **New deferred work:** Check the diff for `TODO`, `FIXME`, `HACK`, and `XXX` comments. For
each one that represents meaningful deferred work (not a trivial inline note), use
AskUserQuestion to ask whether it should be captured in TODOS.md.
---
## Step 8: VERSION Bump Question
**CRITICAL — NEVER BUMP VERSION WITHOUT ASKING.**
1. **If VERSION does not exist:** Skip silently.
2. Check if VERSION was already modified on this branch:
```bash
git diff <base>...HEAD -- VERSION
```
3. **If VERSION was NOT bumped:** Use AskUserQuestion:
- RECOMMENDATION: Choose C (Skip) because docs-only changes rarely warrant a version bump
- A) Bump PATCH (X.Y.Z+1) — if doc changes ship alongside code changes
- B) Bump MINOR (X.Y+1.0) — if this is a significant standalone release
- C) Skip — no version bump needed
4. **If VERSION was already bumped:** Do NOT skip silently. Instead, check whether the bump
still covers the full scope of changes on this branch:
a. Read the CHANGELOG entry for the current VERSION. What features does it describe?
b. Read the full diff (`git diff <base>...HEAD --stat` and `git diff <base>...HEAD --name-only`).
Are there significant changes (new features, new skills, new commands, major refactors)
that are NOT mentioned in the CHANGELOG entry for the current version?
c. **If the CHANGELOG entry covers everything:** Skip — output "VERSION: Already bumped to
vX.Y.Z, covers all changes."
d. **If there are significant uncovered changes:** Use AskUserQuestion explaining what the
current version covers vs what's new, and ask:
- RECOMMENDATION: Choose A because the new changes warrant their own version
- A) Bump to next patch (X.Y.Z+1) — give the new changes their own version
- B) Keep current version — add new changes to the existing CHANGELOG entry
- C) Skip — leave version as-is, handle later
The key insight: a VERSION bump set for "feature A" should not silently absorb "feature B"
if feature B is substantial enough to deserve its own version entry.
---
## Step 9: Commit & Output
**Empty check first:** Run `git status` (never use `-uall`). If no documentation files were
modified by any previous step, output "All documentation is up to date." and exit without
committing.
**Commit:**
1. Stage modified documentation files by name (never `git add -A` or `git add .`).
2. Create a single commit:
```bash
git commit -m "$(cat <<'EOF'
docs: update project documentation for vX.Y.Z.W
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
EOF
)"
```
3. Push to the current branch:
```bash
git push
```
**PR/MR body update (idempotent, race-safe):**
1. Read the existing PR/MR body into a PID-unique tempfile (use the platform detected in Step 0):
**If GitHub:**
```bash
gh pr view --json body -q .body > /tmp/gstack-pr-body-$$.md
```
**If GitLab:**
```bash
glab mr view -F json 2>/dev/null | python3 -c "import sys,json; print(json.load(sys.stdin).get('description',''))" > /tmp/gstack-pr-body-$$.md
```
2. If the tempfile already contains a `## Documentation` section, replace that section with the
updated content. If it does not contain one, append a `## Documentation` section at the end.
3. The Documentation section should include:
a. **Doc diff preview** — for each file modified, describe what specifically changed (e.g.,
"README.md: added /document-release to skills table, updated skill count from 9 to 10").
b. **Documentation debt** — if the coverage map from Step 1.5 found gaps, append a
`### Documentation Debt` subsection listing:
- Critical gaps: new public surface with zero documentation coverage
- Common gaps: features with reference-only coverage (no how-to or tutorial)
- Stale diagrams: architecture diagrams with entity names that drifted from the code
- Each item should include a one-line description of what's missing and which Diataxis
quadrant would fill it (e.g., "⚠️ `/new-skill` — has reference in AGENTS.md but no
how-to example in README")
If there are any documentation debt items, suggest adding a `docs-debt` label to the PR.
4. Redaction scan-at-sink, then write the updated body back. The body is already
in a temp file (`/tmp/gstack-pr-body-$$.md`); scan THAT file before editing so
the bytes scanned are the bytes sent:
```bash
REDACT_VIS=$(~/.claude/skills/gstack/bin/gstack-config get redact_repo_visibility 2>/dev/null)
[ -z "$REDACT_VIS" ] && REDACT_VIS=$(gh repo view --json visibility -q .visibility 2>/dev/null | tr 'A-Z' 'a-z')
~/.claude/skills/gstack/bin/gstack-redact --from-file /tmp/gstack-pr-body-$$.md --repo-visibility "${REDACT_VIS:-unknown}" --json
# exit 3 (HIGH) → do NOT edit, rotate+redact; exit 2 (MEDIUM) → confirm per finding.
```
**If GitHub:**
```bash
gh pr edit --body-file /tmp/gstack-pr-body-$$.md
```
**If GitLab:**
Read the contents of `/tmp/gstack-pr-body-$$.md` using the Read tool, then pass it to `glab mr update` using a heredoc to avoid shell metacharacter issues:
```bash
glab mr update -d "$(cat <<'MRBODY'
<paste the file contents here>
MRBODY
)"
```
5. Clean up the tempfile:
```bash
rm -f /tmp/gstack-pr-body-$$.md
```
6. If `gh pr view` / `glab mr view` fails (no PR/MR exists): skip with message "No PR/MR found — skipping body update."
7. If `gh pr edit` / `glab mr update` fails: warn "Could not update PR/MR body — documentation changes are in the
commit." and continue.
**PR/MR title sync (idempotent, always-on):**
PR titles must always start with `v<VERSION>` — same rule as `/ship`. If Step 8 bumped VERSION after `/ship` had already created the PR, the title is now stale. This sub-step fixes it.
1. Read the current VERSION:
```bash
V=$(cat VERSION 2>/dev/null | tr -d '[:space:]')
```
If `VERSION` does not exist or is empty, skip this sub-step entirely.
2. Read the current PR/MR title:
**If GitHub:**
```bash
CURRENT_TITLE=$(gh pr view --json title -q .title 2>/dev/null || true)
```
**If GitLab:**
```bash
CURRENT_TITLE=$(glab mr view -F json 2>/dev/null | jq -r .title 2>/dev/null || true)
```
If `CURRENT_TITLE` is empty (no open PR/MR), skip with message "No PR/MR found — skipping title sync."
3. Compute the corrected title using the shared helper (single source of truth — same one `/ship` uses):
```bash
NEW_TITLE=$(~/.claude/skills/gstack/bin/gstack-pr-title-rewrite.sh "$V" "$CURRENT_TITLE")
```
The helper handles three cases: title already correct (no-op), title has a different `v<X.Y.Z.W>` prefix (replace it), or title has no version prefix (prepend one).
4. If `NEW_TITLE` differs from `CURRENT_TITLE`, update it:
**If GitHub:**
```bash
gh pr edit --title "$NEW_TITLE"
```
**If GitLab:**
```bash
glab mr update -t "$NEW_TITLE"
```
5. If the edit command fails: warn "Could not update PR/MR title — documentation changes are still in the commit." and continue. Do not block on title sync failure.
**Structured doc health summary (final output):**
Output a scannable summary showing every documentation file's status:
```
Documentation health:
README.md [status] ([details])
ARCHITECTURE.md [status] ([details])
CONTRIBUTING.md [status] ([details])
CHANGELOG.md [status] ([details])
TODOS.md [status] ([details])
VERSION [status] ([details])
```
Where status is one of:
- Updated — with description of what changed
- Current — no changes needed
- Voice polished — wording adjusted
- Not bumped — user chose to skip
- Already bumped — version was set by /ship
- Skipped — file does not exist
If the coverage map from Step 1.5 identified any gaps, append:
```
Documentation coverage:
[entity] [reference] [how-to] [tutorial] [explanation]
/new-skill ✅ ❌ ❌ ❌
--new-flag ✅ ✅ ❌ ❌
Diagram drift:
ARCHITECTURE.md: "FooProcessor" renamed to "BarProcessor" in code — diagram may be stale
```
If all coverage is complete and no diagrams drifted, output: "Coverage: all shipped features have adequate documentation."
---
## Codex Documentation Review (default-on)
After the documentation updates above are written, run an independent cross-model pass that
checks the docs against what actually shipped. This is a standard part of /document-release,
not an opt-in. The user turns it off only by asking explicitly
(`gstack-config set codex_reviews disabled`).
**Preflight — decide whether and how the doc review runs:**
```bash
# Codex preflight: one block (functions sourced here don't persist to later blocks).
_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || echo off)
_CODEX_CFG=$(~/.claude/skills/gstack/bin/gstack-config get codex_reviews 2>/dev/null || echo enabled)
source ~/.claude/skills/gstack/bin/gstack-codex-probe 2>/dev/null || true
if [ "$_CODEX_CFG" = "disabled" ]; then
_CODEX_MODE="disabled"
elif ! command -v codex >/dev/null 2>&1; then
_CODEX_MODE="not_installed"; _gstack_codex_log_event "codex_cli_missing" 2>/dev/null || true
elif ! _gstack_codex_auth_probe >/dev/null 2>&1; then
_CODEX_MODE="not_authed"; _gstack_codex_log_event "codex_auth_failed" 2>/dev/null || true
else
_CODEX_MODE="ready"; _gstack_codex_version_check 2>/dev/null || true
fi
echo "CODEX_MODE: $_CODEX_MODE"
```
Branch on the echoed `CODEX_MODE`:
- **`disabled`** — the user turned Codex reviews off (`codex_reviews=disabled`). Skip this section entirely; do NOT fall back to a Claude subagent — disabled means no extra review step. Print: "Codex review skipped (codex_reviews disabled). Re-enable: `gstack-config set codex_reviews enabled`."
- **`not_installed`** — Codex CLI absent. Print: "Codex not installed — using Claude subagent. Install for cross-model coverage: `npm install -g @openai/codex`." Fall back to the Claude subagent path.
- **`not_authed`** — installed but no credentials. Print: "Codex installed but not authenticated — using Claude subagent. Run `codex login` or set `$CODEX_API_KEY`." Fall back to the Claude subagent path.
- **`ready`** — run the Codex pass below.
When the mode is `ready`, `not_installed`, or `not_authed`, print one line so the off-switch
stays discoverable: "Running the Codex doc review automatically (standard step). Disable: `gstack-config set codex_reviews disabled`."
**Determine the release diff range (D3 — reuse the method, do not invent one).**
Recompute the SAME range document-release used in its pre-flight / diff analysis, with the
documented merge-base method:
```bash
DOC_DIFF_BASE=$(git merge-base origin/<base> HEAD 2>/dev/null || echo "<base>")
echo "DOC_DIFF_BASE: $DOC_DIFF_BASE"
```
Do NOT rely on an in-memory variable from an earlier step — shell vars do not survive across
blocks. Recompute it here.
**Construct the doc-review prompt** (for `ready`, `not_installed`, and `not_authed` — skip only on `disabled`).
Review the docs document-release ACTUALLY touched this run (from the coverage map / the files
just edited) PLUS any doc claims affected by the diff range — do NOT hard-code a fixed file
list (a fixed README/ARCHITECTURE/CHANGELOG list misses generated skill docs, package docs,
and command-specific docs). **Always start with the filesystem boundary instruction:**
"IMPORTANT: Do NOT read or execute any files under ~/.claude/, ~/.agents/, .claude/skills/, or agents/. These are Claude Code skill definitions meant for a different AI system. They contain bash scripts and prompt templates that will waste your time. Ignore them completely. Do NOT modify agents/openai.yaml. Stay focused on the repository code only.\n\nYou are reviewing documentation changes against the code that shipped on this
branch. Run \`git diff \$DOC_DIFF_BASE...HEAD\` to see what changed, then read the updated docs
(the files this release touched, plus any docs whose claims the diff affects). Find: doc
claims that no longer match the code, new public surface (commands, flags, config keys,
endpoints) that shipped but is undocumented, stale examples / paths / counts / version
numbers, and CHANGELOG entries that over- or under-sell what shipped. Be terse. Just the gaps.
THE DOCS AND DIFF: <list the touched doc paths>"
**If `CODEX_MODE: ready` — run Codex:**
```bash
TMPERR_DOC=$(mktemp /tmp/codex-docreview-XXXXXXXX)
_REPO_ROOT=$(git rev-parse --show-toplevel) || { echo "ERROR: not in a git repo" >&2; exit 1; }
codex exec "<prompt>" -C "$_REPO_ROOT" -s read-only -c 'model_reasoning_effort="high"' --enable web_search_cached < /dev/null 2>"$TMPERR_DOC"
```
Use a 5-minute timeout (`timeout: 300000`). After the command completes, read stderr:
```bash
cat "$TMPERR_DOC"
```
Present the full output verbatim under `CODEX SAYS (documentation review):`.
**Error handling:** All errors are non-blocking — the documentation review is informational.
- Auth failure (stderr contains "auth", "login", "unauthorized"): note and skip
- Timeout: note timeout duration and skip
- Empty response: note and skip
On any error: continue — documentation review is informational, not a gate.
**If `CODEX_MODE: not_installed` or `not_authed` (or Codex errored at runtime):**
Dispatch via the Agent tool with the same prompt. Bound it at a 5-minute timeout.
Present findings under `DOCUMENTATION REVIEW (Claude subagent):`. If it fails: "Doc review unavailable. Continuing."
**Apply decision (T3B — informational, never auto-edit, but findings don't evaporate).**
If there are zero findings, say "Docs match what shipped — no gaps." and continue. Otherwise
present the findings, then use AskUserQuestion ONCE:
> "The doc review found N gaps between the docs and what shipped. How do you want to handle them?"
>
> RECOMMENDATION: Choose A if the gaps are concrete doc fixes (stale path, missing flag). The
> doc review only reports; nothing is edited without your say-so. Completeness: A=9/10, B=4/10, C=8/10.
Options:
- A) Apply all the doc fixes now
- B) Skip — leave docs as-is
- C) Decide per-finding
On A or per-finding approvals, make the approved edits yourself (the tool never silently
rewrites docs). On B, note the gaps in the output so they're visible.
**Persist the result:**
```bash
~/.claude/skills/gstack/bin/gstack-review-log '{"skill":"codex-doc-review","timestamp":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'","status":"STATUS","source":"SOURCE","commit":"'"$(git rev-parse --short HEAD)"'"}'
```
Substitute: STATUS = "clean" if no gaps, "issues_found" if gaps exist. SOURCE = "codex" if Codex ran, "claude" if the subagent ran.
**Cleanup:** Run `rm -f "$TMPERR_DOC"` after processing (if Codex was used).
---

View File

@ -0,0 +1,362 @@
## Step 2: Per-File Documentation Audit
Read each documentation file and cross-reference it against the diff. Use these generic heuristics
(adapt to whatever project you're in — these are not gstack-specific):
**README.md:**
- Does it describe all features and capabilities visible in the diff?
- Are install/setup instructions consistent with the changes?
- Are examples, demos, and usage descriptions still valid?
- Are troubleshooting steps still accurate?
**ARCHITECTURE.md:**
- Do ASCII diagrams and component descriptions match the current code?
- Are design decisions and "why" explanations still accurate?
- Be conservative — only update things clearly contradicted by the diff. Architecture docs
describe things unlikely to change frequently.
**CONTRIBUTING.md — New contributor smoke test:**
- Walk through the setup instructions as if you are a brand new contributor.
- Are the listed commands accurate? Would each step succeed?
- Do test tier descriptions match the current test infrastructure?
- Are workflow descriptions (dev setup, operational learnings, etc.) current?
- Flag anything that would fail or confuse a first-time contributor.
**CLAUDE.md / project instructions:**
- Does the project structure section match the actual file tree?
- Are listed commands and scripts accurate?
- Do build/test instructions match what's in package.json (or equivalent)?
**Any other .md files:**
- Read the file, determine its purpose and audience.
- Cross-reference against the diff to check if it contradicts anything the file says.
For each file, classify needed updates as:
- **Auto-update** — Factual corrections clearly warranted by the diff: adding an item to a
table, updating a file path, fixing a count, updating a project structure tree.
- **Ask user** — Narrative changes, section removal, security model changes, large rewrites
(more than ~10 lines in one section), ambiguous relevance, adding entirely new sections.
---
## Step 3: Apply Auto-Updates
Make all clear, factual updates directly using the Edit tool.
For each file modified, output a one-line summary describing **what specifically changed** — not
just "Updated README.md" but "README.md: added /new-skill to skills table, updated skill count
from 9 to 10."
**Never auto-update:**
- README introduction or project positioning
- ARCHITECTURE philosophy or design rationale
- Security model descriptions
- Do not remove entire sections from any document
---
## Step 4: Ask About Risky/Questionable Changes
For each risky or questionable update identified in Step 2, use AskUserQuestion with:
- Context: project name, branch, which doc file, what we're reviewing
- The specific documentation decision
- `RECOMMENDATION: Choose [X] because [one-line reason]`
- Options including C) Skip — leave as-is
Apply approved changes immediately after each answer.
---
## Step 5: CHANGELOG Voice Polish
**CRITICAL — NEVER CLOBBER CHANGELOG ENTRIES.**
This step polishes voice. It does NOT rewrite, replace, or regenerate CHANGELOG content.
A real incident occurred where an agent replaced existing CHANGELOG entries when it should have
preserved them. This skill must NEVER do that.
**Rules:**
1. Read the entire CHANGELOG.md first. Understand what is already there.
2. Only modify wording within existing entries. Never delete, reorder, or replace entries.
3. Never regenerate a CHANGELOG entry from scratch. The entry was written by `/ship` from the
actual diff and commit history. It is the source of truth. You are polishing prose, not
rewriting history.
4. If an entry looks wrong or incomplete, use AskUserQuestion — do NOT silently fix it.
5. Use Edit tool with exact `old_string` matches — never use Write to overwrite CHANGELOG.md.
**If CHANGELOG was not modified in this branch:** skip this step.
**If CHANGELOG was modified in this branch**, review the entry for voice:
- **Sell test (Diataxis rubric):** Score each CHANGELOG entry 0-3:
- **1 point** — answers "What changed?" (reference: names the feature/fix)
- **1 point** — answers "Why should I care?" (explanation: user impact, pain removed)
- **1 point** — answers "How do I use it?" (how-to: command, flag, or link to docs)
- Entries scoring <2 need a rewrite. Entries scoring 3 are gold.
- Lead with what the user can now **do** — not implementation details.
- "You can now..." not "Refactored the..."
- Flag and rewrite any entry that reads like a commit message.
- Internal/contributor changes belong in a separate "### For contributors" subsection.
- Auto-fix minor voice adjustments. Use AskUserQuestion if a rewrite would alter meaning.
---
## Step 6: Cross-Doc Consistency & Discoverability Check
After auditing each file individually, do a cross-doc consistency pass:
1. Does the README's feature/capability list match what CLAUDE.md (or project instructions) describes?
2. Does ARCHITECTURE's component list match CONTRIBUTING's project structure description?
3. Does CHANGELOG's latest version match the VERSION file?
4. **Discoverability:** Is every documentation file reachable from README.md or CLAUDE.md? If
ARCHITECTURE.md exists but neither README nor CLAUDE.md links to it, flag it. Every doc
should be discoverable from one of the two entry-point files.
5. Flag any contradictions between documents. Auto-fix clear factual inconsistencies (e.g., a
version mismatch). Use AskUserQuestion for narrative contradictions.
---
## Step 7: TODOS.md Cleanup
This is a second pass that complements `/ship`'s Step 5.5. Read `review/TODOS-format.md` (if
available) for the canonical TODO item format.
If TODOS.md does not exist, skip this step.
1. **Completed items not yet marked:** Cross-reference the diff against open TODO items. If a
TODO is clearly completed by the changes in this branch, move it to the Completed section
with `**Completed:** vX.Y.Z.W (YYYY-MM-DD)`. Be conservative — only mark items with clear
evidence in the diff.
2. **Items needing description updates:** If a TODO references files or components that were
significantly changed, its description may be stale. Use AskUserQuestion to confirm whether
the TODO should be updated, completed, or left as-is.
3. **New deferred work:** Check the diff for `TODO`, `FIXME`, `HACK`, and `XXX` comments. For
each one that represents meaningful deferred work (not a trivial inline note), use
AskUserQuestion to ask whether it should be captured in TODOS.md.
---
## Step 8: VERSION Bump Question
**CRITICAL — NEVER BUMP VERSION WITHOUT ASKING.**
1. **If VERSION does not exist:** Skip silently.
2. Check if VERSION was already modified on this branch:
```bash
git diff <base>...HEAD -- VERSION
```
3. **If VERSION was NOT bumped:** Use AskUserQuestion:
- RECOMMENDATION: Choose C (Skip) because docs-only changes rarely warrant a version bump
- A) Bump PATCH (X.Y.Z+1) — if doc changes ship alongside code changes
- B) Bump MINOR (X.Y+1.0) — if this is a significant standalone release
- C) Skip — no version bump needed
4. **If VERSION was already bumped:** Do NOT skip silently. Instead, check whether the bump
still covers the full scope of changes on this branch:
a. Read the CHANGELOG entry for the current VERSION. What features does it describe?
b. Read the full diff (`git diff <base>...HEAD --stat` and `git diff <base>...HEAD --name-only`).
Are there significant changes (new features, new skills, new commands, major refactors)
that are NOT mentioned in the CHANGELOG entry for the current version?
c. **If the CHANGELOG entry covers everything:** Skip — output "VERSION: Already bumped to
vX.Y.Z, covers all changes."
d. **If there are significant uncovered changes:** Use AskUserQuestion explaining what the
current version covers vs what's new, and ask:
- RECOMMENDATION: Choose A because the new changes warrant their own version
- A) Bump to next patch (X.Y.Z+1) — give the new changes their own version
- B) Keep current version — add new changes to the existing CHANGELOG entry
- C) Skip — leave version as-is, handle later
The key insight: a VERSION bump set for "feature A" should not silently absorb "feature B"
if feature B is substantial enough to deserve its own version entry.
---
## Step 9: Commit & Output
**Empty check first:** Run `git status` (never use `-uall`). If no documentation files were
modified by any previous step, output "All documentation is up to date." and exit without
committing.
**Commit:**
1. Stage modified documentation files by name (never `git add -A` or `git add .`).
2. Create a single commit:
```bash
git commit -m "$(cat <<'EOF'
docs: update project documentation for vX.Y.Z.W
{{CO_AUTHOR_TRAILER}}
EOF
)"
```
3. Push to the current branch:
```bash
git push
```
**PR/MR body update (idempotent, race-safe):**
1. Read the existing PR/MR body into a PID-unique tempfile (use the platform detected in Step 0):
**If GitHub:**
```bash
gh pr view --json body -q .body > /tmp/gstack-pr-body-$$.md
```
**If GitLab:**
```bash
glab mr view -F json 2>/dev/null | python3 -c "import sys,json; print(json.load(sys.stdin).get('description',''))" > /tmp/gstack-pr-body-$$.md
```
2. If the tempfile already contains a `## Documentation` section, replace that section with the
updated content. If it does not contain one, append a `## Documentation` section at the end.
3. The Documentation section should include:
a. **Doc diff preview** — for each file modified, describe what specifically changed (e.g.,
"README.md: added /document-release to skills table, updated skill count from 9 to 10").
b. **Documentation debt** — if the coverage map from Step 1.5 found gaps, append a
`### Documentation Debt` subsection listing:
- Critical gaps: new public surface with zero documentation coverage
- Common gaps: features with reference-only coverage (no how-to or tutorial)
- Stale diagrams: architecture diagrams with entity names that drifted from the code
- Each item should include a one-line description of what's missing and which Diataxis
quadrant would fill it (e.g., "⚠️ `/new-skill` — has reference in AGENTS.md but no
how-to example in README")
If there are any documentation debt items, suggest adding a `docs-debt` label to the PR.
4. Redaction scan-at-sink, then write the updated body back. The body is already
in a temp file (`/tmp/gstack-pr-body-$$.md`); scan THAT file before editing so
the bytes scanned are the bytes sent:
```bash
REDACT_VIS=$(~/.claude/skills/gstack/bin/gstack-config get redact_repo_visibility 2>/dev/null)
[ -z "$REDACT_VIS" ] && REDACT_VIS=$(gh repo view --json visibility -q .visibility 2>/dev/null | tr 'A-Z' 'a-z')
~/.claude/skills/gstack/bin/gstack-redact --from-file /tmp/gstack-pr-body-$$.md --repo-visibility "${REDACT_VIS:-unknown}" --json
# exit 3 (HIGH) → do NOT edit, rotate+redact; exit 2 (MEDIUM) → confirm per finding.
```
**If GitHub:**
```bash
gh pr edit --body-file /tmp/gstack-pr-body-$$.md
```
**If GitLab:**
Read the contents of `/tmp/gstack-pr-body-$$.md` using the Read tool, then pass it to `glab mr update` using a heredoc to avoid shell metacharacter issues:
```bash
glab mr update -d "$(cat <<'MRBODY'
<paste the file contents here>
MRBODY
)"
```
5. Clean up the tempfile:
```bash
rm -f /tmp/gstack-pr-body-$$.md
```
6. If `gh pr view` / `glab mr view` fails (no PR/MR exists): skip with message "No PR/MR found — skipping body update."
7. If `gh pr edit` / `glab mr update` fails: warn "Could not update PR/MR body — documentation changes are in the
commit." and continue.
**PR/MR title sync (idempotent, always-on):**
PR titles must always start with `v<VERSION>` — same rule as `/ship`. If Step 8 bumped VERSION after `/ship` had already created the PR, the title is now stale. This sub-step fixes it.
1. Read the current VERSION:
```bash
V=$(cat VERSION 2>/dev/null | tr -d '[:space:]')
```
If `VERSION` does not exist or is empty, skip this sub-step entirely.
2. Read the current PR/MR title:
**If GitHub:**
```bash
CURRENT_TITLE=$(gh pr view --json title -q .title 2>/dev/null || true)
```
**If GitLab:**
```bash
CURRENT_TITLE=$(glab mr view -F json 2>/dev/null | jq -r .title 2>/dev/null || true)
```
If `CURRENT_TITLE` is empty (no open PR/MR), skip with message "No PR/MR found — skipping title sync."
3. Compute the corrected title using the shared helper (single source of truth — same one `/ship` uses):
```bash
NEW_TITLE=$(~/.claude/skills/gstack/bin/gstack-pr-title-rewrite.sh "$V" "$CURRENT_TITLE")
```
The helper handles three cases: title already correct (no-op), title has a different `v<X.Y.Z.W>` prefix (replace it), or title has no version prefix (prepend one).
4. If `NEW_TITLE` differs from `CURRENT_TITLE`, update it:
**If GitHub:**
```bash
gh pr edit --title "$NEW_TITLE"
```
**If GitLab:**
```bash
glab mr update -t "$NEW_TITLE"
```
5. If the edit command fails: warn "Could not update PR/MR title — documentation changes are still in the commit." and continue. Do not block on title sync failure.
**Structured doc health summary (final output):**
Output a scannable summary showing every documentation file's status:
```
Documentation health:
README.md [status] ([details])
ARCHITECTURE.md [status] ([details])
CONTRIBUTING.md [status] ([details])
CHANGELOG.md [status] ([details])
TODOS.md [status] ([details])
VERSION [status] ([details])
```
Where status is one of:
- Updated — with description of what changed
- Current — no changes needed
- Voice polished — wording adjusted
- Not bumped — user chose to skip
- Already bumped — version was set by /ship
- Skipped — file does not exist
If the coverage map from Step 1.5 identified any gaps, append:
```
Documentation coverage:
[entity] [reference] [how-to] [tutorial] [explanation]
/new-skill ✅ ❌ ❌ ❌
--new-flag ✅ ✅ ❌ ❌
Diagram drift:
ARCHITECTURE.md: "FooProcessor" renamed to "BarProcessor" in code — diagram may be stale
```
If all coverage is complete and no diagrams drifted, output: "Coverage: all shipped features have adequate documentation."
---
{{CODEX_DOC_REVIEW}}

View File

@ -15,12 +15,12 @@ hooks:
- matcher: "Edit"
hooks:
- type: command
command: "bash ${CLAUDE_SKILL_DIR}/bin/check-freeze.sh"
command: "bash $HOME/.claude/skills/gstack/freeze/bin/check-freeze.sh"
statusMessage: "Checking freeze boundary..."
- matcher: "Write"
hooks:
- type: command
command: "bash ${CLAUDE_SKILL_DIR}/bin/check-freeze.sh"
command: "bash $HOME/.claude/skills/gstack/freeze/bin/check-freeze.sh"
statusMessage: "Checking freeze boundary..."
---
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->

View File

@ -20,12 +20,12 @@ hooks:
- matcher: "Edit"
hooks:
- type: command
command: "bash ${CLAUDE_SKILL_DIR}/bin/check-freeze.sh"
command: "bash $HOME/.claude/skills/gstack/freeze/bin/check-freeze.sh"
statusMessage: "Checking freeze boundary..."
- matcher: "Write"
hooks:
- type: command
command: "bash ${CLAUDE_SKILL_DIR}/bin/check-freeze.sh"
command: "bash $HOME/.claude/skills/gstack/freeze/bin/check-freeze.sh"
statusMessage: "Checking freeze boundary..."
sensitive: true
---

View File

@ -81,10 +81,10 @@ Run with `browse <command> [args]`. Full reference: `browse/SKILL.md`.
- `cookies`: All cookies as JSON
- `css <sel> <prop>`: Computed CSS value
- `dialog [--clear]`: Dialog messages
- `eval <file>`: Run JavaScript from a file in the page context and return result as string.
- `eval <file> [--out <file>] [--raw]`: Run JavaScript from a file in the page context and return result as string.
- `inspect [selector] [--all] [--history]`: Deep CSS inspection via CDP — full rule cascade, box model, computed styles
- `is <prop> <sel|@ref>`: State check on element.
- `js <expr>`: Run inline JavaScript expression in the page context and return result as string.
- `js <expr> [--out <file>] [--raw]`: Run inline JavaScript expression in the page context and return result as string.
- `network [--clear]`: Network requests
- `perf`: Page load timings
- `storage | storage set <key> <value>`: Read both localStorage and sessionStorage as JSON.

View File

@ -15,17 +15,17 @@ hooks:
- matcher: "Bash"
hooks:
- type: command
command: "bash ${CLAUDE_SKILL_DIR}/../careful/bin/check-careful.sh"
command: "bash $HOME/.claude/skills/gstack/careful/bin/check-careful.sh"
statusMessage: "Checking for destructive commands..."
- matcher: "Edit"
hooks:
- type: command
command: "bash ${CLAUDE_SKILL_DIR}/../freeze/bin/check-freeze.sh"
command: "bash $HOME/.claude/skills/gstack/freeze/bin/check-freeze.sh"
statusMessage: "Checking freeze boundary..."
- matcher: "Write"
hooks:
- type: command
command: "bash ${CLAUDE_SKILL_DIR}/../freeze/bin/check-freeze.sh"
command: "bash $HOME/.claude/skills/gstack/freeze/bin/check-freeze.sh"
statusMessage: "Checking freeze boundary..."
---
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->

View File

@ -20,17 +20,17 @@ hooks:
- matcher: "Bash"
hooks:
- type: command
command: "bash ${CLAUDE_SKILL_DIR}/../careful/bin/check-careful.sh"
command: "bash $HOME/.claude/skills/gstack/careful/bin/check-careful.sh"
statusMessage: "Checking for destructive commands..."
- matcher: "Edit"
hooks:
- type: command
command: "bash ${CLAUDE_SKILL_DIR}/../freeze/bin/check-freeze.sh"
command: "bash $HOME/.claude/skills/gstack/freeze/bin/check-freeze.sh"
statusMessage: "Checking freeze boundary..."
- matcher: "Write"
hooks:
- type: command
command: "bash ${CLAUDE_SKILL_DIR}/../freeze/bin/check-freeze.sh"
command: "bash $HOME/.claude/skills/gstack/freeze/bin/check-freeze.sh"
statusMessage: "Checking freeze boundary..."
sensitive: true
---

View File

@ -48,6 +48,9 @@ echo "SKILL_PREFIX: $_SKILL_PREFIX"
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
REPO_MODE=${REPO_MODE:-unknown}
echo "REPO_MODE: $REPO_MODE"
_SESSION_KIND=$(~/.claude/skills/gstack/bin/gstack-session-kind 2>/dev/null || echo "interactive")
case "$_SESSION_KIND" in spawned|headless|interactive) ;; *) _SESSION_KIND="interactive" ;; esac
echo "SESSION_KIND: $_SESSION_KIND"
_LAKE_SEEN=$([ -f ~/.gstack/.completeness-intro-seen ] && echo "yes" || echo "no")
echo "LAKE_INTRO: $_LAKE_SEEN"
_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || true)
@ -127,7 +130,7 @@ In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`co
## Skill Invocation During Plan Mode
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If no AskUserQuestion variant is callable, use the normal chat fallback from the AskUserQuestion Format rule and stop for the user's reply. At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If AskUserQuestion is unavailable or a call fails, follow the AskUserQuestion Format failure fallback: `headless` → BLOCKED; `interactive` → the prose fallback (also satisfies end-of-turn). At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If `PROACTIVE` is `"false"`, do not auto-invoke or proactively suggest skills. If a skill seems useful, ask: "I think /skillname might help here — want me to run it?"
@ -162,7 +165,7 @@ touch ~/.gstack/.writing-style-prompted
Skip if `WRITING_STYLE_PENDING` is `no`.
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Lake** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Ocean** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
```bash
open https://garryslist.org/posts/boil-the-ocean
@ -299,11 +302,31 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
**Rule:** if any `mcp__*__AskUserQuestion` variant is in your tool list, prefer it. Hosts may disable native AUQ via `--disallowedTools AskUserQuestion` (Conductor does, by default) and route through their MCP variant; calling native there silently fails. Same questions/options shape; same decision-brief format applies.
**If no AskUserQuestion variant appears in your tool list:** fall back to the host's normal user-facing chat question. Render the same decision brief in prose, stop, and wait for the user's reply. This fallback is for interactive chat hosts that can ask the user normally but do not expose an AUQ tool (for example Codex API sessions). Do not write decisions to the plan file as a substitute, and do not silently auto-decide (only `/plan-tune` AUTO_DECIDE opt-ins authorize auto-picking).
If AskUserQuestion is unavailable (no variant in your tool list) OR a call to it fails, do NOT silently auto-decide or write the decision to the plan file as a substitute. Follow the **failure fallback** below.
### When AskUserQuestion is unavailable or a call fails
Tell three outcomes apart:
1. **Auto-decide denial (NOT a failure).** The result contains `[plan-tune auto-decide] <id> → <option>` — the preference hook working as designed. Proceed with that option. Do NOT retry, do NOT fall back to prose.
2. **Genuine failure** — no variant in your tool list, OR the variant is present but the call returns an error / missing result (MCP transport error, empty result, host bug — e.g. Conductor's MCP AskUserQuestion is flaky and returns `[Tool result missing due to internal error]`).
- If it was present and **errored** (not absent), retry the SAME call **once** — but only if no answer could have surfaced (a missing-result error can arrive after the user already saw the question; retrying would double-prompt, so if it may have reached them, treat as pending, don't retry).
- Then branch on `SESSION_KIND` (echoed by the preamble; empty/absent ⇒ `interactive`):
- `spawned` → defer to the **Spawned session** block: auto-choose the recommended option. Never prose, never BLOCKED.
- `headless``BLOCKED — AskUserQuestion unavailable`; stop and wait (no human can answer).
- `interactive`**prose fallback** (below).
**Prose fallback — render the decision brief as a markdown message, not a tool call.** Same information as the tool format below, different structure (paragraphs, not ✅/❌ bullets). It MUST surface this triad:
1. **A clear ELI10 of the issue itself** — plain English on what's being decided and why it matters (the question, not per-choice), naming the stakes. Lead with it.
2. **Completeness scores per choice** — explicit `Completeness: X/10` on EACH choice (10 complete, 7 happy-path, 3 shortcut); use the kind-note when options differ in kind not coverage, but never silently drop the score.
3. **The recommendation and why** — a `Recommendation: <choice> because <reason>` line plus the `(recommended)` marker on that choice.
Layout: a `D<N>` title + a one-line note that AskUserQuestion failed and to reply with a letter; the issue ELI10; the Recommendation line; then ONE paragraph per choice carrying its `(recommended)` marker, its `Completeness: X/10`, and 2-4 sentences of reasoning — never a bare bullet list; a closing `Net:` line. Split chains / 5+ options: one prose block per per-option call, in sequence. Then STOP and wait — the user's typed answer is the decision. In plan mode this satisfies end-of-turn like a tool call.
### Format
Every AskUserQuestion is a decision brief. Send it as tool_use when an AskUserQuestion tool exists; otherwise use the normal chat fallback above and wait for the user's reply.
Every AskUserQuestion is a decision brief and must be sent as tool_use, not prose — unless the documented failure fallback above applies (interactive session + the call is unavailable/erroring), in which case the prose fallback is the correct output.
```
D<N><one-line question title>
@ -383,7 +406,7 @@ Before calling AskUserQuestion, verify:
- [ ] (recommended) label on one option (even for neutral-posture)
- [ ] Dual-scale effort labels on effort-bearing options (human / CC)
- [ ] Net line closes the decision
- [ ] You are calling the tool when available; otherwise you are using the normal chat fallback and stopping for the user's reply
- [ ] You are calling the tool, not writing prose — unless the documented failure fallback applies (then: prose with the mandatory triad — issue ELI10, per-choice Completeness, Recommendation + `(recommended)` — and a "reply with a letter" instruction, then STOP)
- [ ] Non-ASCII characters (CJK / accents) written directly, NOT \u-escaped
- [ ] If you had 5+ options, you split (or batched into ≤4-groups) — did NOT drop any
- [ ] If you split, you checked dependencies between options before firing the chain
@ -570,12 +593,19 @@ if [ -d "$_PROJ" ]; then
fi
_LATEST_CP=$(find "$_PROJ/checkpoints" -name "*.md" -type f 2>/dev/null | xargs ls -t 2>/dev/null | head -1)
[ -n "$_LATEST_CP" ] && echo "LATEST_CHECKPOINT: $_LATEST_CP"
if [ -f "$_PROJ/decisions.active.json" ]; then
echo "--- ACTIVE DECISIONS (recent, scope-relevant) ---"
~/.claude/skills/gstack/bin/gstack-decision-search --recent 5 2>/dev/null
echo "--- END DECISIONS ---"
fi
echo "--- END ARTIFACTS ---"
fi
```
If artifacts are listed, read the newest useful one. If `LAST_SESSION` or `LATEST_CHECKPOINT` appears, give a 2-sentence welcome back summary. If `RECENT_PATTERN` clearly implies a next skill, suggest it once.
**Cross-session decisions.** If `ACTIVE DECISIONS` are listed, treat them as prior settled calls with their rationale — do not silently re-litigate them; if you're about to reverse one, say so explicitly. Reach for `~/.claude/skills/gstack/bin/gstack-decision-search` whenever a question touches a past decision ("what did we decide / why / did we try"). When you or the user make a DURABLE decision (architecture, scope, tool/vendor choice, or a reversal) — NOT a turn-level or trivial choice — log it with `~/.claude/skills/gstack/bin/gstack-decision-log` (`--supersede <id>` for a reversal). Reliable and local; gbrain not required.
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format is structure; this is prose quality.
@ -590,9 +620,9 @@ Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format i
Curated jargon list lives at `~/.claude/skills/gstack/scripts/jargon-list.json` (80+ terms). On the first jargon term you encounter this session, Read that file once; treat the `terms` array as the canonical list. The list is repo-owned and may grow between releases.
## Completeness Principle — Boil the Lake
## Completeness Principle — Boil the Ocean
AI makes completeness cheap. Recommend complete lakes (tests, edge cases, error paths); flag oceans (rewrites, multi-quarter migrations).
AI makes completeness cheap, so the complete thing is the goal. Recommend full coverage (tests, edge cases, error paths) — boil the ocean one lake at a time. The only thing out of scope is genuinely unrelated work (rewrites, multi-quarter migrations); flag that as separate scope, never as an excuse for a shortcut.
When options differ in coverage, include `Completeness: X/10` (10 = all edge cases, 7 = happy path, 3 = shortcut). When options differ in kind, write: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate scores.

View File

@ -0,0 +1,7 @@
#!/usr/bin/env bash
# Bash shim — Claude Code hooks run `command` strings via /bin/sh, so this
# wrapper makes the TypeScript hook executable via bun. Settings.json
# references this file directly.
set -e
HERE="$(cd "$(dirname "$0")" && pwd)"
exec bun "$HERE/auq-error-fallback-hook.ts"

View File

@ -0,0 +1,202 @@
#!/usr/bin/env bun
/**
* PostToolUse hook for AskUserQuestion runtime reliability layer for the
* AUQ-failure prose fallback (OV3:B).
*
* When an AskUserQuestion call comes back as an ERROR / missing result (the
* Conductor MCP bug returns `[Tool result missing due to internal error]`), this
* hook injects `additionalContext` reminding the model of the failure-fallback
* rule, tailored to the session kind. It does NOT render the prose itself the
* model still emits it. The hook only guarantees the reminder fires at the moment
* of failure, instead of relying on the model noticing the error result and
* recalling the echoed SESSION_KIND.
*
* DEFENSIVE / INERT-IF-UNSUPPORTED: it is unverified whether Claude Code invokes
* PostToolUse hooks when an MCP tool returns a transport/missing-result error (we
* could not force that Conductor-internal failure in a harness see
* docs/spikes/claude-code-hook-mutation.md §"PostToolUse on tool error"). If the
* platform does NOT fire the hook on that path, this is simply never invoked no
* harm; the prompt-level fallback in generate-ask-user-format.ts still covers it.
* On a SUCCESSFUL AskUserQuestion (a real answer), the hook defers (no output).
*
* Triggered by ~/.claude/settings.json (registered by `setup` next to
* question-log-hook):
* PostToolUse matcher "(AskUserQuestion|mcp__.*__AskUserQuestion)"
*
* Invariants:
* - Always exits 0. A failing hook MUST NOT block the user's session.
* - Never triggers on a successful answer (would corrupt a normal AUQ).
* - Errors land in ~/.gstack/hook-errors.log.
*/
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { spawnSync } from 'child_process';
interface HookStdin {
tool_name?: string;
tool_response?: unknown;
cwd?: string;
}
function stateRoot(): string {
return (
process.env.GSTACK_STATE_ROOT ||
process.env.GSTACK_HOME ||
path.join(os.homedir(), '.gstack')
);
}
function logHookError(msg: string): void {
try {
const sr = stateRoot();
fs.mkdirSync(sr, { recursive: true });
fs.appendFileSync(
path.join(sr, 'hook-errors.log'),
`${new Date().toISOString()} auq-error-fallback-hook: ${msg}\n`,
);
} catch {
// last-resort swallow
}
}
function readStdin(): Promise<string> {
return new Promise((resolve) => {
let buf = '';
process.stdin.setEncoding('utf-8');
process.stdin.on('data', (chunk) => (buf += chunk));
process.stdin.on('end', () => resolve(buf));
process.stdin.on('error', () => resolve(buf));
setTimeout(() => resolve(buf), 2000);
});
}
/** No-op output — let the tool result stand untouched. */
function defer(): void {
process.stdout.write(
JSON.stringify({ hookSpecificOutput: { hookEventName: 'PostToolUse' } }),
);
process.exit(0);
}
function inject(additionalContext: string): void {
process.stdout.write(
JSON.stringify({
hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext },
}),
);
process.exit(0);
}
/**
* Decide whether the tool_response is an ERROR / missing result rather than a
* real answer. Conservative: only flag clear failure shapes, so a successful
* AskUserQuestion (which carries the user's choice) is never misread as failure.
*/
export function isErrorResponse(response: unknown): boolean {
if (response === null || response === undefined) return true;
if (typeof response === 'string') {
const s = response.trim();
if (s === '') return true;
// Match ONLY the specific missing-result sentinel phrase, not any string that
// merely contains "error" — a real answer like "Investigate the internal error"
// must NOT trigger the fallback. (Codex review finding.)
return /tool result missing/i.test(s);
}
if (typeof response === 'object') {
const rec = response as Record<string, unknown>;
// Structured flag must be the boolean true — not the substring "is_error" inside
// a serialized success payload like '{"is_error": false}'.
if (rec.is_error === true || rec.isError === true) return true;
if (typeof rec.error === 'string' && rec.error.trim() !== '') return true;
// Some hosts wrap the payload as { content: "..." } or { content: [{text}] }.
const content = rec.content;
if (typeof content === 'string') return /tool result missing/i.test(content);
if (Array.isArray(content)) {
const text = content
.map((c) => (typeof c === 'string' ? c : (c as Record<string, unknown>)?.text ?? ''))
.join(' ');
return /tool result missing/i.test(text);
}
}
return false;
}
/** Resolve SESSION_KIND via the shared helper (same classification the preamble
* echoes). Falls back to 'interactive' (degrade-safe) on any failure. */
export function sessionKind(cwd?: string): 'spawned' | 'headless' | 'interactive' {
try {
const here = path.dirname(new URL(import.meta.url).pathname);
const bin = path.resolve(here, '..', '..', '..', 'bin', 'gstack-session-kind');
const res = spawnSync(bin, [], {
encoding: 'utf-8',
timeout: 3000,
cwd: cwd && fs.existsSync(cwd) ? cwd : undefined,
});
const out = (res.stdout || '').trim();
if (out === 'spawned' || out === 'headless' || out === 'interactive') return out;
} catch (e) {
logHookError(`sessionKind failed: ${(e as Error).message}`);
}
return 'interactive';
}
/** The directive injected per session kind. Exported for unit testing. */
export function directiveFor(kind: 'spawned' | 'headless' | 'interactive'): string {
const lead =
'The AskUserQuestion call did not return a usable answer (error / missing result). ' +
'Per the AskUserQuestion failure-fallback rule: ';
switch (kind) {
case 'spawned':
return (
lead +
'SESSION_KIND=spawned — auto-choose the recommended option per the Spawned session block. ' +
'Do not emit prose, do not BLOCK.'
);
case 'headless':
return (
lead +
'SESSION_KIND=headless — report `BLOCKED — AskUserQuestion unavailable` and stop; no human can answer.'
);
case 'interactive':
default:
return (
lead +
'SESSION_KIND=interactive — render the decision as a PROSE message now: a clear ELI10 of the issue, ' +
'then a Recommendation line, then ONE paragraph per choice carrying its `(recommended)` marker, its ' +
'`Completeness: X/10`, and 2-4 sentences of reasoning. Tell the user to reply with a letter, then STOP. ' +
'(Retry the call once first only if no answer could have surfaced.)'
);
}
}
async function main(): Promise<void> {
const raw = await readStdin();
if (!raw.trim()) return defer();
let stdin: HookStdin;
try {
stdin = JSON.parse(raw);
} catch (e) {
logHookError(`stdin parse failed: ${(e as Error).message}`);
return defer();
}
const toolName = stdin.tool_name || '';
if (toolName !== 'AskUserQuestion' && !/^mcp__.+__AskUserQuestion$/.test(toolName)) {
return defer();
}
if (!isErrorResponse(stdin.tool_response)) return defer();
inject(directiveFor(sessionKind(stdin.cwd)));
}
// Only run the stdin→stdout pipeline when executed as a hook, not when imported
// by the unit test (which exercises the exported pure functions).
if (import.meta.main) {
main().catch((e) => {
logHookError(`main crash: ${(e as Error).message}`);
defer();
});
}

View File

@ -87,6 +87,9 @@ echo "SKILL_PREFIX: $_SKILL_PREFIX"
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
REPO_MODE=${REPO_MODE:-unknown}
echo "REPO_MODE: $REPO_MODE"
_SESSION_KIND=$(~/.claude/skills/gstack/bin/gstack-session-kind 2>/dev/null || echo "interactive")
case "$_SESSION_KIND" in spawned|headless|interactive) ;; *) _SESSION_KIND="interactive" ;; esac
echo "SESSION_KIND: $_SESSION_KIND"
_LAKE_SEEN=$([ -f ~/.gstack/.completeness-intro-seen ] && echo "yes" || echo "no")
echo "LAKE_INTRO: $_LAKE_SEEN"
_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || true)
@ -166,7 +169,7 @@ In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`co
## Skill Invocation During Plan Mode
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If no AskUserQuestion variant is callable, use the normal chat fallback from the AskUserQuestion Format rule and stop for the user's reply. At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If AskUserQuestion is unavailable or a call fails, follow the AskUserQuestion Format failure fallback: `headless` → BLOCKED; `interactive` → the prose fallback (also satisfies end-of-turn). At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If `PROACTIVE` is `"false"`, do not auto-invoke or proactively suggest skills. If a skill seems useful, ask: "I think /skillname might help here — want me to run it?"
@ -201,7 +204,7 @@ touch ~/.gstack/.writing-style-prompted
Skip if `WRITING_STYLE_PENDING` is `no`.
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Lake** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Ocean** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
```bash
open https://garryslist.org/posts/boil-the-ocean
@ -338,11 +341,31 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
**Rule:** if any `mcp__*__AskUserQuestion` variant is in your tool list, prefer it. Hosts may disable native AUQ via `--disallowedTools AskUserQuestion` (Conductor does, by default) and route through their MCP variant; calling native there silently fails. Same questions/options shape; same decision-brief format applies.
**If no AskUserQuestion variant appears in your tool list:** fall back to the host's normal user-facing chat question. Render the same decision brief in prose, stop, and wait for the user's reply. This fallback is for interactive chat hosts that can ask the user normally but do not expose an AUQ tool (for example Codex API sessions). Do not write decisions to the plan file as a substitute, and do not silently auto-decide (only `/plan-tune` AUTO_DECIDE opt-ins authorize auto-picking).
If AskUserQuestion is unavailable (no variant in your tool list) OR a call to it fails, do NOT silently auto-decide or write the decision to the plan file as a substitute. Follow the **failure fallback** below.
### When AskUserQuestion is unavailable or a call fails
Tell three outcomes apart:
1. **Auto-decide denial (NOT a failure).** The result contains `[plan-tune auto-decide] <id> → <option>` — the preference hook working as designed. Proceed with that option. Do NOT retry, do NOT fall back to prose.
2. **Genuine failure** — no variant in your tool list, OR the variant is present but the call returns an error / missing result (MCP transport error, empty result, host bug — e.g. Conductor's MCP AskUserQuestion is flaky and returns `[Tool result missing due to internal error]`).
- If it was present and **errored** (not absent), retry the SAME call **once** — but only if no answer could have surfaced (a missing-result error can arrive after the user already saw the question; retrying would double-prompt, so if it may have reached them, treat as pending, don't retry).
- Then branch on `SESSION_KIND` (echoed by the preamble; empty/absent ⇒ `interactive`):
- `spawned` → defer to the **Spawned session** block: auto-choose the recommended option. Never prose, never BLOCKED.
- `headless``BLOCKED — AskUserQuestion unavailable`; stop and wait (no human can answer).
- `interactive`**prose fallback** (below).
**Prose fallback — render the decision brief as a markdown message, not a tool call.** Same information as the tool format below, different structure (paragraphs, not ✅/❌ bullets). It MUST surface this triad:
1. **A clear ELI10 of the issue itself** — plain English on what's being decided and why it matters (the question, not per-choice), naming the stakes. Lead with it.
2. **Completeness scores per choice** — explicit `Completeness: X/10` on EACH choice (10 complete, 7 happy-path, 3 shortcut); use the kind-note when options differ in kind not coverage, but never silently drop the score.
3. **The recommendation and why** — a `Recommendation: <choice> because <reason>` line plus the `(recommended)` marker on that choice.
Layout: a `D<N>` title + a one-line note that AskUserQuestion failed and to reply with a letter; the issue ELI10; the Recommendation line; then ONE paragraph per choice carrying its `(recommended)` marker, its `Completeness: X/10`, and 2-4 sentences of reasoning — never a bare bullet list; a closing `Net:` line. Split chains / 5+ options: one prose block per per-option call, in sequence. Then STOP and wait — the user's typed answer is the decision. In plan mode this satisfies end-of-turn like a tool call.
### Format
Every AskUserQuestion is a decision brief. Send it as tool_use when an AskUserQuestion tool exists; otherwise use the normal chat fallback above and wait for the user's reply.
Every AskUserQuestion is a decision brief and must be sent as tool_use, not prose — unless the documented failure fallback above applies (interactive session + the call is unavailable/erroring), in which case the prose fallback is the correct output.
```
D<N><one-line question title>
@ -422,7 +445,7 @@ Before calling AskUserQuestion, verify:
- [ ] (recommended) label on one option (even for neutral-posture)
- [ ] Dual-scale effort labels on effort-bearing options (human / CC)
- [ ] Net line closes the decision
- [ ] You are calling the tool when available; otherwise you are using the normal chat fallback and stopping for the user's reply
- [ ] You are calling the tool, not writing prose — unless the documented failure fallback applies (then: prose with the mandatory triad — issue ELI10, per-choice Completeness, Recommendation + `(recommended)` — and a "reply with a letter" instruction, then STOP)
- [ ] Non-ASCII characters (CJK / accents) written directly, NOT \u-escaped
- [ ] If you had 5+ options, you split (or batched into ≤4-groups) — did NOT drop any
- [ ] If you split, you checked dependencies between options before firing the chain
@ -609,12 +632,19 @@ if [ -d "$_PROJ" ]; then
fi
_LATEST_CP=$(find "$_PROJ/checkpoints" -name "*.md" -type f 2>/dev/null | xargs ls -t 2>/dev/null | head -1)
[ -n "$_LATEST_CP" ] && echo "LATEST_CHECKPOINT: $_LATEST_CP"
if [ -f "$_PROJ/decisions.active.json" ]; then
echo "--- ACTIVE DECISIONS (recent, scope-relevant) ---"
~/.claude/skills/gstack/bin/gstack-decision-search --recent 5 2>/dev/null
echo "--- END DECISIONS ---"
fi
echo "--- END ARTIFACTS ---"
fi
```
If artifacts are listed, read the newest useful one. If `LAST_SESSION` or `LATEST_CHECKPOINT` appears, give a 2-sentence welcome back summary. If `RECENT_PATTERN` clearly implies a next skill, suggest it once.
**Cross-session decisions.** If `ACTIVE DECISIONS` are listed, treat them as prior settled calls with their rationale — do not silently re-litigate them; if you're about to reverse one, say so explicitly. Reach for `~/.claude/skills/gstack/bin/gstack-decision-search` whenever a question touches a past decision ("what did we decide / why / did we try"). When you or the user make a DURABLE decision (architecture, scope, tool/vendor choice, or a reversal) — NOT a turn-level or trivial choice — log it with `~/.claude/skills/gstack/bin/gstack-decision-log` (`--supersede <id>` for a reversal). Reliable and local; gbrain not required.
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format is structure; this is prose quality.
@ -629,9 +659,9 @@ Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format i
Curated jargon list lives at `~/.claude/skills/gstack/scripts/jargon-list.json` (80+ terms). On the first jargon term you encounter this session, Read that file once; treat the `terms` array as the canonical list. The list is repo-owned and may grow between releases.
## Completeness Principle — Boil the Lake
## Completeness Principle — Boil the Ocean
AI makes completeness cheap. Recommend complete lakes (tests, edge cases, error paths); flag oceans (rewrites, multi-quarter migrations).
AI makes completeness cheap, so the complete thing is the goal. Recommend full coverage (tests, edge cases, error paths) — boil the ocean one lake at a time. The only thing out of scope is genuinely unrelated work (rewrites, multi-quarter migrations); flag that as separate scope, never as an excuse for a shortcut.
When options differ in coverage, include `Completeness: X/10` (10 = all edge cases, 7 = happy path, 3 = shortcut). When options differ in kind, write: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate scores.

View File

@ -50,6 +50,9 @@ echo "SKILL_PREFIX: $_SKILL_PREFIX"
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
REPO_MODE=${REPO_MODE:-unknown}
echo "REPO_MODE: $REPO_MODE"
_SESSION_KIND=$(~/.claude/skills/gstack/bin/gstack-session-kind 2>/dev/null || echo "interactive")
case "$_SESSION_KIND" in spawned|headless|interactive) ;; *) _SESSION_KIND="interactive" ;; esac
echo "SESSION_KIND: $_SESSION_KIND"
_LAKE_SEEN=$([ -f ~/.gstack/.completeness-intro-seen ] && echo "yes" || echo "no")
echo "LAKE_INTRO: $_LAKE_SEEN"
_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || true)
@ -129,7 +132,7 @@ In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`co
## Skill Invocation During Plan Mode
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If no AskUserQuestion variant is callable, use the normal chat fallback from the AskUserQuestion Format rule and stop for the user's reply. At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If AskUserQuestion is unavailable or a call fails, follow the AskUserQuestion Format failure fallback: `headless` → BLOCKED; `interactive` → the prose fallback (also satisfies end-of-turn). At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If `PROACTIVE` is `"false"`, do not auto-invoke or proactively suggest skills. If a skill seems useful, ask: "I think /skillname might help here — want me to run it?"
@ -164,7 +167,7 @@ touch ~/.gstack/.writing-style-prompted
Skip if `WRITING_STYLE_PENDING` is `no`.
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Lake** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Ocean** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
```bash
open https://garryslist.org/posts/boil-the-ocean
@ -301,11 +304,31 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
**Rule:** if any `mcp__*__AskUserQuestion` variant is in your tool list, prefer it. Hosts may disable native AUQ via `--disallowedTools AskUserQuestion` (Conductor does, by default) and route through their MCP variant; calling native there silently fails. Same questions/options shape; same decision-brief format applies.
**If no AskUserQuestion variant appears in your tool list:** fall back to the host's normal user-facing chat question. Render the same decision brief in prose, stop, and wait for the user's reply. This fallback is for interactive chat hosts that can ask the user normally but do not expose an AUQ tool (for example Codex API sessions). Do not write decisions to the plan file as a substitute, and do not silently auto-decide (only `/plan-tune` AUTO_DECIDE opt-ins authorize auto-picking).
If AskUserQuestion is unavailable (no variant in your tool list) OR a call to it fails, do NOT silently auto-decide or write the decision to the plan file as a substitute. Follow the **failure fallback** below.
### When AskUserQuestion is unavailable or a call fails
Tell three outcomes apart:
1. **Auto-decide denial (NOT a failure).** The result contains `[plan-tune auto-decide] <id> → <option>` — the preference hook working as designed. Proceed with that option. Do NOT retry, do NOT fall back to prose.
2. **Genuine failure** — no variant in your tool list, OR the variant is present but the call returns an error / missing result (MCP transport error, empty result, host bug — e.g. Conductor's MCP AskUserQuestion is flaky and returns `[Tool result missing due to internal error]`).
- If it was present and **errored** (not absent), retry the SAME call **once** — but only if no answer could have surfaced (a missing-result error can arrive after the user already saw the question; retrying would double-prompt, so if it may have reached them, treat as pending, don't retry).
- Then branch on `SESSION_KIND` (echoed by the preamble; empty/absent ⇒ `interactive`):
- `spawned` → defer to the **Spawned session** block: auto-choose the recommended option. Never prose, never BLOCKED.
- `headless``BLOCKED — AskUserQuestion unavailable`; stop and wait (no human can answer).
- `interactive`**prose fallback** (below).
**Prose fallback — render the decision brief as a markdown message, not a tool call.** Same information as the tool format below, different structure (paragraphs, not ✅/❌ bullets). It MUST surface this triad:
1. **A clear ELI10 of the issue itself** — plain English on what's being decided and why it matters (the question, not per-choice), naming the stakes. Lead with it.
2. **Completeness scores per choice** — explicit `Completeness: X/10` on EACH choice (10 complete, 7 happy-path, 3 shortcut); use the kind-note when options differ in kind not coverage, but never silently drop the score.
3. **The recommendation and why** — a `Recommendation: <choice> because <reason>` line plus the `(recommended)` marker on that choice.
Layout: a `D<N>` title + a one-line note that AskUserQuestion failed and to reply with a letter; the issue ELI10; the Recommendation line; then ONE paragraph per choice carrying its `(recommended)` marker, its `Completeness: X/10`, and 2-4 sentences of reasoning — never a bare bullet list; a closing `Net:` line. Split chains / 5+ options: one prose block per per-option call, in sequence. Then STOP and wait — the user's typed answer is the decision. In plan mode this satisfies end-of-turn like a tool call.
### Format
Every AskUserQuestion is a decision brief. Send it as tool_use when an AskUserQuestion tool exists; otherwise use the normal chat fallback above and wait for the user's reply.
Every AskUserQuestion is a decision brief and must be sent as tool_use, not prose — unless the documented failure fallback above applies (interactive session + the call is unavailable/erroring), in which case the prose fallback is the correct output.
```
D<N><one-line question title>
@ -385,7 +408,7 @@ Before calling AskUserQuestion, verify:
- [ ] (recommended) label on one option (even for neutral-posture)
- [ ] Dual-scale effort labels on effort-bearing options (human / CC)
- [ ] Net line closes the decision
- [ ] You are calling the tool when available; otherwise you are using the normal chat fallback and stopping for the user's reply
- [ ] You are calling the tool, not writing prose — unless the documented failure fallback applies (then: prose with the mandatory triad — issue ELI10, per-choice Completeness, Recommendation + `(recommended)` — and a "reply with a letter" instruction, then STOP)
- [ ] Non-ASCII characters (CJK / accents) written directly, NOT \u-escaped
- [ ] If you had 5+ options, you split (or batched into ≤4-groups) — did NOT drop any
- [ ] If you split, you checked dependencies between options before firing the chain
@ -572,12 +595,19 @@ if [ -d "$_PROJ" ]; then
fi
_LATEST_CP=$(find "$_PROJ/checkpoints" -name "*.md" -type f 2>/dev/null | xargs ls -t 2>/dev/null | head -1)
[ -n "$_LATEST_CP" ] && echo "LATEST_CHECKPOINT: $_LATEST_CP"
if [ -f "$_PROJ/decisions.active.json" ]; then
echo "--- ACTIVE DECISIONS (recent, scope-relevant) ---"
~/.claude/skills/gstack/bin/gstack-decision-search --recent 5 2>/dev/null
echo "--- END DECISIONS ---"
fi
echo "--- END ARTIFACTS ---"
fi
```
If artifacts are listed, read the newest useful one. If `LAST_SESSION` or `LATEST_CHECKPOINT` appears, give a 2-sentence welcome back summary. If `RECENT_PATTERN` clearly implies a next skill, suggest it once.
**Cross-session decisions.** If `ACTIVE DECISIONS` are listed, treat them as prior settled calls with their rationale — do not silently re-litigate them; if you're about to reverse one, say so explicitly. Reach for `~/.claude/skills/gstack/bin/gstack-decision-search` whenever a question touches a past decision ("what did we decide / why / did we try"). When you or the user make a DURABLE decision (architecture, scope, tool/vendor choice, or a reversal) — NOT a turn-level or trivial choice — log it with `~/.claude/skills/gstack/bin/gstack-decision-log` (`--supersede <id>` for a reversal). Reliable and local; gbrain not required.
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format is structure; this is prose quality.
@ -592,9 +622,9 @@ Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format i
Curated jargon list lives at `~/.claude/skills/gstack/scripts/jargon-list.json` (80+ terms). On the first jargon term you encounter this session, Read that file once; treat the `terms` array as the canonical list. The list is repo-owned and may grow between releases.
## Completeness Principle — Boil the Lake
## Completeness Principle — Boil the Ocean
AI makes completeness cheap. Recommend complete lakes (tests, edge cases, error paths); flag oceans (rewrites, multi-quarter migrations).
AI makes completeness cheap, so the complete thing is the goal. Recommend full coverage (tests, edge cases, error paths) — boil the ocean one lake at a time. The only thing out of scope is genuinely unrelated work (rewrites, multi-quarter migrations); flag that as separate scope, never as an excuse for a shortcut.
When options differ in coverage, include `Completeness: X/10` (10 = all edge cases, 7 = happy path, 3 = shortcut). When options differ in kind, write: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate scores.

View File

@ -52,6 +52,9 @@ echo "SKILL_PREFIX: $_SKILL_PREFIX"
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
REPO_MODE=${REPO_MODE:-unknown}
echo "REPO_MODE: $REPO_MODE"
_SESSION_KIND=$(~/.claude/skills/gstack/bin/gstack-session-kind 2>/dev/null || echo "interactive")
case "$_SESSION_KIND" in spawned|headless|interactive) ;; *) _SESSION_KIND="interactive" ;; esac
echo "SESSION_KIND: $_SESSION_KIND"
_LAKE_SEEN=$([ -f ~/.gstack/.completeness-intro-seen ] && echo "yes" || echo "no")
echo "LAKE_INTRO: $_LAKE_SEEN"
_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || true)
@ -131,7 +134,7 @@ In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`co
## Skill Invocation During Plan Mode
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If no AskUserQuestion variant is callable, use the normal chat fallback from the AskUserQuestion Format rule and stop for the user's reply. At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If AskUserQuestion is unavailable or a call fails, follow the AskUserQuestion Format failure fallback: `headless` → BLOCKED; `interactive` → the prose fallback (also satisfies end-of-turn). At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If `PROACTIVE` is `"false"`, do not auto-invoke or proactively suggest skills. If a skill seems useful, ask: "I think /skillname might help here — want me to run it?"
@ -166,7 +169,7 @@ touch ~/.gstack/.writing-style-prompted
Skip if `WRITING_STYLE_PENDING` is `no`.
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Lake** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Ocean** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
```bash
open https://garryslist.org/posts/boil-the-ocean
@ -303,11 +306,31 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
**Rule:** if any `mcp__*__AskUserQuestion` variant is in your tool list, prefer it. Hosts may disable native AUQ via `--disallowedTools AskUserQuestion` (Conductor does, by default) and route through their MCP variant; calling native there silently fails. Same questions/options shape; same decision-brief format applies.
**If no AskUserQuestion variant appears in your tool list:** fall back to the host's normal user-facing chat question. Render the same decision brief in prose, stop, and wait for the user's reply. This fallback is for interactive chat hosts that can ask the user normally but do not expose an AUQ tool (for example Codex API sessions). Do not write decisions to the plan file as a substitute, and do not silently auto-decide (only `/plan-tune` AUTO_DECIDE opt-ins authorize auto-picking).
If AskUserQuestion is unavailable (no variant in your tool list) OR a call to it fails, do NOT silently auto-decide or write the decision to the plan file as a substitute. Follow the **failure fallback** below.
### When AskUserQuestion is unavailable or a call fails
Tell three outcomes apart:
1. **Auto-decide denial (NOT a failure).** The result contains `[plan-tune auto-decide] <id> → <option>` — the preference hook working as designed. Proceed with that option. Do NOT retry, do NOT fall back to prose.
2. **Genuine failure** — no variant in your tool list, OR the variant is present but the call returns an error / missing result (MCP transport error, empty result, host bug — e.g. Conductor's MCP AskUserQuestion is flaky and returns `[Tool result missing due to internal error]`).
- If it was present and **errored** (not absent), retry the SAME call **once** — but only if no answer could have surfaced (a missing-result error can arrive after the user already saw the question; retrying would double-prompt, so if it may have reached them, treat as pending, don't retry).
- Then branch on `SESSION_KIND` (echoed by the preamble; empty/absent ⇒ `interactive`):
- `spawned` → defer to the **Spawned session** block: auto-choose the recommended option. Never prose, never BLOCKED.
- `headless``BLOCKED — AskUserQuestion unavailable`; stop and wait (no human can answer).
- `interactive`**prose fallback** (below).
**Prose fallback — render the decision brief as a markdown message, not a tool call.** Same information as the tool format below, different structure (paragraphs, not ✅/❌ bullets). It MUST surface this triad:
1. **A clear ELI10 of the issue itself** — plain English on what's being decided and why it matters (the question, not per-choice), naming the stakes. Lead with it.
2. **Completeness scores per choice** — explicit `Completeness: X/10` on EACH choice (10 complete, 7 happy-path, 3 shortcut); use the kind-note when options differ in kind not coverage, but never silently drop the score.
3. **The recommendation and why** — a `Recommendation: <choice> because <reason>` line plus the `(recommended)` marker on that choice.
Layout: a `D<N>` title + a one-line note that AskUserQuestion failed and to reply with a letter; the issue ELI10; the Recommendation line; then ONE paragraph per choice carrying its `(recommended)` marker, its `Completeness: X/10`, and 2-4 sentences of reasoning — never a bare bullet list; a closing `Net:` line. Split chains / 5+ options: one prose block per per-option call, in sequence. Then STOP and wait — the user's typed answer is the decision. In plan mode this satisfies end-of-turn like a tool call.
### Format
Every AskUserQuestion is a decision brief. Send it as tool_use when an AskUserQuestion tool exists; otherwise use the normal chat fallback above and wait for the user's reply.
Every AskUserQuestion is a decision brief and must be sent as tool_use, not prose — unless the documented failure fallback above applies (interactive session + the call is unavailable/erroring), in which case the prose fallback is the correct output.
```
D<N><one-line question title>
@ -387,7 +410,7 @@ Before calling AskUserQuestion, verify:
- [ ] (recommended) label on one option (even for neutral-posture)
- [ ] Dual-scale effort labels on effort-bearing options (human / CC)
- [ ] Net line closes the decision
- [ ] You are calling the tool when available; otherwise you are using the normal chat fallback and stopping for the user's reply
- [ ] You are calling the tool, not writing prose — unless the documented failure fallback applies (then: prose with the mandatory triad — issue ELI10, per-choice Completeness, Recommendation + `(recommended)` — and a "reply with a letter" instruction, then STOP)
- [ ] Non-ASCII characters (CJK / accents) written directly, NOT \u-escaped
- [ ] If you had 5+ options, you split (or batched into ≤4-groups) — did NOT drop any
- [ ] If you split, you checked dependencies between options before firing the chain
@ -574,12 +597,19 @@ if [ -d "$_PROJ" ]; then
fi
_LATEST_CP=$(find "$_PROJ/checkpoints" -name "*.md" -type f 2>/dev/null | xargs ls -t 2>/dev/null | head -1)
[ -n "$_LATEST_CP" ] && echo "LATEST_CHECKPOINT: $_LATEST_CP"
if [ -f "$_PROJ/decisions.active.json" ]; then
echo "--- ACTIVE DECISIONS (recent, scope-relevant) ---"
~/.claude/skills/gstack/bin/gstack-decision-search --recent 5 2>/dev/null
echo "--- END DECISIONS ---"
fi
echo "--- END ARTIFACTS ---"
fi
```
If artifacts are listed, read the newest useful one. If `LAST_SESSION` or `LATEST_CHECKPOINT` appears, give a 2-sentence welcome back summary. If `RECENT_PATTERN` clearly implies a next skill, suggest it once.
**Cross-session decisions.** If `ACTIVE DECISIONS` are listed, treat them as prior settled calls with their rationale — do not silently re-litigate them; if you're about to reverse one, say so explicitly. Reach for `~/.claude/skills/gstack/bin/gstack-decision-search` whenever a question touches a past decision ("what did we decide / why / did we try"). When you or the user make a DURABLE decision (architecture, scope, tool/vendor choice, or a reversal) — NOT a turn-level or trivial choice — log it with `~/.claude/skills/gstack/bin/gstack-decision-log` (`--supersede <id>` for a reversal). Reliable and local; gbrain not required.
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format is structure; this is prose quality.
@ -594,9 +624,9 @@ Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format i
Curated jargon list lives at `~/.claude/skills/gstack/scripts/jargon-list.json` (80+ terms). On the first jargon term you encounter this session, Read that file once; treat the `terms` array as the canonical list. The list is repo-owned and may grow between releases.
## Completeness Principle — Boil the Lake
## Completeness Principle — Boil the Ocean
AI makes completeness cheap. Recommend complete lakes (tests, edge cases, error paths); flag oceans (rewrites, multi-quarter migrations).
AI makes completeness cheap, so the complete thing is the goal. Recommend full coverage (tests, edge cases, error paths) — boil the ocean one lake at a time. The only thing out of scope is genuinely unrelated work (rewrites, multi-quarter migrations); flag that as separate scope, never as an excuse for a shortcut.
When options differ in coverage, include `Completeness: X/10` (10 = all edge cases, 7 = happy path, 3 = shortcut). When options differ in kind, write: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate scores.

View File

@ -53,6 +53,9 @@ echo "SKILL_PREFIX: $_SKILL_PREFIX"
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
REPO_MODE=${REPO_MODE:-unknown}
echo "REPO_MODE: $REPO_MODE"
_SESSION_KIND=$(~/.claude/skills/gstack/bin/gstack-session-kind 2>/dev/null || echo "interactive")
case "$_SESSION_KIND" in spawned|headless|interactive) ;; *) _SESSION_KIND="interactive" ;; esac
echo "SESSION_KIND: $_SESSION_KIND"
_LAKE_SEEN=$([ -f ~/.gstack/.completeness-intro-seen ] && echo "yes" || echo "no")
echo "LAKE_INTRO: $_LAKE_SEEN"
_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || true)
@ -132,7 +135,7 @@ In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`co
## Skill Invocation During Plan Mode
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If no AskUserQuestion variant is callable, use the normal chat fallback from the AskUserQuestion Format rule and stop for the user's reply. At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If AskUserQuestion is unavailable or a call fails, follow the AskUserQuestion Format failure fallback: `headless` → BLOCKED; `interactive` → the prose fallback (also satisfies end-of-turn). At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If `PROACTIVE` is `"false"`, do not auto-invoke or proactively suggest skills. If a skill seems useful, ask: "I think /skillname might help here — want me to run it?"
@ -167,7 +170,7 @@ touch ~/.gstack/.writing-style-prompted
Skip if `WRITING_STYLE_PENDING` is `no`.
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Lake** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Ocean** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
```bash
open https://garryslist.org/posts/boil-the-ocean
@ -304,11 +307,31 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
**Rule:** if any `mcp__*__AskUserQuestion` variant is in your tool list, prefer it. Hosts may disable native AUQ via `--disallowedTools AskUserQuestion` (Conductor does, by default) and route through their MCP variant; calling native there silently fails. Same questions/options shape; same decision-brief format applies.
**If no AskUserQuestion variant appears in your tool list:** fall back to the host's normal user-facing chat question. Render the same decision brief in prose, stop, and wait for the user's reply. This fallback is for interactive chat hosts that can ask the user normally but do not expose an AUQ tool (for example Codex API sessions). Do not write decisions to the plan file as a substitute, and do not silently auto-decide (only `/plan-tune` AUTO_DECIDE opt-ins authorize auto-picking).
If AskUserQuestion is unavailable (no variant in your tool list) OR a call to it fails, do NOT silently auto-decide or write the decision to the plan file as a substitute. Follow the **failure fallback** below.
### When AskUserQuestion is unavailable or a call fails
Tell three outcomes apart:
1. **Auto-decide denial (NOT a failure).** The result contains `[plan-tune auto-decide] <id> → <option>` — the preference hook working as designed. Proceed with that option. Do NOT retry, do NOT fall back to prose.
2. **Genuine failure** — no variant in your tool list, OR the variant is present but the call returns an error / missing result (MCP transport error, empty result, host bug — e.g. Conductor's MCP AskUserQuestion is flaky and returns `[Tool result missing due to internal error]`).
- If it was present and **errored** (not absent), retry the SAME call **once** — but only if no answer could have surfaced (a missing-result error can arrive after the user already saw the question; retrying would double-prompt, so if it may have reached them, treat as pending, don't retry).
- Then branch on `SESSION_KIND` (echoed by the preamble; empty/absent ⇒ `interactive`):
- `spawned` → defer to the **Spawned session** block: auto-choose the recommended option. Never prose, never BLOCKED.
- `headless``BLOCKED — AskUserQuestion unavailable`; stop and wait (no human can answer).
- `interactive`**prose fallback** (below).
**Prose fallback — render the decision brief as a markdown message, not a tool call.** Same information as the tool format below, different structure (paragraphs, not ✅/❌ bullets). It MUST surface this triad:
1. **A clear ELI10 of the issue itself** — plain English on what's being decided and why it matters (the question, not per-choice), naming the stakes. Lead with it.
2. **Completeness scores per choice** — explicit `Completeness: X/10` on EACH choice (10 complete, 7 happy-path, 3 shortcut); use the kind-note when options differ in kind not coverage, but never silently drop the score.
3. **The recommendation and why** — a `Recommendation: <choice> because <reason>` line plus the `(recommended)` marker on that choice.
Layout: a `D<N>` title + a one-line note that AskUserQuestion failed and to reply with a letter; the issue ELI10; the Recommendation line; then ONE paragraph per choice carrying its `(recommended)` marker, its `Completeness: X/10`, and 2-4 sentences of reasoning — never a bare bullet list; a closing `Net:` line. Split chains / 5+ options: one prose block per per-option call, in sequence. Then STOP and wait — the user's typed answer is the decision. In plan mode this satisfies end-of-turn like a tool call.
### Format
Every AskUserQuestion is a decision brief. Send it as tool_use when an AskUserQuestion tool exists; otherwise use the normal chat fallback above and wait for the user's reply.
Every AskUserQuestion is a decision brief and must be sent as tool_use, not prose — unless the documented failure fallback above applies (interactive session + the call is unavailable/erroring), in which case the prose fallback is the correct output.
```
D<N><one-line question title>
@ -388,7 +411,7 @@ Before calling AskUserQuestion, verify:
- [ ] (recommended) label on one option (even for neutral-posture)
- [ ] Dual-scale effort labels on effort-bearing options (human / CC)
- [ ] Net line closes the decision
- [ ] You are calling the tool when available; otherwise you are using the normal chat fallback and stopping for the user's reply
- [ ] You are calling the tool, not writing prose — unless the documented failure fallback applies (then: prose with the mandatory triad — issue ELI10, per-choice Completeness, Recommendation + `(recommended)` — and a "reply with a letter" instruction, then STOP)
- [ ] Non-ASCII characters (CJK / accents) written directly, NOT \u-escaped
- [ ] If you had 5+ options, you split (or batched into ≤4-groups) — did NOT drop any
- [ ] If you split, you checked dependencies between options before firing the chain
@ -575,12 +598,19 @@ if [ -d "$_PROJ" ]; then
fi
_LATEST_CP=$(find "$_PROJ/checkpoints" -name "*.md" -type f 2>/dev/null | xargs ls -t 2>/dev/null | head -1)
[ -n "$_LATEST_CP" ] && echo "LATEST_CHECKPOINT: $_LATEST_CP"
if [ -f "$_PROJ/decisions.active.json" ]; then
echo "--- ACTIVE DECISIONS (recent, scope-relevant) ---"
~/.claude/skills/gstack/bin/gstack-decision-search --recent 5 2>/dev/null
echo "--- END DECISIONS ---"
fi
echo "--- END ARTIFACTS ---"
fi
```
If artifacts are listed, read the newest useful one. If `LAST_SESSION` or `LATEST_CHECKPOINT` appears, give a 2-sentence welcome back summary. If `RECENT_PATTERN` clearly implies a next skill, suggest it once.
**Cross-session decisions.** If `ACTIVE DECISIONS` are listed, treat them as prior settled calls with their rationale — do not silently re-litigate them; if you're about to reverse one, say so explicitly. Reach for `~/.claude/skills/gstack/bin/gstack-decision-search` whenever a question touches a past decision ("what did we decide / why / did we try"). When you or the user make a DURABLE decision (architecture, scope, tool/vendor choice, or a reversal) — NOT a turn-level or trivial choice — log it with `~/.claude/skills/gstack/bin/gstack-decision-log` (`--supersede <id>` for a reversal). Reliable and local; gbrain not required.
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format is structure; this is prose quality.
@ -595,9 +625,9 @@ Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format i
Curated jargon list lives at `~/.claude/skills/gstack/scripts/jargon-list.json` (80+ terms). On the first jargon term you encounter this session, Read that file once; treat the `terms` array as the canonical list. The list is repo-owned and may grow between releases.
## Completeness Principle — Boil the Lake
## Completeness Principle — Boil the Ocean
AI makes completeness cheap. Recommend complete lakes (tests, edge cases, error paths); flag oceans (rewrites, multi-quarter migrations).
AI makes completeness cheap, so the complete thing is the goal. Recommend full coverage (tests, edge cases, error paths) — boil the ocean one lake at a time. The only thing out of scope is genuinely unrelated work (rewrites, multi-quarter migrations); flag that as separate scope, never as an excuse for a shortcut.
When options differ in coverage, include `Completeness: X/10` (10 = all edge cases, 7 = happy path, 3 = shortcut). When options differ in kind, write: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate scores.

View File

@ -56,6 +56,9 @@ echo "SKILL_PREFIX: $_SKILL_PREFIX"
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
REPO_MODE=${REPO_MODE:-unknown}
echo "REPO_MODE: $REPO_MODE"
_SESSION_KIND=$(~/.claude/skills/gstack/bin/gstack-session-kind 2>/dev/null || echo "interactive")
case "$_SESSION_KIND" in spawned|headless|interactive) ;; *) _SESSION_KIND="interactive" ;; esac
echo "SESSION_KIND: $_SESSION_KIND"
_LAKE_SEEN=$([ -f ~/.gstack/.completeness-intro-seen ] && echo "yes" || echo "no")
echo "LAKE_INTRO: $_LAKE_SEEN"
_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || true)
@ -135,7 +138,7 @@ In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`co
## Skill Invocation During Plan Mode
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If no AskUserQuestion variant is callable, use the normal chat fallback from the AskUserQuestion Format rule and stop for the user's reply. At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If AskUserQuestion is unavailable or a call fails, follow the AskUserQuestion Format failure fallback: `headless` → BLOCKED; `interactive` → the prose fallback (also satisfies end-of-turn). At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If `PROACTIVE` is `"false"`, do not auto-invoke or proactively suggest skills. If a skill seems useful, ask: "I think /skillname might help here — want me to run it?"
@ -170,7 +173,7 @@ touch ~/.gstack/.writing-style-prompted
Skip if `WRITING_STYLE_PENDING` is `no`.
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Lake** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Ocean** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
```bash
open https://garryslist.org/posts/boil-the-ocean
@ -307,11 +310,31 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
**Rule:** if any `mcp__*__AskUserQuestion` variant is in your tool list, prefer it. Hosts may disable native AUQ via `--disallowedTools AskUserQuestion` (Conductor does, by default) and route through their MCP variant; calling native there silently fails. Same questions/options shape; same decision-brief format applies.
**If no AskUserQuestion variant appears in your tool list:** fall back to the host's normal user-facing chat question. Render the same decision brief in prose, stop, and wait for the user's reply. This fallback is for interactive chat hosts that can ask the user normally but do not expose an AUQ tool (for example Codex API sessions). Do not write decisions to the plan file as a substitute, and do not silently auto-decide (only `/plan-tune` AUTO_DECIDE opt-ins authorize auto-picking).
If AskUserQuestion is unavailable (no variant in your tool list) OR a call to it fails, do NOT silently auto-decide or write the decision to the plan file as a substitute. Follow the **failure fallback** below.
### When AskUserQuestion is unavailable or a call fails
Tell three outcomes apart:
1. **Auto-decide denial (NOT a failure).** The result contains `[plan-tune auto-decide] <id> → <option>` — the preference hook working as designed. Proceed with that option. Do NOT retry, do NOT fall back to prose.
2. **Genuine failure** — no variant in your tool list, OR the variant is present but the call returns an error / missing result (MCP transport error, empty result, host bug — e.g. Conductor's MCP AskUserQuestion is flaky and returns `[Tool result missing due to internal error]`).
- If it was present and **errored** (not absent), retry the SAME call **once** — but only if no answer could have surfaced (a missing-result error can arrive after the user already saw the question; retrying would double-prompt, so if it may have reached them, treat as pending, don't retry).
- Then branch on `SESSION_KIND` (echoed by the preamble; empty/absent ⇒ `interactive`):
- `spawned` → defer to the **Spawned session** block: auto-choose the recommended option. Never prose, never BLOCKED.
- `headless``BLOCKED — AskUserQuestion unavailable`; stop and wait (no human can answer).
- `interactive`**prose fallback** (below).
**Prose fallback — render the decision brief as a markdown message, not a tool call.** Same information as the tool format below, different structure (paragraphs, not ✅/❌ bullets). It MUST surface this triad:
1. **A clear ELI10 of the issue itself** — plain English on what's being decided and why it matters (the question, not per-choice), naming the stakes. Lead with it.
2. **Completeness scores per choice** — explicit `Completeness: X/10` on EACH choice (10 complete, 7 happy-path, 3 shortcut); use the kind-note when options differ in kind not coverage, but never silently drop the score.
3. **The recommendation and why** — a `Recommendation: <choice> because <reason>` line plus the `(recommended)` marker on that choice.
Layout: a `D<N>` title + a one-line note that AskUserQuestion failed and to reply with a letter; the issue ELI10; the Recommendation line; then ONE paragraph per choice carrying its `(recommended)` marker, its `Completeness: X/10`, and 2-4 sentences of reasoning — never a bare bullet list; a closing `Net:` line. Split chains / 5+ options: one prose block per per-option call, in sequence. Then STOP and wait — the user's typed answer is the decision. In plan mode this satisfies end-of-turn like a tool call.
### Format
Every AskUserQuestion is a decision brief. Send it as tool_use when an AskUserQuestion tool exists; otherwise use the normal chat fallback above and wait for the user's reply.
Every AskUserQuestion is a decision brief and must be sent as tool_use, not prose — unless the documented failure fallback above applies (interactive session + the call is unavailable/erroring), in which case the prose fallback is the correct output.
```
D<N><one-line question title>
@ -391,7 +414,7 @@ Before calling AskUserQuestion, verify:
- [ ] (recommended) label on one option (even for neutral-posture)
- [ ] Dual-scale effort labels on effort-bearing options (human / CC)
- [ ] Net line closes the decision
- [ ] You are calling the tool when available; otherwise you are using the normal chat fallback and stopping for the user's reply
- [ ] You are calling the tool, not writing prose — unless the documented failure fallback applies (then: prose with the mandatory triad — issue ELI10, per-choice Completeness, Recommendation + `(recommended)` — and a "reply with a letter" instruction, then STOP)
- [ ] Non-ASCII characters (CJK / accents) written directly, NOT \u-escaped
- [ ] If you had 5+ options, you split (or batched into ≤4-groups) — did NOT drop any
- [ ] If you split, you checked dependencies between options before firing the chain
@ -578,12 +601,19 @@ if [ -d "$_PROJ" ]; then
fi
_LATEST_CP=$(find "$_PROJ/checkpoints" -name "*.md" -type f 2>/dev/null | xargs ls -t 2>/dev/null | head -1)
[ -n "$_LATEST_CP" ] && echo "LATEST_CHECKPOINT: $_LATEST_CP"
if [ -f "$_PROJ/decisions.active.json" ]; then
echo "--- ACTIVE DECISIONS (recent, scope-relevant) ---"
~/.claude/skills/gstack/bin/gstack-decision-search --recent 5 2>/dev/null
echo "--- END DECISIONS ---"
fi
echo "--- END ARTIFACTS ---"
fi
```
If artifacts are listed, read the newest useful one. If `LAST_SESSION` or `LATEST_CHECKPOINT` appears, give a 2-sentence welcome back summary. If `RECENT_PATTERN` clearly implies a next skill, suggest it once.
**Cross-session decisions.** If `ACTIVE DECISIONS` are listed, treat them as prior settled calls with their rationale — do not silently re-litigate them; if you're about to reverse one, say so explicitly. Reach for `~/.claude/skills/gstack/bin/gstack-decision-search` whenever a question touches a past decision ("what did we decide / why / did we try"). When you or the user make a DURABLE decision (architecture, scope, tool/vendor choice, or a reversal) — NOT a turn-level or trivial choice — log it with `~/.claude/skills/gstack/bin/gstack-decision-log` (`--supersede <id>` for a reversal). Reliable and local; gbrain not required.
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format is structure; this is prose quality.
@ -598,9 +628,9 @@ Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format i
Curated jargon list lives at `~/.claude/skills/gstack/scripts/jargon-list.json` (80+ terms). On the first jargon term you encounter this session, Read that file once; treat the `terms` array as the canonical list. The list is repo-owned and may grow between releases.
## Completeness Principle — Boil the Lake
## Completeness Principle — Boil the Ocean
AI makes completeness cheap. Recommend complete lakes (tests, edge cases, error paths); flag oceans (rewrites, multi-quarter migrations).
AI makes completeness cheap, so the complete thing is the goal. Recommend full coverage (tests, edge cases, error paths) — boil the ocean one lake at a time. The only thing out of scope is genuinely unrelated work (rewrites, multi-quarter migrations); flag that as separate scope, never as an excuse for a shortcut.
When options differ in coverage, include `Completeness: X/10` (10 = all edge cases, 7 = happy path, 3 = shortcut). When options differ in kind, write: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate scores.

View File

@ -50,6 +50,9 @@ echo "SKILL_PREFIX: $_SKILL_PREFIX"
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
REPO_MODE=${REPO_MODE:-unknown}
echo "REPO_MODE: $REPO_MODE"
_SESSION_KIND=$(~/.claude/skills/gstack/bin/gstack-session-kind 2>/dev/null || echo "interactive")
case "$_SESSION_KIND" in spawned|headless|interactive) ;; *) _SESSION_KIND="interactive" ;; esac
echo "SESSION_KIND: $_SESSION_KIND"
_LAKE_SEEN=$([ -f ~/.gstack/.completeness-intro-seen ] && echo "yes" || echo "no")
echo "LAKE_INTRO: $_LAKE_SEEN"
_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || true)
@ -129,7 +132,7 @@ In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`co
## Skill Invocation During Plan Mode
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If no AskUserQuestion variant is callable, use the normal chat fallback from the AskUserQuestion Format rule and stop for the user's reply. At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If AskUserQuestion is unavailable or a call fails, follow the AskUserQuestion Format failure fallback: `headless` → BLOCKED; `interactive` → the prose fallback (also satisfies end-of-turn). At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If `PROACTIVE` is `"false"`, do not auto-invoke or proactively suggest skills. If a skill seems useful, ask: "I think /skillname might help here — want me to run it?"
@ -164,7 +167,7 @@ touch ~/.gstack/.writing-style-prompted
Skip if `WRITING_STYLE_PENDING` is `no`.
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Lake** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Ocean** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
```bash
open https://garryslist.org/posts/boil-the-ocean
@ -301,11 +304,31 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
**Rule:** if any `mcp__*__AskUserQuestion` variant is in your tool list, prefer it. Hosts may disable native AUQ via `--disallowedTools AskUserQuestion` (Conductor does, by default) and route through their MCP variant; calling native there silently fails. Same questions/options shape; same decision-brief format applies.
**If no AskUserQuestion variant appears in your tool list:** fall back to the host's normal user-facing chat question. Render the same decision brief in prose, stop, and wait for the user's reply. This fallback is for interactive chat hosts that can ask the user normally but do not expose an AUQ tool (for example Codex API sessions). Do not write decisions to the plan file as a substitute, and do not silently auto-decide (only `/plan-tune` AUTO_DECIDE opt-ins authorize auto-picking).
If AskUserQuestion is unavailable (no variant in your tool list) OR a call to it fails, do NOT silently auto-decide or write the decision to the plan file as a substitute. Follow the **failure fallback** below.
### When AskUserQuestion is unavailable or a call fails
Tell three outcomes apart:
1. **Auto-decide denial (NOT a failure).** The result contains `[plan-tune auto-decide] <id> → <option>` — the preference hook working as designed. Proceed with that option. Do NOT retry, do NOT fall back to prose.
2. **Genuine failure** — no variant in your tool list, OR the variant is present but the call returns an error / missing result (MCP transport error, empty result, host bug — e.g. Conductor's MCP AskUserQuestion is flaky and returns `[Tool result missing due to internal error]`).
- If it was present and **errored** (not absent), retry the SAME call **once** — but only if no answer could have surfaced (a missing-result error can arrive after the user already saw the question; retrying would double-prompt, so if it may have reached them, treat as pending, don't retry).
- Then branch on `SESSION_KIND` (echoed by the preamble; empty/absent ⇒ `interactive`):
- `spawned` → defer to the **Spawned session** block: auto-choose the recommended option. Never prose, never BLOCKED.
- `headless``BLOCKED — AskUserQuestion unavailable`; stop and wait (no human can answer).
- `interactive`**prose fallback** (below).
**Prose fallback — render the decision brief as a markdown message, not a tool call.** Same information as the tool format below, different structure (paragraphs, not ✅/❌ bullets). It MUST surface this triad:
1. **A clear ELI10 of the issue itself** — plain English on what's being decided and why it matters (the question, not per-choice), naming the stakes. Lead with it.
2. **Completeness scores per choice** — explicit `Completeness: X/10` on EACH choice (10 complete, 7 happy-path, 3 shortcut); use the kind-note when options differ in kind not coverage, but never silently drop the score.
3. **The recommendation and why** — a `Recommendation: <choice> because <reason>` line plus the `(recommended)` marker on that choice.
Layout: a `D<N>` title + a one-line note that AskUserQuestion failed and to reply with a letter; the issue ELI10; the Recommendation line; then ONE paragraph per choice carrying its `(recommended)` marker, its `Completeness: X/10`, and 2-4 sentences of reasoning — never a bare bullet list; a closing `Net:` line. Split chains / 5+ options: one prose block per per-option call, in sequence. Then STOP and wait — the user's typed answer is the decision. In plan mode this satisfies end-of-turn like a tool call.
### Format
Every AskUserQuestion is a decision brief. Send it as tool_use when an AskUserQuestion tool exists; otherwise use the normal chat fallback above and wait for the user's reply.
Every AskUserQuestion is a decision brief and must be sent as tool_use, not prose — unless the documented failure fallback above applies (interactive session + the call is unavailable/erroring), in which case the prose fallback is the correct output.
```
D<N><one-line question title>
@ -385,7 +408,7 @@ Before calling AskUserQuestion, verify:
- [ ] (recommended) label on one option (even for neutral-posture)
- [ ] Dual-scale effort labels on effort-bearing options (human / CC)
- [ ] Net line closes the decision
- [ ] You are calling the tool when available; otherwise you are using the normal chat fallback and stopping for the user's reply
- [ ] You are calling the tool, not writing prose — unless the documented failure fallback applies (then: prose with the mandatory triad — issue ELI10, per-choice Completeness, Recommendation + `(recommended)` — and a "reply with a letter" instruction, then STOP)
- [ ] Non-ASCII characters (CJK / accents) written directly, NOT \u-escaped
- [ ] If you had 5+ options, you split (or batched into ≤4-groups) — did NOT drop any
- [ ] If you split, you checked dependencies between options before firing the chain
@ -572,12 +595,19 @@ if [ -d "$_PROJ" ]; then
fi
_LATEST_CP=$(find "$_PROJ/checkpoints" -name "*.md" -type f 2>/dev/null | xargs ls -t 2>/dev/null | head -1)
[ -n "$_LATEST_CP" ] && echo "LATEST_CHECKPOINT: $_LATEST_CP"
if [ -f "$_PROJ/decisions.active.json" ]; then
echo "--- ACTIVE DECISIONS (recent, scope-relevant) ---"
~/.claude/skills/gstack/bin/gstack-decision-search --recent 5 2>/dev/null
echo "--- END DECISIONS ---"
fi
echo "--- END ARTIFACTS ---"
fi
```
If artifacts are listed, read the newest useful one. If `LAST_SESSION` or `LATEST_CHECKPOINT` appears, give a 2-sentence welcome back summary. If `RECENT_PATTERN` clearly implies a next skill, suggest it once.
**Cross-session decisions.** If `ACTIVE DECISIONS` are listed, treat them as prior settled calls with their rationale — do not silently re-litigate them; if you're about to reverse one, say so explicitly. Reach for `~/.claude/skills/gstack/bin/gstack-decision-search` whenever a question touches a past decision ("what did we decide / why / did we try"). When you or the user make a DURABLE decision (architecture, scope, tool/vendor choice, or a reversal) — NOT a turn-level or trivial choice — log it with `~/.claude/skills/gstack/bin/gstack-decision-log` (`--supersede <id>` for a reversal). Reliable and local; gbrain not required.
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format is structure; this is prose quality.
@ -592,9 +622,9 @@ Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format i
Curated jargon list lives at `~/.claude/skills/gstack/scripts/jargon-list.json` (80+ terms). On the first jargon term you encounter this session, Read that file once; treat the `terms` array as the canonical list. The list is repo-owned and may grow between releases.
## Completeness Principle — Boil the Lake
## Completeness Principle — Boil the Ocean
AI makes completeness cheap. Recommend complete lakes (tests, edge cases, error paths); flag oceans (rewrites, multi-quarter migrations).
AI makes completeness cheap, so the complete thing is the goal. Recommend full coverage (tests, edge cases, error paths) — boil the ocean one lake at a time. The only thing out of scope is genuinely unrelated work (rewrites, multi-quarter migrations); flag that as separate scope, never as an excuse for a shortcut.
When options differ in coverage, include `Completeness: X/10` (10 = all edge cases, 7 = happy path, 3 = shortcut). When options differ in kind, write: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate scores.

View File

@ -45,6 +45,9 @@ echo "SKILL_PREFIX: $_SKILL_PREFIX"
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
REPO_MODE=${REPO_MODE:-unknown}
echo "REPO_MODE: $REPO_MODE"
_SESSION_KIND=$(~/.claude/skills/gstack/bin/gstack-session-kind 2>/dev/null || echo "interactive")
case "$_SESSION_KIND" in spawned|headless|interactive) ;; *) _SESSION_KIND="interactive" ;; esac
echo "SESSION_KIND: $_SESSION_KIND"
_LAKE_SEEN=$([ -f ~/.gstack/.completeness-intro-seen ] && echo "yes" || echo "no")
echo "LAKE_INTRO: $_LAKE_SEEN"
_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || true)
@ -124,7 +127,7 @@ In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`co
## Skill Invocation During Plan Mode
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If no AskUserQuestion variant is callable, use the normal chat fallback from the AskUserQuestion Format rule and stop for the user's reply. At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If AskUserQuestion is unavailable or a call fails, follow the AskUserQuestion Format failure fallback: `headless` → BLOCKED; `interactive` → the prose fallback (also satisfies end-of-turn). At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If `PROACTIVE` is `"false"`, do not auto-invoke or proactively suggest skills. If a skill seems useful, ask: "I think /skillname might help here — want me to run it?"
@ -159,7 +162,7 @@ touch ~/.gstack/.writing-style-prompted
Skip if `WRITING_STYLE_PENDING` is `no`.
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Lake** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Ocean** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
```bash
open https://garryslist.org/posts/boil-the-ocean
@ -296,11 +299,31 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
**Rule:** if any `mcp__*__AskUserQuestion` variant is in your tool list, prefer it. Hosts may disable native AUQ via `--disallowedTools AskUserQuestion` (Conductor does, by default) and route through their MCP variant; calling native there silently fails. Same questions/options shape; same decision-brief format applies.
**If no AskUserQuestion variant appears in your tool list:** fall back to the host's normal user-facing chat question. Render the same decision brief in prose, stop, and wait for the user's reply. This fallback is for interactive chat hosts that can ask the user normally but do not expose an AUQ tool (for example Codex API sessions). Do not write decisions to the plan file as a substitute, and do not silently auto-decide (only `/plan-tune` AUTO_DECIDE opt-ins authorize auto-picking).
If AskUserQuestion is unavailable (no variant in your tool list) OR a call to it fails, do NOT silently auto-decide or write the decision to the plan file as a substitute. Follow the **failure fallback** below.
### When AskUserQuestion is unavailable or a call fails
Tell three outcomes apart:
1. **Auto-decide denial (NOT a failure).** The result contains `[plan-tune auto-decide] <id> → <option>` — the preference hook working as designed. Proceed with that option. Do NOT retry, do NOT fall back to prose.
2. **Genuine failure** — no variant in your tool list, OR the variant is present but the call returns an error / missing result (MCP transport error, empty result, host bug — e.g. Conductor's MCP AskUserQuestion is flaky and returns `[Tool result missing due to internal error]`).
- If it was present and **errored** (not absent), retry the SAME call **once** — but only if no answer could have surfaced (a missing-result error can arrive after the user already saw the question; retrying would double-prompt, so if it may have reached them, treat as pending, don't retry).
- Then branch on `SESSION_KIND` (echoed by the preamble; empty/absent ⇒ `interactive`):
- `spawned` → defer to the **Spawned session** block: auto-choose the recommended option. Never prose, never BLOCKED.
- `headless``BLOCKED — AskUserQuestion unavailable`; stop and wait (no human can answer).
- `interactive`**prose fallback** (below).
**Prose fallback — render the decision brief as a markdown message, not a tool call.** Same information as the tool format below, different structure (paragraphs, not ✅/❌ bullets). It MUST surface this triad:
1. **A clear ELI10 of the issue itself** — plain English on what's being decided and why it matters (the question, not per-choice), naming the stakes. Lead with it.
2. **Completeness scores per choice** — explicit `Completeness: X/10` on EACH choice (10 complete, 7 happy-path, 3 shortcut); use the kind-note when options differ in kind not coverage, but never silently drop the score.
3. **The recommendation and why** — a `Recommendation: <choice> because <reason>` line plus the `(recommended)` marker on that choice.
Layout: a `D<N>` title + a one-line note that AskUserQuestion failed and to reply with a letter; the issue ELI10; the Recommendation line; then ONE paragraph per choice carrying its `(recommended)` marker, its `Completeness: X/10`, and 2-4 sentences of reasoning — never a bare bullet list; a closing `Net:` line. Split chains / 5+ options: one prose block per per-option call, in sequence. Then STOP and wait — the user's typed answer is the decision. In plan mode this satisfies end-of-turn like a tool call.
### Format
Every AskUserQuestion is a decision brief. Send it as tool_use when an AskUserQuestion tool exists; otherwise use the normal chat fallback above and wait for the user's reply.
Every AskUserQuestion is a decision brief and must be sent as tool_use, not prose — unless the documented failure fallback above applies (interactive session + the call is unavailable/erroring), in which case the prose fallback is the correct output.
```
D<N><one-line question title>
@ -380,7 +403,7 @@ Before calling AskUserQuestion, verify:
- [ ] (recommended) label on one option (even for neutral-posture)
- [ ] Dual-scale effort labels on effort-bearing options (human / CC)
- [ ] Net line closes the decision
- [ ] You are calling the tool when available; otherwise you are using the normal chat fallback and stopping for the user's reply
- [ ] You are calling the tool, not writing prose — unless the documented failure fallback applies (then: prose with the mandatory triad — issue ELI10, per-choice Completeness, Recommendation + `(recommended)` — and a "reply with a letter" instruction, then STOP)
- [ ] Non-ASCII characters (CJK / accents) written directly, NOT \u-escaped
- [ ] If you had 5+ options, you split (or batched into ≤4-groups) — did NOT drop any
- [ ] If you split, you checked dependencies between options before firing the chain
@ -567,12 +590,19 @@ if [ -d "$_PROJ" ]; then
fi
_LATEST_CP=$(find "$_PROJ/checkpoints" -name "*.md" -type f 2>/dev/null | xargs ls -t 2>/dev/null | head -1)
[ -n "$_LATEST_CP" ] && echo "LATEST_CHECKPOINT: $_LATEST_CP"
if [ -f "$_PROJ/decisions.active.json" ]; then
echo "--- ACTIVE DECISIONS (recent, scope-relevant) ---"
~/.claude/skills/gstack/bin/gstack-decision-search --recent 5 2>/dev/null
echo "--- END DECISIONS ---"
fi
echo "--- END ARTIFACTS ---"
fi
```
If artifacts are listed, read the newest useful one. If `LAST_SESSION` or `LATEST_CHECKPOINT` appears, give a 2-sentence welcome back summary. If `RECENT_PATTERN` clearly implies a next skill, suggest it once.
**Cross-session decisions.** If `ACTIVE DECISIONS` are listed, treat them as prior settled calls with their rationale — do not silently re-litigate them; if you're about to reverse one, say so explicitly. Reach for `~/.claude/skills/gstack/bin/gstack-decision-search` whenever a question touches a past decision ("what did we decide / why / did we try"). When you or the user make a DURABLE decision (architecture, scope, tool/vendor choice, or a reversal) — NOT a turn-level or trivial choice — log it with `~/.claude/skills/gstack/bin/gstack-decision-log` (`--supersede <id>` for a reversal). Reliable and local; gbrain not required.
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format is structure; this is prose quality.
@ -587,9 +617,9 @@ Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format i
Curated jargon list lives at `~/.claude/skills/gstack/scripts/jargon-list.json` (80+ terms). On the first jargon term you encounter this session, Read that file once; treat the `terms` array as the canonical list. The list is repo-owned and may grow between releases.
## Completeness Principle — Boil the Lake
## Completeness Principle — Boil the Ocean
AI makes completeness cheap. Recommend complete lakes (tests, edge cases, error paths); flag oceans (rewrites, multi-quarter migrations).
AI makes completeness cheap, so the complete thing is the goal. Recommend full coverage (tests, edge cases, error paths) — boil the ocean one lake at a time. The only thing out of scope is genuinely unrelated work (rewrites, multi-quarter migrations); flag that as separate scope, never as an excuse for a shortcut.
When options differ in coverage, include `Completeness: X/10` (10 = all edge cases, 7 = happy path, 3 = shortcut). When options differ in kind, write: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate scores.

View File

@ -46,6 +46,9 @@ echo "SKILL_PREFIX: $_SKILL_PREFIX"
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
REPO_MODE=${REPO_MODE:-unknown}
echo "REPO_MODE: $REPO_MODE"
_SESSION_KIND=$(~/.claude/skills/gstack/bin/gstack-session-kind 2>/dev/null || echo "interactive")
case "$_SESSION_KIND" in spawned|headless|interactive) ;; *) _SESSION_KIND="interactive" ;; esac
echo "SESSION_KIND: $_SESSION_KIND"
_LAKE_SEEN=$([ -f ~/.gstack/.completeness-intro-seen ] && echo "yes" || echo "no")
echo "LAKE_INTRO: $_LAKE_SEEN"
_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || true)
@ -125,7 +128,7 @@ In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`co
## Skill Invocation During Plan Mode
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If no AskUserQuestion variant is callable, use the normal chat fallback from the AskUserQuestion Format rule and stop for the user's reply. At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If AskUserQuestion is unavailable or a call fails, follow the AskUserQuestion Format failure fallback: `headless` → BLOCKED; `interactive` → the prose fallback (also satisfies end-of-turn). At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If `PROACTIVE` is `"false"`, do not auto-invoke or proactively suggest skills. If a skill seems useful, ask: "I think /skillname might help here — want me to run it?"
@ -160,7 +163,7 @@ touch ~/.gstack/.writing-style-prompted
Skip if `WRITING_STYLE_PENDING` is `no`.
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Lake** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Ocean** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
```bash
open https://garryslist.org/posts/boil-the-ocean
@ -297,11 +300,31 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
**Rule:** if any `mcp__*__AskUserQuestion` variant is in your tool list, prefer it. Hosts may disable native AUQ via `--disallowedTools AskUserQuestion` (Conductor does, by default) and route through their MCP variant; calling native there silently fails. Same questions/options shape; same decision-brief format applies.
**If no AskUserQuestion variant appears in your tool list:** fall back to the host's normal user-facing chat question. Render the same decision brief in prose, stop, and wait for the user's reply. This fallback is for interactive chat hosts that can ask the user normally but do not expose an AUQ tool (for example Codex API sessions). Do not write decisions to the plan file as a substitute, and do not silently auto-decide (only `/plan-tune` AUTO_DECIDE opt-ins authorize auto-picking).
If AskUserQuestion is unavailable (no variant in your tool list) OR a call to it fails, do NOT silently auto-decide or write the decision to the plan file as a substitute. Follow the **failure fallback** below.
### When AskUserQuestion is unavailable or a call fails
Tell three outcomes apart:
1. **Auto-decide denial (NOT a failure).** The result contains `[plan-tune auto-decide] <id> → <option>` — the preference hook working as designed. Proceed with that option. Do NOT retry, do NOT fall back to prose.
2. **Genuine failure** — no variant in your tool list, OR the variant is present but the call returns an error / missing result (MCP transport error, empty result, host bug — e.g. Conductor's MCP AskUserQuestion is flaky and returns `[Tool result missing due to internal error]`).
- If it was present and **errored** (not absent), retry the SAME call **once** — but only if no answer could have surfaced (a missing-result error can arrive after the user already saw the question; retrying would double-prompt, so if it may have reached them, treat as pending, don't retry).
- Then branch on `SESSION_KIND` (echoed by the preamble; empty/absent ⇒ `interactive`):
- `spawned` → defer to the **Spawned session** block: auto-choose the recommended option. Never prose, never BLOCKED.
- `headless``BLOCKED — AskUserQuestion unavailable`; stop and wait (no human can answer).
- `interactive`**prose fallback** (below).
**Prose fallback — render the decision brief as a markdown message, not a tool call.** Same information as the tool format below, different structure (paragraphs, not ✅/❌ bullets). It MUST surface this triad:
1. **A clear ELI10 of the issue itself** — plain English on what's being decided and why it matters (the question, not per-choice), naming the stakes. Lead with it.
2. **Completeness scores per choice** — explicit `Completeness: X/10` on EACH choice (10 complete, 7 happy-path, 3 shortcut); use the kind-note when options differ in kind not coverage, but never silently drop the score.
3. **The recommendation and why** — a `Recommendation: <choice> because <reason>` line plus the `(recommended)` marker on that choice.
Layout: a `D<N>` title + a one-line note that AskUserQuestion failed and to reply with a letter; the issue ELI10; the Recommendation line; then ONE paragraph per choice carrying its `(recommended)` marker, its `Completeness: X/10`, and 2-4 sentences of reasoning — never a bare bullet list; a closing `Net:` line. Split chains / 5+ options: one prose block per per-option call, in sequence. Then STOP and wait — the user's typed answer is the decision. In plan mode this satisfies end-of-turn like a tool call.
### Format
Every AskUserQuestion is a decision brief. Send it as tool_use when an AskUserQuestion tool exists; otherwise use the normal chat fallback above and wait for the user's reply.
Every AskUserQuestion is a decision brief and must be sent as tool_use, not prose — unless the documented failure fallback above applies (interactive session + the call is unavailable/erroring), in which case the prose fallback is the correct output.
```
D<N><one-line question title>
@ -381,7 +404,7 @@ Before calling AskUserQuestion, verify:
- [ ] (recommended) label on one option (even for neutral-posture)
- [ ] Dual-scale effort labels on effort-bearing options (human / CC)
- [ ] Net line closes the decision
- [ ] You are calling the tool when available; otherwise you are using the normal chat fallback and stopping for the user's reply
- [ ] You are calling the tool, not writing prose — unless the documented failure fallback applies (then: prose with the mandatory triad — issue ELI10, per-choice Completeness, Recommendation + `(recommended)` — and a "reply with a letter" instruction, then STOP)
- [ ] Non-ASCII characters (CJK / accents) written directly, NOT \u-escaped
- [ ] If you had 5+ options, you split (or batched into ≤4-groups) — did NOT drop any
- [ ] If you split, you checked dependencies between options before firing the chain
@ -568,12 +591,19 @@ if [ -d "$_PROJ" ]; then
fi
_LATEST_CP=$(find "$_PROJ/checkpoints" -name "*.md" -type f 2>/dev/null | xargs ls -t 2>/dev/null | head -1)
[ -n "$_LATEST_CP" ] && echo "LATEST_CHECKPOINT: $_LATEST_CP"
if [ -f "$_PROJ/decisions.active.json" ]; then
echo "--- ACTIVE DECISIONS (recent, scope-relevant) ---"
~/.claude/skills/gstack/bin/gstack-decision-search --recent 5 2>/dev/null
echo "--- END DECISIONS ---"
fi
echo "--- END ARTIFACTS ---"
fi
```
If artifacts are listed, read the newest useful one. If `LAST_SESSION` or `LATEST_CHECKPOINT` appears, give a 2-sentence welcome back summary. If `RECENT_PATTERN` clearly implies a next skill, suggest it once.
**Cross-session decisions.** If `ACTIVE DECISIONS` are listed, treat them as prior settled calls with their rationale — do not silently re-litigate them; if you're about to reverse one, say so explicitly. Reach for `~/.claude/skills/gstack/bin/gstack-decision-search` whenever a question touches a past decision ("what did we decide / why / did we try"). When you or the user make a DURABLE decision (architecture, scope, tool/vendor choice, or a reversal) — NOT a turn-level or trivial choice — log it with `~/.claude/skills/gstack/bin/gstack-decision-log` (`--supersede <id>` for a reversal). Reliable and local; gbrain not required.
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format is structure; this is prose quality.
@ -588,9 +618,9 @@ Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format i
Curated jargon list lives at `~/.claude/skills/gstack/scripts/jargon-list.json` (80+ terms). On the first jargon term you encounter this session, Read that file once; treat the `terms` array as the canonical list. The list is repo-owned and may grow between releases.
## Completeness Principle — Boil the Lake
## Completeness Principle — Boil the Ocean
AI makes completeness cheap. Recommend complete lakes (tests, edge cases, error paths); flag oceans (rewrites, multi-quarter migrations).
AI makes completeness cheap, so the complete thing is the goal. Recommend full coverage (tests, edge cases, error paths) — boil the ocean one lake at a time. The only thing out of scope is genuinely unrelated work (rewrites, multi-quarter migrations); flag that as separate scope, never as an excuse for a shortcut.
When options differ in coverage, include `Completeness: X/10` (10 = all edge cases, 7 = happy path, 3 = shortcut). When options differ in kind, write: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate scores.

View File

@ -48,6 +48,9 @@ echo "SKILL_PREFIX: $_SKILL_PREFIX"
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
REPO_MODE=${REPO_MODE:-unknown}
echo "REPO_MODE: $REPO_MODE"
_SESSION_KIND=$(~/.claude/skills/gstack/bin/gstack-session-kind 2>/dev/null || echo "interactive")
case "$_SESSION_KIND" in spawned|headless|interactive) ;; *) _SESSION_KIND="interactive" ;; esac
echo "SESSION_KIND: $_SESSION_KIND"
_LAKE_SEEN=$([ -f ~/.gstack/.completeness-intro-seen ] && echo "yes" || echo "no")
echo "LAKE_INTRO: $_LAKE_SEEN"
_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || true)
@ -127,7 +130,7 @@ In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`co
## Skill Invocation During Plan Mode
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If no AskUserQuestion variant is callable, use the normal chat fallback from the AskUserQuestion Format rule and stop for the user's reply. At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If AskUserQuestion is unavailable or a call fails, follow the AskUserQuestion Format failure fallback: `headless` → BLOCKED; `interactive` → the prose fallback (also satisfies end-of-turn). At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If `PROACTIVE` is `"false"`, do not auto-invoke or proactively suggest skills. If a skill seems useful, ask: "I think /skillname might help here — want me to run it?"
@ -162,7 +165,7 @@ touch ~/.gstack/.writing-style-prompted
Skip if `WRITING_STYLE_PENDING` is `no`.
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Lake** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Ocean** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
```bash
open https://garryslist.org/posts/boil-the-ocean
@ -299,11 +302,31 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
**Rule:** if any `mcp__*__AskUserQuestion` variant is in your tool list, prefer it. Hosts may disable native AUQ via `--disallowedTools AskUserQuestion` (Conductor does, by default) and route through their MCP variant; calling native there silently fails. Same questions/options shape; same decision-brief format applies.
**If no AskUserQuestion variant appears in your tool list:** fall back to the host's normal user-facing chat question. Render the same decision brief in prose, stop, and wait for the user's reply. This fallback is for interactive chat hosts that can ask the user normally but do not expose an AUQ tool (for example Codex API sessions). Do not write decisions to the plan file as a substitute, and do not silently auto-decide (only `/plan-tune` AUTO_DECIDE opt-ins authorize auto-picking).
If AskUserQuestion is unavailable (no variant in your tool list) OR a call to it fails, do NOT silently auto-decide or write the decision to the plan file as a substitute. Follow the **failure fallback** below.
### When AskUserQuestion is unavailable or a call fails
Tell three outcomes apart:
1. **Auto-decide denial (NOT a failure).** The result contains `[plan-tune auto-decide] <id> → <option>` — the preference hook working as designed. Proceed with that option. Do NOT retry, do NOT fall back to prose.
2. **Genuine failure** — no variant in your tool list, OR the variant is present but the call returns an error / missing result (MCP transport error, empty result, host bug — e.g. Conductor's MCP AskUserQuestion is flaky and returns `[Tool result missing due to internal error]`).
- If it was present and **errored** (not absent), retry the SAME call **once** — but only if no answer could have surfaced (a missing-result error can arrive after the user already saw the question; retrying would double-prompt, so if it may have reached them, treat as pending, don't retry).
- Then branch on `SESSION_KIND` (echoed by the preamble; empty/absent ⇒ `interactive`):
- `spawned` → defer to the **Spawned session** block: auto-choose the recommended option. Never prose, never BLOCKED.
- `headless``BLOCKED — AskUserQuestion unavailable`; stop and wait (no human can answer).
- `interactive`**prose fallback** (below).
**Prose fallback — render the decision brief as a markdown message, not a tool call.** Same information as the tool format below, different structure (paragraphs, not ✅/❌ bullets). It MUST surface this triad:
1. **A clear ELI10 of the issue itself** — plain English on what's being decided and why it matters (the question, not per-choice), naming the stakes. Lead with it.
2. **Completeness scores per choice** — explicit `Completeness: X/10` on EACH choice (10 complete, 7 happy-path, 3 shortcut); use the kind-note when options differ in kind not coverage, but never silently drop the score.
3. **The recommendation and why** — a `Recommendation: <choice> because <reason>` line plus the `(recommended)` marker on that choice.
Layout: a `D<N>` title + a one-line note that AskUserQuestion failed and to reply with a letter; the issue ELI10; the Recommendation line; then ONE paragraph per choice carrying its `(recommended)` marker, its `Completeness: X/10`, and 2-4 sentences of reasoning — never a bare bullet list; a closing `Net:` line. Split chains / 5+ options: one prose block per per-option call, in sequence. Then STOP and wait — the user's typed answer is the decision. In plan mode this satisfies end-of-turn like a tool call.
### Format
Every AskUserQuestion is a decision brief. Send it as tool_use when an AskUserQuestion tool exists; otherwise use the normal chat fallback above and wait for the user's reply.
Every AskUserQuestion is a decision brief and must be sent as tool_use, not prose — unless the documented failure fallback above applies (interactive session + the call is unavailable/erroring), in which case the prose fallback is the correct output.
```
D<N><one-line question title>
@ -383,7 +406,7 @@ Before calling AskUserQuestion, verify:
- [ ] (recommended) label on one option (even for neutral-posture)
- [ ] Dual-scale effort labels on effort-bearing options (human / CC)
- [ ] Net line closes the decision
- [ ] You are calling the tool when available; otherwise you are using the normal chat fallback and stopping for the user's reply
- [ ] You are calling the tool, not writing prose — unless the documented failure fallback applies (then: prose with the mandatory triad — issue ELI10, per-choice Completeness, Recommendation + `(recommended)` — and a "reply with a letter" instruction, then STOP)
- [ ] Non-ASCII characters (CJK / accents) written directly, NOT \u-escaped
- [ ] If you had 5+ options, you split (or batched into ≤4-groups) — did NOT drop any
- [ ] If you split, you checked dependencies between options before firing the chain
@ -570,12 +593,19 @@ if [ -d "$_PROJ" ]; then
fi
_LATEST_CP=$(find "$_PROJ/checkpoints" -name "*.md" -type f 2>/dev/null | xargs ls -t 2>/dev/null | head -1)
[ -n "$_LATEST_CP" ] && echo "LATEST_CHECKPOINT: $_LATEST_CP"
if [ -f "$_PROJ/decisions.active.json" ]; then
echo "--- ACTIVE DECISIONS (recent, scope-relevant) ---"
~/.claude/skills/gstack/bin/gstack-decision-search --recent 5 2>/dev/null
echo "--- END DECISIONS ---"
fi
echo "--- END ARTIFACTS ---"
fi
```
If artifacts are listed, read the newest useful one. If `LAST_SESSION` or `LATEST_CHECKPOINT` appears, give a 2-sentence welcome back summary. If `RECENT_PATTERN` clearly implies a next skill, suggest it once.
**Cross-session decisions.** If `ACTIVE DECISIONS` are listed, treat them as prior settled calls with their rationale — do not silently re-litigate them; if you're about to reverse one, say so explicitly. Reach for `~/.claude/skills/gstack/bin/gstack-decision-search` whenever a question touches a past decision ("what did we decide / why / did we try"). When you or the user make a DURABLE decision (architecture, scope, tool/vendor choice, or a reversal) — NOT a turn-level or trivial choice — log it with `~/.claude/skills/gstack/bin/gstack-decision-log` (`--supersede <id>` for a reversal). Reliable and local; gbrain not required.
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format is structure; this is prose quality.
@ -590,9 +620,9 @@ Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format i
Curated jargon list lives at `~/.claude/skills/gstack/scripts/jargon-list.json` (80+ terms). On the first jargon term you encounter this session, Read that file once; treat the `terms` array as the canonical list. The list is repo-owned and may grow between releases.
## Completeness Principle — Boil the Lake
## Completeness Principle — Boil the Ocean
AI makes completeness cheap. Recommend complete lakes (tests, edge cases, error paths); flag oceans (rewrites, multi-quarter migrations).
AI makes completeness cheap, so the complete thing is the goal. Recommend full coverage (tests, edge cases, error paths) — boil the ocean one lake at a time. The only thing out of scope is genuinely unrelated work (rewrites, multi-quarter migrations); flag that as separate scope, never as an excuse for a shortcut.
When options differ in coverage, include `Completeness: X/10` (10 = all edge cases, 7 = happy path, 3 = shortcut). When options differ in kind, write: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate scores.

28
lib/bin-context.ts Normal file
View File

@ -0,0 +1,28 @@
/**
* bin-context tiny shared helpers for non-interactive gstack bins that need the
* project slug, current branch, and argv flags. Extracted from the decision bins
* (gstack-decision-log / gstack-decision-search) so the slug/branch/flag plumbing
* lives in one audited place instead of being copy-pasted per bin.
*/
import { spawnSync } from "child_process";
/** Resolve the project slug via the `gstack-slug` helper (parses `SLUG=...`). */
export function resolveSlug(slugBinPath: string): string {
const r = spawnSync(slugBinPath, { encoding: "utf-8" });
const m = (r.stdout || "").match(/^SLUG=(.+)$/m);
return m ? m[1].trim() : "unknown";
}
/** Current git branch, or undefined on detached HEAD / outside a repo. */
export function gitBranch(): string | undefined {
const r = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], { encoding: "utf-8" });
const b = (r.stdout || "").trim();
return b && b !== "HEAD" ? b : undefined;
}
/** The value following `--flag` in argv, or undefined if absent. */
export function flagValue(args: string[], name: string): string | undefined {
const i = args.indexOf(name);
return i >= 0 ? args[i + 1] : undefined;
}

View File

@ -29,7 +29,7 @@
*/
import { spawnSync } from "child_process";
import { existsSync, realpathSync } from "fs";
import { existsSync, realpathSync, readFileSync } from "fs";
import { homedir } from "os";
import { join, resolve, sep } from "path";
import { execGbrainJson, execGbrainText, NEEDS_SHELL_ON_WINDOWS } from "./gbrain-exec";
@ -92,7 +92,20 @@ export function detectAutopilot(
join(homedir(), ".gbrain", "autopilot.pid"),
];
for (const lp of lockPaths) {
if (existsSync(lp)) return { active: true, signal: `lock:${lp}` };
if (!existsSync(lp)) continue;
// A lock FILE alone is not proof of life — a crashed daemon leaves a stale
// lock that would otherwise wedge every sync forever (observed: a dead pid
// refused --full indefinitely). Read the holder pid and check liveness.
const pid = readLockPid(lp);
if (pid === null) {
// Can't introspect (no parseable pid) → stay conservative: treat as active.
return { active: true, signal: `lock:${lp}` };
}
if (isPidAlive(pid)) {
return { active: true, signal: `lock:${lp} (pid ${pid})` };
}
// Stale lock (holder pid is dead): ignore this signal, keep checking. Pure
// decision function — we do NOT delete the file here; the caller may clean it.
}
// Primary signal: a live `gbrain autopilot` process.
const running = (probe.processRunning ?? defaultProcessRunning)();
@ -100,6 +113,34 @@ export function detectAutopilot(
return { active: false, signal: null };
}
/** Read the holder pid from a lock/pid file. Returns null if no integer pid is present. */
function readLockPid(lockPath: string): number | null {
try {
const raw = readFileSync(lockPath, "utf-8").trim();
// Files seen: a bare pid ("65495"), or JSON like {"pid":65495,...}.
const m = raw.match(/"pid"\s*:\s*(\d+)/) ?? raw.match(/^(\d+)$/);
if (!m) return null;
const pid = Number.parseInt(m[1], 10);
return Number.isFinite(pid) && pid > 0 ? pid : null;
} catch {
return null;
}
}
/**
* Liveness via signal 0: no signal sent, just an existence/permission check.
* ESRCH dead; EPERM alive but owned by another user. Cross-host pids are
* meaningless, but the autopilot lock is same-host by construction.
*/
function isPidAlive(pid: number): boolean {
try {
process.kill(pid, 0);
return true;
} catch (err) {
return (err as NodeJS.ErrnoException).code === "EPERM";
}
}
function defaultProcessRunning(): boolean {
// No reliable pgrep on Windows; rely on the lock-file signal there.
if (process.platform === "win32") return false;

View File

@ -11,7 +11,7 @@
import { execFileSync, spawnSync } from "child_process";
import { withErrorContext } from "./gstack-memory-helpers";
import { NEEDS_SHELL_ON_WINDOWS } from "./gbrain-exec";
import { execGbrainJson, NEEDS_SHELL_ON_WINDOWS } from "./gbrain-exec";
export interface SourceState {
/** "absent" — id not registered. "match" — id at expected path. "drift" — id at different path. */
@ -217,3 +217,60 @@ export function sourcePageCount(id: string, env?: NodeJS.ProcessEnv): number | n
return null;
}
}
/**
* Whether a source's call graph has been built.
*
* "completed" `gbrain dream` has run a full maintenance cycle, so the
* brain-global `resolve_symbol_edges` phase populated this
* source's call graph (`gbrain code-callers`/`code-callees`
* return edges).
* "never" a cycle has provably NOT completed for this source.
* "unknown" doctor is unavailable, unparseable, or reports a failure
* that doesn't name this source. Callers MUST treat unknown
* conservatively (the orchestrator skips auto-dream and WARNs
* rather than launch a ~35-min cycle on a flaky-doctor signal
* see the `gbrain-doctor-overstrict` learning).
*/
export type CycleStatus = "completed" | "never" | "unknown";
interface DoctorCheck {
name?: string;
status?: string;
message?: string;
}
interface DoctorReport {
checks?: DoctorCheck[];
}
/**
* Read `gbrain doctor --json --fast` and decide whether <sourceId>'s call
* graph is built, by inspecting the `cycle_freshness` check.
*
* Decision table (cycle_freshness.status / message):
* - ok "completed"
* - fail|warn AND message names <sourceId> "never"
* - fail|warn AND message omits <sourceId> "unknown" (a real failure
* about OTHER sources must not be silently read as completed for us)
* - check absent / doctor null / other status "unknown"
*
* `sourceId` is matched as a LITERAL substring (not a regex) so an id with
* regex metacharacters can never misfire. Routes through `execGbrainJson` so
* DATABASE_URL is seeded from gbrain's config (consistent with every other
* gstack-side gbrain call). `env` is the caller's base env (tests inject a
* shim on PATH).
*/
export function cycleCompleted(sourceId: string, env?: NodeJS.ProcessEnv): CycleStatus {
const report = execGbrainJson<DoctorReport>(["doctor", "--json", "--fast"], { baseEnv: env });
if (!report || !Array.isArray(report.checks)) return "unknown";
const check = report.checks.find((c) => c.name === "cycle_freshness");
if (!check) return "unknown";
if (check.status === "ok") return "completed";
if (check.status === "fail" || check.status === "warn") {
const msg = check.message || "";
return msg.includes(sourceId) ? "never" : "unknown";
}
return "unknown";
}

View File

@ -0,0 +1,93 @@
/**
* gstack-decision-semantic OPTIONAL gbrain enhancement for decision resurfacing.
*
* This is the ONLY decision module that touches gbrain. The reliable core
* (lib/gstack-decision.ts) has zero gbrain imports and works with gbrain OFF; this
* module is loaded lazily by `gstack-decision-search` only on `--semantic`, and every
* path degrades to `null` (caller shows the reliable file results) when gbrain is
* absent, unconfigured, times out, or returns nothing. It NEVER throws and NEVER
* hangs (10s spawn timeout). We do not wire core function to this gbrain is an
* enhancement, never a dependency (the code-search lesson).
*
* Surface reality (verified against gbrain 0.42.x, not guessed):
* - `gbrain search "<q>"` prints TEXT lines `[score] slug -- snippet`, NOT JSON
* (so we parse the text surface; execGbrainJson would always null here).
* - The curated-memory source is the one whose local_path is the gstack brain
* worktree (`~/.gstack-brain-worktree`), id `default` by convention NOT a
* `gstack-brain-<user>` id. Scoping search to it keeps code/doc corpora out.
*/
import { spawnGbrain } from "./gbrain-exec";
import { parseSourcesList } from "./gbrain-sources";
const TIMEOUT_MS = 10_000;
const BRAIN_WORKTREE_SUFFIX = ".gstack-brain-worktree";
export interface SemanticHit {
score: number;
slug: string;
snippet: string;
}
/**
* Resolve the curated-memory source id (the gstack brain worktree). Returns null
* when gbrain is down/unparseable OR no worktree-backed source is registered the
* caller then searches unscoped (best-effort) rather than failing.
*/
export function resolveMemorySourceId(env?: NodeJS.ProcessEnv): string | null {
const r = spawnGbrain(["sources", "list", "--json"], { baseEnv: env, timeout: TIMEOUT_MS });
if (r.status !== 0) return null;
let rows;
try {
rows = parseSourcesList(JSON.parse(r.stdout || "null"));
} catch {
return null;
}
const atWorktree = rows.filter(
(s) => typeof s.local_path === "string" && s.local_path.endsWith(BRAIN_WORKTREE_SUFFIX),
);
const pick = atWorktree.find((s) => s.id === "default") ?? atWorktree[0];
return pick?.id ?? null;
}
/**
* Parse gbrain search's text output into scored hits. Lines look like:
* `[0.4361] slug -- snippet text...`
* Non-matching lines (banners, blanks) are skipped. Exported for deterministic
* unit testing of the parser without a live gbrain.
*/
export function parseSearchHits(stdout: string, minScore: number, limit: number): SemanticHit[] {
const hits: SemanticHit[] = [];
for (const line of stdout.split("\n")) {
const m = line.match(/^\[([\d.]+)\]\s+(\S+)\s+--\s+(.*)$/);
if (!m) continue;
const score = parseFloat(m[1]);
if (!Number.isFinite(score) || score < minScore) continue;
hits.push({ score, slug: m[2], snippet: m[3].trim() });
}
return hits.slice(0, limit);
}
/**
* Semantic recall over the curated-memory source. Returns parsed hits, or `null`
* when gbrain is unavailable / errors (caller MUST degrade to the reliable file
* results on null). An empty array means gbrain ran but found nothing relevant
* (e.g. memory not synced yet) also honest, distinct from null. Never throws,
* never hangs.
*/
export function semanticRecall(
query: string,
env?: NodeJS.ProcessEnv,
minScore = 0.1,
limit = 3,
): SemanticHit[] | null {
if (!query.trim()) return null;
// Require the curated-memory source. If it's absent (gbrain down OR no worktree-backed
// source), degrade to null rather than searching UNSCOPED — an unscoped search pulls
// code/doc corpora that would be mislabeled as "related decisions" (Codex finding).
const sourceId = resolveMemorySourceId(env);
if (!sourceId) return null;
const r = spawnGbrain(["search", query, "--source", sourceId], { baseEnv: env, timeout: TIMEOUT_MS });
if (r.status !== 0) return null; // gbrain down / not on PATH / errored → degrade
return parseSearchHits(r.stdout || "", minScore, limit);
}

325
lib/gstack-decision.ts Normal file
View File

@ -0,0 +1,325 @@
/**
* gstack-decision event-sourced institutional decision memory.
*
* decisions.jsonl is an APPEND-ONLY EVENT LOG (not mutable rows): `decide`,
* `supersede`, and `redact` events. "Active" is COMPUTED a `decide` whose id is
* not later referenced by a `supersede`/`redact`. This is the eng-review event-
* sourcing decision (a mutable `status` field would contradict append-only).
*
* Built on lib/jsonl-store.ts (shared injection-reject + atomic append + tolerant
* read). Free-text fields are injection-checked AND redact-scanned on write
* (HIGH-tier secret reject), so a secret never silently persists and resurfaced
* text can't carry instructions. gbrain is never required this is the reliable
* file-only core; semantic recall is a later, optional enhancement.
*/
import { join } from "path";
import { homedir } from "os";
import { randomUUID } from "crypto";
import { writeFileSync, renameSync, existsSync, readFileSync, appendFileSync, statSync, openSync, closeSync, unlinkSync } from "fs";
import { appendJsonl, readJsonl, hasInjection } from "./jsonl-store";
import { scan } from "./redact-engine";
export type DecisionKind = "decide" | "supersede" | "redact";
export type DecisionScope = "repo" | "branch" | "issue";
export type DecisionSource = "user" | "skill" | "agent";
export const DECISION_SCOPES: readonly DecisionScope[] = ["repo", "branch", "issue"];
export const DECISION_SOURCES: readonly DecisionSource[] = ["user", "skill", "agent"];
export interface DecisionEvent {
id: string;
kind: DecisionKind;
decision?: string;
rationale?: string;
alternatives_considered?: string;
/** For supersede/redact: the id of the `decide` event being acted on. */
supersedes?: string;
scope: DecisionScope;
branch?: string;
issue?: string;
date: string;
session?: string;
source: DecisionSource;
confidence?: number;
}
export interface ActiveDecision extends DecisionEvent {
kind: "decide";
}
export interface DecisionPaths {
log: string;
snapshot: string;
archive: string;
}
/** Resolve the per-project decision store paths. Bins pass slug + GSTACK_HOME. */
export function decisionPaths(slug: string, gstackHome?: string): DecisionPaths {
const home = gstackHome || process.env.GSTACK_HOME || join(homedir(), ".gstack");
const dir = join(home, "projects", slug || "unknown");
return {
log: join(dir, "decisions.jsonl"),
snapshot: join(dir, "decisions.active.json"),
archive: join(dir, "decisions.archive.jsonl"),
};
}
/**
* Datamark resurfaced decision text so a stored string can't masquerade as
* instructions or break out of the Context Recovery fence when it lands in agent
* context (codex hardening #3: resurface = DATA, not instructions). Write-time
* `hasInjection` is a denylist; this is the render-boundary defense-in-depth that
* also covers `--all`/snapshot reads and records written before a pattern existed.
* Neutralizes: control chars, newlines (defensive events are single-line),
* code fences, `---` banner sentinels, and `<|role|>` / `</system>` markers.
*/
export function datamark(text: string): string {
const ZWSP = "\u200b"; // zero-width space: breaks token recognition, near-invisible
return text
// strip C0/C1 control chars + Unicode line terminators (U+0085/2028/2029 render as
// newlines in many tokenizers/markdown; "strip newlines" must cover them)
.replace(/[\u0000-\u001f\u007f\u0085\u2028\u2029]/g, " ")
.replace(/`{3,}/g, "'''") // neutralize markdown code fences
.replace(/-{3,}/g, "\u2014") // neutralize `---` banner sentinels (em dash)
.replace(/<\|/g, `<${ZWSP}|`) // neutralize <|im_start|>-style chat markers
.replace(/\|>/g, `|${ZWSP}>`)
.replace(/<(\/?)(system|user|assistant|tool)>/gi, `<${ZWSP}$1$2>`) // neutralize role tags
// neutralize chat turn-prefixes (Human:/Assistant:/System:/User:) — defeat the
// angle-tag pass and are Claude's native turn delimiters
.replace(/\b(human|assistant|system|user)(\s*):/gi, `$1${ZWSP}$2:`);
}
export type ValidateResult =
| { ok: true; event: DecisionEvent }
| { ok: false; error: string };
/**
* Validate + stamp a `decide` event. Rejects (no silent persist) on:
* - missing/empty decision text or invalid scope/source,
* - injection-like content in any free-text field (datamark-on-write),
* - a HIGH-tier secret (redact engine) in any free-text field.
*/
export function validateDecide(input: Partial<DecisionEvent>): ValidateResult {
if (!input.decision || typeof input.decision !== "string" || !input.decision.trim()) {
return { ok: false, error: "decision text is required" };
}
const scope = input.scope ?? "repo";
if (!DECISION_SCOPES.includes(scope)) {
return { ok: false, error: `invalid scope "${scope}"; must be ${DECISION_SCOPES.join("|")}` };
}
const source = input.source ?? "agent";
if (!DECISION_SOURCES.includes(source)) {
return { ok: false, error: `invalid source "${source}"; must be ${DECISION_SOURCES.join("|")}` };
}
if (input.confidence !== undefined) {
const c = Number(input.confidence);
if (!Number.isInteger(c) || c < 1 || c > 10) {
return { ok: false, error: "confidence must be integer 1-10" };
}
}
// Scan ALL stored free-text — incl. branch/issue, which are surfaced (and emitted raw
// via --json), so they must not carry secrets or injection either (Codex finding).
const freeText = [input.decision, input.rationale, input.alternatives_considered, input.branch, input.issue]
.filter((s): s is string => typeof s === "string")
.join("\n");
if (hasInjection(freeText)) {
return { ok: false, error: "decision contains instruction-like content (injection), rejected" };
}
const redacted = scan(freeText);
if (redacted.counts.HIGH > 0) {
return {
ok: false,
error: `decision contains a HIGH-tier secret (${redacted.counts.HIGH} finding(s)); rotate + remove it, do not log secrets`,
};
}
// MEDIUM = PII / credential-shaped content. The taxonomy says "confirm via
// AskUserQuestion", but this store is NON-INTERACTIVE and syncs cross-machine,
// so there is no confirm path — fail closed rather than silently persist + sync a
// secret that later resurfaces into agent context.
if (redacted.counts.MEDIUM > 0) {
return {
ok: false,
error: `decision contains MEDIUM-tier sensitive content (${redacted.counts.MEDIUM} finding(s): PII or credential-shaped). This store is non-interactive and syncs across machines, so it fails closed — remove or rephrase the value before logging.`,
};
}
const event: DecisionEvent = {
id: input.id || randomUUID(),
kind: "decide",
decision: input.decision.trim(),
rationale: input.rationale,
alternatives_considered: input.alternatives_considered,
scope,
branch: input.branch || undefined,
issue: input.issue || undefined,
date: input.date || new Date().toISOString(),
session: input.session,
source,
confidence: input.confidence === undefined ? undefined : Number(input.confidence),
};
return { ok: true, event };
}
/** Build a supersede/redact event referencing an existing decide-event id. */
export function makeRefEvent(kind: "supersede" | "redact", targetId: string, opts: { session?: string; source?: DecisionSource } = {}): DecisionEvent {
return {
id: randomUUID(),
kind,
supersedes: targetId,
scope: "repo",
date: new Date().toISOString(),
session: opts.session,
source: opts.source ?? "agent",
};
}
/**
* Compute the ACTIVE decisions: `decide` events whose id is NOT referenced by any
* later `supersede`/`redact`. Dangling refs (supersede/redact pointing at an id
* that has no `decide`) are tolerated ignored, never thrown. Returned in date
* order (oldest first).
*/
export function computeActive(events: DecisionEvent[]): ActiveDecision[] {
const retired = new Set<string>();
for (const e of events) {
if ((e.kind === "supersede" || e.kind === "redact") && e.supersedes) {
retired.add(e.supersedes); // dangling target id is harmless — just a no-op
}
}
return events
.filter((e): e is ActiveDecision => e.kind === "decide" && !retired.has(e.id))
.sort((a, b) => (a.date < b.date ? -1 : a.date > b.date ? 1 : 0));
}
/**
* Scope filter for resurfacing: repo-scoped decisions always apply; branch-scoped
* only when the branch matches the current context; issue-scoped only when the
* issue matches. (Recency != relevance callers filter by scope, not just date.)
*/
export function filterByScope(active: ActiveDecision[], ctx: { branch?: string; issue?: string }): ActiveDecision[] {
return active.filter((d) => {
if (d.scope === "repo") return true;
if (d.scope === "branch") return !!ctx.branch && d.branch === ctx.branch;
if (d.scope === "issue") return !!ctx.issue && d.issue === ctx.issue;
return false; // unknown/garbage scope: fail conservative, don't leak into every context
});
}
/** Append a validated event atomically (single-line, concurrency-safe). */
export function appendEvent(paths: DecisionPaths, event: DecisionEvent): void {
appendJsonl(paths.log, event);
}
/** Read all events tolerantly (skips malformed/partial-tail lines). */
export function readEvents(paths: DecisionPaths): DecisionEvent[] {
return readJsonl<DecisionEvent>(paths.log);
}
/**
* Write the bounded active snapshot (`decisions.active.json`) atomically. Context
* Recovery and search read THIS, not the full history session start stays
* O(active), not O(history).
*/
export function writeSnapshot(paths: DecisionPaths, active: ActiveDecision[]): void {
const tmp = `${paths.snapshot}.tmp.${process.pid}`;
writeFileSync(tmp, JSON.stringify(active), "utf-8");
renameSync(tmp, paths.snapshot);
}
/** Read the bounded active snapshot. Returns [] if missing/corrupt (caller may rebuild). */
export function readSnapshot(paths: DecisionPaths): ActiveDecision[] {
if (!existsSync(paths.snapshot)) return [];
try {
const v = JSON.parse(readFileSync(paths.snapshot, "utf-8"));
return Array.isArray(v) ? (v as ActiveDecision[]) : [];
} catch {
return [];
}
}
/** Recompute active from the event log and refresh the snapshot. Returns active. */
export function rebuildSnapshot(paths: DecisionPaths): ActiveDecision[] {
const active = computeActive(readEvents(paths));
writeSnapshot(paths, active);
return active;
}
export interface CompactResult {
activeCount: number;
/** superseded decisions moved to the archive (history kept). */
archivedCount: number;
/** redacted decisions DROPPED entirely (expunged, NOT archived). */
expungedCount: number;
/** true when compaction was skipped to avoid clobbering a concurrent writer/compactor. */
skipped?: boolean;
}
/**
* Compact the event log to the active set.
* - active decisions kept in `decisions.jsonl`,
* - superseded decisions appended to `decisions.archive.jsonl` (history),
* - REDACTED decisions expunged (dropped, NOT archived) that's redact's job:
* a `redact` is how an accidentally-captured secret leaves the store for good.
*
* Concurrency: appends are lock-free (O_APPEND), but compact is a read-modify-rewrite
* that would clobber an append landing in its window. Two guards: (1) an O_EXCL lock
* file serializes compactions (no double-archive / tmp tear); (2) the log size is
* re-checked immediately before the destructive write if an append landed since the
* read, compact ABORTS untouched (returns skipped) so no decision is ever lost. The
* caller re-runs. Atomic rewrite (tmp + rename); refreshes the snapshot.
*/
export function compact(paths: DecisionPaths): CompactResult {
const lockPath = `${paths.log}.compact.lock`;
let lockFd: number;
try {
lockFd = openSync(lockPath, "wx"); // O_EXCL|O_CREAT — throws EEXIST if a compact holds it
} catch (err) {
if ((err as NodeJS.ErrnoException).code === "EEXIST") {
return { activeCount: computeActive(readEvents(paths)).length, archivedCount: 0, expungedCount: 0, skipped: true };
}
throw err;
}
try {
const sizeBefore = existsSync(paths.log) ? statSync(paths.log).size : 0;
const events = readEvents(paths);
const active = computeActive(events);
const activeIds = new Set(active.map((d) => d.id));
const redactedIds = new Set(
events.filter((e) => e.kind === "redact" && e.supersedes).map((e) => e.supersedes as string),
);
// Superseded = a decide that's neither active nor redacted. Archive these for history.
const superseded = events.filter(
(e): e is DecisionEvent => e.kind === "decide" && !activeIds.has(e.id) && !redactedIds.has(e.id),
);
// Append-race guard: if the log grew/changed since we read it, an append landed —
// rewriting now would drop it. Abort untouched; the caller re-runs.
const sizeNow = existsSync(paths.log) ? statSync(paths.log).size : 0;
if (sizeNow !== sizeBefore) {
return { activeCount: active.length, archivedCount: 0, expungedCount: 0, skipped: true };
}
// One batched append (not one open/write/close per event) — matches the atomic
// batched rewrite of the active log below and shrinks the mid-compact crash window.
if (superseded.length) {
appendFileSync(paths.archive, superseded.map((e) => JSON.stringify(e)).join("\n") + "\n", "utf-8");
}
const tmp = `${paths.log}.tmp.${process.pid}`;
writeFileSync(tmp, active.map((d) => JSON.stringify(d)).join("\n") + (active.length ? "\n" : ""), "utf-8");
renameSync(tmp, paths.log);
writeSnapshot(paths, active);
return { activeCount: active.length, archivedCount: superseded.length, expungedCount: redactedIds.size };
} finally {
closeSync(lockFd);
try {
unlinkSync(lockPath);
} catch {
// best-effort lock cleanup; a leftover lock only blocks the NEXT compact, which re-runs
}
}
}

96
lib/jsonl-store.ts Normal file
View File

@ -0,0 +1,96 @@
/**
* jsonl-store shared, audited plumbing for gstack's append-only JSONL stores.
*
* Single source of truth for the three things every JSONL store must get right:
* 1. Injection sanitization (the prompt-injection patterns that must NOT survive
* into agent context when a record is later resurfaced).
* 2. Atomic single-line append (concurrent agents must not corrupt the file).
* 3. Tolerant read (a partially-written tail or one corrupt line must not take
* down the whole read).
*
* Extracted from `bin/gstack-learnings-log` (D2A) so `gstack-learnings-*` and the
* new `gstack-decision-*` bins share ONE audited path a new injection pattern or
* a write-atomicity fix lands in both at once, never drifts. Per the
* `squash-with-regen` / DRY discipline + the eng-review D2A decision.
*/
import { appendFileSync, readFileSync, existsSync } from "fs";
/**
* Prompt-injection patterns. If any matches a free-text field (insight, rationale,
* decision), the record is REJECTED at write time these strings could otherwise
* be replayed into a future agent's context as instructions when the record is
* resurfaced. Keep this list the ONLY copy (callers import it; do not re-declare).
*/
export const INJECTION_PATTERNS: readonly RegExp[] = [
/ignore\s+(all\s+)?previous\s+(instructions|context|rules)/i,
/you\s+are\s+now\s+/i,
/always\s+output\s+no\s+findings/i,
/skip\s+(all\s+)?(security|review|checks)/i,
/override[:\s]/i,
/\bsystem\s*:/i,
/\bassistant\s*:/i,
/\buser\s*:/i,
/\bhuman\s*:/i, // Claude's native turn prefix — bypassed the denylist AND datamark
/disregard\s+(all\s+)?(previous|above|prior)/i,
/from\s+now\s+on\b/i,
/do\s+not\s+(report|flag|mention)/i,
/approve\s+(all|every|this)/i,
];
/** True if `text` contains an instruction-like injection pattern. */
export function hasInjection(text: string): boolean {
return INJECTION_PATTERNS.some((p) => p.test(text));
}
/** Returns the first injection pattern that matches, or null. For actionable errors. */
export function firstInjectionMatch(text: string): RegExp | null {
return INJECTION_PATTERNS.find((p) => p.test(text)) ?? null;
}
/**
* Atomic single-line append of `obj` as one JSON line.
*
* Concurrency: opens with `a` (O_APPEND); a single write under PIPE_BUF (>=512,
* 4096+ on macOS/Linux) is atomic across processes, so concurrent agents appending
* never interleave. Records MUST serialize to a single line (no embedded newline)
* we throw rather than risk a multi-line record breaking the one-record-per-line
* invariant the tolerant reader relies on.
*
* Caveat: a record larger than PIPE_BUF loses the cross-process atomicity guarantee.
* Keep records line-bounded; very large free-text should be truncated by the caller.
*/
export function appendJsonl(path: string, obj: unknown): void {
const line = JSON.stringify(obj);
if (line.includes("\n")) {
throw new Error("jsonl-store: record serialized to multiple lines (embedded newline)");
}
appendFileSync(path, line + "\n", { encoding: "utf-8" });
}
/**
* Tolerant reader: parse each line, SKIP malformed ones (partial-write tail, a
* corrupt line, a non-JSON line) rather than throwing. A broken line never takes
* down the whole read. Missing file empty array. Unknown fields are preserved
* (forward-compatible: a schema bump on the writer doesn't break older readers).
*/
export function readJsonl<T = unknown>(path: string): T[] {
if (!existsSync(path)) return [];
let raw: string;
try {
raw = readFileSync(path, "utf-8");
} catch {
return [];
}
const out: T[] = [];
for (const line of raw.split("\n")) {
const trimmed = line.trim();
if (!trimmed) continue;
try {
out.push(JSON.parse(trimmed) as T);
} catch {
// Malformed line (partial tail / corruption) — skip, keep reading.
}
}
return out;
}

View File

@ -253,7 +253,16 @@ function emailAllowed(email: string, opts: ScanOptions): boolean {
export function scan(input: string, opts: ScanOptions = {}): ScanResult {
const repoVisibility: RepoVisibility = opts.repoVisibility ?? "unknown";
const maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
// #1824: ?? only catches null/undefined, not NaN or <= 0. A bad value
// (NaN from a malformed --max-bytes, or a negative) would make `byteLen >
// maxBytes` always false and silently disable the fail-closed oversize guard.
// Guardrail: any non-finite or non-positive value falls back to the default
// cap. The CLI is the layer that rejects bad args; this is belt-and-suspenders
// so the engine never silently runs uncapped.
const maxBytes =
Number.isFinite(opts.maxBytes) && (opts.maxBytes as number) > 0
? (opts.maxBytes as number)
: DEFAULT_MAX_BYTES;
// Fail CLOSED on oversize input. Check byte length BEFORE heavy work.
const byteLen = Buffer.byteLength(input, "utf8");

View File

@ -233,8 +233,13 @@ export const PATTERNS: RedactPattern[] = [
id: "openai.key",
tier: "HIGH",
category: "secret",
description: "OpenAI API key (incl. sk-proj-)",
regex: /\b(sk-(?:proj-)?[A-Za-z0-9]{32,})\b/,
description: "OpenAI API key (incl. sk-proj-/sk-svcacct-/sk-admin-)",
// Two explicit shapes (NOT a globally-optional prefix, which would match
// malformed sk--... or separator-less sk-projabc...):
// prefixed: sk-{proj,svcacct,admin}- + base64url-ish body (allows -_)
// bare: sk- + contiguous alphanumeric run (legacy), keeps {32,} floor
regex:
/\b(sk-(?:proj|svcacct|admin)-[A-Za-z0-9_-]{20,}|sk-[A-Za-z0-9]{32,})\b/,
},
{
id: "sendgrid.key",

116
lib/staging-guard.ts Normal file
View File

@ -0,0 +1,116 @@
/**
* staging-guard fail-closed ownership proof for gstack ingest staging dirs.
*
* Fixes #1802. The /sync-gbrain memory stage stages prepared pages to a
* throwaway dir under ~/.gstack and `rm -rf`s it when done. The resume path
* (#1611) reused gbrain's `import-checkpoint.json` `dir` field as that staging
* dir WITHOUT proving it was one. A poisoned checkpoint `dir` = the repo
* root, written when an autopilot `gbrain import` was SIGTERM'd while CWD was
* the repo was then adopted as the staging dir and recursively deleted,
* destroying the user's working tree.
*
* Root cause is a TRUST failure, not path math: code deleted a path it never
* proved it owned. This module is the single definition of "a path gstack is
* allowed to recurse-delete or resume into", shared by the resume gate
* (decideResume) and the deletion chokepoint (cleanupStagingDir).
*
* Ownership requires ALL of the following (fail-closed any failure refuse):
* 1. Resolvable realpathSync succeeds (resolves symlinks and `..` to a
* real location before any structural reasoning).
* 2. Structural canonical path is a DIRECT child of $GSTACK_HOME named
* `.staging-ingest-*` (makeStagingDir's contract).
* 3. Not a repo no `.git` entry inside. A screaming last-line tripwire:
* even a logic error elsewhere can never recurse-delete a
* git working tree.
* 4. Minted by us a `.gstack-staging` marker file (written by
* makeStagingDir) is present. Turns "looks like ours"
* into "was created by us this lineage".
*
* Design note (steelman, 2026-06-02): a 4-model review panel split 3-1 on the
* marker. The dissent argued the structural check alone is sufficient and the
* marker adds a missing-token failure mode. Adopted anyway because that failure
* mode is fail-SAFE: a missing marker only forces an unnecessary re-stage
* (seconds), never a wrong deletion. The asymmetry the marker can cost work
* but never data settles it. The structural check still runs first and cheap.
*
* The deeper, "inevitable" fix lives upstream in gbrain: checkpoint.dir should
* always be a gbrain-minted staging dir, never CWD. This guard is the
* mitigation at gstack's own rm -rf boundary; see the companion gbrain issue.
*/
import { realpathSync, existsSync, statSync, lstatSync } from "fs";
import { join, dirname, basename } from "path";
/** Basename prefix every makeStagingDir() directory carries. */
export const STAGING_PREFIX = ".staging-ingest-";
/** Marker file minted inside each staging dir at creation. */
export const STAGING_MARKER = ".gstack-staging";
export interface StagingVerdict {
ok: boolean;
/** Precise rejection reason, for actionable logging. Undefined when ok. */
reason?: string;
/**
* The realpath-resolved directory the verdict actually validated. Present only
* when ok. Callers that delete MUST `rmSync` this path, not the raw input
* deleting the canonical path closes the TOCTOU gap where the input is a
* symlink swapped between this check and the delete (#1802 C5).
*/
canonicalPath?: string;
}
/**
* Prove (fail-closed) that `dir` is a gstack-owned ingest staging directory
* that is safe to recurse-delete or resume into. Returns a structured verdict
* so callers can log exactly why a path was rejected.
*
* @param dir Candidate path (e.g. gbrain checkpoint.dir, or the active staging dir).
* @param gstackHome Resolved $GSTACK_HOME (injected for testability).
*/
export function checkOwnedStagingDir(dir: string, gstackHome: string): StagingVerdict {
if (!dir || typeof dir !== "string") {
return { ok: false, reason: "empty or non-string path" };
}
let canon: string;
let home: string;
try {
canon = realpathSync(dir);
home = realpathSync(gstackHome);
} catch {
// Missing path or broken symlink ⇒ cannot prove ownership ⇒ refuse.
return { ok: false, reason: "unresolvable path (missing dir or broken symlink)" };
}
// The target itself must be a directory (not a file/socket/etc named like one).
try {
if (!statSync(canon).isDirectory()) {
return { ok: false, reason: "not a directory" };
}
} catch {
return { ok: false, reason: "unstattable target" };
}
if (dirname(canon) !== home) {
return { ok: false, reason: `not a direct child of GSTACK_HOME (${home})` };
}
if (!basename(canon).startsWith(STAGING_PREFIX)) {
return { ok: false, reason: `basename does not start with "${STAGING_PREFIX}"` };
}
if (existsSync(join(canon, ".git"))) {
// Tripwire: never recurse-delete anything that looks like a git work tree.
return { ok: false, reason: "path contains .git — refusing to touch a git working tree" };
}
// Marker must be a REGULAR FILE we minted — not a directory or symlink that
// merely shares the name (lstat, not stat, so a symlink can't impersonate it).
try {
if (!lstatSync(join(canon, STAGING_MARKER)).isFile()) {
return { ok: false, reason: `"${STAGING_MARKER}" exists but is not a regular file` };
}
} catch {
return { ok: false, reason: `missing "${STAGING_MARKER}" marker — not minted by makeStagingDir` };
}
return { ok: true, canonicalPath: canon };
}
/** Boolean convenience wrapper around {@link checkOwnedStagingDir}. */
export function isOwnedStagingDir(dir: string, gstackHome: string): boolean {
return checkOwnedStagingDir(dir, gstackHome).ok;
}

View File

@ -47,6 +47,9 @@ echo "SKILL_PREFIX: $_SKILL_PREFIX"
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
REPO_MODE=${REPO_MODE:-unknown}
echo "REPO_MODE: $REPO_MODE"
_SESSION_KIND=$(~/.claude/skills/gstack/bin/gstack-session-kind 2>/dev/null || echo "interactive")
case "$_SESSION_KIND" in spawned|headless|interactive) ;; *) _SESSION_KIND="interactive" ;; esac
echo "SESSION_KIND: $_SESSION_KIND"
_LAKE_SEEN=$([ -f ~/.gstack/.completeness-intro-seen ] && echo "yes" || echo "no")
echo "LAKE_INTRO: $_LAKE_SEEN"
_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || true)
@ -162,7 +165,7 @@ In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`co
## Skill Invocation During Plan Mode
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If no AskUserQuestion variant is callable, use the normal chat fallback from the AskUserQuestion Format rule and stop for the user's reply. At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If AskUserQuestion is unavailable or a call fails, follow the AskUserQuestion Format failure fallback: `headless` → BLOCKED; `interactive` → the prose fallback (also satisfies end-of-turn). At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If `PROACTIVE` is `"false"`, do not auto-invoke or proactively suggest skills. If a skill seems useful, ask: "I think /skillname might help here — want me to run it?"
@ -197,7 +200,7 @@ touch ~/.gstack/.writing-style-prompted
Skip if `WRITING_STYLE_PENDING` is `no`.
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Lake** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Ocean** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
```bash
open https://garryslist.org/posts/boil-the-ocean

View File

@ -83,6 +83,9 @@ echo "SKILL_PREFIX: $_SKILL_PREFIX"
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
REPO_MODE=${REPO_MODE:-unknown}
echo "REPO_MODE: $REPO_MODE"
_SESSION_KIND=$(~/.claude/skills/gstack/bin/gstack-session-kind 2>/dev/null || echo "interactive")
case "$_SESSION_KIND" in spawned|headless|interactive) ;; *) _SESSION_KIND="interactive" ;; esac
echo "SESSION_KIND: $_SESSION_KIND"
_LAKE_SEEN=$([ -f ~/.gstack/.completeness-intro-seen ] && echo "yes" || echo "no")
echo "LAKE_INTRO: $_LAKE_SEEN"
_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || true)
@ -162,7 +165,7 @@ In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`co
## Skill Invocation During Plan Mode
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If no AskUserQuestion variant is callable, use the normal chat fallback from the AskUserQuestion Format rule and stop for the user's reply. At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If AskUserQuestion is unavailable or a call fails, follow the AskUserQuestion Format failure fallback: `headless` → BLOCKED; `interactive` → the prose fallback (also satisfies end-of-turn). At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If `PROACTIVE` is `"false"`, do not auto-invoke or proactively suggest skills. If a skill seems useful, ask: "I think /skillname might help here — want me to run it?"
@ -197,7 +200,7 @@ touch ~/.gstack/.writing-style-prompted
Skip if `WRITING_STYLE_PENDING` is `no`.
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Lake** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Ocean** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
```bash
open https://garryslist.org/posts/boil-the-ocean
@ -334,11 +337,31 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
**Rule:** if any `mcp__*__AskUserQuestion` variant is in your tool list, prefer it. Hosts may disable native AUQ via `--disallowedTools AskUserQuestion` (Conductor does, by default) and route through their MCP variant; calling native there silently fails. Same questions/options shape; same decision-brief format applies.
**If no AskUserQuestion variant appears in your tool list:** fall back to the host's normal user-facing chat question. Render the same decision brief in prose, stop, and wait for the user's reply. This fallback is for interactive chat hosts that can ask the user normally but do not expose an AUQ tool (for example Codex API sessions). Do not write decisions to the plan file as a substitute, and do not silently auto-decide (only `/plan-tune` AUTO_DECIDE opt-ins authorize auto-picking).
If AskUserQuestion is unavailable (no variant in your tool list) OR a call to it fails, do NOT silently auto-decide or write the decision to the plan file as a substitute. Follow the **failure fallback** below.
### When AskUserQuestion is unavailable or a call fails
Tell three outcomes apart:
1. **Auto-decide denial (NOT a failure).** The result contains `[plan-tune auto-decide] <id> → <option>` — the preference hook working as designed. Proceed with that option. Do NOT retry, do NOT fall back to prose.
2. **Genuine failure** — no variant in your tool list, OR the variant is present but the call returns an error / missing result (MCP transport error, empty result, host bug — e.g. Conductor's MCP AskUserQuestion is flaky and returns `[Tool result missing due to internal error]`).
- If it was present and **errored** (not absent), retry the SAME call **once** — but only if no answer could have surfaced (a missing-result error can arrive after the user already saw the question; retrying would double-prompt, so if it may have reached them, treat as pending, don't retry).
- Then branch on `SESSION_KIND` (echoed by the preamble; empty/absent ⇒ `interactive`):
- `spawned` → defer to the **Spawned session** block: auto-choose the recommended option. Never prose, never BLOCKED.
- `headless``BLOCKED — AskUserQuestion unavailable`; stop and wait (no human can answer).
- `interactive`**prose fallback** (below).
**Prose fallback — render the decision brief as a markdown message, not a tool call.** Same information as the tool format below, different structure (paragraphs, not ✅/❌ bullets). It MUST surface this triad:
1. **A clear ELI10 of the issue itself** — plain English on what's being decided and why it matters (the question, not per-choice), naming the stakes. Lead with it.
2. **Completeness scores per choice** — explicit `Completeness: X/10` on EACH choice (10 complete, 7 happy-path, 3 shortcut); use the kind-note when options differ in kind not coverage, but never silently drop the score.
3. **The recommendation and why** — a `Recommendation: <choice> because <reason>` line plus the `(recommended)` marker on that choice.
Layout: a `D<N>` title + a one-line note that AskUserQuestion failed and to reply with a letter; the issue ELI10; the Recommendation line; then ONE paragraph per choice carrying its `(recommended)` marker, its `Completeness: X/10`, and 2-4 sentences of reasoning — never a bare bullet list; a closing `Net:` line. Split chains / 5+ options: one prose block per per-option call, in sequence. Then STOP and wait — the user's typed answer is the decision. In plan mode this satisfies end-of-turn like a tool call.
### Format
Every AskUserQuestion is a decision brief. Send it as tool_use when an AskUserQuestion tool exists; otherwise use the normal chat fallback above and wait for the user's reply.
Every AskUserQuestion is a decision brief and must be sent as tool_use, not prose — unless the documented failure fallback above applies (interactive session + the call is unavailable/erroring), in which case the prose fallback is the correct output.
```
D<N><one-line question title>
@ -418,7 +441,7 @@ Before calling AskUserQuestion, verify:
- [ ] (recommended) label on one option (even for neutral-posture)
- [ ] Dual-scale effort labels on effort-bearing options (human / CC)
- [ ] Net line closes the decision
- [ ] You are calling the tool when available; otherwise you are using the normal chat fallback and stopping for the user's reply
- [ ] You are calling the tool, not writing prose — unless the documented failure fallback applies (then: prose with the mandatory triad — issue ELI10, per-choice Completeness, Recommendation + `(recommended)` — and a "reply with a letter" instruction, then STOP)
- [ ] Non-ASCII characters (CJK / accents) written directly, NOT \u-escaped
- [ ] If you had 5+ options, you split (or batched into ≤4-groups) — did NOT drop any
- [ ] If you split, you checked dependencies between options before firing the chain
@ -605,12 +628,19 @@ if [ -d "$_PROJ" ]; then
fi
_LATEST_CP=$(find "$_PROJ/checkpoints" -name "*.md" -type f 2>/dev/null | xargs ls -t 2>/dev/null | head -1)
[ -n "$_LATEST_CP" ] && echo "LATEST_CHECKPOINT: $_LATEST_CP"
if [ -f "$_PROJ/decisions.active.json" ]; then
echo "--- ACTIVE DECISIONS (recent, scope-relevant) ---"
~/.claude/skills/gstack/bin/gstack-decision-search --recent 5 2>/dev/null
echo "--- END DECISIONS ---"
fi
echo "--- END ARTIFACTS ---"
fi
```
If artifacts are listed, read the newest useful one. If `LAST_SESSION` or `LATEST_CHECKPOINT` appears, give a 2-sentence welcome back summary. If `RECENT_PATTERN` clearly implies a next skill, suggest it once.
**Cross-session decisions.** If `ACTIVE DECISIONS` are listed, treat them as prior settled calls with their rationale — do not silently re-litigate them; if you're about to reverse one, say so explicitly. Reach for `~/.claude/skills/gstack/bin/gstack-decision-search` whenever a question touches a past decision ("what did we decide / why / did we try"). When you or the user make a DURABLE decision (architecture, scope, tool/vendor choice, or a reversal) — NOT a turn-level or trivial choice — log it with `~/.claude/skills/gstack/bin/gstack-decision-log` (`--supersede <id>` for a reversal). Reliable and local; gbrain not required.
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format is structure; this is prose quality.
@ -625,9 +655,9 @@ Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format i
Curated jargon list lives at `~/.claude/skills/gstack/scripts/jargon-list.json` (80+ terms). On the first jargon term you encounter this session, Read that file once; treat the `terms` array as the canonical list. The list is repo-owned and may grow between releases.
## Completeness Principle — Boil the Lake
## Completeness Principle — Boil the Ocean
AI makes completeness cheap. Recommend complete lakes (tests, edge cases, error paths); flag oceans (rewrites, multi-quarter migrations).
AI makes completeness cheap, so the complete thing is the goal. Recommend full coverage (tests, edge cases, error paths) — boil the ocean one lake at a time. The only thing out of scope is genuinely unrelated work (rewrites, multi-quarter migrations); flag that as separate scope, never as an excuse for a shortcut.
When options differ in coverage, include `Completeness: X/10` (10 = all edge cases, 7 = happy path, 3 = shortcut). When options differ in kind, write: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate scores.

View File

@ -45,6 +45,9 @@ echo "SKILL_PREFIX: $_SKILL_PREFIX"
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
REPO_MODE=${REPO_MODE:-unknown}
echo "REPO_MODE: $REPO_MODE"
_SESSION_KIND=$(~/.claude/skills/gstack/bin/gstack-session-kind 2>/dev/null || echo "interactive")
case "$_SESSION_KIND" in spawned|headless|interactive) ;; *) _SESSION_KIND="interactive" ;; esac
echo "SESSION_KIND: $_SESSION_KIND"
_LAKE_SEEN=$([ -f ~/.gstack/.completeness-intro-seen ] && echo "yes" || echo "no")
echo "LAKE_INTRO: $_LAKE_SEEN"
_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || true)
@ -124,7 +127,7 @@ In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`co
## Skill Invocation During Plan Mode
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If no AskUserQuestion variant is callable, use the normal chat fallback from the AskUserQuestion Format rule and stop for the user's reply. At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If AskUserQuestion is unavailable or a call fails, follow the AskUserQuestion Format failure fallback: `headless` → BLOCKED; `interactive` → the prose fallback (also satisfies end-of-turn). At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If `PROACTIVE` is `"false"`, do not auto-invoke or proactively suggest skills. If a skill seems useful, ask: "I think /skillname might help here — want me to run it?"
@ -159,7 +162,7 @@ touch ~/.gstack/.writing-style-prompted
Skip if `WRITING_STYLE_PENDING` is `no`.
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Lake** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Ocean** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
```bash
open https://garryslist.org/posts/boil-the-ocean
@ -296,11 +299,31 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
**Rule:** if any `mcp__*__AskUserQuestion` variant is in your tool list, prefer it. Hosts may disable native AUQ via `--disallowedTools AskUserQuestion` (Conductor does, by default) and route through their MCP variant; calling native there silently fails. Same questions/options shape; same decision-brief format applies.
**If no AskUserQuestion variant appears in your tool list:** fall back to the host's normal user-facing chat question. Render the same decision brief in prose, stop, and wait for the user's reply. This fallback is for interactive chat hosts that can ask the user normally but do not expose an AUQ tool (for example Codex API sessions). Do not write decisions to the plan file as a substitute, and do not silently auto-decide (only `/plan-tune` AUTO_DECIDE opt-ins authorize auto-picking).
If AskUserQuestion is unavailable (no variant in your tool list) OR a call to it fails, do NOT silently auto-decide or write the decision to the plan file as a substitute. Follow the **failure fallback** below.
### When AskUserQuestion is unavailable or a call fails
Tell three outcomes apart:
1. **Auto-decide denial (NOT a failure).** The result contains `[plan-tune auto-decide] <id> → <option>` — the preference hook working as designed. Proceed with that option. Do NOT retry, do NOT fall back to prose.
2. **Genuine failure** — no variant in your tool list, OR the variant is present but the call returns an error / missing result (MCP transport error, empty result, host bug — e.g. Conductor's MCP AskUserQuestion is flaky and returns `[Tool result missing due to internal error]`).
- If it was present and **errored** (not absent), retry the SAME call **once** — but only if no answer could have surfaced (a missing-result error can arrive after the user already saw the question; retrying would double-prompt, so if it may have reached them, treat as pending, don't retry).
- Then branch on `SESSION_KIND` (echoed by the preamble; empty/absent ⇒ `interactive`):
- `spawned` → defer to the **Spawned session** block: auto-choose the recommended option. Never prose, never BLOCKED.
- `headless``BLOCKED — AskUserQuestion unavailable`; stop and wait (no human can answer).
- `interactive`**prose fallback** (below).
**Prose fallback — render the decision brief as a markdown message, not a tool call.** Same information as the tool format below, different structure (paragraphs, not ✅/❌ bullets). It MUST surface this triad:
1. **A clear ELI10 of the issue itself** — plain English on what's being decided and why it matters (the question, not per-choice), naming the stakes. Lead with it.
2. **Completeness scores per choice** — explicit `Completeness: X/10` on EACH choice (10 complete, 7 happy-path, 3 shortcut); use the kind-note when options differ in kind not coverage, but never silently drop the score.
3. **The recommendation and why** — a `Recommendation: <choice> because <reason>` line plus the `(recommended)` marker on that choice.
Layout: a `D<N>` title + a one-line note that AskUserQuestion failed and to reply with a letter; the issue ELI10; the Recommendation line; then ONE paragraph per choice carrying its `(recommended)` marker, its `Completeness: X/10`, and 2-4 sentences of reasoning — never a bare bullet list; a closing `Net:` line. Split chains / 5+ options: one prose block per per-option call, in sequence. Then STOP and wait — the user's typed answer is the decision. In plan mode this satisfies end-of-turn like a tool call.
### Format
Every AskUserQuestion is a decision brief. Send it as tool_use when an AskUserQuestion tool exists; otherwise use the normal chat fallback above and wait for the user's reply.
Every AskUserQuestion is a decision brief and must be sent as tool_use, not prose — unless the documented failure fallback above applies (interactive session + the call is unavailable/erroring), in which case the prose fallback is the correct output.
```
D<N><one-line question title>
@ -380,7 +403,7 @@ Before calling AskUserQuestion, verify:
- [ ] (recommended) label on one option (even for neutral-posture)
- [ ] Dual-scale effort labels on effort-bearing options (human / CC)
- [ ] Net line closes the decision
- [ ] You are calling the tool when available; otherwise you are using the normal chat fallback and stopping for the user's reply
- [ ] You are calling the tool, not writing prose — unless the documented failure fallback applies (then: prose with the mandatory triad — issue ELI10, per-choice Completeness, Recommendation + `(recommended)` — and a "reply with a letter" instruction, then STOP)
- [ ] Non-ASCII characters (CJK / accents) written directly, NOT \u-escaped
- [ ] If you had 5+ options, you split (or batched into ≤4-groups) — did NOT drop any
- [ ] If you split, you checked dependencies between options before firing the chain
@ -567,12 +590,19 @@ if [ -d "$_PROJ" ]; then
fi
_LATEST_CP=$(find "$_PROJ/checkpoints" -name "*.md" -type f 2>/dev/null | xargs ls -t 2>/dev/null | head -1)
[ -n "$_LATEST_CP" ] && echo "LATEST_CHECKPOINT: $_LATEST_CP"
if [ -f "$_PROJ/decisions.active.json" ]; then
echo "--- ACTIVE DECISIONS (recent, scope-relevant) ---"
~/.claude/skills/gstack/bin/gstack-decision-search --recent 5 2>/dev/null
echo "--- END DECISIONS ---"
fi
echo "--- END ARTIFACTS ---"
fi
```
If artifacts are listed, read the newest useful one. If `LAST_SESSION` or `LATEST_CHECKPOINT` appears, give a 2-sentence welcome back summary. If `RECENT_PATTERN` clearly implies a next skill, suggest it once.
**Cross-session decisions.** If `ACTIVE DECISIONS` are listed, treat them as prior settled calls with their rationale — do not silently re-litigate them; if you're about to reverse one, say so explicitly. Reach for `~/.claude/skills/gstack/bin/gstack-decision-search` whenever a question touches a past decision ("what did we decide / why / did we try"). When you or the user make a DURABLE decision (architecture, scope, tool/vendor choice, or a reversal) — NOT a turn-level or trivial choice — log it with `~/.claude/skills/gstack/bin/gstack-decision-log` (`--supersede <id>` for a reversal). Reliable and local; gbrain not required.
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format is structure; this is prose quality.
@ -587,9 +617,9 @@ Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format i
Curated jargon list lives at `~/.claude/skills/gstack/scripts/jargon-list.json` (80+ terms). On the first jargon term you encounter this session, Read that file once; treat the `terms` array as the canonical list. The list is repo-owned and may grow between releases.
## Completeness Principle — Boil the Lake
## Completeness Principle — Boil the Ocean
AI makes completeness cheap. Recommend complete lakes (tests, edge cases, error paths); flag oceans (rewrites, multi-quarter migrations).
AI makes completeness cheap, so the complete thing is the goal. Recommend full coverage (tests, edge cases, error paths) — boil the ocean one lake at a time. The only thing out of scope is genuinely unrelated work (rewrites, multi-quarter migrations); flag that as separate scope, never as an excuse for a shortcut.
When options differ in coverage, include `Completeness: X/10` (10 = all edge cases, 7 = happy path, 3 = shortcut). When options differ in kind, write: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate scores.

View File

@ -1,6 +1,6 @@
{
"name": "gstack",
"version": "1.56.0.0",
"version": "1.57.10.0",
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
"license": "MIT",
"type": "module",

View File

@ -47,6 +47,9 @@ echo "SKILL_PREFIX: $_SKILL_PREFIX"
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
REPO_MODE=${REPO_MODE:-unknown}
echo "REPO_MODE: $REPO_MODE"
_SESSION_KIND=$(~/.claude/skills/gstack/bin/gstack-session-kind 2>/dev/null || echo "interactive")
case "$_SESSION_KIND" in spawned|headless|interactive) ;; *) _SESSION_KIND="interactive" ;; esac
echo "SESSION_KIND: $_SESSION_KIND"
_LAKE_SEEN=$([ -f ~/.gstack/.completeness-intro-seen ] && echo "yes" || echo "no")
echo "LAKE_INTRO: $_LAKE_SEEN"
_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || true)
@ -126,7 +129,7 @@ In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`co
## Skill Invocation During Plan Mode
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If no AskUserQuestion variant is callable, use the normal chat fallback from the AskUserQuestion Format rule and stop for the user's reply. At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If AskUserQuestion is unavailable or a call fails, follow the AskUserQuestion Format failure fallback: `headless` → BLOCKED; `interactive` → the prose fallback (also satisfies end-of-turn). At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If `PROACTIVE` is `"false"`, do not auto-invoke or proactively suggest skills. If a skill seems useful, ask: "I think /skillname might help here — want me to run it?"
@ -161,7 +164,7 @@ touch ~/.gstack/.writing-style-prompted
Skip if `WRITING_STYLE_PENDING` is `no`.
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Lake** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Ocean** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
```bash
open https://garryslist.org/posts/boil-the-ocean
@ -298,11 +301,31 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
**Rule:** if any `mcp__*__AskUserQuestion` variant is in your tool list, prefer it. Hosts may disable native AUQ via `--disallowedTools AskUserQuestion` (Conductor does, by default) and route through their MCP variant; calling native there silently fails. Same questions/options shape; same decision-brief format applies.
**If no AskUserQuestion variant appears in your tool list:** fall back to the host's normal user-facing chat question. Render the same decision brief in prose, stop, and wait for the user's reply. This fallback is for interactive chat hosts that can ask the user normally but do not expose an AUQ tool (for example Codex API sessions). Do not write decisions to the plan file as a substitute, and do not silently auto-decide (only `/plan-tune` AUTO_DECIDE opt-ins authorize auto-picking).
If AskUserQuestion is unavailable (no variant in your tool list) OR a call to it fails, do NOT silently auto-decide or write the decision to the plan file as a substitute. Follow the **failure fallback** below.
### When AskUserQuestion is unavailable or a call fails
Tell three outcomes apart:
1. **Auto-decide denial (NOT a failure).** The result contains `[plan-tune auto-decide] <id> → <option>` — the preference hook working as designed. Proceed with that option. Do NOT retry, do NOT fall back to prose.
2. **Genuine failure** — no variant in your tool list, OR the variant is present but the call returns an error / missing result (MCP transport error, empty result, host bug — e.g. Conductor's MCP AskUserQuestion is flaky and returns `[Tool result missing due to internal error]`).
- If it was present and **errored** (not absent), retry the SAME call **once** — but only if no answer could have surfaced (a missing-result error can arrive after the user already saw the question; retrying would double-prompt, so if it may have reached them, treat as pending, don't retry).
- Then branch on `SESSION_KIND` (echoed by the preamble; empty/absent ⇒ `interactive`):
- `spawned` → defer to the **Spawned session** block: auto-choose the recommended option. Never prose, never BLOCKED.
- `headless``BLOCKED — AskUserQuestion unavailable`; stop and wait (no human can answer).
- `interactive`**prose fallback** (below).
**Prose fallback — render the decision brief as a markdown message, not a tool call.** Same information as the tool format below, different structure (paragraphs, not ✅/❌ bullets). It MUST surface this triad:
1. **A clear ELI10 of the issue itself** — plain English on what's being decided and why it matters (the question, not per-choice), naming the stakes. Lead with it.
2. **Completeness scores per choice** — explicit `Completeness: X/10` on EACH choice (10 complete, 7 happy-path, 3 shortcut); use the kind-note when options differ in kind not coverage, but never silently drop the score.
3. **The recommendation and why** — a `Recommendation: <choice> because <reason>` line plus the `(recommended)` marker on that choice.
Layout: a `D<N>` title + a one-line note that AskUserQuestion failed and to reply with a letter; the issue ELI10; the Recommendation line; then ONE paragraph per choice carrying its `(recommended)` marker, its `Completeness: X/10`, and 2-4 sentences of reasoning — never a bare bullet list; a closing `Net:` line. Split chains / 5+ options: one prose block per per-option call, in sequence. Then STOP and wait — the user's typed answer is the decision. In plan mode this satisfies end-of-turn like a tool call.
### Format
Every AskUserQuestion is a decision brief. Send it as tool_use when an AskUserQuestion tool exists; otherwise use the normal chat fallback above and wait for the user's reply.
Every AskUserQuestion is a decision brief and must be sent as tool_use, not prose — unless the documented failure fallback above applies (interactive session + the call is unavailable/erroring), in which case the prose fallback is the correct output.
```
D<N><one-line question title>
@ -382,7 +405,7 @@ Before calling AskUserQuestion, verify:
- [ ] (recommended) label on one option (even for neutral-posture)
- [ ] Dual-scale effort labels on effort-bearing options (human / CC)
- [ ] Net line closes the decision
- [ ] You are calling the tool when available; otherwise you are using the normal chat fallback and stopping for the user's reply
- [ ] You are calling the tool, not writing prose — unless the documented failure fallback applies (then: prose with the mandatory triad — issue ELI10, per-choice Completeness, Recommendation + `(recommended)` — and a "reply with a letter" instruction, then STOP)
- [ ] Non-ASCII characters (CJK / accents) written directly, NOT \u-escaped
- [ ] If you had 5+ options, you split (or batched into ≤4-groups) — did NOT drop any
- [ ] If you split, you checked dependencies between options before firing the chain
@ -569,12 +592,19 @@ if [ -d "$_PROJ" ]; then
fi
_LATEST_CP=$(find "$_PROJ/checkpoints" -name "*.md" -type f 2>/dev/null | xargs ls -t 2>/dev/null | head -1)
[ -n "$_LATEST_CP" ] && echo "LATEST_CHECKPOINT: $_LATEST_CP"
if [ -f "$_PROJ/decisions.active.json" ]; then
echo "--- ACTIVE DECISIONS (recent, scope-relevant) ---"
~/.claude/skills/gstack/bin/gstack-decision-search --recent 5 2>/dev/null
echo "--- END DECISIONS ---"
fi
echo "--- END ARTIFACTS ---"
fi
```
If artifacts are listed, read the newest useful one. If `LAST_SESSION` or `LATEST_CHECKPOINT` appears, give a 2-sentence welcome back summary. If `RECENT_PATTERN` clearly implies a next skill, suggest it once.
**Cross-session decisions.** If `ACTIVE DECISIONS` are listed, treat them as prior settled calls with their rationale — do not silently re-litigate them; if you're about to reverse one, say so explicitly. Reach for `~/.claude/skills/gstack/bin/gstack-decision-search` whenever a question touches a past decision ("what did we decide / why / did we try"). When you or the user make a DURABLE decision (architecture, scope, tool/vendor choice, or a reversal) — NOT a turn-level or trivial choice — log it with `~/.claude/skills/gstack/bin/gstack-decision-log` (`--supersede <id>` for a reversal). Reliable and local; gbrain not required.
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format is structure; this is prose quality.
@ -589,9 +619,9 @@ Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format i
Curated jargon list lives at `~/.claude/skills/gstack/scripts/jargon-list.json` (80+ terms). On the first jargon term you encounter this session, Read that file once; treat the `terms` array as the canonical list. The list is repo-owned and may grow between releases.
## Completeness Principle — Boil the Lake
## Completeness Principle — Boil the Ocean
AI makes completeness cheap. Recommend complete lakes (tests, edge cases, error paths); flag oceans (rewrites, multi-quarter migrations).
AI makes completeness cheap, so the complete thing is the goal. Recommend full coverage (tests, edge cases, error paths) — boil the ocean one lake at a time. The only thing out of scope is genuinely unrelated work (rewrites, multi-quarter migrations); flag that as separate scope, never as an excuse for a shortcut.
When options differ in coverage, include `Completeness: X/10` (10 = all edge cases, 7 = happy path, 3 = shortcut). When options differ in kind, write: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate scores.

View File

@ -77,6 +77,9 @@ echo "SKILL_PREFIX: $_SKILL_PREFIX"
source <(~/.claude/skills/gstack/bin/gstack-repo-mode 2>/dev/null) || true
REPO_MODE=${REPO_MODE:-unknown}
echo "REPO_MODE: $REPO_MODE"
_SESSION_KIND=$(~/.claude/skills/gstack/bin/gstack-session-kind 2>/dev/null || echo "interactive")
case "$_SESSION_KIND" in spawned|headless|interactive) ;; *) _SESSION_KIND="interactive" ;; esac
echo "SESSION_KIND: $_SESSION_KIND"
_LAKE_SEEN=$([ -f ~/.gstack/.completeness-intro-seen ] && echo "yes" || echo "no")
echo "LAKE_INTRO: $_LAKE_SEEN"
_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || true)
@ -156,7 +159,7 @@ In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`co
## Skill Invocation During Plan Mode
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If no AskUserQuestion variant is callable, use the normal chat fallback from the AskUserQuestion Format rule and stop for the user's reply. At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; the first AskUserQuestion is the workflow entering plan mode, not a violation of it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If AskUserQuestion is unavailable or a call fails, follow the AskUserQuestion Format failure fallback: `headless` → BLOCKED; `interactive` → the prose fallback (also satisfies end-of-turn). At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.
If `PROACTIVE` is `"false"`, do not auto-invoke or proactively suggest skills. If a skill seems useful, ask: "I think /skillname might help here — want me to run it?"
@ -191,7 +194,7 @@ touch ~/.gstack/.writing-style-prompted
Skip if `WRITING_STYLE_PENDING` is `no`.
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Lake** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
If `LAKE_INTRO` is `no`: say "gstack follows the **Boil the Ocean** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
```bash
open https://garryslist.org/posts/boil-the-ocean
@ -328,11 +331,31 @@ AI orchestrator (e.g., OpenClaw). In spawned sessions:
**Rule:** if any `mcp__*__AskUserQuestion` variant is in your tool list, prefer it. Hosts may disable native AUQ via `--disallowedTools AskUserQuestion` (Conductor does, by default) and route through their MCP variant; calling native there silently fails. Same questions/options shape; same decision-brief format applies.
**If no AskUserQuestion variant appears in your tool list:** fall back to the host's normal user-facing chat question. Render the same decision brief in prose, stop, and wait for the user's reply. This fallback is for interactive chat hosts that can ask the user normally but do not expose an AUQ tool (for example Codex API sessions). Do not write decisions to the plan file as a substitute, and do not silently auto-decide (only `/plan-tune` AUTO_DECIDE opt-ins authorize auto-picking).
If AskUserQuestion is unavailable (no variant in your tool list) OR a call to it fails, do NOT silently auto-decide or write the decision to the plan file as a substitute. Follow the **failure fallback** below.
### When AskUserQuestion is unavailable or a call fails
Tell three outcomes apart:
1. **Auto-decide denial (NOT a failure).** The result contains `[plan-tune auto-decide] <id> → <option>` — the preference hook working as designed. Proceed with that option. Do NOT retry, do NOT fall back to prose.
2. **Genuine failure** — no variant in your tool list, OR the variant is present but the call returns an error / missing result (MCP transport error, empty result, host bug — e.g. Conductor's MCP AskUserQuestion is flaky and returns `[Tool result missing due to internal error]`).
- If it was present and **errored** (not absent), retry the SAME call **once** — but only if no answer could have surfaced (a missing-result error can arrive after the user already saw the question; retrying would double-prompt, so if it may have reached them, treat as pending, don't retry).
- Then branch on `SESSION_KIND` (echoed by the preamble; empty/absent ⇒ `interactive`):
- `spawned` → defer to the **Spawned session** block: auto-choose the recommended option. Never prose, never BLOCKED.
- `headless``BLOCKED — AskUserQuestion unavailable`; stop and wait (no human can answer).
- `interactive`**prose fallback** (below).
**Prose fallback — render the decision brief as a markdown message, not a tool call.** Same information as the tool format below, different structure (paragraphs, not ✅/❌ bullets). It MUST surface this triad:
1. **A clear ELI10 of the issue itself** — plain English on what's being decided and why it matters (the question, not per-choice), naming the stakes. Lead with it.
2. **Completeness scores per choice** — explicit `Completeness: X/10` on EACH choice (10 complete, 7 happy-path, 3 shortcut); use the kind-note when options differ in kind not coverage, but never silently drop the score.
3. **The recommendation and why** — a `Recommendation: <choice> because <reason>` line plus the `(recommended)` marker on that choice.
Layout: a `D<N>` title + a one-line note that AskUserQuestion failed and to reply with a letter; the issue ELI10; the Recommendation line; then ONE paragraph per choice carrying its `(recommended)` marker, its `Completeness: X/10`, and 2-4 sentences of reasoning — never a bare bullet list; a closing `Net:` line. Split chains / 5+ options: one prose block per per-option call, in sequence. Then STOP and wait — the user's typed answer is the decision. In plan mode this satisfies end-of-turn like a tool call.
### Format
Every AskUserQuestion is a decision brief. Send it as tool_use when an AskUserQuestion tool exists; otherwise use the normal chat fallback above and wait for the user's reply.
Every AskUserQuestion is a decision brief and must be sent as tool_use, not prose — unless the documented failure fallback above applies (interactive session + the call is unavailable/erroring), in which case the prose fallback is the correct output.
```
D<N><one-line question title>
@ -412,7 +435,7 @@ Before calling AskUserQuestion, verify:
- [ ] (recommended) label on one option (even for neutral-posture)
- [ ] Dual-scale effort labels on effort-bearing options (human / CC)
- [ ] Net line closes the decision
- [ ] You are calling the tool when available; otherwise you are using the normal chat fallback and stopping for the user's reply
- [ ] You are calling the tool, not writing prose — unless the documented failure fallback applies (then: prose with the mandatory triad — issue ELI10, per-choice Completeness, Recommendation + `(recommended)` — and a "reply with a letter" instruction, then STOP)
- [ ] Non-ASCII characters (CJK / accents) written directly, NOT \u-escaped
- [ ] If you had 5+ options, you split (or batched into ≤4-groups) — did NOT drop any
- [ ] If you split, you checked dependencies between options before firing the chain
@ -599,12 +622,19 @@ if [ -d "$_PROJ" ]; then
fi
_LATEST_CP=$(find "$_PROJ/checkpoints" -name "*.md" -type f 2>/dev/null | xargs ls -t 2>/dev/null | head -1)
[ -n "$_LATEST_CP" ] && echo "LATEST_CHECKPOINT: $_LATEST_CP"
if [ -f "$_PROJ/decisions.active.json" ]; then
echo "--- ACTIVE DECISIONS (recent, scope-relevant) ---"
~/.claude/skills/gstack/bin/gstack-decision-search --recent 5 2>/dev/null
echo "--- END DECISIONS ---"
fi
echo "--- END ARTIFACTS ---"
fi
```
If artifacts are listed, read the newest useful one. If `LAST_SESSION` or `LATEST_CHECKPOINT` appears, give a 2-sentence welcome back summary. If `RECENT_PATTERN` clearly implies a next skill, suggest it once.
**Cross-session decisions.** If `ACTIVE DECISIONS` are listed, treat them as prior settled calls with their rationale — do not silently re-litigate them; if you're about to reverse one, say so explicitly. Reach for `~/.claude/skills/gstack/bin/gstack-decision-search` whenever a question touches a past decision ("what did we decide / why / did we try"). When you or the user make a DURABLE decision (architecture, scope, tool/vendor choice, or a reversal) — NOT a turn-level or trivial choice — log it with `~/.claude/skills/gstack/bin/gstack-decision-log` (`--supersede <id>` for a reversal). Reliable and local; gbrain not required.
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format is structure; this is prose quality.
@ -619,9 +649,9 @@ Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format i
Curated jargon list lives at `~/.claude/skills/gstack/scripts/jargon-list.json` (80+ terms). On the first jargon term you encounter this session, Read that file once; treat the `terms` array as the canonical list. The list is repo-owned and may grow between releases.
## Completeness Principle — Boil the Lake
## Completeness Principle — Boil the Ocean
AI makes completeness cheap. Recommend complete lakes (tests, edge cases, error paths); flag oceans (rewrites, multi-quarter migrations).
AI makes completeness cheap, so the complete thing is the goal. Recommend full coverage (tests, edge cases, error paths) — boil the ocean one lake at a time. The only thing out of scope is genuinely unrelated work (rewrites, multi-quarter migrations); flag that as separate scope, never as an excuse for a shortcut.
When options differ in coverage, include `Completeness: X/10` (10 = all edge cases, 7 = happy path, 3 = shortcut). When options differ in kind, write: `Note: options differ in kind, not coverage — no completeness score.` Do not fabricate scores.
@ -803,7 +833,7 @@ But your posture depends on what the user needs:
* SELECTIVE EXPANSION: You are a rigorous reviewer who also has taste. Hold the current scope as your baseline — make it bulletproof. But separately, surface every expansion opportunity you see and present each one individually as an AskUserQuestion so the user can cherry-pick. Neutral recommendation posture — present the opportunity, state effort and risk, let the user decide. Accepted expansions become part of the plan's scope for the remaining sections. Rejected ones go to "NOT in scope."
* HOLD SCOPE: You are a rigorous reviewer. The plan's scope is accepted. Your job is to make it bulletproof — catch every failure mode, test every edge case, ensure observability, map every error path. Do not silently reduce OR expand.
* SCOPE REDUCTION: You are a surgeon. Find the minimum viable version that achieves the core outcome. Cut everything else. Be ruthless.
* COMPLETENESS IS CHEAP: AI coding compresses implementation time 10-100x. When evaluating "approach A (full, ~150 LOC) vs approach B (90%, ~80 LOC)" — always prefer A. The 70-line delta costs seconds with CC. "Ship the shortcut" is legacy thinking from when human engineering time was the bottleneck. Boil the lake.
* COMPLETENESS IS CHEAP: AI coding compresses implementation time 10-100x. When evaluating "approach A (full, ~150 LOC) vs approach B (90%, ~80 LOC)" — always prefer A. The 70-line delta costs seconds with CC. "Ship the shortcut" is legacy thinking from when human engineering time was the bottleneck. Boil the ocean.
Critical rule: In ALL modes, the user is 100% in control. Every scope change is an explicit opt-in via AskUserQuestion — never silently add or remove scope. Once the user selects a mode, COMMIT to it. Do not silently drift toward a different mode. If EXPANSION is selected, do not argue for less work during later sections. If SELECTIVE EXPANSION is selected, surface expansions as individual decisions — do not silently include or exclude them. If REDUCTION is selected, do not sneak scope back in. Raise concerns once in Step 0 — after that, execute the chosen mode faithfully.
Do NOT make any code changes. Do NOT start implementation. Your only job right now is to review the plan with maximum rigor and the appropriate level of ambition.
@ -930,7 +960,7 @@ Read the `/office-hours` skill file at `~/.claude/skills/gstack/office-hours/SKI
Follow its instructions from top to bottom, **skipping these sections** (already handled by the parent skill):
- Preamble (run first)
- AskUserQuestion Format
- Completeness Principle — Boil the Lake
- Completeness Principle — Boil the Ocean
- Search Before Building
- Contributor Mode
- Completion Status Protocol
@ -976,7 +1006,7 @@ Read the `/office-hours` skill file at `~/.claude/skills/gstack/office-hours/SKI
Follow its instructions from top to bottom, **skipping these sections** (already handled by the parent skill):
- Preamble (run first)
- AskUserQuestion Format
- Completeness Principle — Boil the Lake
- Completeness Principle — Boil the Ocean
- Search Before Building
- Contributor Mode
- Completion Status Protocol
@ -1383,12 +1413,17 @@ missing work — do NOT call ExitPlanMode:
In-body prose that mentions "outside voice", "codex findings", or similar
does NOT count — only the structured `## GSTACK REVIEW REPORT` section
satisfies this check.
3. Confirm the report contains: a Runs / Status / Findings table, a VERDICT
line, and absorbs CODEX / CROSS-MODEL / UNRESOLVED lines if applicable.
4. If a plan file is in context for this skill invocation: confirm
3. Confirm the report has a Runs / Status / Findings table and a VERDICT line
(CODEX / CROSS-MODEL absorbed if applicable).
4. Confirm the report's FINAL non-whitespace line is the unresolved-decisions
status: the exact unbolded `NO UNRESOLVED DECISIONS`, or a bullet of a final
`**UNRESOLVED DECISIONS:**` block. BLOCKING, no "if applicable" escape — a
bolded sentinel, any trailing CODEX/CROSS-MODEL/VERDICT/prose, or a missing
status each FAILS the gate.
5. If a plan file is in context for this skill invocation: confirm
`gstack-review-log` was called and `gstack-review-read` was run at least
once. If no plan file is in context (e.g. `/codex consult` against a
diff with no plan), this check short-circuits — checks 1-3 already
diff with no plan), this check short-circuits — checks 1-4 already
short-circuit when no plan file exists.
Failing this gate and calling ExitPlanMode anyway is a contract violation —

View File

@ -64,7 +64,7 @@ But your posture depends on what the user needs:
* SELECTIVE EXPANSION: You are a rigorous reviewer who also has taste. Hold the current scope as your baseline — make it bulletproof. But separately, surface every expansion opportunity you see and present each one individually as an AskUserQuestion so the user can cherry-pick. Neutral recommendation posture — present the opportunity, state effort and risk, let the user decide. Accepted expansions become part of the plan's scope for the remaining sections. Rejected ones go to "NOT in scope."
* HOLD SCOPE: You are a rigorous reviewer. The plan's scope is accepted. Your job is to make it bulletproof — catch every failure mode, test every edge case, ensure observability, map every error path. Do not silently reduce OR expand.
* SCOPE REDUCTION: You are a surgeon. Find the minimum viable version that achieves the core outcome. Cut everything else. Be ruthless.
* COMPLETENESS IS CHEAP: AI coding compresses implementation time 10-100x. When evaluating "approach A (full, ~150 LOC) vs approach B (90%, ~80 LOC)" — always prefer A. The 70-line delta costs seconds with CC. "Ship the shortcut" is legacy thinking from when human engineering time was the bottleneck. Boil the lake.
* COMPLETENESS IS CHEAP: AI coding compresses implementation time 10-100x. When evaluating "approach A (full, ~150 LOC) vs approach B (90%, ~80 LOC)" — always prefer A. The 70-line delta costs seconds with CC. "Ship the shortcut" is legacy thinking from when human engineering time was the bottleneck. Boil the ocean.
Critical rule: In ALL modes, the user is 100% in control. Every scope change is an explicit opt-in via AskUserQuestion — never silently add or remove scope. Once the user selects a mode, COMMIT to it. Do not silently drift toward a different mode. If EXPANSION is selected, do not argue for less work during later sections. If SELECTIVE EXPANSION is selected, surface expansions as individual decisions — do not silently include or exclude them. If REDUCTION is selected, do not sneak scope back in. Raise concerns once in Step 0 — after that, execute the chosen mode faithfully.
Do NOT make any code changes. Do NOT start implementation. Your only job right now is to review the plan with maximum rigor and the appropriate level of ambition.

View File

@ -253,38 +253,46 @@ If this plan has significant UI scope, recommend: "Consider running /plan-design
**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. If this section turned up zero findings, state "No issues, moving on" and proceed. If the section has findings, you MUST call AskUserQuestion as a tool_use — a finding with an "obvious fix" is still a finding and still needs user approval before any change lands in the plan. Do NOT proceed until the user responds.
**Reminder: Do NOT make any code changes. Review only.**
## Outside Voice — Independent Plan Challenge (optional, recommended)
## Outside Voice — Independent Plan Challenge (default-on)
After all review sections are complete, offer an independent second opinion from a
different AI system. Two models agreeing on a plan is stronger signal than one model's
thorough review.
After all review sections are complete, run an independent second opinion from a
different AI system automatically — it is a standard part of plan review, not an
opt-in. Two models agreeing on a plan is stronger signal than one model's thorough
review. The user turns this off only by asking explicitly
(`gstack-config set codex_reviews disabled`).
**Check tool availability:**
**Preflight — decide whether and how the outside voice runs:**
```bash
command -v codex >/dev/null 2>&1 && echo "CODEX_AVAILABLE" || echo "CODEX_NOT_AVAILABLE"
# Codex preflight: one block (functions sourced here don't persist to later blocks).
_TEL=$(~/.claude/skills/gstack/bin/gstack-config get telemetry 2>/dev/null || echo off)
_CODEX_CFG=$(~/.claude/skills/gstack/bin/gstack-config get codex_reviews 2>/dev/null || echo enabled)
source ~/.claude/skills/gstack/bin/gstack-codex-probe 2>/dev/null || true
if [ "$_CODEX_CFG" = "disabled" ]; then
_CODEX_MODE="disabled"
elif ! command -v codex >/dev/null 2>&1; then
_CODEX_MODE="not_installed"; _gstack_codex_log_event "codex_cli_missing" 2>/dev/null || true
elif ! _gstack_codex_auth_probe >/dev/null 2>&1; then
_CODEX_MODE="not_authed"; _gstack_codex_log_event "codex_auth_failed" 2>/dev/null || true
else
_CODEX_MODE="ready"; _gstack_codex_version_check 2>/dev/null || true
fi
echo "CODEX_MODE: $_CODEX_MODE"
```
Use AskUserQuestion:
Branch on the echoed `CODEX_MODE`:
- **`disabled`** — the user turned Codex reviews off (`codex_reviews=disabled`). Skip this section entirely; do NOT fall back to a Claude subagent — disabled means no extra review step. Print: "Codex review skipped (codex_reviews disabled). Re-enable: `gstack-config set codex_reviews enabled`."
- **`not_installed`** — Codex CLI absent. Print: "Codex not installed — using Claude subagent. Install for cross-model coverage: `npm install -g @openai/codex`." Fall back to the Claude subagent path.
- **`not_authed`** — installed but no credentials. Print: "Codex installed but not authenticated — using Claude subagent. Run `codex login` or set `$CODEX_API_KEY`." Fall back to the Claude subagent path.
- **`ready`** — run the Codex pass below.
> "All review sections are complete. Want an outside voice? A different AI system can
> give a brutally honest, independent challenge of this plan — logical gaps, feasibility
> risks, and blind spots that are hard to catch from inside the review. Takes about 2
> minutes."
>
> RECOMMENDATION: Choose A — an independent second opinion catches structural blind
> spots. Two different AI models agreeing on a plan is stronger signal than one model's
> thorough review. Completeness: A=9/10, B=7/10.
When the mode is `ready`, `not_installed`, or `not_authed`, print one line so the off-switch
stays discoverable: "Running the outside voice automatically (standard step). Disable: `gstack-config set codex_reviews disabled`."
Options:
- A) Get the outside voice (recommended)
- B) Skip — proceed to outputs
**If B:** Print "Skipping outside voice." and continue to the next section.
**If A:** Construct the plan review prompt. Read the plan file being reviewed (the file
the user pointed this review at, or the branch diff scope). If a CEO plan document
was written in Step 0D-POST, read that too — it contains the scope decisions and vision.
**Construct the plan review prompt** (for `ready`, `not_installed`, and `not_authed` — skip only on `disabled`).
Read the plan file being reviewed (the file the user pointed this review at, or the branch
diff scope). If a CEO plan document was written in Step 0D-POST, read that too — it contains
the scope decisions and vision.
Construct this prompt (substitute the actual plan content — if plan content exceeds 30KB,
truncate to the first 30KB and note "Plan truncated for size"). **Always start with the
@ -302,7 +310,7 @@ compliments. Just the problems.
THE PLAN:
<plan content>"
**If CODEX_AVAILABLE:**
**If `CODEX_MODE: ready` — run Codex:**
```bash
TMPERR_PV=$(mktemp /tmp/codex-planreview-XXXXXXXX)
@ -325,15 +333,15 @@ CODEX SAYS (plan review — outside voice):
```
**Error handling:** All errors are non-blocking — the outside voice is informational.
- Auth failure (stderr contains "auth", "login", "unauthorized"): "Codex auth failed. Run \`codex login\` to authenticate."
- Timeout: "Codex timed out after 5 minutes."
- Empty response: "Codex returned no response."
- Auth failure (stderr contains "auth", "login", "unauthorized"): "Codex auth failed. Run \`codex login\` to authenticate." Fall back to the Claude subagent below.
- Timeout: "Codex timed out after 5 minutes." Fall back to the Claude subagent below.
- Empty response: "Codex returned no response." Fall back to the Claude subagent below.
On any Codex error, fall back to the Claude adversarial subagent.
**If CODEX_NOT_AVAILABLE (or Codex errored):**
**If `CODEX_MODE: not_installed` or `not_authed` (or Codex errored at runtime):**
Dispatch via the Agent tool. The subagent has fresh context — genuine independence.
Bound it the same way as Codex: cap the dispatch at a 5-minute timeout so "never blocking"
is also "never hanging."
Subagent prompt: same plan review prompt as above.
@ -341,6 +349,8 @@ Present findings under an `OUTSIDE VOICE (Claude subagent):` header.
If the subagent fails or times out: "Outside voice unavailable. Continuing to outputs."
(On `CODEX_MODE: disabled` you already skipped this section per the preflight — do not reach here.)
**Cross-model tension:**
After presenting the outside voice findings, note any points where the outside voice
@ -597,8 +607,11 @@ command breaks the review readiness dashboard in /ship.
```bash
~/.claude/skills/gstack/bin/gstack-review-log '{"skill":"plan-ceo-review","timestamp":"TIMESTAMP","status":"STATUS","unresolved":N,"critical_gaps":N,"mode":"MODE","scope_proposed":N,"scope_accepted":N,"scope_deferred":N,"commit":"COMMIT"}'
~/.claude/skills/gstack/bin/gstack-decision-log '{"decision":"CEO review (MODE): SCOPE_SUMMARY","rationale":"VERDICT","scope":"branch","source":"skill","confidence":8}' 2>/dev/null || true
```
The second command records the accepted scope as a durable cross-session decision so the next session sees what was settled (and why) without re-litigating it. It writes to `~/.gstack/` (same pattern as review-log), is non-interactive, and is best-effort (`|| true` — never blocks the review). Substitute `SCOPE_SUMMARY` (e.g. "accepted 4 of 6 proposals" for expansion, or "held scope" / "cut 3 items" for HOLD/REDUCTION) and `VERDICT` (the one-line verdict from the summary).
Before running this command, substitute the placeholder values from the Completion Summary you just produced:
- **TIMESTAMP**: current ISO 8601 datetime (e.g., 2026-03-16T14:30:00)
- **STATUS**: "clean" if 0 unresolved decisions AND 0 critical gaps; otherwise "issues_open"
@ -709,14 +722,24 @@ Produce this markdown table:
| DX Review | \`/plan-devex-review\` | Developer experience gaps | {runs} | {status} | {findings} |
\`\`\`
Below the table, add these lines (omit any that are empty/not applicable):
Below the table, add these lines. **CODEX** and **CROSS-MODEL** are optional (omit when
empty); **VERDICT** is always present:
- **CODEX:** (only if codex-review ran) — one-line summary of codex fixes
- **CROSS-MODEL:** (only if both Claude and Codex reviews exist) — overlap analysis
- **UNRESOLVED:** total unresolved decisions across all reviews
- **VERDICT:** list reviews that are CLEAR (e.g., "CEO + ENG CLEARED — ready to implement").
If Eng Review is not CLEAR and not skipped globally, append "eng review required".
**Unresolved-decisions status (MANDATORY — never omitted; the report's final non-whitespace
line).** After VERDICT, end the report (content under the \`## GSTACK REVIEW REPORT\`
heading — a bold label, never a new \`## \` heading; exempt from the "omit when empty"
rule) with exactly one: the exact unbolded line \`NO UNRESOLVED DECISIONS\` (a bolded one
does NOT count), OR a \`**UNRESOLVED DECISIONS:**\` header + one bullet per open item
(last bullet = final line; add \`+ N unresolved from prior reviews\` only when N > 0).
This avoids double-counting: list THIS review's open items from context; for prior reviews
sum \`unresolved\` over the latest fresh row per skill (dashboard 7-day window) after you
DROP the current skill's row; emit the sentinel only when both are zero.
### Write to the plan file
**PLAN MODE EXCEPTION — ALWAYS RUN:** This writes to the plan file, which is the one

Some files were not shown because too many files have changed in this diff Show More