From 23fccb982aff8acb42a8c27685d7707d9ff0d730 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Fri, 4 Sep 2026 18:35:29 +0000 Subject: [PATCH] docs: update project documentation for v1.80.0.0 README troubleshooting + manual uninstall cover the skill ownership gate (.gstack-owned marker, ~/.gstack/backups/skills//, foreign same-name skills left untouched). CLAUDE.md and CONTRIBUTING carry the ownership and best-effort Chromium bootstrap invariants for people editing setup and gstack-relink. PROJECT_STRUCTURE gains careful/, freeze/, guard/, unfreeze/, gstack-upgrade/, gstack-relink, and the setup/relink/hook test files. TESTING_INTERNALS documents the anchor-sliced setup harness convention. Co-Authored-By: Claude Fable 5.1 --- CLAUDE.md | 14 ++++++++++++++ CONTRIBUTING.md | 6 +++++- README.md | 18 ++++++++++++++++++ docs/PROJECT_STRUCTURE.md | 11 ++++++++--- docs/TESTING_INTERNALS.md | 20 ++++++++++++++++++++ 5 files changed, 65 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 115a52282..66a67f5d1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -254,6 +254,20 @@ Names are either short (`qa`) or namespaced (`gstack-qa`), controlled by `skill_prefix` in `~/.gstack/config.yaml`. Pass `--no-prefix` or `--prefix` to skip the interactive prompt. +**Ownership gate (#2119):** `setup` writes a `.gstack-owned` marker into every +skill directory it creates, and `setup` (the linker, the alias installer, and +both prefix-flip cleanups) and `bin/gstack-relink` only delete or link over an +entry they can prove is gstack's. Strong proof (a symlink resolving into gstack, +or the marker) allows deleting or refreshing the whole directory. Weak proof (a +real SKILL.md byte-identical to the source, or carrying gen-skill-docs' two-line +banner) covers only that one file, and a weakly-proven file that differs is +moved to `~/.gstack/backups/skills///SKILL.md` before gstack links +over it. Anything else is a foreign skill: skipped, and named in setup's final +summary. The rule lives in two copies (`setup` and `bin/gstack-relink`); keep +them in sync until the shared helper filed in TODOS.md lands. Pinned by +`test/setup-link-ownership.test.ts`, `test/setup-cleanup-orphans.test.ts`, and +`test/relink.test.ts`. + **Note:** Vendoring gstack into a project's repo is deprecated. Use global install + `./setup --team` instead. See README.md for team mode instructions. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ba2d5e0d5..1502420ff 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -460,6 +460,8 @@ When Conductor creates a new workspace, `bin/dev-setup` runs automatically. It d - **`.claude/skills/` is gitignored.** The symlinks never get committed. - **Never write raw `ln -snf` in `setup`.** Every link site in `setup` MUST route through the `_link_or_copy SRC DST` helper near the `IS_WINDOWS` detection. The helper preserves `ln -snf` on Unix and switches to `cp -R` / `cp -f` on Windows without Developer Mode, where plain `ln -snf` produces frozen file copies that don't refresh on `git pull`. `test/setup-windows-fallback.test.ts` enforces this with a static invariant — a single raw `ln` call outside the helper body fails CI. - **Synchronous subagent dispatches must state the flag.** Claude Code runs Agent-tool subagents in the background by default (since v2.1.198), so any template step that dispatches a subagent and consumes its output must carry `run_in_background: false`. Use the `{{FOREGROUND_DISPATCH_NOTE}}` placeholder (`scripts/resolvers/constants.ts`) instead of hand-writing the guidance, and add the generated carrier file to `GENERATED_WITH_GUIDANCE` in `test/run-in-background-guidance.test.ts` in the same commit — its structural scanner fails CI on any generated dispatch imperative that lacks the flag. +- **Never delete or link over a skill entry `setup` cannot prove is gstack's.** Every destructive site in `setup` (the linker, the alias installer, both prefix-flip cleanups) and in `bin/gstack-relink` goes through the ownership helpers (`_claude_entry_is_ours` / `_claude_entry_owned_strongly` in `setup`, `_entry_is_ours` / `_entry_owned_strongly` in relink). A symlink into gstack or the `.gstack-owned` marker proves the whole directory; a byte-identical or generated-banner SKILL.md proves only that file, and a differing one is moved to `~/.gstack/backups/skills//` first. `test/setup-link-ownership.test.ts`, `test/setup-cleanup-orphans.test.ts`, and `test/relink.test.ts` pin it. The rule is duplicated in the two scripts until the shared helper filed in TODOS.md lands: change both. +- **`./setup` never fails on Chromium.** The Playwright bootstrap (section `# 2` of `setup`) is best-effort and bounded: every failure becomes a reason code (`skipped`, `chromium-install`, `chromium-install-timeout`, `chromium-install-locked`, `windows-no-node`, `windows-node-modules`, `post-install-launch`) printed in the final summary alongside the browser-dependent skills, and skill registration always runs. `GSTACK_PLAYWRIGHT_INSTALL_TIMEOUT=` (default 600) bounds the download; `GSTACK_SKIP_PLAYWRIGHT=1` skips it, the right knob for a no-browser box or a setup-only test loop. Anything you add after the bootstrap must stay independent of the browser. `test/setup-playwright-best-effort.test.ts` pins the block. ## Testing your changes in a real project @@ -513,7 +515,9 @@ cd .claude/skills/gstack && ./setup --no-prefix # switch to /qa, /ship cd .claude/skills/gstack && ./setup --prefix # switch to /gstack-qa, /gstack-ship ``` -Setup cleans up the old symlinks automatically. No manual cleanup needed. +Setup cleans up the old symlinks automatically. No manual cleanup needed. Only +entries gstack created are removed: a skill of your own that shares a name (a +hand-written `qa/`, say) is left in place and named in setup's final summary. ### Alternative: point your global install at a branch diff --git a/README.md b/README.md index 77d0e43ba..c894bae14 100644 --- a/README.md +++ b/README.md @@ -393,6 +393,11 @@ while IFS= read -r dir; do ;; esac done +# Directories gstack created carry a .gstack-owned marker (the only signal on +# Windows, where installs are file copies with no symlink to read) +for marker in ~/.claude/skills/*/.gstack-owned; do + [ -f "$marker" ] && rm -rf "$(dirname "$marker")" +done # Alias skills install as copies (no symlink to detect) — remove by name rm -rf ~/.claude/skills/_gstack-command ~/.claude/skills/connect-chrome 2>/dev/null @@ -540,6 +545,19 @@ install entirely (CI, no-browser boxes); `GSTACK_CHROMIUM_NO_SANDBOX=1` is the fix when Chromium installs but cannot launch because the host blocks unprivileged user namespaces (Ubuntu 24.04+ AppArmor default, #2157). +**Setup ended with "Not registered (a skill you own already uses the name; left untouched)"?** +gstack only deletes or links over a skill entry it can prove it created: a +symlink into gstack, a directory carrying the `.gstack-owned` marker `./setup` +writes into every directory it creates, or a SKILL.md that is byte-identical to +gstack's or carries the generated `` banner. A +`qa/` or `ship/` you wrote yourself is left untouched by `./setup`, +`gstack-relink`, and both prefix-mode flips, and the linker names it in the +final summary. Rename or move yours, or switch modes (`./setup --prefix` / +`--no-prefix`) so the names stop colliding. If you started your own skill from +a generated gstack SKILL.md and then edited it, that file is moved to +`~/.gstack/backups/skills///SKILL.md` before gstack's is +linked in, never deleted. + **Claude says it can't see the skills?** Make sure your project's `CLAUDE.md` has a gstack section. Add this: ``` diff --git a/docs/PROJECT_STRUCTURE.md b/docs/PROJECT_STRUCTURE.md index 6d043e7dc..dd801ade4 100644 --- a/docs/PROJECT_STRUCTURE.md +++ b/docs/PROJECT_STRUCTURE.md @@ -36,6 +36,7 @@ gstack/ │ ├── fixtures/ # Ground truth JSON, planted-bug fixtures, eval baselines │ ├── skill-validation.test.ts # Tier 1: static validation (free, <1s) │ ├── gen-skill-docs.test.ts # Tier 1: generator quality (free, <1s) +│ ├── setup-*.test.ts, relink.test.ts, hook-scripts.test.ts # Tier 1: setup linker ownership + Chromium bootstrap (anchor-sliced from setup), gstack-relink, PreToolUse hooks (free) │ ├── skill-llm-eval.test.ts # Tier 3: LLM-as-judge (~$0.15/run) │ └── skill-e2e-*.test.ts # Tier 2: E2E via claude -p (~$3.85/run, split by category) ├── qa-only/ # /qa-only skill (report-only QA, no fixes) @@ -54,7 +55,11 @@ gstack/ ├── investigate/ # /investigate skill (systematic root-cause debugging) ├── spec/ # /spec skill (five-phase spec → GitHub issue, optional agent spawn, /ship auto-closes) ├── retro/ # Retrospective skill (includes /retro global cross-project mode) -├── bin/ # CLI utilities (gstack-repo-mode, gstack-slug, gstack-config, gstack-wtree, gstack-evidence, gstack-issue-guard, etc.) +├── careful/ # /careful skill; bin/check-careful.sh (PreToolUse destructive-command hook) + bin/hook-extract.sh (shared hook helpers: payload extraction, deny JSON, gstack_hook_state_root) +├── freeze/ # /freeze skill; bin/check-freeze.sh (PreToolUse edit-boundary hook; sources careful/bin/hook-extract.sh, fails closed) +├── guard/, unfreeze/ # /guard (careful + freeze in one), /unfreeze +├── gstack-upgrade/ # /gstack-upgrade skill + migrations/ (run after ./setup during an upgrade) +├── bin/ # CLI utilities (gstack-repo-mode, gstack-slug, gstack-config, gstack-wtree, gstack-evidence, gstack-issue-guard, gstack-relink, etc.) ├── document-release/ # /document-release skill (post-ship doc updates + Diataxis coverage map) ├── document-generate/ # /document-generate skill (Diataxis doc generator: tutorial/how-to/reference/explanation) ├── cso/ # /cso skill (OWASP Top 10 + STRIDE security audit) @@ -70,14 +75,14 @@ gstack/ ├── extension/ # Chrome extension (side panel + activity feed + CSS inspector) ├── lib/ # Shared libraries (worktree.ts, egress-receipt.ts, context-bill.ts, redact-engine.ts, tracker-guard.ts, version-source.ts, code-intelligence/) ├── patches/ # bun `patchedDependencies` patches (playwright-core windowsHide) -├── docs/designs/ # Design documents +├── docs/designs/ # Design documents (incl. fork-port-residual-2026-09/ evaluation evidence) ├── setup-deploy/ # /setup-deploy skill (one-time deploy config) ├── .github/ # CI workflows + shared composite actions (.github/actions/) + Docker image (claude CLI pinned) │ ├── workflows/ # evals.yml (E2E on Ubicloud), quality-gate.yml (secret scan), dependency-review.yml, osv-scanner.yml, skill-docs.yml, actionlint.yml, and 8 more (windows, periodic evals, release gates, ci-image) │ └── docker/ # Dockerfile.ci (pre-baked toolchain + Playwright/Chromium) ├── contrib/ # Contributor-only tools (never installed for users) │ └── add-host/ # /gstack-contrib-add-host skill -├── setup # One-time setup: build binary + symlink skills +├── setup # One-time setup: build binary + best-effort Chromium bootstrap + link skills (ownership-gated) ├── 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 Ocean, Search Before Building) diff --git a/docs/TESTING_INTERNALS.md b/docs/TESTING_INTERNALS.md index dbab9d8b4..8b1497859 100644 --- a/docs/TESTING_INTERNALS.md +++ b/docs/TESTING_INTERNALS.md @@ -122,6 +122,26 @@ in the test trees must carry a `timeout`, enforced by `test/spawnsync-timeout-tripwire.test.ts` with a shrink-only exemption ratchet. +**Anchor-sliced `setup` harnesses.** `setup` is one large bash script, so the +free tests that pin its linker, cleanup, and Chromium-bootstrap behavior never +run the whole thing. They slice the source by anchor (`extractFn(name)` takes +`name() {` through the next `\n}\n`; `test/setup-playwright-best-effort.test.ts` +slices the `# 2. Ensure Playwright's Chromium is available` block up to +`# 2b.`), join the extracted functions with stubbed collaborators, and execute +the REAL bash under a temp `HOME` with stubbed probes and installers. Two rules +keep the harness honest: renaming a function or anchor comment in `setup` fails +the test with `function not found` / `anchor not found` instead of silently +testing nothing, and `test/setup-link-ownership.test.ts` and +`test/setup-playwright-best-effort.test.ts` throw on any `command not found` on +stderr as harness drift (a helper the test forgot to extract) rather than +letting it degrade into a pass. Files: `test/setup-link-ownership.test.ts`, +`test/setup-cleanup-orphans.test.ts`, `test/setup-playwright-best-effort.test.ts`. +`test/relink.test.ts` shells out to a copy of the real `bin/gstack-relink` +against a temp `GSTACK_INSTALL_DIR` / `GSTACK_SKILLS_DIR`, and +`test/hook-scripts.test.ts` runs the real `careful/bin/check-careful.sh` and +`freeze/bin/check-freeze.sh` with JSON payloads on stdin (including the +`GSTACK_HOME` state-root parity against `bin/gstack-paths`). + ## Cloud sandboxes (Vercel / Conductor cloud workspaces) Syscall-supervised sandboxes need environment setup before `bun run test` can