implement six-skill gstack 2 runtime

This commit is contained in:
Sinabina
2026-07-17 11:08:14 -07:00
parent ce37bd36a9
commit b6572ebbb7
455 changed files with 108945 additions and 2622 deletions
+20
View File
@@ -0,0 +1,20 @@
FROM oven/bun:1.3.14-debian@sha256:9dba1a1b43ce28c9d7931bfc4eb00feb63b0114720a0277a8f939ae4dfc9db6f
ENV DEBIAN_FRONTEND=noninteractive
ENV PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
bash ca-certificates curl git jq nodejs npm poppler-utils fonts-liberation \
&& rm -rf /var/lib/apt/lists/*
# Install the exact Playwright release locked by this repository, including
# Chromium and its Linux shared-library/font dependencies. The runtime smoke
# below drives this browser rather than merely checking that a launcher exists.
RUN bunx playwright@1.58.2 install --with-deps chromium \
&& chmod -R a+rX /opt/playwright-browsers \
&& rm -rf /root/.bun/install/cache
WORKDIR /workspaces/gstack
RUN git config --system --add safe.directory /workspaces/gstack
+17
View File
@@ -0,0 +1,17 @@
{
"name": "GStack 2",
"build": {
"dockerfile": "Dockerfile",
"context": ".."
},
"remoteUser": "root",
"postCreateCommand": "bun install --frozen-lockfile",
"containerEnv": {
"PLAYWRIGHT_BROWSERS_PATH": "/workspaces/gstack/.cache/ms-playwright"
},
"customizations": {
"vscode": {
"extensions": []
}
}
}
+83
View File
@@ -0,0 +1,83 @@
name: GStack 2 Gate
permissions:
contents: read
on:
pull_request:
branches: [main]
workflow_dispatch:
concurrency:
group: gstack2-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
native:
name: Native ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [macos-14, ubuntu-24.04, windows-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 25
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: 1.3.14
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
- name: Configure isolated test identity
run: |
git config --global user.email "gstack2-ci@example.invalid"
git config --global user.name "GStack 2 CI"
git config --global init.defaultBranch main
- run: bun install --frozen-lockfile
- name: Canonical six-skill, parity, state, and runtime gates
run: bun run test:gstack2
- name: Standard installer discovery
run: npx --yes skills@1.5.19 add . --list
installer-matrix:
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: 1.3.14
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
- run: bun install --frozen-lockfile
- run: bun run test:gstack2:install
dev-container:
runs-on: ubuntu-24.04
timeout-minutes: 25
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- name: Build the declared development container
run: docker build --file .devcontainer/Dockerfile --tag gstack2-devcontainer .
- name: Run the GStack 2 gate inside the container
run: >-
docker run --rm
--volume "${{ github.workspace }}:/workspaces/gstack"
--workdir /workspaces/gstack
gstack2-devcontainer
/workspaces/gstack/scripts/gstack2/devcontainer-gate.sh
- name: Exercise a clean Linux runtime install inside the container
run: >-
docker run --rm
--volume "${{ github.workspace }}:/source:ro"
--workdir /source
gstack2-devcontainer
/source/scripts/gstack2/runtime-install-smoke.sh /source
+12 -84
View File
@@ -1,91 +1,19 @@
---
name: gstack
preamble-tier: 1
version: 1.2.0
description: |
Router for the gstack skill suite. Sends any gstack request to the right skill
(planning, review, QA, shipping, debugging, docs, security, design). For browser/QA
and dogfooding it points you at /browse. Use when you invoke gstack without a specific
skill, or ask "which gstack skill fits this?". (gstack)
allowed-tools:
- Bash
- Read
- AskUserQuestion
triggers:
- gstack
- which gstack skill
- route this with gstack
description: Compatibility router for GStack 1.x commands. Use only when a user invokes the legacy `/gstack` command or asks which GStack 2.0 skill replaces an old command.
metadata:
internal: true
---
{{PREAMBLE}}
# GStack compatibility router
## Route first
This alias is opt-in and not part of the default public surface. Route to one of the six public skills, print the replacement invocation, and let that dispatcher load the exact preserved specialist module.
This is the gstack router. Its one job is to send the request to the right skill.
- Product discovery, scope, architecture, DX planning, specification, retros, memory, or context checkpoints: `/plan`
- Design exploration, generation, critique, implementation, HTML, web visual review, or iOS HIG review: `/design`
- Web, API, CLI, backend, accessibility, performance, canary, developer-experience, authenticated, or physical-iOS testing: `/qa`
- Investigation, root-cause diagnosis, mitigation, or platform-specific repair: `/debug`
- Diff, code, architecture, data, dependency, security, performance, health, or documentation review: `/review`
- Test/release preparation, PRs, versioning, documentation, land, deploy, monitor, rollback, upgrade, or resume: `/ship`
1. If the request is about a browser, QA, dogfooding, screenshots, or inspecting a page
(open a site, test a deploy, take a screenshot, check a flow visually) → invoke `/browse`.
2. Otherwise, route by the rules below. If nothing matches, answer directly.
Best-effort, record which way you routed (never block on it). Set `ROUTE_OUTCOME` to
`browse` (sent to /browse), `routed` (sent to another skill), or `direct` (answered
directly, no skill matched):
```bash
~/.claude/skills/gstack/bin/gstack-telemetry-log --event-type route --skill gstack --outcome ROUTE_OUTCOME --session-id "$_SESSION_ID" 2>/dev/null || true
```
If `PROACTIVE` is `false`: do NOT proactively invoke or suggest other gstack skills during
this session. Only run skills the user explicitly invokes. This preference persists across
sessions via `gstack-config`.
If `PROACTIVE` is `true` (default): **invoke the Skill tool** when the user's request
matches a skill's purpose. Do NOT answer directly when a skill exists for the task.
Use the Skill tool to invoke it. The skill has specialized workflows, checklists, and
quality gates that produce better results than answering inline.
**Routing rules — when you see these patterns, INVOKE the skill via the Skill tool:**
- User describes a new idea, asks "is this worth building", brainstorms, pitches a concept → invoke `/office-hours`
- User asks to spec something out, file an issue, write up a ticket, "turn this into a GitHub issue", "backlog item" → invoke `/spec`
- User asks about strategy, scope, ambition, "think bigger", "what should we build" → invoke `/plan-ceo-review`
- User asks to review architecture, lock in the plan, "does this design make sense" → invoke `/plan-eng-review`
- User asks about design system, brand, visual identity, "how should this look" → invoke `/design-consultation`
- User asks to review design of a plan → invoke `/plan-design-review`
- User asks about developer experience of a plan, API/CLI/SDK design → invoke `/plan-devex-review`
- User wants all reviews done automatically, "review everything" → invoke `/autoplan`
- User reports a bug, error, broken behavior, "why is this broken", "this doesn't work", "wtf", "something's wrong" → invoke `/investigate`
- User asks to test the site, find bugs, QA, "does this work", "check the deploy" → invoke `/qa`
- User asks to just report bugs without fixing → invoke `/qa-only`
- User asks to review code, check the diff, pre-landing review, "look at my changes" → invoke `/review`
- User asks about visual polish, design audit of a live site, "this looks off" → invoke `/design-review`
- User asks to audit the live developer experience, time-to-hello-world → invoke `/devex-review`
- User asks to ship, deploy, push, create a PR, "let's land this", "send it" → invoke `/ship`
- User asks to merge + deploy + verify as one flow → invoke `/land-and-deploy`
- User asks to configure deployment for the project → invoke `/setup-deploy`
- User asks to monitor prod after shipping, post-deploy checks → invoke `/canary`
- User asks to update docs after shipping → invoke `/document-release`
- User asks to write docs from scratch, generate documentation, "document this feature/module" → invoke `/document-generate`
- User asks for a weekly retro, what did we ship, "how'd we do" → invoke `/retro`
- User asks for a second opinion, codex review → invoke `/codex`
- User asks for safety mode, careful mode → invoke `/careful` or `/guard`
- User asks to restrict edits to a directory → invoke `/freeze` or `/unfreeze`
- User asks to upgrade gstack → invoke `/gstack-upgrade`
- User asks to save progress, checkpoint, "save my work" → invoke `/context-save`
- User asks to resume, restore, "where was I" → invoke `/context-restore`
- User asks about security, OWASP, vulnerabilities, "is this secure" → invoke `/cso`
- User asks to make a PDF, document, publication → invoke `/make-pdf`
- User asks to launch a real browser for QA, "open the browser" → invoke `/open-gstack-browser`
- User asks to import cookies for authenticated testing → invoke `/setup-browser-cookies`
- User asks about page speed, performance regression, benchmarks → invoke `/benchmark`
- User asks what gstack has learned, "show learnings" → invoke `/learn`
- User asks to tune question sensitivity, "stop asking me that" → invoke `/plan-tune`
- User asks for code quality dashboard, "health check" → invoke `/health`
**When in doubt, invoke the skill.** A false positive (invoking a skill that wasn't
needed) is cheaper than a false negative (answering ad-hoc when a structured workflow
exists). The skill provides multi-step workflows, checklists, and quality gates that
always produce better results than an ad-hoc answer. If no skill matches, answer
directly as usual.
If the user opts out of suggestions, run `gstack-config set proactive false`.
If they opt back in, run `gstack-config set proactive true`.
Do not reproduce specialist judgment here. If the request names an old command, use its compatibility alias or the canonical dispatcher's migration table so the exact legacy module is selected.
+3 -1
View File
@@ -1,5 +1,5 @@
---
name: autoplan
name: gstack-1-autoplan
preamble-tier: 3
version: 1.0.0
description: Auto-review pipeline — reads the full CEO, design, eng, and DX review skills from disk and runs them sequentially with auto-decisions using 6 decision principles. (gstack)
@@ -17,6 +17,8 @@ allowed-tools:
- Grep
- WebSearch
- AskUserQuestion
metadata:
internal: true
---
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
<!-- Regenerate: bun run gen:skill-docs -->
+3 -1
View File
@@ -1,5 +1,5 @@
---
name: benchmark-models
name: gstack-1-benchmark-models
preamble-tier: 1
version: 1.0.0
description: Cross-model benchmark for gstack skills. (gstack)
@@ -12,6 +12,8 @@ allowed-tools:
- Bash
- Read
- AskUserQuestion
metadata:
internal: true
---
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
<!-- Regenerate: bun run gen:skill-docs -->
+3 -1
View File
@@ -1,5 +1,5 @@
---
name: benchmark
name: gstack-1-benchmark
preamble-tier: 1
version: 1.0.0
description: Performance regression detection using the browse daemon. (gstack)
@@ -13,6 +13,8 @@ allowed-tools:
- Write
- Glob
- AskUserQuestion
metadata:
internal: true
---
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
<!-- Regenerate: bun run gen:skill-docs -->
+10 -26
View File
@@ -56,31 +56,14 @@ if [ ! -e "$AGENTS_LINK" ]; then
ln -s "$REPO_ROOT" "$AGENTS_LINK"
fi
# 6. Run setup via the symlink so it detects .claude/skills/ as its parent.
# 6. Do not call the user runtime installer from a development worktree.
#
# Workspace/dev setup MUST be non-interactive: Conductor runs this under a
# forwarded pty, so any `read` in setup (skill-prefix prompt, plan-tune hook
# consent) would hang the workspace forever. Detaching stdin makes every setup
# prompt take its smart non-interactive default (flat skill names, etc.).
# GStack 2 delegates skill placement to the standard Agent Skills installer,
# and root ./setup installs only the optional per-user runtime. Development can
# execute the checked-out binaries directly, so invoking ./setup here would
# mutate user state without adding any workspace capability.
#
# `--plan-tune-hooks=prompt` is load-bearing, not redundant: stdin alone only
# suppresses the *prompt* branch. A saved `plan_tune_hooks: yes` or an exported
# GSTACK_PLAN_TUNE_HOOKS=yes would still resolve to "install" and rewrite the
# user's global ~/.claude/settings.json to point at THIS ephemeral worktree —
# which breaks once the workspace is deleted. The flag has highest precedence,
# so it pins resolution to "prompt", and closed stdin then makes prompt-mode a
# 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_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.
# 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 +
@@ -88,8 +71,8 @@ GSTACK_SKIP_GBRAIN_REGEN=1 "$GSTACK_LINK/setup" --plan-tune-hooks=prompt </dev/n
# 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).
# while git stays clean. Production skill placement and updates remain owned by
# the standard Agent Skills installer.
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
@@ -121,7 +104,8 @@ 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 "To refresh managed gbrain detection state, run:"
echo " gstack-config gbrain-refresh"
echo "Then use the standard Agent Skills installer for production skill updates."
echo ""
echo "To tear down: bin/dev-teardown"
Executable
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env node
import { main } from "../runtime/cli.js";
const code = await main();
process.exitCode = code;
+106 -83
View File
@@ -13,7 +13,7 @@
*
* Cache layout:
* ~/.gstack/brain-cache/ ← cross-project (user-profile only)
* ~/.gstack/projects/<slug>/brain-cache/ ← per-project (everything else)
* $GSTACK_HOME/projects/<project-id>/brain-cache/ ← local, worktree-specific
*
* Atomic writes via .tmp + rename. Stale-but-usable fallback when brain
* unreachable. Concurrent-refresh dedup is a follow-up commit (T15).
@@ -24,6 +24,7 @@ import { join, dirname } from 'path';
import { homedir, hostname } from 'os';
import { spawnSync } from 'child_process';
import { execGbrainJson, spawnGbrain } from '../lib/gbrain-exec';
import { discoverProjectIdentity } from '../runtime/identity.js';
import {
BRAIN_CACHE_ENTITIES,
CACHE_REFRESH_LOCK_TIMEOUT_MS,
@@ -39,6 +40,25 @@ import {
const GSTACK_HOME = process.env.GSTACK_HOME || join(homedir(), '.gstack');
interface ProjectTarget {
/** Human-facing namespace used for GBrain page slugs. */
slug: string;
/** Worktree-specific local storage key from runtime/identity.js. */
stateId: string;
}
type ProjectRef = string | ProjectTarget | null;
function projectNamespace(project: ProjectRef): string | null {
if (!project) return null;
return typeof project === 'string' ? project : project.slug;
}
function projectStateId(project: ProjectRef): string | null {
if (!project) return null;
return typeof project === 'string' ? project : project.stateId;
}
interface CacheMeta {
/** Version of the schema pack the cache was built against. Mismatch → full rebuild. */
schema_version: string;
@@ -51,34 +71,36 @@ interface CacheMeta {
}
/** Returns the directory holding a given entity's cache file. */
export function entityDir(entity: BrainCacheEntity, projectSlug: string | null): string {
export function entityDir(entity: BrainCacheEntity, project: ProjectRef): string {
if (entity.scope === 'cross-project') {
return join(GSTACK_HOME, 'brain-cache');
}
if (!projectSlug) {
const stateId = projectStateId(project);
if (!stateId) {
throw new Error(`Per-project entity needs a project slug: ${entity.file}`);
}
return join(GSTACK_HOME, 'projects', projectSlug, 'brain-cache');
return join(GSTACK_HOME, 'projects', stateId, 'brain-cache');
}
/** Returns the path to the cache file for a given entity. */
export function entityPath(entityName: string, projectSlug: string | null): string {
export function entityPath(entityName: string, project: ProjectRef): string {
const entity = BRAIN_CACHE_ENTITIES[entityName];
if (!entity) throw new Error(`Unknown brain cache entity: ${entityName}`);
return join(entityDir(entity, projectSlug), entity.file);
return join(entityDir(entity, project), entity.file);
}
/** Returns the path to the _meta.json for a given scope. */
export function metaPath(scope: 'cross-project' | 'per-project', projectSlug: string | null): string {
export function metaPath(scope: 'cross-project' | 'per-project', project: ProjectRef): string {
if (scope === 'cross-project') {
return join(GSTACK_HOME, 'brain-cache', '_meta.json');
}
if (!projectSlug) throw new Error('Per-project meta needs a project slug');
return join(GSTACK_HOME, 'projects', projectSlug, 'brain-cache', '_meta.json');
const stateId = projectStateId(project);
if (!stateId) throw new Error('Per-project meta needs a project slug');
return join(GSTACK_HOME, 'projects', stateId, 'brain-cache', '_meta.json');
}
function loadMeta(scope: 'cross-project' | 'per-project', projectSlug: string | null): CacheMeta {
const path = metaPath(scope, projectSlug);
function loadMeta(scope: 'cross-project' | 'per-project', project: ProjectRef): CacheMeta {
const path = metaPath(scope, project);
if (!existsSync(path)) {
return { schema_version: GSTACK_SCHEMA_PACK_VERSION, endpoint_hash: detectEndpointHash(), last_refresh: {}, last_attempt: {} };
}
@@ -106,8 +128,8 @@ function loadMeta(scope: 'cross-project' | 'per-project', projectSlug: string |
}
}
function saveMeta(scope: 'cross-project' | 'per-project', projectSlug: string | null, meta: CacheMeta): void {
const path = metaPath(scope, projectSlug);
function saveMeta(scope: 'cross-project' | 'per-project', project: ProjectRef, meta: CacheMeta): void {
const path = metaPath(scope, project);
mkdirSync(dirname(path), { recursive: true });
atomicWrite(path, JSON.stringify(meta, null, 2));
}
@@ -128,17 +150,8 @@ function sha8(input: string): string {
* (different endpoint → different cache).
*/
export function detectEndpointHash(): string {
const claudeJsonPath = join(homedir(), '.claude.json');
if (existsSync(claudeJsonPath)) {
try {
const cfg = JSON.parse(readFileSync(claudeJsonPath, 'utf-8'));
const gbrainServer = cfg?.mcpServers?.gbrain;
const url = gbrainServer?.url || gbrainServer?.transport?.url;
if (typeof url === 'string' && url.length > 0) {
return sha8(url);
}
} catch { /* fall through to local */ }
}
const endpoint = process.env.GSTACK_GBRAIN_ENDPOINT || process.env.GBRAIN_URL;
if (typeof endpoint === 'string' && endpoint.length > 0) return sha8(endpoint);
// Local engine — no endpoint URL; use a stable literal hash.
return 'local';
}
@@ -168,8 +181,8 @@ function isStale(entityName: string, meta: CacheMeta): boolean {
}
/** Returns true if the cache file exists on disk. */
function hasFile(entityName: string, projectSlug: string | null): boolean {
return existsSync(entityPath(entityName, projectSlug));
function hasFile(entityName: string, project: ProjectRef): boolean {
return existsSync(entityPath(entityName, project));
}
/** Returns true if schema version recorded in meta differs from current pack version. */
@@ -195,39 +208,39 @@ interface GetResult {
message?: string;
}
export function cmdGet(entityName: string, projectSlug: string | null): GetResult {
export function cmdGet(entityName: string, project: ProjectRef): GetResult {
const entity = BRAIN_CACHE_ENTITIES[entityName];
if (!entity) throw new Error(`Unknown entity: ${entityName}`);
const scope = entity.scope;
const meta = loadMeta(scope, projectSlug);
const meta = loadMeta(scope, project);
// Schema-version mismatch → full rebuild (D4 A4).
if (schemaVersionMismatch(meta) || endpointSwitched(meta)) {
rebuildAllForScope(scope, projectSlug);
rebuildAllForScope(scope, project);
// After rebuild, meta is fresh; fall through to warm path.
const newMeta = loadMeta(scope, projectSlug);
if (hasFile(entityName, projectSlug) && !isStale(entityName, newMeta)) {
return { path: entityPath(entityName, projectSlug), state: 'warm' };
const newMeta = loadMeta(scope, project);
if (hasFile(entityName, project) && !isStale(entityName, newMeta)) {
return { path: entityPath(entityName, project), state: 'warm' };
}
// Rebuild may have failed for this entity specifically.
return { path: entityPath(entityName, projectSlug), state: 'missing', message: 'rebuild after schema/endpoint change' };
return { path: entityPath(entityName, project), state: 'missing', message: 'rebuild after schema/endpoint change' };
}
if (hasFile(entityName, projectSlug) && !isStale(entityName, meta)) {
return { path: entityPath(entityName, projectSlug), state: 'warm' };
if (hasFile(entityName, project) && !isStale(entityName, meta)) {
return { path: entityPath(entityName, project), state: 'warm' };
}
// Stale or missing — try cold refresh.
const refreshed = refreshEntity(entityName, projectSlug);
const refreshed = refreshEntity(entityName, project);
if (refreshed) {
return { path: entityPath(entityName, projectSlug), state: 'cold-refreshed' };
return { path: entityPath(entityName, project), state: 'cold-refreshed' };
}
// Refresh failed. Use stale-but-usable if file exists.
if (hasFile(entityName, projectSlug)) {
return { path: entityPath(entityName, projectSlug), state: 'stale-fallback', message: 'brain unreachable; using stale cache' };
if (hasFile(entityName, project)) {
return { path: entityPath(entityName, project), state: 'stale-fallback', message: 'brain unreachable; using stale cache' };
}
// No cache and no refresh = missing.
return { path: entityPath(entityName, projectSlug), state: 'missing', message: 'brain unreachable; no cache available' };
return { path: entityPath(entityName, project), state: 'missing', message: 'brain unreachable; no cache available' };
}
// ──────────────────────────────────────────────────────────────────────────
@@ -244,9 +257,10 @@ export function cmdGet(entityName: string, projectSlug: string | null): GetResul
* concurrent attempts from different projects on cross-project entities
* serialize naturally because they're rare and the lock window is short.
*/
function lockPath(projectSlug: string | null): string {
const dir = projectSlug
? join(GSTACK_HOME, 'projects', projectSlug, 'brain-cache')
function lockPath(project: ProjectRef): string {
const stateId = projectStateId(project);
const dir = stateId
? join(GSTACK_HOME, 'projects', stateId, 'brain-cache')
: join(GSTACK_HOME, 'brain-cache');
return join(dir, '.refresh.lock');
}
@@ -261,8 +275,8 @@ interface LockHandle {
* (and the lock is fresh). Stale locks (process dead OR older than the
* timeout) are taken over.
*/
function tryAcquireLock(projectSlug: string | null): LockHandle | null {
const path = lockPath(projectSlug);
function tryAcquireLock(project: ProjectRef): LockHandle | null {
const path = lockPath(project);
mkdirSync(dirname(path), { recursive: true });
// If a lock exists, see if it's stale
@@ -325,8 +339,8 @@ function isPidAlive(pid: number): boolean {
* (the resolver does this) or fall through to stale-but-usable. Stale locks
* (process dead, or older than CACHE_REFRESH_LOCK_TIMEOUT_MS) are taken over.
*/
export function withRefreshLock<T>(projectSlug: string | null, fn: () => T): T | 'dedup' {
const handle = tryAcquireLock(projectSlug);
export function withRefreshLock<T>(project: ProjectRef, fn: () => T): T | 'dedup' {
const handle = tryAcquireLock(project);
if (!handle) return 'dedup';
try {
return fn();
@@ -336,12 +350,12 @@ export function withRefreshLock<T>(projectSlug: string | null, fn: () => T): T |
}
/** Refreshes one entity from the brain. Returns true on success. */
export function refreshEntity(entityName: string, projectSlug: string | null): boolean {
export function refreshEntity(entityName: string, project: ProjectRef): boolean {
const entity = BRAIN_CACHE_ENTITIES[entityName];
if (!entity) return false;
// Mark attempt
const meta = loadMeta(entity.scope, projectSlug);
const meta = loadMeta(entity.scope, project);
meta.last_attempt = meta.last_attempt || {};
meta.last_attempt[entityName] = Date.now();
@@ -349,9 +363,9 @@ export function refreshEntity(entityName: string, projectSlug: string | null): b
// (recent-decisions, salience) need different queries from direct page reads.
// For T2a we implement the direct-page path; derived digests get filled in by
// the resolver / write-back paths in later commits.
const digestContent = fetchAndCompressEntity(entityName, projectSlug);
const digestContent = fetchAndCompressEntity(entityName, project);
if (digestContent === null) {
saveMeta(entity.scope, projectSlug, meta);
saveMeta(entity.scope, project, meta);
return false;
}
@@ -363,12 +377,12 @@ export function refreshEntity(entityName: string, projectSlug: string | null): b
final = truncateToBudget(final, entity.budget_bytes);
}
atomicWrite(entityPath(entityName, projectSlug), final);
atomicWrite(entityPath(entityName, project), final);
meta.last_refresh[entityName] = Date.now();
// Keep schema/endpoint identity fresh.
meta.schema_version = GSTACK_SCHEMA_PACK_VERSION;
meta.endpoint_hash = detectEndpointHash();
saveMeta(entity.scope, projectSlug, meta);
saveMeta(entity.scope, project, meta);
return true;
}
@@ -376,24 +390,24 @@ export function refreshEntity(entityName: string, projectSlug: string | null): b
* Refresh all entities for a scope (per-project or cross-project).
* Used by --full and by schema/endpoint-change rebuilds.
*/
export function refreshAll(projectSlug: string | null): { success: number; failed: number } {
export function refreshAll(project: ProjectRef): { success: number; failed: number } {
let success = 0;
let failed = 0;
for (const [name, entity] of Object.entries(BRAIN_CACHE_ENTITIES)) {
// Cross-project entities only refresh when explicitly targeted via no-slug calls
if (entity.scope === 'cross-project' && projectSlug) continue;
if (entity.scope === 'per-project' && !projectSlug) continue;
if (refreshEntity(name, projectSlug)) success++; else failed++;
if (entity.scope === 'cross-project' && project) continue;
if (entity.scope === 'per-project' && !project) continue;
if (refreshEntity(name, project)) success++; else failed++;
}
return { success, failed };
}
/** Rebuild on schema-version mismatch or endpoint switch. Wipes affected scope first. */
function rebuildAllForScope(scope: 'cross-project' | 'per-project', projectSlug: string | null): void {
function rebuildAllForScope(scope: 'cross-project' | 'per-project', project: ProjectRef): void {
// Wipe files but preserve dir; meta gets fully rewritten by refreshes below.
for (const [name, entity] of Object.entries(BRAIN_CACHE_ENTITIES)) {
if (entity.scope !== scope) continue;
const p = entityPath(name, projectSlug);
const p = entityPath(name, project);
if (existsSync(p)) {
try { unlinkSync(p); } catch { /* best effort */ }
}
@@ -405,11 +419,11 @@ function rebuildAllForScope(scope: 'cross-project' | 'per-project', projectSlug:
last_refresh: {},
last_attempt: {},
};
saveMeta(scope, projectSlug, fresh);
saveMeta(scope, project, fresh);
// Refresh all entities in this scope
for (const [name, entity] of Object.entries(BRAIN_CACHE_ENTITIES)) {
if (entity.scope !== scope) continue;
refreshEntity(name, projectSlug);
refreshEntity(name, project);
}
}
@@ -417,12 +431,12 @@ function rebuildAllForScope(scope: 'cross-project' | 'per-project', projectSlug:
// Subcommand: invalidate
// ──────────────────────────────────────────────────────────────────────────
export function cmdInvalidate(entityName: string, projectSlug: string | null): void {
export function cmdInvalidate(entityName: string, project: ProjectRef): void {
const entity = BRAIN_CACHE_ENTITIES[entityName];
if (!entity) throw new Error(`Unknown entity: ${entityName}`);
const meta = loadMeta(entity.scope, projectSlug);
const meta = loadMeta(entity.scope, project);
delete meta.last_refresh[entityName];
saveMeta(entity.scope, projectSlug, meta);
saveMeta(entity.scope, project, meta);
}
// ──────────────────────────────────────────────────────────────────────────
@@ -436,7 +450,8 @@ export function cmdInvalidate(entityName: string, projectSlug: string | null): v
* For T2a we implement the entity → page-slug mapping for the simple cases.
* Derived digests (recent-decisions, salience) get specialized paths.
*/
function fetchAndCompressEntity(entityName: string, projectSlug: string | null): string | null {
function fetchAndCompressEntity(entityName: string, project: ProjectRef): string | null {
const projectSlug = projectNamespace(project);
switch (entityName) {
case 'user-profile':
return fetchUserProfile();
@@ -541,8 +556,7 @@ export function getSalienceAllowlist(): ReadonlyArray<string> {
// Shell out to gstack-config with a tight timeout. Falls back to defaults
// on any failure (config script missing, command non-zero, parse error).
try {
const skillRoot = join(homedir(), '.claude', 'skills', 'gstack');
const bin = join(skillRoot, 'bin', 'gstack-config');
const bin = join(process.env.GSTACK_BIN || join(GSTACK_HOME, 'bin'), 'gstack-config');
if (!existsSync(bin)) return SALIENCE_DEFAULT_ALLOWLIST;
const result = spawnSync(bin, ['get', 'salience_allowlist'], { timeout: 2000, encoding: 'utf-8' });
if (result.status !== 0 || !result.stdout) return SALIENCE_DEFAULT_ALLOWLIST;
@@ -639,8 +653,8 @@ export function cmdDigest(slug: string): string | null {
// Subcommand: meta
// ──────────────────────────────────────────────────────────────────────────
export function cmdMeta(projectSlug: string | null): CacheMeta {
if (projectSlug) return loadMeta('per-project', projectSlug);
export function cmdMeta(project: ProjectRef): CacheMeta {
if (project) return loadMeta('per-project', project);
return loadMeta('cross-project', null);
}
@@ -665,8 +679,10 @@ export interface BootstrapDraft {
competitive_intel?: { slug: string; title: string; body: string };
}
export function cmdBootstrap(projectSlug: string): BootstrapDraft {
export function cmdBootstrap(project: Exclude<ProjectRef, null>): BootstrapDraft {
const draft: BootstrapDraft = {};
const projectSlug = projectNamespace(project) as string;
const stateId = projectStateId(project) as string;
const repoRoot = process.env.GSTACK_REPO_ROOT || process.cwd();
// Product synthesis: CLAUDE.md headline + README first paragraph
@@ -685,7 +701,7 @@ export function cmdBootstrap(projectSlug: string): BootstrapDraft {
}
// Goals: try learnings.jsonl + recent commit messages mentioning "goal" or "ship"
const learningsPath = join(GSTACK_HOME, 'projects', projectSlug, 'learnings.jsonl');
const learningsPath = join(GSTACK_HOME, 'projects', stateId, 'learnings.jsonl');
const goalsHints = synthesizeGoalsHints(learningsPath, repoRoot);
if (goalsHints.length > 0) {
draft.goals = goalsHints.slice(0, 3).map((hint, idx) => ({
@@ -757,7 +773,8 @@ function synthesizeGoalsHints(learningsPath: string, repoRoot: string): Array<{
* Lists all gstack-owned pages currently in the brain for a project, grouped
* by type. Powers the user's ability to audit what gstack has written.
*/
export function cmdList(projectSlug: string | null): Array<{ type: string; slug: string; title?: string }> {
export function cmdList(project: ProjectRef): Array<{ type: string; slug: string; title?: string }> {
const projectSlug = projectNamespace(project);
// We probe each gstack/<type>/ namespace via list-pages with a type filter.
const types = ['gstack/user-profile', 'gstack/product', 'gstack/goal', 'gstack/developer-persona', 'gstack/brand', 'gstack/competitive-intel', 'gstack/skill-run', 'gstack/take'];
const all: Array<{ type: string; slug: string; title?: string }> = [];
@@ -827,9 +844,15 @@ function parseArgs(argv: string[]): { cmd: string; positional: string[]; flags:
return { cmd, positional, flags };
}
function projectSlugFromFlag(flags: Record<string, string | boolean>): string | null {
async function projectTargetFromFlag(flags: Record<string, string | boolean>): Promise<ProjectRef> {
const v = flags.project;
return typeof v === 'string' ? v : null;
if (typeof v !== 'string') return null;
// --project is the human namespace for GBrain pages. Local cache placement
// always follows the checkout in which the command runs; callers inspecting
// another checkout should run there, which also avoids path-key injection.
const identity = await discoverProjectIdentity();
return { slug: v, stateId: identity.projectId };
}
function printUsage(): void {
@@ -849,14 +872,14 @@ Subcommands:
async function main(): Promise<number> {
const { cmd, positional, flags } = parseArgs(process.argv);
const projectSlug = projectSlugFromFlag(flags);
const project = await projectTargetFromFlag(flags);
try {
switch (cmd) {
case 'get': {
const entityName = positional[0];
if (!entityName) { printUsage(); return 1; }
const result = cmdGet(entityName, projectSlug);
const result = cmdGet(entityName, project);
if (result.state === 'missing') {
process.stderr.write(`(${result.state}: ${result.message ?? 'no cache'})\n`);
return 2;
@@ -872,7 +895,7 @@ async function main(): Promise<number> {
// another process is already mid-refresh on the same project.
if (flags.entity) {
const entityName = String(flags.entity);
const result = withRefreshLock(projectSlug, () => refreshEntity(entityName, projectSlug));
const result = withRefreshLock(project, () => refreshEntity(entityName, project));
if (result === 'dedup') {
process.stderr.write(`(dedup: another refresh in flight)\n`);
return 3;
@@ -880,7 +903,7 @@ async function main(): Promise<number> {
process.stdout.write(result ? `refreshed ${entityName}\n` : `failed to refresh ${entityName}\n`);
return result ? 0 : 1;
}
const allResult = withRefreshLock(projectSlug, () => refreshAll(projectSlug));
const allResult = withRefreshLock(project, () => refreshAll(project));
if (allResult === 'dedup') {
process.stderr.write(`(dedup: another refresh in flight)\n`);
return 3;
@@ -891,7 +914,7 @@ async function main(): Promise<number> {
case 'invalidate': {
const entityName = positional[0];
if (!entityName) { printUsage(); return 1; }
cmdInvalidate(entityName, projectSlug);
cmdInvalidate(entityName, project);
process.stdout.write(`invalidated ${entityName}\n`);
return 0;
}
@@ -907,21 +930,21 @@ async function main(): Promise<number> {
return 0;
}
case 'meta': {
const meta = cmdMeta(projectSlug);
const meta = cmdMeta(project);
process.stdout.write(JSON.stringify(meta, null, 2) + '\n');
return 0;
}
case 'bootstrap': {
if (!projectSlug) {
if (!project) {
process.stderr.write('bootstrap requires --project <slug>\n');
return 1;
}
const draft = cmdBootstrap(projectSlug);
const draft = cmdBootstrap(project);
process.stdout.write(JSON.stringify(draft, null, 2) + '\n');
return 0;
}
case 'list': {
const pages = cmdList(projectSlug);
const pages = cmdList(project);
if (flags.json) {
process.stdout.write(JSON.stringify(pages, null, 2) + '\n');
} else {
+7 -3
View File
@@ -78,12 +78,13 @@ _gstack_codex_log_event() {
local _event="$1"
local _duration="${2:-0}"
[ "${_TEL:-off}" = "off" ] && return 0
mkdir -p "$HOME/.gstack/analytics" 2>/dev/null || return 0
local _state_home="${GSTACK_HOME:-$HOME/.gstack}"
mkdir -p "$_state_home/analytics" 2>/dev/null || return 0
local _ts
_ts=$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo unknown)
printf '{"skill":"codex","event":"%s","duration_s":"%s","ts":"%s"}\n' \
"$_event" "$_duration" "$_ts" \
>> "$HOME/.gstack/analytics/skill-usage.jsonl" 2>/dev/null || true
>> "$_state_home/analytics/skill-usage.jsonl" 2>/dev/null || true
}
# --- Learnings log on hang --------------------------------------------------
@@ -94,7 +95,10 @@ _gstack_codex_log_hang() {
# Best-effort: errors swallowed.
local _mode="${1:-unknown}"
local _prompt_size="${2:-0}"
local _log_bin="$HOME/.claude/skills/gstack/bin/gstack-learnings-log"
local _log_bin="${GSTACK_BIN:-}/gstack-learnings-log"
if [ ! -x "$_log_bin" ]; then
_log_bin=$(command -v gstack-learnings-log 2>/dev/null || true)
fi
[ -x "$_log_bin" ] || return 0
local _key="codex-hang-$(date +%s 2>/dev/null || echo unknown)"
"$_log_bin" "$(printf '{"skill":"codex","type":"operational","key":"%s","insight":"Codex timed out after 600s during [%s] invocation. Prompt size: %s. Consider splitting prompt or checking network.","confidence":8,"source":"observed","files":["codex/SKILL.md.tmpl","autoplan/SKILL.md.tmpl"]}' "$_key" "$_mode" "$_prompt_size")" \
+232 -435
View File
@@ -1,451 +1,248 @@
#!/usr/bin/env bash
# gstack-config — read/write ~/.gstack/config.yaml
#
# Usage:
# gstack-config get <key> — read a config value (falls back to DEFAULTS)
# gstack-config set <key> <value> — write a config value
# gstack-config list — show all config (values + defaults)
# gstack-config defaults — show just the defaults table
#
# Env overrides (for testing):
# GSTACK_STATE_ROOT — override ~/.gstack state directory (highest priority,
# matches D16 cathedral isolation convention)
# GSTACK_HOME — override ~/.gstack state directory (aligns with writer scripts)
# GSTACK_STATE_DIR — legacy alias for GSTACK_HOME (kept for backwards compat)
set -euo pipefail
#!/usr/bin/env node
// Compatibility adapter for preserved specialist modules.
// config.json is the only writable config authority. A legacy config.yaml may
// be read as a migration fallback, but this command never writes YAML.
STATE_DIR="${GSTACK_STATE_ROOT:-${GSTACK_HOME:-${GSTACK_STATE_DIR:-$HOME/.gstack}}}"
CONFIG_FILE="$STATE_DIR/config.yaml"
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { createHash } from "node:crypto";
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import {
configGet,
configSet,
ensureConfig,
loadConfig,
parseConfigValue,
readLegacyConfig,
} from "../runtime/config.js";
import { ensureManagedHome, withRuntimeLifecycleLock } from "../runtime/managed-home.js";
# Annotated header for new config files. Written once on first `set`.
# Default semantics: DEFAULTS table below is the canonical source. Header text
# is documentation that must stay in sync with DEFAULTS.
CONFIG_HEADER='# gstack configuration — edit freely, changes take effect on next skill run.
# Docs: https://github.com/garrytan/gstack
#
# ─── Behavior ────────────────────────────────────────────────────────
# proactive: true # Auto-invoke skills when your request matches one.
# # Set to false to only run skills you type explicitly.
#
# routing_declined: false # Set to true to skip the CLAUDE.md routing injection
# # prompt. Set back to false to be asked again.
#
# ─── Telemetry ───────────────────────────────────────────────────────
# telemetry: off # off | anonymous | community
# # off — no data sent, no local analytics (default)
# # anonymous — counter only, no device ID
# # community — usage data + stable device ID
#
# ─── Updates ─────────────────────────────────────────────────────────
# auto_upgrade: false # true = silently upgrade on session start
# update_check: true # false = suppress version check notifications
#
# ─── Skill naming ────────────────────────────────────────────────────
# skill_prefix: false # true = namespace skills as /gstack-qa, /gstack-ship
# # false = short names /qa, /ship
#
# ─── Checkpoint ──────────────────────────────────────────────────────
# checkpoint_mode: explicit # explicit | continuous
# # explicit — commit only when you run /ship or /checkpoint
# # continuous — auto-commit after each significant change
# # with WIP: prefix + [gstack-context] body
#
# checkpoint_push: false # true = push WIP commits to remote as you go
# # false = keep WIP commits local only (default)
# # Pushing can trigger CI/deploy hooks — opt in carefully.
#
# ─── Writing style (V1) ──────────────────────────────────────────────
# explain_level: default # default = jargon-glossed, outcome-framed prose
# # (V1 default — more accessible for everyone)
# # terse = V0 prose style, no glosses, no outcome-framing layer
# # (for power users who know the terms)
# # Unknown values default to "default" with a warning.
# # See docs/designs/PLAN_TUNING_V1.md for rationale.
#
# ─── Artifacts sync (renamed from gbrain_sync_mode in v1.27.0.0) ─────
# artifacts_sync_mode: off # off | artifacts-only | full
# # off — no sync (default)
# # artifacts-only — sync plans/designs/retros/learnings only
# # (skip behavioral data: question-log,
# # developer-profile, timeline)
# # full — sync everything allowlisted
# # Set by the first-run privacy stop-gate. See docs/gbrain-sync.md.
#
# artifacts_sync_mode_prompted: false
# # Set to true once the privacy gate has asked the user.
# # Flip back to false to be re-prompted.
#
# ─── Plan-tune hooks ─────────────────────────────────────────────────
# plan_tune_hooks: prompt # Controls whether ./setup installs the plan-tune
# # Claude Code hooks (PostToolUse capture +
# # PreToolUse preference enforcement).
# # prompt — ask on a real TTY, skip otherwise (default)
# # yes — install non-interactively
# # no — skip non-interactively
# # Override per-run: ./setup --plan-tune-hooks /
# # --no-plan-tune-hooks, or env GSTACK_PLAN_TUNE_HOOKS.
#
# ─── Advanced ────────────────────────────────────────────────────────
# 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)
#
# ─── Workspace-aware ship ────────────────────────────────────────────
# workspace_root: $HOME/conductor/workspaces # Where /ship looks for sibling
# # Conductor worktrees when picking a VERSION slot.
# # Set to "null" to disable sibling scanning entirely.
# # Non-Conductor users can point this at any directory
# # that holds parallel worktrees of the same repo.
#
'
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const env = process.env.GSTACK_HOME
? process.env
: process.env.GSTACK_STATE_ROOT
? { ...process.env, GSTACK_HOME: process.env.GSTACK_STATE_ROOT }
: process.env.GSTACK_STATE_DIR
? { ...process.env, GSTACK_HOME: process.env.GSTACK_STATE_DIR }
: process.env;
const home = path.resolve(env.GSTACK_HOME || path.join(os.homedir(), ".gstack"));
const legacyConfig = path.join(home, "config.yaml");
const defaults = Object.freeze({
proactive: true,
routing_declined: false,
telemetry: "off",
auto_upgrade: false,
update_check: true,
skill_prefix: false,
checkpoint_mode: "explicit",
checkpoint_push: false,
explain_level: "default",
codex_reviews: "enabled",
gstack_contributor: false,
skip_eng_review: false,
workspace_root: path.join(os.homedir(), "conductor", "workspaces"),
cross_project_learnings: "",
artifacts_sync_mode: "off",
artifacts_sync_mode_prompted: false,
plan_tune_hooks: "prompt",
redact_repo_visibility: "",
redact_prepush_hook: false,
salience_allowlist: "",
});
# DEFAULTS table — canonical default values for known keys.
# `get <key>` returns DEFAULTS[key] when the key is absent from the config file
# AND the env override is not set. Keep in sync with the CONFIG_HEADER comments.
lookup_default() {
case "$1" in
proactive) echo "true" ;;
routing_declined) echo "false" ;;
telemetry) echo "off" ;;
auto_upgrade) echo "false" ;;
update_check) echo "true" ;;
skill_prefix) echo "false" ;;
checkpoint_mode) echo "explicit" ;;
checkpoint_push) echo "false" ;;
explain_level) echo "default" ;;
codex_reviews) echo "enabled" ;;
gstack_contributor) echo "false" ;;
skip_eng_review) echo "false" ;;
workspace_root) echo "$HOME/conductor/workspaces" ;;
cross_project_learnings) echo "" ;; # intentionally empty → unset triggers first-time prompt
artifacts_sync_mode) echo "off" ;;
artifacts_sync_mode_prompted) echo "false" ;;
plan_tune_hooks) echo "prompt" ;; # prompt | yes | no — controls ./setup plan-tune hook install
const [command, ...args] = process.argv.slice(2);
redact_repo_visibility) echo "" ;; # empty → fall through to gh/glab detection
redact_prepush_hook) echo "false" ;;
# Brain-aware planning (v1.48 / T5+T10+T16). Defaults documented inline:
# brain_trust_policy@<hash> — unset on fresh install; setup-gbrain
# writes 'personal' for local engines,
# asks the user for remote-ambiguous.
# salience_allowlist — empty falls through to
# SALIENCE_DEFAULT_ALLOWLIST (D9).
# user_slug_at_<hash> — empty triggers resolve-user-slug
# fallback chain (D4 A3) on first call.
brain_trust_policy*) echo "unset" ;;
salience_allowlist) echo "" ;;
user_slug_at_*) echo "" ;;
*) echo "" ;;
esac
try {
switch (command) {
case "get":
await getCommand(args);
break;
case "set":
await setCommand(args);
break;
case "list":
await listCommand();
break;
case "defaults":
printEntries(defaults);
break;
case "endpoint-hash":
process.stdout.write(endpointHash());
break;
case "resolve-user-slug":
await resolveUserSlug();
break;
case "gbrain-refresh":
await refreshGbrainDetection();
break;
default:
usage();
process.exitCode = 1;
}
} catch (error) {
process.stderr.write(`gstack-config: ${error?.message ?? error}\n`);
process.exitCode = 1;
}
# ──────────────────────────────────────────────────────────────────────
# Brain-integration helpers (T5+T10+T16)
# ──────────────────────────────────────────────────────────────────────
# Compute sha8 of a string. Used for endpoint hashing.
sha8_of() {
printf '%s' "$1" | shasum -a 256 | cut -c1-8
async function getCommand(args) {
if (args.length !== 1) throw new Error("Usage: gstack-config get <key>");
const key = validateKey(args[0]);
let value;
if (await exists(path.join(home, "config.json"))) value = await configGet(home, key);
if (value === undefined && await exists(legacyConfig)) value = await readLegacyValue(key);
if (value === undefined) value = defaultFor(key);
process.stdout.write(formatValue(value));
}
# Detect the active brain endpoint hash. Reads ~/.claude.json for the gbrain
# MCP server URL. Falls back to the literal 'local' when no MCP is configured.
endpoint_hash() {
_claude_json="$HOME/.claude.json"
if [ -f "$_claude_json" ] && command -v jq >/dev/null 2>&1; then
_url=$(jq -r '.mcpServers.gbrain.url // .mcpServers.gbrain.transport.url // empty' "$_claude_json" 2>/dev/null)
if [ -n "$_url" ] && [ "$_url" != "null" ]; then
sha8_of "$_url"
return 0
fi
fi
printf '%s' "local"
async function setCommand(args) {
if (args.length !== 2) throw new Error("Usage: gstack-config set <key> <value>");
const key = validateKey(args[0]);
const raw = validateClosedValue(key, args[1]);
await mutateConfigHome(() => configSet(home, key, parseConfigValue(raw)));
}
# Detect endpoint hash collisions. When two distinct endpoints share the same
# sha8 prefix (rare but possible), escalate to sha16 by emitting the longer
# hash. Detection: scan config file for existing brain_trust_policy@<hash> or
# user_slug_at_<hash> keys; if any non-active hash equals the active sha8 but
# would differ at sha16, the active endpoint needs sha16.
endpoint_hash_with_collision_check() {
_active=$(endpoint_hash)
if [ "$_active" = "local" ]; then
printf '%s' "$_active"
return 0
fi
# If a different endpoint (different URL) shares this sha8, escalate.
# We only catch this when the config has another endpoint recorded.
_matching=$(grep -E "^(brain_trust_policy|user_slug_at)@${_active}" "$CONFIG_FILE" 2>/dev/null | head -1 || true)
_claude_json="$HOME/.claude.json"
if [ -n "$_matching" ] && [ -f "$_claude_json" ] && command -v jq >/dev/null 2>&1; then
_url=$(jq -r '.mcpServers.gbrain.url // .mcpServers.gbrain.transport.url // empty' "$_claude_json" 2>/dev/null)
_sha16=$(printf '%s' "$_url" | shasum -a 256 | cut -c1-16)
# Look for any sha16-namespaced key that conflicts. If a stored sha16 exists
# and differs from current sha16, that's the collision evidence; emit sha16.
_stored16=$(grep -E "^(brain_trust_policy|user_slug_at)@${_sha16}" "$CONFIG_FILE" 2>/dev/null | head -1 || true)
if [ -n "$_stored16" ]; then
printf '%s' "$_sha16"
return 0
fi
fi
printf '%s' "$_active"
async function listCommand() {
const stored = await exists(path.join(home, "config.json"))
? await loadConfig(home)
: await readLegacyConfig(home);
const flattened = { ...defaults, ...flatten(stored) };
printEntries(flattened);
}
# Resolve the user-slug per D4 A3 chain:
# 1. mcp__gbrain__whoami.client_name (best effort via gbrain CLI shell-out)
# 2. $USER env
# 3. sha8($(git config user.email))
# 4. anonymous-<sha8(hostname)>
# Persists result via gstack-config set user_slug_at_<endpoint-hash> on first call.
resolve_user_slug() {
_hash=$(endpoint_hash_with_collision_check)
_stored=$(grep -E "^user_slug_at_${_hash}:" "$CONFIG_FILE" 2>/dev/null | tail -1 | awk '{print $2}' | tr -d '[:space:]' || true)
if [ -n "$_stored" ]; then
printf '%s' "$_stored"
return 0
fi
_slug=""
# Layer 1: gbrain whoami
if command -v gbrain >/dev/null 2>&1; then
_whoami=$(gbrain whoami --json 2>/dev/null || true)
if [ -n "$_whoami" ] && command -v jq >/dev/null 2>&1; then
_client_name=$(printf '%s' "$_whoami" | jq -r '.client_name // .token_name // empty' 2>/dev/null || true)
if [ -n "$_client_name" ] && [ "$_client_name" != "null" ]; then
_slug=$(printf '%s' "$_client_name" | tr '[:upper:] ' '[:lower:]-' | tr -dc '[:alnum:]-')
fi
fi
fi
# Layer 2: $USER
if [ -z "$_slug" ] && [ -n "${USER:-}" ]; then
_slug=$(printf '%s' "$USER" | tr '[:upper:] ' '[:lower:]-' | tr -dc '[:alnum:]-')
fi
# Layer 3: sha8 of git email
if [ -z "$_slug" ]; then
_email=$(git config user.email 2>/dev/null || true)
if [ -n "$_email" ]; then
_slug="email-$(sha8_of "$_email")"
fi
fi
# Layer 4: anonymous-<sha8(hostname)>
if [ -z "$_slug" ]; then
_slug="anonymous-$(sha8_of "$(hostname 2>/dev/null || echo unknown)")"
fi
# Persist via direct file write (avoid recursion into gstack-config set)
mkdir -p "$STATE_DIR"
if [ ! -f "$CONFIG_FILE" ]; then
printf '%s' "$CONFIG_HEADER" > "$CONFIG_FILE"
fi
if ! grep -qE "^user_slug_at_${_hash}:" "$CONFIG_FILE" 2>/dev/null; then
echo "user_slug_at_${_hash}: ${_slug}" >> "$CONFIG_FILE"
fi
printf '%s' "$_slug"
function defaultFor(key) {
if (/^brain_trust_policy(?:@|$)/.test(key)) return "unset";
return Object.hasOwn(defaults, key) ? defaults[key] : "";
}
case "${1:-}" in
get)
KEY="${2:?Usage: gstack-config get <key>}"
# Validate key (alphanumeric + underscore + optional @<hash> suffix for
# endpoint-namespaced keys introduced by the brain-aware planning layer)
if ! printf '%s' "$KEY" | grep -qE '^[a-zA-Z0-9_]+(@[a-f0-9]+)?$'; then
echo "Error: key must contain only alphanumeric characters, underscores, and an optional @<hex-hash> suffix" >&2
exit 1
fi
# Use literal match for keys containing @ (sha hashes), regex otherwise
VALUE=$(grep -F "${KEY}:" "$CONFIG_FILE" 2>/dev/null | grep -E "^${KEY%@*}(@[a-f0-9]+)?:" | grep -F "${KEY}:" | tail -1 | awk '{print $2}' | tr -d '[:space:]' || true)
if [ -z "$VALUE" ]; then
VALUE=$(lookup_default "$KEY")
fi
printf '%s' "$VALUE"
;;
set)
KEY="${2:?Usage: gstack-config set <key> <value>}"
VALUE="${3:?Usage: gstack-config set <key> <value>}"
# Validate key (alphanumeric + underscore + optional @<hash> suffix)
if ! printf '%s' "$KEY" | grep -qE '^[a-zA-Z0-9_]+(@[a-f0-9]+)?$'; then
echo "Error: key must contain only alphanumeric characters, underscores, and an optional @<hex-hash> suffix" >&2
exit 1
fi
# Validate brain_trust_policy value domain (D4 / D11)
if printf '%s' "$KEY" | grep -qE '^brain_trust_policy(@|$)' && \
[ "$VALUE" != "personal" ] && [ "$VALUE" != "shared" ] && [ "$VALUE" != "unset" ]; then
echo "Warning: brain_trust_policy '$VALUE' not recognized. Valid values: personal, shared, unset. Using unset." >&2
VALUE="unset"
fi
# V1: whitelist values for keys with closed value domains. Unknown values warn + default.
if [ "$KEY" = "explain_level" ] && [ "$VALUE" != "default" ] && [ "$VALUE" != "terse" ]; then
echo "Warning: explain_level '$VALUE' not recognized. Valid values: default, terse. Using default." >&2
VALUE="default"
fi
if [ "$KEY" = "artifacts_sync_mode" ] && [ "$VALUE" != "off" ] && [ "$VALUE" != "artifacts-only" ] && [ "$VALUE" != "full" ]; then
echo "Warning: artifacts_sync_mode '$VALUE' not recognized. Valid values: off, artifacts-only, full. Using off." >&2
VALUE="off"
fi
# redact_repo_visibility: a LOCAL override for repos gh/glab can't read (e.g.
# self-hosted GitLab). It lives in ~/.gstack/config.yaml (never committed), so
# it can't be used to weaken the gate repo-wide for other contributors.
if [ "$KEY" = "redact_repo_visibility" ] && [ "$VALUE" != "public" ] && [ "$VALUE" != "private" ] && [ "$VALUE" != "unknown" ]; then
echo "Warning: redact_repo_visibility '$VALUE' not recognized. Valid values: public, private, unknown. Using unknown." >&2
VALUE="unknown"
fi
if [ "$KEY" = "redact_prepush_hook" ] && [ "$VALUE" != "true" ] && [ "$VALUE" != "false" ]; then
echo "Warning: redact_prepush_hook '$VALUE' not recognized. Valid values: true, false. Using false." >&2
VALUE="false"
fi
if [ "$KEY" = "plan_tune_hooks" ] && [ "$VALUE" != "prompt" ] && [ "$VALUE" != "yes" ] && [ "$VALUE" != "no" ]; then
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
printf '%s' "$CONFIG_HEADER" > "$CONFIG_FILE"
fi
# Escape sed special chars in value and drop embedded newlines
ESC_VALUE="$(printf '%s' "$VALUE" | head -1 | sed 's/[&/\]/\\&/g')"
if grep -qE "^${KEY}:" "$CONFIG_FILE" 2>/dev/null; then
# Portable in-place edit (BSD sed uses -i '', GNU sed uses -i without arg)
_tmpfile="$(mktemp "${CONFIG_FILE}.XXXXXX")"
sed "/^${KEY}:/s/.*/${KEY}: ${ESC_VALUE}/" "$CONFIG_FILE" > "$_tmpfile" && mv "$_tmpfile" "$CONFIG_FILE"
else
echo "${KEY}: ${VALUE}" >> "$CONFIG_FILE"
fi
# Auto-relink skills when prefix setting changes (skip during setup to avoid recursive call)
if [ "$KEY" = "skill_prefix" ] && [ -z "${GSTACK_SETUP_RUNNING:-}" ]; then
GSTACK_RELINK="$(dirname "$0")/gstack-relink"
[ -x "$GSTACK_RELINK" ] && "$GSTACK_RELINK" || true
fi
;;
list)
if [ -f "$CONFIG_FILE" ]; then
cat "$CONFIG_FILE"
fi
echo ""
echo "# ─── Active values (including defaults for unset keys) ───"
for KEY in proactive routing_declined telemetry auto_upgrade update_check \
skill_prefix checkpoint_mode checkpoint_push explain_level \
codex_reviews gstack_contributor skip_eng_review workspace_root \
artifacts_sync_mode artifacts_sync_mode_prompted plan_tune_hooks; do
VALUE=$(grep -E "^${KEY}:" "$CONFIG_FILE" 2>/dev/null | tail -1 | awk '{print $2}' | tr -d '[:space:]' || true)
SOURCE="default"
if [ -n "$VALUE" ]; then
SOURCE="set"
else
VALUE=$(lookup_default "$KEY")
fi
printf ' %-24s %s (%s)\n' "$KEY:" "$VALUE" "$SOURCE"
done
;;
defaults)
echo "# gstack-config defaults"
for KEY in proactive routing_declined telemetry auto_upgrade update_check \
skill_prefix checkpoint_mode checkpoint_push explain_level \
codex_reviews gstack_contributor skip_eng_review workspace_root \
artifacts_sync_mode artifacts_sync_mode_prompted plan_tune_hooks; do
printf ' %-24s %s\n' "$KEY:" "$(lookup_default "$KEY")"
done
;;
endpoint-hash)
# Brain integration helper (T10): print active brain endpoint sha8
endpoint_hash_with_collision_check
;;
resolve-user-slug)
# Brain integration helper (T16 / D4 A3): resolve + persist user-slug
resolve_user_slug
;;
gbrain-refresh)
# Brain integration helper: re-detect gbrain installation state and
# persist to ~/.gstack/gbrain-detection.json. gen-skill-docs reads this
# file (when invoked with --respect-detection) to decide whether to
# render GBRAIN_CONTEXT_LOAD and GBRAIN_SAVE_RESULTS blocks in
# generated SKILL.md files.
#
# Run this after installing or uninstalling gbrain so your locally
# generated SKILL.md files match your installation state.
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
DETECT_BIN="$SCRIPT_DIR/gstack-gbrain-detect"
DETECTION_FILE="$STATE_DIR/gbrain-detection.json"
mkdir -p "$STATE_DIR"
if [ ! -x "$DETECT_BIN" ]; then
echo "gstack-gbrain-detect not found at $DETECT_BIN" >&2
exit 1
fi
if ! "$DETECT_BIN" > "$DETECTION_FILE.tmp" 2>/dev/null; then
printf '{"gbrain_on_path":false,"gbrain_local_status":"no-cli"}\n' > "$DETECTION_FILE.tmp"
fi
mv "$DETECTION_FILE.tmp" "$DETECTION_FILE"
function validateKey(key) {
if (typeof key !== "string" || !/^[a-zA-Z0-9_]+(?:@[a-f0-9]+)?$/.test(key)) {
throw new Error("key must contain only alphanumeric characters, underscores, and an optional @<hex-hash> suffix");
}
return key;
}
# Summarize for the user. Use python (already required elsewhere) to
# parse the JSON portably; fall back to grep if python is unavailable.
PYTHON_CMD=$(command -v python3 || command -v python || true)
if [ -n "$PYTHON_CMD" ]; then
STATUS=$("$PYTHON_CMD" -c "import json,sys; d=json.load(open('$DETECTION_FILE')); print(d.get('gbrain_local_status','unknown'))" 2>/dev/null || echo unknown)
VERSION=$("$PYTHON_CMD" -c "import json,sys; d=json.load(open('$DETECTION_FILE')); print(d.get('gbrain_version') or 'unknown')" 2>/dev/null || echo unknown)
else
STATUS=$(grep -o '"gbrain_local_status":[[:space:]]*"[^"]*"' "$DETECTION_FILE" | sed 's/.*"\([^"]*\)"$/\1/')
VERSION=$(grep -o '"gbrain_version":[[:space:]]*"[^"]*"' "$DETECTION_FILE" | sed 's/.*"\([^"]*\)"$/\1/')
[ -z "$STATUS" ] && STATUS=unknown
[ -z "$VERSION" ] && VERSION=unknown
fi
function validateClosedValue(key, value) {
const domains = [
[/^brain_trust_policy(?:@|$)/, ["personal", "shared", "unset"], "unset"],
[/^explain_level$/, ["default", "terse"], "default"],
[/^artifacts_sync_mode$/, ["off", "artifacts-only", "full"], "off"],
[/^redact_repo_visibility$/, ["public", "private", "unknown"], "unknown"],
[/^redact_prepush_hook$/, ["true", "false"], "false"],
[/^plan_tune_hooks$/, ["prompt", "yes", "no"], "prompt"],
];
if (key === "codex_reviews" && !["enabled", "disabled"].includes(value)) {
throw new Error(`codex_reviews '${value}' not recognized. Valid values: enabled, disabled. Existing value left unchanged.`);
}
for (const [pattern, allowed, fallback] of domains) {
if (pattern.test(key) && !allowed.includes(value)) {
process.stderr.write(`Warning: ${key} '${value}' not recognized. Valid values: ${allowed.join(", ")}. Using ${fallback}.\n`);
return fallback;
}
}
return value;
}
case "$STATUS" in
ok|timeout)
# "timeout" = slow-but-healthy engine (#1964) — same treatment as
# "ok", matching gstack-gbrain-detect --is-ok and gen-skill-docs.
echo "Detected gbrain v$VERSION (local-status: $STATUS)."
# 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."
echo "Install gbrain (see /setup-gbrain) and re-run 'gstack-config gbrain-refresh' once it's configured."
;;
esac
;;
*)
echo "Usage: gstack-config {get|set|list|defaults|endpoint-hash|resolve-user-slug|gbrain-refresh} [key] [value]"
exit 1
;;
esac
async function readLegacyValue(key) {
const content = await fs.readFile(legacyConfig, "utf8");
let found;
for (const line of content.split(/\r?\n/)) {
const match = line.match(/^([A-Za-z0-9_]+(?:@[a-f0-9]+)?):\s*(.*?)\s*(?:#.*)?$/);
if (match?.[1] === key) found = parseConfigValue(unquote(match[2]));
}
return found;
}
function unquote(value) {
if (value.length >= 2 && ((value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'")))) return value.slice(1, -1);
return value;
}
function endpointHash() {
const endpoint = env.GSTACK_GBRAIN_ENDPOINT || env.GBRAIN_URL || "";
return endpoint ? createHash("sha256").update(endpoint).digest("hex").slice(0, 8) : "local";
}
async function resolveUserSlug() {
const key = `user_slug_at_${endpointHash()}`;
if (await exists(path.join(home, "config.json"))) {
const stored = await configGet(home, key);
if (typeof stored === "string" && stored) {
process.stdout.write(stored);
return;
}
}
const user = sanitizeSlug(env.USER || "");
const email = spawnSync("git", ["config", "user.email"], { encoding: "utf8" }).stdout?.trim();
const fallback = email
? `email-${sha8(email)}`
: `anonymous-${sha8(os.hostname() || "unknown")}`;
const slug = user || fallback;
await mutateConfigHome(() => configSet(home, key, slug));
process.stdout.write(slug);
}
async function refreshGbrainDetection() {
await mutateConfigHome(async () => {
const detector = path.join(scriptDir, "gstack-gbrain-detect");
const result = spawnSync(detector, [], { encoding: "utf8", env });
const payload = result.status === 0 && result.stdout.trim()
? result.stdout.trim()
: '{"gbrain_on_path":false,"gbrain_local_status":"no-cli"}';
JSON.parse(payload);
const target = path.join(home, "gbrain-detection.json");
const temporary = `${target}.tmp-${process.pid}`;
await fs.writeFile(temporary, `${payload}\n`, { mode: 0o600 });
await fs.rename(temporary, target);
});
process.stdout.write("GBrain detection refreshed. Re-run the standard Agent Skills installer if skill content must change.\n");
}
async function mutateConfigHome(callback) {
return withRuntimeLifecycleLock(home, async () => {
await ensureManagedHome(home);
await ensureConfig(home);
return callback();
});
}
function sanitizeSlug(value) {
return value.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
}
function sha8(value) {
return createHash("sha256").update(value).digest("hex").slice(0, 8);
}
function flatten(value, prefix = "", output = {}) {
for (const [key, child] of Object.entries(value ?? {})) {
const name = prefix ? `${prefix}.${key}` : key;
if (child && typeof child === "object" && !Array.isArray(child)) flatten(child, name, output);
else output[name] = child;
}
return output;
}
function printEntries(entries) {
for (const key of Object.keys(entries).sort()) {
process.stdout.write(`${key}: ${formatValue(entries[key])}\n`);
}
}
function formatValue(value) {
if (value === undefined || value === null) return "";
return typeof value === "string" ? value : JSON.stringify(value);
}
async function exists(target) {
return fs.lstat(target).then(() => true, (error) => {
if (error?.code === "ENOENT") return false;
throw error;
});
}
function usage() {
process.stderr.write("Usage: gstack-config {get|set|list|defaults|endpoint-hash|resolve-user-slug|gbrain-refresh} [key] [value]\n");
}
+5 -4
View File
@@ -26,18 +26,19 @@ import {
compact,
type DecisionEvent,
} from "../lib/gstack-decision";
import { resolveSlug, gitBranch, flagValue } from "../lib/bin-context";
import { gitBranch, flagValue } from "../lib/bin-context";
import { discoverProjectIdentity } from "../runtime/identity.js";
const HERE = import.meta.dir;
const args = process.argv.slice(2);
const slug = resolveSlug(`${HERE}/gstack-slug`);
const paths = decisionPaths(slug);
const project = await discoverProjectIdentity();
const paths = decisionPaths(project.projectId);
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" });
spawnSync(`${HERE}/gstack-brain-enqueue`, [`projects/${project.projectId}/decisions.jsonl`], { stdio: "ignore" });
}
if (args.includes("--compact")) {
+4 -3
View File
@@ -28,13 +28,14 @@ import {
datamark,
type ActiveDecision,
} from "../lib/gstack-decision";
import { resolveSlug, gitBranch, flagValue } from "../lib/bin-context";
import { gitBranch, flagValue } from "../lib/bin-context";
import { discoverProjectIdentity } from "../runtime/identity.js";
const HERE = import.meta.dir;
const args = process.argv.slice(2);
const slug = resolveSlug(`${HERE}/gstack-slug`);
const paths = decisionPaths(slug);
const projectId = (await discoverProjectIdentity()).projectId;
const paths = decisionPaths(projectId);
const queryRaw = flagValue(args, "--query");
const query = queryRaw?.toLowerCase();
const scope = flagValue(args, "--scope");
+1 -1
View File
@@ -69,7 +69,7 @@ def acquire_lock(name, log):
Returns the held fd (kept open for the process lifetime)."""
import fcntl
d = os.path.expanduser("~/.gstack/locks")
d = os.path.join(os.environ.get("GSTACK_HOME", os.path.expanduser("~/.gstack")), "locks")
os.makedirs(d, exist_ok=True)
fd = open(os.path.join(d, f"{name}.lock"), "w")
try:
+5 -5
View File
@@ -22,7 +22,7 @@
# date, mode. Silent skip on invalid input.
#
# Profile file: ~/.gstack/developer-profile.json (unified schema — see
# docs/designs/PLAN_TUNING_V0.md). Event file: ~/.gstack/projects/{SLUG}/
# docs/designs/PLAN_TUNING_V0.md). Event file: $GSTACK_HOME/projects/{PROJECT_ID}/
# question-events.jsonl.
set -euo pipefail
@@ -32,8 +32,8 @@ ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
GSTACK_HOME="${GSTACK_STATE_ROOT:-${GSTACK_HOME:-$HOME/.gstack}}"
PROFILE_FILE="$GSTACK_HOME/developer-profile.json"
LEGACY_FILE="$GSTACK_HOME/builder-profile.jsonl"
eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null || true)"
SLUG="${SLUG:-unknown}"
eval "$(GSTACK_HOME="$GSTACK_HOME" "$SCRIPT_DIR/gstack-slug" 2>/dev/null || true)"
PROJECT_ID="${PROJECT_ID:?gstack project identity unavailable}"
CMD="${1:---read}"
shift || true
@@ -308,7 +308,7 @@ do_gap() {
# -----------------------------------------------------------------------
do_derive() {
ensure_profile
local EVENTS="$GSTACK_HOME/projects/$SLUG/question-log.jsonl"
local EVENTS="$GSTACK_HOME/projects/$PROJECT_ID/question-log.jsonl"
local REGISTRY="$ROOT_DIR/scripts/question-registry.ts"
local SIGNALS="$ROOT_DIR/scripts/psychographic-signals.ts"
if [ ! -f "$REGISTRY" ] || [ ! -f "$SIGNALS" ]; then
@@ -394,7 +394,7 @@ do_trace() {
echo "TRACE: missing dimension argument" >&2
exit 1
fi
local EVENTS="$GSTACK_HOME/projects/$SLUG/question-log.jsonl"
local EVENTS="$GSTACK_HOME/projects/$PROJECT_ID/question-log.jsonl"
if [ ! -f "$EVENTS" ]; then
echo "TRACE: no events for this project"
return 0
+3 -3
View File
@@ -25,9 +25,9 @@
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
GSTACK_HOME="${GSTACK_STATE_ROOT:-${GSTACK_HOME:-$HOME/.gstack}}"
eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null || true)"
SLUG="${SLUG:-unknown}"
PROJECT_DIR="$GSTACK_HOME/projects/$SLUG"
eval "$(GSTACK_HOME="$GSTACK_HOME" "$SCRIPT_DIR/gstack-slug" 2>/dev/null || true)"
PROJECT_ID="${PROJECT_ID:?gstack project identity unavailable}"
PROJECT_DIR="$GSTACK_HOME/projects/$PROJECT_ID"
PROPOSAL_FILE="$PROJECT_DIR/distillation-proposals.json"
MEMORY_FILE="$GSTACK_HOME/free-text-memory.json"
PROFILE_FILE="$GSTACK_HOME/developer-profile.json"
+8 -7
View File
@@ -23,9 +23,10 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
GSTACK_HOME="${GSTACK_STATE_ROOT:-${GSTACK_HOME:-$HOME/.gstack}}"
eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null || true)"
eval "$(GSTACK_HOME="$GSTACK_HOME" "$SCRIPT_DIR/gstack-slug" 2>/dev/null || true)"
SLUG="${SLUG:-unknown}"
PROJECT_DIR="$GSTACK_HOME/projects/$SLUG"
PROJECT_ID="${PROJECT_ID:?gstack project identity unavailable}"
PROJECT_DIR="$GSTACK_HOME/projects/$PROJECT_ID"
LOG_FILE="$PROJECT_DIR/question-log.jsonl"
PROPOSAL_FILE="$PROJECT_DIR/distillation-proposals.json"
COST_LOG="$GSTACK_HOME/distill-cost.jsonl"
@@ -47,14 +48,14 @@ esac
# --- Status subcommand --------------------------------------------------
if [ "$MODE" = "status" ]; then
COST_LOG_PATH="$COST_LOG" SLUG_PATH="$SLUG" bun -e '
COST_LOG_PATH="$COST_LOG" PROJECT_ID_PATH="$PROJECT_ID" bun -e '
const fs = require("fs");
const slug = process.env.SLUG_PATH;
const projectId = process.env.PROJECT_ID_PATH;
const path = process.env.COST_LOG_PATH;
if (!fs.existsSync(path)) { console.log("no distill runs yet"); process.exit(0); }
const lines = fs.readFileSync(path, "utf-8").trim().split("\n").filter(Boolean);
const mine = lines.map((l) => JSON.parse(l)).filter((e) => e.slug === slug);
if (mine.length === 0) { console.log("no distill runs yet for slug=" + slug); process.exit(0); }
const mine = lines.map((l) => JSON.parse(l)).filter((e) => e.project_id === projectId);
if (mine.length === 0) { console.log("no distill runs yet for project=" + projectId); process.exit(0); }
const totalUsd = mine.reduce((a, e) => a + (e.cost_usd_est || 0), 0);
const todayIso = new Date().toISOString().slice(0, 10);
const today = mine.filter((e) => (e.ts || "").startsWith(todayIso));
@@ -265,7 +266,7 @@ RESULT=$(EVENTS_JSON="$EVENTS_JSON" DISTILL_PROMPT="$DISTILL_PROMPT" \
# Append cost log line.
TS=$(date -u +%Y-%m-%dT%H:%M:%SZ)
echo "{\"ts\":\"$TS\",\"slug\":\"$SLUG\",$(echo "$RESULT" | sed 's/^{//; s/}$//')}" >> "$COST_LOG"
echo "{\"ts\":\"$TS\",\"project_id\":\"$PROJECT_ID\",\"slug\":\"$SLUG\",$(echo "$RESULT" | sed 's/^{//; s/}$//')}" >> "$COST_LOG"
echo "DISTILL_COMPLETE:"
echo " proposals_file: $PROPOSAL_FILE"
+6 -4
View File
@@ -13,9 +13,11 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
case "$(uname -s)" in
MINGW*|MSYS*|CYGWIN*) command -v cygpath >/dev/null 2>&1 && SCRIPT_DIR="$(cygpath -m "$SCRIPT_DIR")" ;;
esac
eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null)"
GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
mkdir -p "$GSTACK_HOME/projects/$SLUG"
eval "$(GSTACK_HOME="$GSTACK_HOME" "$SCRIPT_DIR/gstack-slug" 2>/dev/null)"
PROJECT_ID="${PROJECT_ID:?gstack project identity unavailable}"
PROJECT_DIR="$GSTACK_HOME/projects/$PROJECT_ID"
mkdir -p "$PROJECT_DIR"
INPUT="$1"
@@ -85,7 +87,7 @@ if [ $VALIDATE_RC -ne 0 ] || [ -z "$VALIDATED" ]; then
exit 1
fi
echo "$VALIDATED" >> "$GSTACK_HOME/projects/$SLUG/learnings.jsonl"
echo "$VALIDATED" >> "$PROJECT_DIR/learnings.jsonl"
# gbrain-sync: enqueue for cross-machine sync (no-op if sync is off).
"$SCRIPT_DIR/gstack-brain-enqueue" "projects/$SLUG/learnings.jsonl" 2>/dev/null &
"$SCRIPT_DIR/gstack-brain-enqueue" "projects/$PROJECT_ID/learnings.jsonl" 2>/dev/null &
+5 -4
View File
@@ -2,13 +2,14 @@
# gstack-learnings-search — read and filter project learnings
# Usage: gstack-learnings-search [--type TYPE] [--query KEYWORD] [--limit N] [--cross-project]
#
# Reads ~/.gstack/projects/$SLUG/learnings.jsonl, applies confidence decay,
# Reads the current worktree's learnings.jsonl, applies confidence decay,
# resolves duplicates (latest winner per key+type), and outputs formatted text.
# Exit 0 silently if no learnings file exists.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null)"
GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
eval "$(GSTACK_HOME="$GSTACK_HOME" "$SCRIPT_DIR/gstack-slug" 2>/dev/null)"
PROJECT_ID="${PROJECT_ID:?gstack project identity unavailable}"
TYPE=""
QUERY=""
@@ -25,7 +26,7 @@ while [[ $# -gt 0 ]]; do
esac
done
LEARNINGS_FILE="$GSTACK_HOME/projects/$SLUG/learnings.jsonl"
LEARNINGS_FILE="$GSTACK_HOME/projects/$PROJECT_ID/learnings.jsonl"
# Collect cross-project JSONL files separately so the trust gate can distinguish
# current-project rows from rows loaded from other projects.
@@ -36,7 +37,7 @@ if [ "$CROSS_PROJECT" = true ]; then
while IFS= read -r f; do
CROSS_FILES+=("$f")
[ ${#CROSS_FILES[@]} -ge 5 ] && break
done < <(find "$GSTACK_HOME/projects" -name "learnings.jsonl" -not -path "*/$SLUG/*" 2>/dev/null)
done < <(find "$GSTACK_HOME/projects" -name "learnings.jsonl" -not -path "*/$PROJECT_ID/*" 2>/dev/null)
fi
if [ ! -f "$LEARNINGS_FILE" ] && [ ${#CROSS_FILES[@]} -eq 0 ]; then
+13 -12
View File
@@ -18,10 +18,10 @@
* ~/.claude/projects/<encoded-cwd>/<uuid>.jsonl Claude Code sessions
* ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl Codex CLI sessions
* ~/Library/Application Support/Cursor/User/*.vscdb Cursor (V1.0.1 follow-up)
* ~/.gstack/projects/<slug>/learnings.jsonl typed: learning
* ~/.gstack/projects/<slug>/timeline.jsonl typed: timeline
* ~/.gstack/projects/<slug>/ceo-plans/*.md typed: ceo-plan
* ~/.gstack/projects/<slug>/*-design-*.md typed: design-doc
* $GSTACK_HOME/projects/<project-id>/learnings.jsonl typed: learning
* $GSTACK_HOME/projects/<project-id>/timeline.jsonl typed: timeline
* $GSTACK_HOME/projects/<project-id>/ceo-plans/*.md typed: ceo-plan
* $GSTACK_HOME/projects/<project-id>/*-design-*.md typed: design-doc
* ~/.gstack/analytics/eureka.jsonl typed: eureka
* ~/.gstack/builder-profile.jsonl typed: builder-profile-entry
*
@@ -53,7 +53,7 @@ import {
closeSync,
rmSync,
} from "fs";
import { join, basename, dirname } from "path";
import { join, basename, dirname, relative as pathRelative } from "path";
import { execFileSync, spawnSync, spawn, type ChildProcess } from "child_process";
import { homedir } from "os";
import { createHash } from "crypto";
@@ -438,14 +438,14 @@ function* walkGstackArtifacts(ctx: WalkContext): Generator<{ path: string; type:
}
if (!existsSync(projectsRoot)) return;
let slugs: string[];
let projectIds: string[];
try {
slugs = readdirSync(projectsRoot);
projectIds = readdirSync(projectsRoot);
} catch {
return;
}
for (const slug of slugs) {
const projDir = join(projectsRoot, slug);
for (const projectId of projectIds) {
const projDir = join(projectsRoot, projectId);
let st;
try {
st = statSync(projDir);
@@ -758,10 +758,11 @@ function buildArtifactPage(path: string, type: MemoryType): PageRecord {
const sha = fileSha256(path);
const raw = readFileSync(path, "utf-8");
// Extract repo slug from path: ~/.gstack/projects/<slug>/...
// Local project IDs are intentionally worktree-specific. Resolve relative to
// GSTACK_HOME so custom state roots and Windows separators remain supported.
let slug_repo = "_unattributed";
const m = path.match(/\/\.gstack\/projects\/([^/]+)\//);
if (m) slug_repo = m[1];
const relative = pathRelative(GSTACK_HOME, path).split(/[\\/]/);
if (relative[0] === "projects" && relative[1]) slug_repo = relative[1];
const date = new Date(stats.mtimeMs).toISOString().slice(0, 10);
const baseName = basename(path, path.endsWith(".jsonl") ? ".jsonl" : ".md");
+5 -5
View File
@@ -27,10 +27,10 @@
import '../lib/conductor-env-shim';
import * as fs from 'fs';
import * as path from 'path';
import { runBenchmark, formatTable, formatJson, formatMarkdown, type BenchmarkInput } from '../test/helpers/benchmark-runner';
import { ClaudeAdapter } from '../test/helpers/providers/claude';
import { GptAdapter } from '../test/helpers/providers/gpt';
import { GeminiAdapter } from '../test/helpers/providers/gemini';
import { runBenchmark, formatTable, formatJson, formatMarkdown, type BenchmarkInput } from '../lib/model-benchmark/runner';
import { ClaudeAdapter } from '../lib/model-benchmark/providers/claude';
import { GptAdapter } from '../lib/model-benchmark/providers/gpt';
import { GeminiAdapter } from '../lib/model-benchmark/providers/gemini';
const ADAPTER_FACTORIES = {
claude: () => new ClaudeAdapter(),
@@ -130,7 +130,7 @@ async function main(): Promise<void> {
if (doJudge) {
try {
const { judgeEntries } = await import('../test/helpers/benchmark-judge');
const { judgeEntries } = await import('../lib/model-benchmark/judge');
await judgeEntries(report);
} catch (err) {
console.error(`WARN: judge unavailable: ${(err as Error).message}`);
+4 -63
View File
@@ -1,65 +1,6 @@
#!/usr/bin/env bash
# gstack-paths — output portable state-root paths for skill bash blocks
# Usage: eval "$(gstack-paths)" → sets GSTACK_STATE_ROOT, PLAN_ROOT, TMP_ROOT
# Or: gstack-paths → prints GSTACK_STATE_ROOT=... etc.
#
# Resolves three roots with explicit fallback chains so skills work the same
# whether installed as a Claude Code plugin (CLAUDE_PLUGIN_DATA / CLAUDE_PLANS_DIR
# set), a global ~/.claude/skills/gstack/ install, or a local checkout under
# CI / container env where HOME may be unset.
#
# Chains:
# GSTACK_STATE_ROOT: GSTACK_HOME -> CLAUDE_PLUGIN_DATA (only when CLAUDE_PLUGIN_ROOT=*gstack*) -> $HOME/.gstack -> .gstack
# PLAN_ROOT: GSTACK_PLAN_DIR -> CLAUDE_PLANS_DIR -> $HOME/.claude/plans -> .claude/plans
# TMP_ROOT: TMPDIR -> TMP -> .gstack/tmp (and mkdir -p, best-effort)
#
# Security: output values are not sanitized — callers may receive paths with
# shell-special characters if env vars contain them. Skills should always quote
# expansions ("$GSTACK_STATE_ROOT", not $GSTACK_STATE_ROOT).
set -u
# Compatibility adapter. Path decisions live only in runtime/paths.js.
set -eu
# State root: where gstack writes projects/, sessions/, analytics/.
if [ -n "${GSTACK_HOME:-}" ]; then
_state_root="$GSTACK_HOME"
elif [ -n "${CLAUDE_PLUGIN_DATA:-}" ] && echo "${CLAUDE_PLUGIN_ROOT:-}" | grep -qi "gstack"; then
# Guard: only trust CLAUDE_PLUGIN_DATA when CLAUDE_PLUGIN_ROOT confirms we are
# running as the gstack plugin. Without this, a CLAUDE_PLUGIN_DATA from another
# plugin (e.g. codex) that leaked into the session env via CLAUDE_ENV_FILE would
# be picked up, writing all gstack state into the wrong directory.
_state_root="$CLAUDE_PLUGIN_DATA"
elif [ -n "${HOME:-}" ]; then
_state_root="$HOME/.gstack"
else
_state_root=".gstack"
fi
# Plan root: where /context-save and /codex consult write plan files.
if [ -n "${GSTACK_PLAN_DIR:-}" ]; then
_plan_root="$GSTACK_PLAN_DIR"
elif [ -n "${CLAUDE_PLANS_DIR:-}" ]; then
_plan_root="$CLAUDE_PLANS_DIR"
elif [ -n "${HOME:-}" ]; then
_plan_root="$HOME/.claude/plans"
else
_plan_root=".claude/plans"
fi
# Tmp root: where ephemeral files (codex stderr captures, etc.) live.
# Honor TMPDIR / TMP for Windows + container compat; fall back to a
# project-local .gstack/tmp so we never write to a system /tmp that may
# be read-only or shared.
if [ -n "${TMPDIR:-}" ]; then
_tmp_root="$TMPDIR"
elif [ -n "${TMP:-}" ]; then
_tmp_root="$TMP"
else
_tmp_root=".gstack/tmp"
fi
# Best-effort mkdir; if it fails (read-only fs, permission denied), the caller
# will discover that on their own write attempt. Don't fail the eval here.
mkdir -p "$_tmp_root" 2>/dev/null || true
echo "GSTACK_STATE_ROOT=$_state_root"
echo "PLAN_ROOT=$_plan_root"
echo "TMP_ROOT=$_tmp_root"
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
exec "$ROOT/bin/gstack" paths --shell
+5 -3
View File
@@ -33,10 +33,12 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
case "$(uname -s)" in
MINGW*|MSYS*|CYGWIN*) command -v cygpath >/dev/null 2>&1 && SCRIPT_DIR="$(cygpath -m "$SCRIPT_DIR")" ;;
esac
eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null)"
# GSTACK_STATE_ROOT takes precedence over GSTACK_HOME (test isolation per D16).
GSTACK_HOME="${GSTACK_STATE_ROOT:-${GSTACK_HOME:-$HOME/.gstack}}"
mkdir -p "$GSTACK_HOME/projects/$SLUG"
eval "$(GSTACK_HOME="$GSTACK_HOME" "$SCRIPT_DIR/gstack-slug" 2>/dev/null)"
PROJECT_ID="${PROJECT_ID:?gstack project identity unavailable}"
PROJECT_DIR="$GSTACK_HOME/projects/$PROJECT_ID"
mkdir -p "$PROJECT_DIR"
INPUT="$1"
@@ -197,7 +199,7 @@ if [ $VALIDATE_RC -ne 0 ] || [ -z "$VALIDATED" ]; then
exit 1
fi
LOG_FILE="$GSTACK_HOME/projects/$SLUG/question-log.jsonl"
LOG_FILE="$PROJECT_DIR/question-log.jsonl"
# Cathedral T5: composite-source dedup. If this exact (source, tool_use_id)
# was already logged within the last 100 lines, skip — protects against
+7 -6
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# gstack-question-preference — read/write/check explicit per-question preferences.
#
# Preference file: ~/.gstack/projects/{SLUG}/question-preferences.json
# Preference file: $GSTACK_HOME/projects/{PROJECT_ID}/question-preferences.json
# Schema: { "<question_id>": "always-ask" | "never-ask" | "ask-only-for-one-way" }
#
# Subcommands:
@@ -25,11 +25,12 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
# GSTACK_STATE_ROOT takes precedence over GSTACK_HOME (test isolation per D16).
GSTACK_HOME="${GSTACK_STATE_ROOT:-${GSTACK_HOME:-$HOME/.gstack}}"
eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null || true)"
SLUG="${SLUG:-unknown}"
PREF_FILE="$GSTACK_HOME/projects/$SLUG/question-preferences.json"
EVENT_FILE="$GSTACK_HOME/projects/$SLUG/question-events.jsonl"
mkdir -p "$GSTACK_HOME/projects/$SLUG"
eval "$(GSTACK_HOME="$GSTACK_HOME" "$SCRIPT_DIR/gstack-slug" 2>/dev/null || true)"
PROJECT_ID="${PROJECT_ID:?gstack project identity unavailable}"
PROJECT_DIR="$GSTACK_HOME/projects/$PROJECT_ID"
PREF_FILE="$PROJECT_DIR/question-preferences.json"
EVENT_FILE="$PROJECT_DIR/question-events.jsonl"
mkdir -p "$PROJECT_DIR"
CMD="${1:-}"
shift || true
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
exec bun "$SCRIPT_DIR/../lib/redact-audit-log.ts" "$@"
+7 -5
View File
@@ -8,18 +8,20 @@
# Collaborative: top author < 80%
#
# Override: gstack-config set repo_mode solo|collaborative
# Cache: ~/.gstack/projects/$SLUG/repo-mode.json (7-day TTL)
# Cache: $GSTACK_HOME/projects/$PROJECT_ID/repo-mode.json (7-day TTL)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# Compute SLUG directly (avoid eval of gstack-slug — branch names can contain shell metacharacters)
GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
REMOTE_URL=$(git remote get-url origin 2>/dev/null || true)
if [ -z "$REMOTE_URL" ]; then
echo "REPO_MODE=unknown"
exit 0
fi
SLUG=$(echo "$REMOTE_URL" | sed 's|.*[:/]\([^/]*/[^/]*\)\.git$|\1|;s|.*[:/]\([^/]*/[^/]*\)$|\1|' | tr '/' '-')
[ -z "${SLUG:-}" ] && { echo "REPO_MODE=unknown"; exit 0; }
# gstack-slug emits only eval-safe values and derives PROJECT_ID from Git's
# common-dir + worktree slot, so linked worktrees cannot share this cache.
eval "$(GSTACK_HOME="$GSTACK_HOME" "$SCRIPT_DIR/gstack-slug" 2>/dev/null || true)"
PROJECT_ID="${PROJECT_ID:?gstack project identity unavailable}"
# Validate: only allow known values (prevent shell injection via source <(...))
validate_mode() {
@@ -34,7 +36,7 @@ if [ -n "$OVERRIDE" ] && [ "$OVERRIDE" != "null" ]; then
fi
# Check cache (7-day TTL)
CACHE_DIR="$HOME/.gstack/projects/$SLUG"
CACHE_DIR="$GSTACK_HOME/projects/$PROJECT_ID"
CACHE_FILE="$CACHE_DIR/repo-mode.json"
if [ -f "$CACHE_FILE" ]; then
CACHE_AGE=$(( $(date +%s) - $(stat -f %m "$CACHE_FILE" 2>/dev/null || stat -c %Y "$CACHE_FILE" 2>/dev/null || echo 0) ))
+6 -4
View File
@@ -3,9 +3,11 @@
# Usage: gstack-review-log '{"skill":"...","timestamp":"...","status":"..."}'
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null)"
GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
mkdir -p "$GSTACK_HOME/projects/$SLUG"
eval "$(GSTACK_HOME="$GSTACK_HOME" "$SCRIPT_DIR/gstack-slug" 2>/dev/null)"
PROJECT_ID="${PROJECT_ID:?gstack project identity unavailable}"
PROJECT_DIR="$GSTACK_HOME/projects/$PROJECT_ID"
mkdir -p "$PROJECT_DIR"
# Validate: input must be parseable JSON (reject malformed or injection attempts)
INPUT="$1"
@@ -15,7 +17,7 @@ if ! printf '%s' "$INPUT" | bun -e "JSON.parse(await Bun.stdin.text())" 2>/dev/n
exit 1
fi
echo "$INPUT" >> "$GSTACK_HOME/projects/$SLUG/$BRANCH-reviews.jsonl"
echo "$INPUT" >> "$PROJECT_DIR/$BRANCH-reviews.jsonl"
# gbrain-sync: enqueue for cross-machine sync (no-op if sync is off).
"$SCRIPT_DIR/gstack-brain-enqueue" "projects/$SLUG/$BRANCH-reviews.jsonl" 2>/dev/null &
"$SCRIPT_DIR/gstack-brain-enqueue" "projects/$PROJECT_ID/$BRANCH-reviews.jsonl" 2>/dev/null &
+3 -2
View File
@@ -3,9 +3,10 @@
# Usage: gstack-review-read
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null)"
GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
cat "$GSTACK_HOME/projects/$SLUG/$BRANCH-reviews.jsonl" 2>/dev/null || echo "NO_REVIEWS"
eval "$(GSTACK_HOME="$GSTACK_HOME" "$SCRIPT_DIR/gstack-slug" 2>/dev/null)"
PROJECT_ID="${PROJECT_ID:?gstack project identity unavailable}"
cat "$GSTACK_HOME/projects/$PROJECT_ID/$BRANCH-reviews.jsonl" 2>/dev/null || echo "NO_REVIEWS"
echo "---CONFIG---"
"$SCRIPT_DIR/gstack-config" get skip_eng_review 2>/dev/null || echo "false"
echo "---HEAD---"
+86 -48
View File
@@ -1,55 +1,93 @@
#!/usr/bin/env bash
# gstack-slug — output project slug and sanitized branch name
# Usage: eval "$(gstack-slug)" → sets SLUG and BRANCH variables
# Or: gstack-slug → prints SLUG=... and BRANCH=... lines
#
# Security: output is sanitized to [a-zA-Z0-9._-] only, preventing
# shell injection when consumed via source or eval.
set -euo pipefail
#!/usr/bin/env node
/**
* gstack-slug — emit human and local project identities for shell callers.
*
* Usage: eval "$(gstack-slug)"
*
* SLUG remains the sanitized, human-facing repository slug used by remote
* namespaces. PROJECT_ID is the canonical local-state key from
* runtime/identity.js; unlike SLUG, it separates linked Git worktrees.
* Every emitted value is restricted to [a-zA-Z0-9._-] so the output remains
* safe to consume with eval/source.
*/
CACHE_DIR="$HOME/.gstack/slug-cache"
PROJECT_DIR="$(pwd)"
# Encode absolute path as cache key: /Users/j/foo → _Users_j_foo
CACHE_KEY=$(printf '%s' "$PROJECT_DIR" | tr '/' '_')
CACHE_FILE="${CACHE_DIR}/${CACHE_KEY}"
import fs from "node:fs/promises";
import path from "node:path";
import { execFile as execFileCallback } from "node:child_process";
import { promisify } from "node:util";
import { discoverProjectIdentity } from "../runtime/identity.js";
import { resolveGstackHome } from "../runtime/paths.js";
# 1. Try cached slug first (guarantees consistency across sessions)
if [[ -f "$CACHE_FILE" ]]; then
SLUG=$(cat "$CACHE_FILE")
fi
const execFile = promisify(execFileCallback);
const cwd = await canonicalPath(process.cwd());
const identity = await discoverProjectIdentity(cwd);
const home = resolveGstackHome({ cwd });
const cacheDir = path.join(home, "slug-cache");
# 2. If no cache, compute from git remote (separated from pipeline to avoid
# pipefail swallowing the error and producing an empty slug)
if [[ -z "${SLUG:-}" ]]; then
REMOTE_URL=$(git remote get-url origin 2>/dev/null) || REMOTE_URL=""
if [[ -n "$REMOTE_URL" ]]; then
RAW_SLUG=$(printf '%s' "$REMOTE_URL" | sed 's|.*[:/]\([^/]*/[^/]*\)\.git$|\1|;s|.*[:/]\([^/]*/[^/]*\)$|\1|' | tr '/' '-')
SLUG=$(printf '%s' "$RAW_SLUG" | tr -cd 'a-zA-Z0-9._-')
fi
fi
// The canonical key is a safe, worktree-stable ID. Read the 1.x path-derived
// key once as a compatibility fallback (it was not valid on native Windows).
const cacheFile = path.join(cacheDir, identity.worktreeId);
const legacyCacheFile = path.join(cacheDir, cwd.replace(/[\\/]/g, "_"));
# 3. Fallback to basename only when there's truly no git remote configured
SLUG="${SLUG:-$(basename "$PWD" | tr -cd 'a-zA-Z0-9._-')}"
let slug = sanitize(await fs.readFile(cacheFile, "utf8").catch(() => ""));
if (!slug) slug = sanitize(await fs.readFile(legacyCacheFile, "utf8").catch(() => ""));
if (!slug) {
const remote = await git(["remote", "get-url", "origin"], cwd).catch(() => "");
slug = sanitize(slugFromRemote(remote));
}
if (!slug) slug = sanitize(path.basename(cwd)) || "unknown";
# 3b. Re-sanitize unconditionally before the value is echoed into `eval`/`source`
# output. The compute (2) and fallback (3) paths already filter, but a value
# read straight from the cache file (1) does NOT — a poisoned
# ~/.gstack/slug-cache/<key> would otherwise inject shell into
# `eval "$(gstack-slug)"`. Filtering here honors the [a-zA-Z0-9._-] invariant
# promised in the header on every path, and heals a poisoned cache on write (4).
SLUG=$(printf '%s' "$SLUG" | tr -cd 'a-zA-Z0-9._-')
await writeCache(cacheDir, cacheFile, slug);
# 4. Cache the slug for future sessions (atomic write, fail silently)
if [[ -n "$SLUG" ]]; then
mkdir -p "$CACHE_DIR" 2>/dev/null || true
CACHE_TMP=$(mktemp "$CACHE_DIR/.slug-XXXXXX" 2>/dev/null) || CACHE_TMP=""
if [[ -n "$CACHE_TMP" ]]; then
printf '%s' "$SLUG" > "$CACHE_TMP" && mv "$CACHE_TMP" "$CACHE_FILE" 2>/dev/null || rm -f "$CACHE_TMP" 2>/dev/null
fi
fi
const rawBranch = await git(["rev-parse", "--abbrev-ref", "HEAD"], cwd).catch(() => "");
const branch = sanitize(rawBranch === "HEAD" ? "" : rawBranch) || "unknown";
RAW_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null) || RAW_BRANCH=""
BRANCH=$(printf '%s' "${RAW_BRANCH:-}" | tr -cd 'a-zA-Z0-9._-')
BRANCH="${BRANCH:-unknown}"
echo "SLUG=$SLUG"
echo "BRANCH=$BRANCH"
for (const [name, value] of [
["SLUG", slug],
["BRANCH", branch],
["PROJECT_ID", identity.projectId],
["REPO_ID", identity.repoId],
["WORKTREE_ID", identity.worktreeId],
]) {
process.stdout.write(`${name}=${sanitize(value) || "unknown"}\n`);
}
function sanitize(value) {
return String(value ?? "").replace(/[^a-zA-Z0-9._-]/g, "");
}
function slugFromRemote(remote) {
const normalized = String(remote ?? "").trim().replace(/\/+$/, "").replace(/\.git$/, "");
const match = normalized.match(/(?:^|[:/])([^/:]+\/[^/]+)$/);
return match ? match[1].replace("/", "-") : "";
}
async function git(args, directory) {
const { stdout } = await execFile("git", args, {
cwd: directory,
encoding: "utf8",
timeout: 5_000,
maxBuffer: 1024 * 1024,
windowsHide: true,
});
return stdout.replace(/[\r\n]+$/, "");
}
async function canonicalPath(value) {
const absolute = path.resolve(value);
return fs.realpath(absolute).catch((error) => {
if (error?.code === "ENOENT") return absolute;
throw error;
});
}
async function writeCache(directory, file, value) {
try {
await fs.mkdir(directory, { recursive: true, mode: 0o700 });
const temporary = path.join(directory, `.slug-${process.pid}-${Date.now()}`);
await fs.writeFile(temporary, value, { mode: 0o600 });
await fs.rename(temporary, file);
} catch {
// Display-slug caching is a best-effort compatibility optimization.
}
}
+3 -2
View File
@@ -7,9 +7,10 @@
# dispatches) or NEVER_GATE (security, data-migration — insurance policy).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null)"
GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
PROJECT_DIR="$GSTACK_HOME/projects/$SLUG"
eval "$(GSTACK_HOME="$GSTACK_HOME" "$SCRIPT_DIR/gstack-slug" 2>/dev/null)"
PROJECT_ID="${PROJECT_ID:?gstack project identity unavailable}"
PROJECT_DIR="$GSTACK_HOME/projects/$PROJECT_ID"
if [ ! -d "$PROJECT_DIR" ]; then
echo "SPECIALIST_STATS: 0 reviews analyzed"
+24 -28
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bun
// gstack-taste-update — update the persistent taste profile at
// ~/.gstack/projects/$SLUG/taste-profile.json
// $GSTACK_HOME/projects/$PROJECT_ID/taste-profile.json
//
// Usage:
// gstack-taste-update approved <variant-path> [--reason "<why>"]
@@ -8,7 +8,7 @@
// gstack-taste-update show — print current profile summary
// gstack-taste-update migrate — upgrade legacy approved.json to v1
//
// Schema v1 at ~/.gstack/projects/$SLUG/taste-profile.json:
// Schema v1 at $GSTACK_HOME/projects/$PROJECT_ID/taste-profile.json:
//
// {
// "version": 1,
@@ -31,12 +31,13 @@
import * as fs from 'fs';
import * as path from 'path';
import { execSync } from 'child_process';
import { discoverProjectIdentity } from '../runtime/identity.js';
const STATE_DIR = process.env.GSTACK_STATE_DIR || path.join(process.env.HOME || '/', '.gstack');
const STATE_DIR = process.env.GSTACK_HOME || process.env.GSTACK_STATE_DIR || path.join(process.env.HOME || '/', '.gstack');
const SCHEMA_VERSION = 1;
const SESSION_CAP = 50;
const DECAY_PER_WEEK = 0.05;
const CURRENT_PROJECT_ID = (await discoverProjectIdentity()).projectId;
type Dimension = 'fonts' | 'colors' | 'layouts' | 'aesthetics';
const DIMENSIONS: Dimension[] = ['fonts', 'colors', 'layouts', 'aesthetics'];
@@ -63,17 +64,12 @@ interface TasteProfile {
sessions: SessionRecord[];
}
function getSlug(): string {
try {
const output = execSync('git rev-parse --show-toplevel', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
return path.basename(output);
} catch {
return 'unknown';
}
function getProjectId(): string {
return CURRENT_PROJECT_ID;
}
function profilePath(slug: string): string {
return path.join(STATE_DIR, 'projects', slug, 'taste-profile.json');
function profilePath(projectId: string): string {
return path.join(STATE_DIR, 'projects', projectId, 'taste-profile.json');
}
function emptyProfile(): TasteProfile {
@@ -90,8 +86,8 @@ function emptyProfile(): TasteProfile {
};
}
function load(slug: string): TasteProfile {
const p = profilePath(slug);
function load(projectId: string): TasteProfile {
const p = profilePath(projectId);
if (!fs.existsSync(p)) return emptyProfile();
try {
const raw = JSON.parse(fs.readFileSync(p, 'utf-8'));
@@ -105,8 +101,8 @@ function load(slug: string): TasteProfile {
}
}
function save(slug: string, profile: TasteProfile): void {
const p = profilePath(slug);
function save(projectId: string, profile: TasteProfile): void {
const p = profilePath(projectId);
fs.mkdirSync(path.dirname(p), { recursive: true });
profile.updated_at = new Date().toISOString();
fs.writeFileSync(p, JSON.stringify(profile, null, 2) + '\n');
@@ -208,8 +204,8 @@ function bumpPref(list: Preference[], value: string, opposite: Preference[], act
}
function cmdUpdate(action: 'approved' | 'rejected', variant: string, reason?: string): void {
const slug = getSlug();
const profile = load(slug);
const projectId = getProjectId();
const profile = load(projectId);
const signals = extractSignals(reason);
for (const dim of DIMENSIONS) {
@@ -227,14 +223,14 @@ function cmdUpdate(action: 'approved' | 'rejected', variant: string, reason?: st
profile.sessions = profile.sessions.slice(-SESSION_CAP);
}
save(slug, profile);
console.log(`${action}: ${variant} → ${profilePath(slug)}`);
save(projectId, profile);
console.log(`${action}: ${variant} → ${profilePath(projectId)}`);
}
function cmdShow(): void {
const slug = getSlug();
const profile = applyDecay(load(slug));
console.log(`taste-profile.json (slug: ${slug}, sessions: ${profile.sessions.length})`);
const projectId = getProjectId();
const profile = applyDecay(load(projectId));
console.log(`taste-profile.json (project: ${projectId}, sessions: ${profile.sessions.length})`);
for (const dim of DIMENSIONS) {
const top = [...profile.dimensions[dim].approved]
.sort((a, b) => b.confidence * b.approved_count - a.confidence * a.approved_count)
@@ -257,10 +253,10 @@ function cmdShow(): void {
}
function cmdMigrate(): void {
const slug = getSlug();
const profile = load(slug);
save(slug, profile);
console.log(`migrated taste profile to v${SCHEMA_VERSION} at ${profilePath(slug)}`);
const projectId = getProjectId();
const profile = load(projectId);
save(projectId, profile);
console.log(`migrated taste profile to v${SCHEMA_VERSION} at ${profilePath(projectId)}`);
}
// ─── CLI entry ────────────────────────────────────────────────
+4 -3
View File
@@ -11,7 +11,8 @@
# --used-browse true --session-id "12345-1710756600"
#
# Env overrides (for testing):
# GSTACK_STATE_DIR — override ~/.gstack state directory
# GSTACK_HOME — canonical GStack state/runtime root
# GSTACK_STATE_DIR — legacy fallback when GSTACK_HOME is unset
# GSTACK_DIR — override auto-detected gstack root
#
# NOTE: Uses set -uo pipefail (no -e) — telemetry must never exit non-zero
@@ -24,7 +25,7 @@ SCRIPT_DIR="$GSTACK_DIR/bin"
case "$(uname -s)" in
MINGW*|MSYS*|CYGWIN*) command -v cygpath >/dev/null 2>&1 && SCRIPT_DIR="$(cygpath -m "$SCRIPT_DIR")" ;;
esac
STATE_DIR="${GSTACK_STATE_DIR:-$HOME/.gstack}"
STATE_DIR="${GSTACK_HOME:-${GSTACK_STATE_DIR:-$HOME/.gstack}}"
ANALYTICS_DIR="$STATE_DIR/analytics"
JSONL_FILE="$ANALYTICS_DIR/skill-usage.jsonl"
PENDING_DIR="$ANALYTICS_DIR" # .pending-* files live here
@@ -137,7 +138,7 @@ fi
# can't be guessed or correlated by someone who knows your machine identity.
INSTALL_ID=""
if [ "$TIER" = "community" ]; then
ID_FILE="$HOME/.gstack/installation-id"
ID_FILE="$STATE_DIR/installation-id"
if [ -f "$ID_FILE" ]; then
INSTALL_ID="$(cat "$ID_FILE" 2>/dev/null)"
fi
+6 -4
View File
@@ -11,9 +11,11 @@
# Validation failure → skip silently (non-blocking).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null)"
GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
mkdir -p "$GSTACK_HOME/projects/$SLUG"
eval "$(GSTACK_HOME="$GSTACK_HOME" "$SCRIPT_DIR/gstack-slug" 2>/dev/null)"
PROJECT_ID="${PROJECT_ID:?gstack project identity unavailable}"
PROJECT_DIR="$GSTACK_HOME/projects/$PROJECT_ID"
mkdir -p "$PROJECT_DIR"
INPUT="$1"
@@ -34,7 +36,7 @@ if ! printf '%s' "$INPUT" | bun -e "const j=JSON.parse(await Bun.stdin.text());
" 2>/dev/null) || true
fi
echo "$INPUT" >> "$GSTACK_HOME/projects/$SLUG/timeline.jsonl"
echo "$INPUT" >> "$PROJECT_DIR/timeline.jsonl"
# gbrain-sync: enqueue for cross-machine sync (no-op if sync is off).
"$SCRIPT_DIR/gstack-brain-enqueue" "projects/$SLUG/timeline.jsonl" 2>/dev/null &
"$SCRIPT_DIR/gstack-brain-enqueue" "projects/$PROJECT_ID/timeline.jsonl" 2>/dev/null &
+4 -3
View File
@@ -3,12 +3,13 @@
# Usage: gstack-timeline-read [--since "7 days ago"] [--limit N] [--branch NAME]
#
# Session timeline: local-only, never sent anywhere.
# Reads ~/.gstack/projects/$SLUG/timeline.jsonl, filters, formats.
# Reads the current worktree's timeline.jsonl, filters, and formats.
# Exit 0 silently if no timeline file exists.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null)"
GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
eval "$(GSTACK_HOME="$GSTACK_HOME" "$SCRIPT_DIR/gstack-slug" 2>/dev/null)"
PROJECT_ID="${PROJECT_ID:?gstack project identity unavailable}"
SINCE=""
LIMIT=20
@@ -23,7 +24,7 @@ while [[ $# -gt 0 ]]; do
esac
done
TIMELINE_FILE="$GSTACK_HOME/projects/$SLUG/timeline.jsonl"
TIMELINE_FILE="$GSTACK_HOME/projects/$PROJECT_ID/timeline.jsonl"
if [ ! -f "$TIMELINE_FILE" ]; then
exit 0
+3 -2
View File
@@ -10,11 +10,12 @@
# GSTACK_DIR — override auto-detected gstack root
# GSTACK_REMOTE_URL — override remote VERSION URL (branch-pinned fallback)
# GSTACK_REMOTE_REPO — override remote git URL for ls-remote SHA resolution
# GSTACK_STATE_DIR — override ~/.gstack state directory
# GSTACK_HOME — canonical GStack state/runtime root
# GSTACK_STATE_DIR — legacy fallback when GSTACK_HOME is unset
set -euo pipefail
GSTACK_DIR="${GSTACK_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
STATE_DIR="${GSTACK_STATE_DIR:-$HOME/.gstack}"
STATE_DIR="${GSTACK_HOME:-${GSTACK_STATE_DIR:-$HOME/.gstack}}"
CACHE_FILE="$STATE_DIR/last-update-check"
MARKER_FILE="$STATE_DIR/just-upgraded-from"
SNOOZE_FILE="$STATE_DIR/update-snoozed"
+3 -1
View File
@@ -1,5 +1,5 @@
---
name: browse
name: gstack-1-browse
preamble-tier: 1
version: 1.1.0
description: Fast headless browser for QA testing and site dogfooding. (gstack)
@@ -12,6 +12,8 @@ allowed-tools:
- Read
- AskUserQuestion
metadata:
internal: true
---
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
<!-- Regenerate: bun run gen:skill-docs -->
+16 -5
View File
@@ -722,7 +722,7 @@ export class BrowserManager {
this.consecutiveFailures = 0;
}
async close() {
async close(timeoutMs = 5000) {
if (this.browser || (this.connectionMode === 'headed' && this.context)) {
if (this.connectionMode === 'headed') {
// Headed/persistent context mode: close the context (which closes the browser)
@@ -730,14 +730,14 @@ export class BrowserManager {
if (this.browser) this.browser.removeAllListeners('disconnected');
await Promise.race([
this.context ? this.context.close() : Promise.resolve(),
new Promise(resolve => setTimeout(resolve, 5000)),
new Promise(resolve => setTimeout(resolve, timeoutMs)),
]).catch(() => {});
} else {
// Launched mode: close the browser we spawned
this.browser.removeAllListeners('disconnected');
await Promise.race([
this.browser.close(),
new Promise(resolve => setTimeout(resolve, 5000)),
new Promise(resolve => setTimeout(resolve, timeoutMs)),
]).catch(() => {});
}
this.browser = null;
@@ -797,6 +797,11 @@ export class BrowserManager {
const tabId = id ?? this.activeTabId;
const page = this.pages.get(tabId);
if (!page) throw new Error(`Tab ${tabId} not found`);
// Capture before page.close(): Playwright may synchronously deliver the
// close event, whose map cleanup changes activeTabId to 0. The public
// closeTab promise still owns the invariant that closing the active last
// tab leaves one usable blank tab.
const wasActive = tabId === this.activeTabId;
await page.close();
this.pages.delete(tabId);
@@ -804,7 +809,7 @@ export class BrowserManager {
this.tabOwnership.delete(tabId);
// Switch to another tab if we closed the active one
if (tabId === this.activeTabId) {
if (wasActive) {
const remaining = [...this.pages.keys()];
if (remaining.length > 0) {
this.activeTabId = remaining[remaining.length - 1];
@@ -1568,9 +1573,15 @@ export class BrowserManager {
console.log('[browse] Handoff: extension not found — headed mode without side panel');
}
const userDataDir = path.join(process.env.HOME || '/tmp', '.gstack', 'chromium-profile');
const userDataDir = resolveChromiumProfile();
fs.mkdirSync(userDataDir, { recursive: true });
// The handoff profile follows the same host-neutral resolution and
// stale-lock cleanup contract as launchHeaded(). The current browser is
// headless and does not own this persistent profile, so cleanup cannot
// disrupt the live rollback path retained below.
cleanSingletonLocks(userDataDir);
// T1: same automation-tell-stripping defaults as launchHeaded().
// The handoff path (headless → headed re-launch) takes the same
// anti-detection posture.
+7 -13
View File
@@ -10,11 +10,15 @@
* 1. Prefer node on PATH + a bundled JS entry at
* browse/dist/security-sidecar.js (built by package.json's
* build:security-sidecar script).
* 2. Dev fallback: node + browse/src/security-sidecar-entry.ts via tsx
* (only available in the source checkout, not the compiled install).
* 3. If Node is missing or no entry resolves, return null. The /pty-inject-scan
* 2. If Node is missing or no compiled entry resolves, return null. The
* /pty-inject-scan
* endpoint then responds with l4 { available: false } and the extension
* degrades to WARN+confirm (D7).
*
* A plain-Node TypeScript fallback is intentionally not offered. It was not
* executable on the supported Node 18 floor and, if partially executed by a
* newer Node, could begin downloading local model weights before failing.
* GStack 2 does not bundle that model runtime or its weights.
*/
import { existsSync } from "fs";
@@ -46,9 +50,6 @@ function browseRoot(): string {
if (existsSync(join(candidate, "browse", "dist", "security-sidecar.js"))) {
return candidate;
}
if (existsSync(join(candidate, "src", "security-sidecar-entry.ts"))) {
return candidate;
}
const next = dirname(candidate);
if (next === candidate) break;
candidate = next;
@@ -67,12 +68,5 @@ export function findSecuritySidecar(): SidecarLocation | null {
return { node, entry: compiled, mode: "compiled" };
}
// Dev fallback. Compiled installs won't have src/ on disk so this only
// resolves when running from the source checkout.
const devEntry = join(root, "src", "security-sidecar-entry.ts");
if (existsSync(devEntry)) {
return { node, entry: devEntry, mode: "dev" };
}
return null;
}
+5 -2
View File
@@ -421,14 +421,17 @@ export async function handleMetaCommand(
}
case 'stop': {
await shutdown();
// Return the acknowledgement before closing the listener. Shutting down
// inline resets the CLI's fetch, which it reasonably interprets as a
// crash and then restarts the daemon it was asked to stop.
setTimeout(() => { void shutdown(); }, 25).unref?.();
return 'Server stopped';
}
case 'restart': {
// Signal that we want a restart — the CLI will detect exit and restart
console.log('[browse] Restart requested. Exiting for CLI to restart.');
await shutdown();
setTimeout(() => { void shutdown(); }, 25).unref?.();
return 'Restarting...';
}
+5 -4
View File
@@ -5,20 +5,21 @@
"": {
"name": "gstack",
"dependencies": {
"@huggingface/transformers": "^4.1.0",
"@anthropic-ai/sdk": "^0.78.0",
"@ngrok/ngrok": "^1.7.0",
"diff": "^9.0.0",
"html-to-docx": "1.8.0",
"marked": "^18.0.2",
"playwright": "^1.58.2",
"puppeteer-core": "^24.40.0",
"sharp": "^0.34.5",
"socks": "^2.8.8",
"xterm": "5",
"xterm-addon-fit": "^0.8.0",
},
"devDependencies": {
"@anthropic-ai/claude-agent-sdk": "0.2.117",
"@anthropic-ai/sdk": "^0.78.0",
"xterm": "5",
"xterm-addon-fit": "^0.8.0",
"@huggingface/transformers": "^4.1.0",
},
},
},
+3 -1
View File
@@ -1,5 +1,5 @@
---
name: canary
name: gstack-1-canary
preamble-tier: 2
version: 1.0.0
description: Post-deploy canary monitoring. (gstack)
@@ -13,6 +13,8 @@ triggers:
- monitor after deploy
- canary check
- watch for errors post-deploy
metadata:
internal: true
---
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
<!-- Regenerate: bun run gen:skill-docs -->
+3 -1
View File
@@ -1,5 +1,5 @@
---
name: careful
name: gstack-1-careful
version: 0.1.0
description: Safety guardrails for destructive commands. (gstack)
triggers:
@@ -16,6 +16,8 @@ hooks:
- type: command
command: "bash $HOME/.claude/skills/gstack/careful/bin/check-careful.sh"
statusMessage: "Checking for destructive commands..."
metadata:
internal: true
---
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
<!-- Regenerate: bun run gen:skill-docs -->
+3 -1
View File
@@ -1,5 +1,5 @@
---
name: codex
name: gstack-1-codex
preamble-tier: 3
version: 1.0.0
description: OpenAI Codex CLI wrapper — three modes. (gstack)
@@ -14,6 +14,8 @@ allowed-tools:
- Glob
- Grep
- AskUserQuestion
metadata:
internal: true
---
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
<!-- Regenerate: bun run gen:skill-docs -->
+62
View File
@@ -0,0 +1,62 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# GStack 2 compatibility aliases
These files preserve all 55 legacy invocation names as internal routing details. They intentionally are not named `SKILL.md`, so only the six dispatcher skills are discoverable.
| Legacy invocation | Replacement | Preserved module |
|---|---|---|
| `/gstack` | `$plan --mode Discovery --module gstack` | `skills/plan/references/legacy/gstack.md` |
| `/office-hours` | `$plan --mode Discovery --module office-hours` | `skills/plan/references/legacy/office-hours.md` |
| `/plan-ceo-review` | `$plan --mode Product --module plan-ceo-review` | `skills/plan/references/legacy/plan-ceo-review.md` |
| `/plan-eng-review` | `$plan --mode Engineering --module plan-eng-review` | `skills/plan/references/legacy/plan-eng-review.md` |
| `/plan-devex-review` | `$plan --mode DX --module plan-devex-review` | `skills/plan/references/legacy/plan-devex-review.md` |
| `/autoplan` | `$plan --mode Full chain --module autoplan` | `skills/plan/references/legacy/autoplan.md` |
| `/spec` | `$plan --mode Specification --module spec` | `skills/plan/references/legacy/spec.md` |
| `/plan-tune` | `$plan --mode Discovery --module plan-tune` | `skills/plan/references/legacy/plan-tune.md` |
| `/context-save` | `$plan --mode Discovery --module context-save` | `skills/plan/references/legacy/context-save.md` |
| `/context-restore` | `$plan --mode Discovery --module context-restore` | `skills/plan/references/legacy/context-restore.md` |
| `/learn` | `$plan --mode Discovery --module learn` | `skills/plan/references/legacy/learn.md` |
| `/retro` | `$plan --mode Discovery --module retro` | `skills/plan/references/legacy/retro.md` |
| `/setup-gbrain` | `$plan --mode Discovery --module setup-gbrain` | `skills/plan/references/legacy/setup-gbrain.md` |
| `/sync-gbrain` | `$plan --mode Discovery --module sync-gbrain` | `skills/plan/references/legacy/sync-gbrain.md` |
| `/design-consultation` | `$design --mode Generate --module design-consultation` | `skills/design/references/legacy/design-consultation.md` |
| `/design-shotgun` | `$design --mode Explore --module design-shotgun` | `skills/design/references/legacy/design-shotgun.md` |
| `/design-html` | `$design --mode Implement --module design-html` | `skills/design/references/legacy/design-html.md` |
| `/plan-design-review` | `$design --mode Critique --module plan-design-review` | `skills/design/references/legacy/plan-design-review.md` |
| `/design-review` | `$design --mode Implement --module design-review` | `skills/design/references/legacy/design-review.md` |
| `/ios-design-review` | `$design --mode Critique --module ios-design-review` | `skills/design/references/legacy/ios-design-review.md` |
| `/diagram` | `$design --mode Generate --module diagram` | `skills/design/references/legacy/diagram.md` |
| `/make-pdf` | `$design --mode Generate --module make-pdf` | `skills/design/references/legacy/make-pdf.md` |
| `/qa` | `$qa --mode Fix --module qa` | `skills/qa/references/legacy/qa.md` |
| `/qa-only` | `$qa --mode Report --module qa-only` | `skills/qa/references/legacy/qa-only.md` |
| `/ios-qa` | `$qa --mode Report --module ios-qa` | `skills/qa/references/legacy/ios-qa.md` |
| `/devex-review` | `$qa --mode Report --module devex-review` | `skills/qa/references/legacy/devex-review.md` |
| `/benchmark` | `$qa --mode Report --module benchmark` | `skills/qa/references/legacy/benchmark.md` |
| `/canary` | `$qa --mode Report --module canary` | `skills/qa/references/legacy/canary.md` |
| `/browse` | `$qa --mode Report --module browse` | `skills/qa/references/legacy/browse.md` |
| `/open-gstack-browser` | `$qa --mode Report --module open-gstack-browser` | `skills/qa/references/legacy/open-gstack-browser.md` |
| `/setup-browser-cookies` | `$qa --mode Report --module setup-browser-cookies` | `skills/qa/references/legacy/setup-browser-cookies.md` |
| `/pair-agent` | `$qa --mode Report --module pair-agent` | `skills/qa/references/legacy/pair-agent.md` |
| `/scrape` | `$qa --mode Report --module scrape` | `skills/qa/references/legacy/scrape.md` |
| `/skillify` | `$qa --mode Report --module skillify` | `skills/qa/references/legacy/skillify.md` |
| `/benchmark-models` | `$qa --mode Report --module benchmark-models` | `skills/qa/references/legacy/benchmark-models.md` |
| `/investigate` | `$debug --mode Diagnose-only --module investigate` | `skills/debug/references/legacy/investigate.md` |
| `/ios-fix` | `$debug --mode Fix --module ios-fix` | `skills/debug/references/legacy/ios-fix.md` |
| `/careful` | `$debug --mode Diagnose-only --module careful` | `skills/debug/references/legacy/careful.md` |
| `/freeze` | `$debug --mode Diagnose-only --module freeze` | `skills/debug/references/legacy/freeze.md` |
| `/guard` | `$debug --mode Diagnose-only --module guard` | `skills/debug/references/legacy/guard.md` |
| `/unfreeze` | `$debug --mode Diagnose-only --module unfreeze` | `skills/debug/references/legacy/unfreeze.md` |
| `/review` | `$review --mode Normal --module review` | `skills/review/references/legacy/review.md` |
| `/cso` | `$review --mode Security --module cso` | `skills/review/references/legacy/cso.md` |
| `/health` | `$review --mode Deep --module health` | `skills/review/references/legacy/health.md` |
| `/codex` | `$review --mode Deep --module codex` | `skills/review/references/legacy/codex.md` |
| `/claude` | `$review --mode Deep --module claude` | `skills/review/references/legacy/claude.md` |
| `/ship` | `$ship --mode Prepare --module ship` | `skills/ship/references/legacy/ship.md` |
| `/land-and-deploy` | `$ship --mode Land --module land-and-deploy` | `skills/ship/references/legacy/land-and-deploy.md` |
| `/landing-report` | `$ship --mode Prepare --module landing-report` | `skills/ship/references/legacy/landing-report.md` |
| `/document-release` | `$ship --mode Prepare --module document-release` | `skills/ship/references/legacy/document-release.md` |
| `/setup-deploy` | `$ship --mode Deploy --module setup-deploy` | `skills/ship/references/legacy/setup-deploy.md` |
| `/document-generate` | `$ship --mode Prepare --module document-generate` | `skills/ship/references/legacy/document-generate.md` |
| `/gstack-upgrade` | `$ship --mode Prepare --module gstack-upgrade` | `skills/ship/references/legacy/gstack-upgrade.md` |
| `/ios-clean` | `$ship --mode Prepare --module ios-clean` | `skills/ship/references/legacy/ios-clean.md` |
| `/ios-sync` | `$ship --mode Prepare --module ios-sync` | `skills/ship/references/legacy/ios-sync.md` |
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /autoplan
This is not a public/discoverable skill. Route the legacy invocation to `$plan --mode Full chain --module autoplan`, then read [the preserved module](../skills/plan/references/legacy/autoplan.md) in full.
- Tree: `plan`
- Public mode: `Full chain`
- Legacy internal alias: `auto`
- Dispatcher role: `primary`
- Mandatory specialist input: `true`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /benchmark-models
This is not a public/discoverable skill. Route the legacy invocation to `$qa --mode Report --module benchmark-models`, then read [the preserved module](../skills/qa/references/legacy/benchmark-models.md) in full.
- Tree: `qa`
- Public mode: `Report`
- Legacy internal alias: `model-benchmark`
- Dispatcher role: `internal`
- Mandatory specialist input: `false`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /benchmark
This is not a public/discoverable skill. Route the legacy invocation to `$qa --mode Report --module benchmark`, then read [the preserved module](../skills/qa/references/legacy/benchmark.md) in full.
- Tree: `qa`
- Public mode: `Report`
- Legacy internal alias: `performance`
- Dispatcher role: `primary`
- Mandatory specialist input: `true`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /browse
This is not a public/discoverable skill. Route the legacy invocation to `$qa --mode Report --module browse`, then read [the preserved module](../skills/qa/references/legacy/browse.md) in full.
- Tree: `qa`
- Public mode: `Report`
- Legacy internal alias: `browser`
- Dispatcher role: `internal`
- Mandatory specialist input: `false`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /canary
This is not a public/discoverable skill. Route the legacy invocation to `$qa --mode Report --module canary`, then read [the preserved module](../skills/qa/references/legacy/canary.md) in full.
- Tree: `qa`
- Public mode: `Report`
- Legacy internal alias: `canary`
- Dispatcher role: `primary`
- Mandatory specialist input: `true`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /careful
This is not a public/discoverable skill. Route the legacy invocation to `$debug --mode Diagnose-only --module careful`, then read [the preserved module](../skills/debug/references/legacy/careful.md) in full.
- Tree: `debug`
- Public mode: `Diagnose-only`
- Legacy internal alias: `careful`
- Dispatcher role: `internal`
- Mandatory specialist input: `false`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /claude
This is not a public/discoverable skill. Route the legacy invocation to `$review --mode Deep --module claude`, then read [the preserved module](../skills/review/references/legacy/claude.md) in full.
- Tree: `review`
- Public mode: `Deep`
- Legacy internal alias: `outside-claude`
- Dispatcher role: `primary`
- Mandatory specialist input: `true`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /codex
This is not a public/discoverable skill. Route the legacy invocation to `$review --mode Deep --module codex`, then read [the preserved module](../skills/review/references/legacy/codex.md) in full.
- Tree: `review`
- Public mode: `Deep`
- Legacy internal alias: `outside-codex`
- Dispatcher role: `primary`
- Mandatory specialist input: `true`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /context-restore
This is not a public/discoverable skill. Route the legacy invocation to `$plan --mode Discovery --module context-restore`, then read [the preserved module](../skills/plan/references/legacy/context-restore.md) in full.
- Tree: `plan`
- Public mode: `Discovery`
- Legacy internal alias: `context-restore`
- Dispatcher role: `internal`
- Mandatory specialist input: `false`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /context-save
This is not a public/discoverable skill. Route the legacy invocation to `$plan --mode Discovery --module context-save`, then read [the preserved module](../skills/plan/references/legacy/context-save.md) in full.
- Tree: `plan`
- Public mode: `Discovery`
- Legacy internal alias: `context-save`
- Dispatcher role: `internal`
- Mandatory specialist input: `false`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /cso
This is not a public/discoverable skill. Route the legacy invocation to `$review --mode Security --module cso`, then read [the preserved module](../skills/review/references/legacy/cso.md) in full.
- Tree: `review`
- Public mode: `Security`
- Legacy internal alias: `security`
- Dispatcher role: `primary`
- Mandatory specialist input: `true`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /design-consultation
This is not a public/discoverable skill. Route the legacy invocation to `$design --mode Generate --module design-consultation`, then read [the preserved module](../skills/design/references/legacy/design-consultation.md) in full.
- Tree: `design`
- Public mode: `Generate`
- Legacy internal alias: `consult`
- Dispatcher role: `primary`
- Mandatory specialist input: `true`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /design-html
This is not a public/discoverable skill. Route the legacy invocation to `$design --mode Implement --module design-html`, then read [the preserved module](../skills/design/references/legacy/design-html.md) in full.
- Tree: `design`
- Public mode: `Implement`
- Legacy internal alias: `html`
- Dispatcher role: `primary`
- Mandatory specialist input: `true`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /design-review
This is not a public/discoverable skill. Route the legacy invocation to `$design --mode Implement --module design-review`, then read [the preserved module](../skills/design/references/legacy/design-review.md) in full.
- Tree: `design`
- Public mode: `Implement`
- Legacy internal alias: `live-review`
- Dispatcher role: `primary`
- Mandatory specialist input: `true`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /design-shotgun
This is not a public/discoverable skill. Route the legacy invocation to `$design --mode Explore --module design-shotgun`, then read [the preserved module](../skills/design/references/legacy/design-shotgun.md) in full.
- Tree: `design`
- Public mode: `Explore`
- Legacy internal alias: `alternatives`
- Dispatcher role: `primary`
- Mandatory specialist input: `true`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /devex-review
This is not a public/discoverable skill. Route the legacy invocation to `$qa --mode Report --module devex-review`, then read [the preserved module](../skills/qa/references/legacy/devex-review.md) in full.
- Tree: `qa`
- Public mode: `Report`
- Legacy internal alias: `dx`
- Dispatcher role: `primary`
- Mandatory specialist input: `true`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /diagram
This is not a public/discoverable skill. Route the legacy invocation to `$design --mode Generate --module diagram`, then read [the preserved module](../skills/design/references/legacy/diagram.md) in full.
- Tree: `design`
- Public mode: `Generate`
- Legacy internal alias: `diagram`
- Dispatcher role: `internal`
- Mandatory specialist input: `false`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /document-generate
This is not a public/discoverable skill. Route the legacy invocation to `$ship --mode Prepare --module document-generate`, then read [the preserved module](../skills/ship/references/legacy/document-generate.md) in full.
- Tree: `ship`
- Public mode: `Prepare`
- Legacy internal alias: `docs-generate`
- Dispatcher role: `internal`
- Mandatory specialist input: `false`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /document-release
This is not a public/discoverable skill. Route the legacy invocation to `$ship --mode Prepare --module document-release`, then read [the preserved module](../skills/ship/references/legacy/document-release.md) in full.
- Tree: `ship`
- Public mode: `Prepare`
- Legacy internal alias: `docs`
- Dispatcher role: `primary`
- Mandatory specialist input: `true`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /freeze
This is not a public/discoverable skill. Route the legacy invocation to `$debug --mode Diagnose-only --module freeze`, then read [the preserved module](../skills/debug/references/legacy/freeze.md) in full.
- Tree: `debug`
- Public mode: `Diagnose-only`
- Legacy internal alias: `freeze`
- Dispatcher role: `internal`
- Mandatory specialist input: `false`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /gstack-upgrade
This is not a public/discoverable skill. Route the legacy invocation to `$ship --mode Prepare --module gstack-upgrade`, then read [the preserved module](../skills/ship/references/legacy/gstack-upgrade.md) in full.
- Tree: `ship`
- Public mode: `Prepare`
- Legacy internal alias: `upgrade`
- Dispatcher role: `internal`
- Mandatory specialist input: `false`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /gstack
This is not a public/discoverable skill. Route the legacy invocation to `$plan --mode Discovery --module gstack`, then read [the preserved module](../skills/plan/references/legacy/gstack.md) in full.
- Tree: `plan`
- Public mode: `Discovery`
- Legacy internal alias: `catalog`
- Dispatcher role: `internal`
- Mandatory specialist input: `false`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /guard
This is not a public/discoverable skill. Route the legacy invocation to `$debug --mode Diagnose-only --module guard`, then read [the preserved module](../skills/debug/references/legacy/guard.md) in full.
- Tree: `debug`
- Public mode: `Diagnose-only`
- Legacy internal alias: `guard`
- Dispatcher role: `internal`
- Mandatory specialist input: `false`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /health
This is not a public/discoverable skill. Route the legacy invocation to `$review --mode Deep --module health`, then read [the preserved module](../skills/review/references/legacy/health.md) in full.
- Tree: `review`
- Public mode: `Deep`
- Legacy internal alias: `health`
- Dispatcher role: `primary`
- Mandatory specialist input: `true`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /investigate
This is not a public/discoverable skill. Route the legacy invocation to `$debug --mode Diagnose-only --module investigate`, then read [the preserved module](../skills/debug/references/legacy/investigate.md) in full.
- Tree: `debug`
- Public mode: `Diagnose-only`
- Legacy internal alias: `investigate`
- Dispatcher role: `primary`
- Mandatory specialist input: `true`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /ios-clean
This is not a public/discoverable skill. Route the legacy invocation to `$ship --mode Prepare --module ios-clean`, then read [the preserved module](../skills/ship/references/legacy/ios-clean.md) in full.
- Tree: `ship`
- Public mode: `Prepare`
- Legacy internal alias: `ios-clean`
- Dispatcher role: `internal`
- Mandatory specialist input: `false`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /ios-design-review
This is not a public/discoverable skill. Route the legacy invocation to `$design --mode Critique --module ios-design-review`, then read [the preserved module](../skills/design/references/legacy/ios-design-review.md) in full.
- Tree: `design`
- Public mode: `Critique`
- Legacy internal alias: `ios-review`
- Dispatcher role: `primary`
- Mandatory specialist input: `true`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /ios-fix
This is not a public/discoverable skill. Route the legacy invocation to `$debug --mode Fix --module ios-fix`, then read [the preserved module](../skills/debug/references/legacy/ios-fix.md) in full.
- Tree: `debug`
- Public mode: `Fix`
- Legacy internal alias: `ios-fix`
- Dispatcher role: `primary`
- Mandatory specialist input: `true`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /ios-qa
This is not a public/discoverable skill. Route the legacy invocation to `$qa --mode Report --module ios-qa`, then read [the preserved module](../skills/qa/references/legacy/ios-qa.md) in full.
- Tree: `qa`
- Public mode: `Report`
- Legacy internal alias: `ios`
- Dispatcher role: `primary`
- Mandatory specialist input: `true`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /ios-sync
This is not a public/discoverable skill. Route the legacy invocation to `$ship --mode Prepare --module ios-sync`, then read [the preserved module](../skills/ship/references/legacy/ios-sync.md) in full.
- Tree: `ship`
- Public mode: `Prepare`
- Legacy internal alias: `ios-sync`
- Dispatcher role: `internal`
- Mandatory specialist input: `false`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /land-and-deploy
This is not a public/discoverable skill. Route the legacy invocation to `$ship --mode Land --module land-and-deploy`, then read [the preserved module](../skills/ship/references/legacy/land-and-deploy.md) in full.
- Tree: `ship`
- Public mode: `Land`
- Legacy internal alias: `land`
- Dispatcher role: `primary`
- Mandatory specialist input: `true`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /landing-report
This is not a public/discoverable skill. Route the legacy invocation to `$ship --mode Prepare --module landing-report`, then read [the preserved module](../skills/ship/references/legacy/landing-report.md) in full.
- Tree: `ship`
- Public mode: `Prepare`
- Legacy internal alias: `queue`
- Dispatcher role: `primary`
- Mandatory specialist input: `true`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /learn
This is not a public/discoverable skill. Route the legacy invocation to `$plan --mode Discovery --module learn`, then read [the preserved module](../skills/plan/references/legacy/learn.md) in full.
- Tree: `plan`
- Public mode: `Discovery`
- Legacy internal alias: `learning`
- Dispatcher role: `internal`
- Mandatory specialist input: `false`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /make-pdf
This is not a public/discoverable skill. Route the legacy invocation to `$design --mode Generate --module make-pdf`, then read [the preserved module](../skills/design/references/legacy/make-pdf.md) in full.
- Tree: `design`
- Public mode: `Generate`
- Legacy internal alias: `pdf`
- Dispatcher role: `internal`
- Mandatory specialist input: `false`
+674
View File
@@ -0,0 +1,674 @@
{
"schema_version": 1,
"policy": {
"default_discoverable": false,
"compatibility_window": "two minor releases or 90 days, whichever is later",
"window_started_at": "2026-07-16",
"earliest_expiry_at": "2026-10-14",
"removal_requires_release_notes": true,
"context_choice_migrated_implicitly": false,
"context_consent_migrated_implicitly": false
},
"aliases": [
{
"legacy_invocation": "/gstack",
"replacement_invocation": "$plan --mode Discovery --module gstack",
"dispatcher": "plan",
"public_mode": "Discovery",
"internal_alias": "catalog",
"preserved_module": "skills/plan/references/legacy/gstack.md",
"opt_in_alias": "skills/.compat/gstack/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/office-hours",
"replacement_invocation": "$plan --mode Discovery --module office-hours",
"dispatcher": "plan",
"public_mode": "Discovery",
"internal_alias": "product",
"preserved_module": "skills/plan/references/legacy/office-hours.md",
"opt_in_alias": "skills/.compat/office-hours/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/plan-ceo-review",
"replacement_invocation": "$plan --mode Product --module plan-ceo-review",
"dispatcher": "plan",
"public_mode": "Product",
"internal_alias": "ceo",
"preserved_module": "skills/plan/references/legacy/plan-ceo-review.md",
"opt_in_alias": "skills/.compat/plan-ceo-review/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/plan-eng-review",
"replacement_invocation": "$plan --mode Engineering --module plan-eng-review",
"dispatcher": "plan",
"public_mode": "Engineering",
"internal_alias": "eng",
"preserved_module": "skills/plan/references/legacy/plan-eng-review.md",
"opt_in_alias": "skills/.compat/plan-eng-review/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/plan-devex-review",
"replacement_invocation": "$plan --mode DX --module plan-devex-review",
"dispatcher": "plan",
"public_mode": "DX",
"internal_alias": "dx",
"preserved_module": "skills/plan/references/legacy/plan-devex-review.md",
"opt_in_alias": "skills/.compat/plan-devex-review/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/autoplan",
"replacement_invocation": "$plan --mode Full chain --module autoplan",
"dispatcher": "plan",
"public_mode": "Full chain",
"internal_alias": "auto",
"preserved_module": "skills/plan/references/legacy/autoplan.md",
"opt_in_alias": "skills/.compat/autoplan/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/spec",
"replacement_invocation": "$plan --mode Specification --module spec",
"dispatcher": "plan",
"public_mode": "Specification",
"internal_alias": "spec",
"preserved_module": "skills/plan/references/legacy/spec.md",
"opt_in_alias": "skills/.compat/spec/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/plan-tune",
"replacement_invocation": "$plan --mode Discovery --module plan-tune",
"dispatcher": "plan",
"public_mode": "Discovery",
"internal_alias": "preferences",
"preserved_module": "skills/plan/references/legacy/plan-tune.md",
"opt_in_alias": "skills/.compat/plan-tune/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/context-save",
"replacement_invocation": "$plan --mode Discovery --module context-save",
"dispatcher": "plan",
"public_mode": "Discovery",
"internal_alias": "context-save",
"preserved_module": "skills/plan/references/legacy/context-save.md",
"opt_in_alias": "skills/.compat/context-save/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/context-restore",
"replacement_invocation": "$plan --mode Discovery --module context-restore",
"dispatcher": "plan",
"public_mode": "Discovery",
"internal_alias": "context-restore",
"preserved_module": "skills/plan/references/legacy/context-restore.md",
"opt_in_alias": "skills/.compat/context-restore/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/learn",
"replacement_invocation": "$plan --mode Discovery --module learn",
"dispatcher": "plan",
"public_mode": "Discovery",
"internal_alias": "learning",
"preserved_module": "skills/plan/references/legacy/learn.md",
"opt_in_alias": "skills/.compat/learn/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/retro",
"replacement_invocation": "$plan --mode Discovery --module retro",
"dispatcher": "plan",
"public_mode": "Discovery",
"internal_alias": "retro",
"preserved_module": "skills/plan/references/legacy/retro.md",
"opt_in_alias": "skills/.compat/retro/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/setup-gbrain",
"replacement_invocation": "$plan --mode Discovery --module setup-gbrain",
"dispatcher": "plan",
"public_mode": "Discovery",
"internal_alias": "memory-setup",
"preserved_module": "skills/plan/references/legacy/setup-gbrain.md",
"opt_in_alias": "skills/.compat/setup-gbrain/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/sync-gbrain",
"replacement_invocation": "$plan --mode Discovery --module sync-gbrain",
"dispatcher": "plan",
"public_mode": "Discovery",
"internal_alias": "memory-sync",
"preserved_module": "skills/plan/references/legacy/sync-gbrain.md",
"opt_in_alias": "skills/.compat/sync-gbrain/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/design-consultation",
"replacement_invocation": "$design --mode Generate --module design-consultation",
"dispatcher": "design",
"public_mode": "Generate",
"internal_alias": "consult",
"preserved_module": "skills/design/references/legacy/design-consultation.md",
"opt_in_alias": "skills/.compat/design-consultation/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/design-shotgun",
"replacement_invocation": "$design --mode Explore --module design-shotgun",
"dispatcher": "design",
"public_mode": "Explore",
"internal_alias": "alternatives",
"preserved_module": "skills/design/references/legacy/design-shotgun.md",
"opt_in_alias": "skills/.compat/design-shotgun/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/design-html",
"replacement_invocation": "$design --mode Implement --module design-html",
"dispatcher": "design",
"public_mode": "Implement",
"internal_alias": "html",
"preserved_module": "skills/design/references/legacy/design-html.md",
"opt_in_alias": "skills/.compat/design-html/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/plan-design-review",
"replacement_invocation": "$design --mode Critique --module plan-design-review",
"dispatcher": "design",
"public_mode": "Critique",
"internal_alias": "plan-review",
"preserved_module": "skills/design/references/legacy/plan-design-review.md",
"opt_in_alias": "skills/.compat/plan-design-review/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/design-review",
"replacement_invocation": "$design --mode Implement --module design-review",
"dispatcher": "design",
"public_mode": "Implement",
"internal_alias": "live-review",
"preserved_module": "skills/design/references/legacy/design-review.md",
"opt_in_alias": "skills/.compat/design-review/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/ios-design-review",
"replacement_invocation": "$design --mode Critique --module ios-design-review",
"dispatcher": "design",
"public_mode": "Critique",
"internal_alias": "ios-review",
"preserved_module": "skills/design/references/legacy/ios-design-review.md",
"opt_in_alias": "skills/.compat/ios-design-review/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/diagram",
"replacement_invocation": "$design --mode Generate --module diagram",
"dispatcher": "design",
"public_mode": "Generate",
"internal_alias": "diagram",
"preserved_module": "skills/design/references/legacy/diagram.md",
"opt_in_alias": "skills/.compat/diagram/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/make-pdf",
"replacement_invocation": "$design --mode Generate --module make-pdf",
"dispatcher": "design",
"public_mode": "Generate",
"internal_alias": "pdf",
"preserved_module": "skills/design/references/legacy/make-pdf.md",
"opt_in_alias": "skills/.compat/make-pdf/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/qa",
"replacement_invocation": "$qa --mode Fix --module qa",
"dispatcher": "qa",
"public_mode": "Fix",
"internal_alias": "fix",
"preserved_module": "skills/qa/references/legacy/qa.md",
"opt_in_alias": null,
"alias_required": false,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/qa-only",
"replacement_invocation": "$qa --mode Report --module qa-only",
"dispatcher": "qa",
"public_mode": "Report",
"internal_alias": "report",
"preserved_module": "skills/qa/references/legacy/qa-only.md",
"opt_in_alias": "skills/.compat/qa-only/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/ios-qa",
"replacement_invocation": "$qa --mode Report --module ios-qa",
"dispatcher": "qa",
"public_mode": "Report",
"internal_alias": "ios",
"preserved_module": "skills/qa/references/legacy/ios-qa.md",
"opt_in_alias": "skills/.compat/ios-qa/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/devex-review",
"replacement_invocation": "$qa --mode Report --module devex-review",
"dispatcher": "qa",
"public_mode": "Report",
"internal_alias": "dx",
"preserved_module": "skills/qa/references/legacy/devex-review.md",
"opt_in_alias": "skills/.compat/devex-review/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/benchmark",
"replacement_invocation": "$qa --mode Report --module benchmark",
"dispatcher": "qa",
"public_mode": "Report",
"internal_alias": "performance",
"preserved_module": "skills/qa/references/legacy/benchmark.md",
"opt_in_alias": "skills/.compat/benchmark/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/canary",
"replacement_invocation": "$qa --mode Report --module canary",
"dispatcher": "qa",
"public_mode": "Report",
"internal_alias": "canary",
"preserved_module": "skills/qa/references/legacy/canary.md",
"opt_in_alias": "skills/.compat/canary/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/browse",
"replacement_invocation": "$qa --mode Report --module browse",
"dispatcher": "qa",
"public_mode": "Report",
"internal_alias": "browser",
"preserved_module": "skills/qa/references/legacy/browse.md",
"opt_in_alias": "skills/.compat/browse/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/open-gstack-browser",
"replacement_invocation": "$qa --mode Report --module open-gstack-browser",
"dispatcher": "qa",
"public_mode": "Report",
"internal_alias": "browser-visible",
"preserved_module": "skills/qa/references/legacy/open-gstack-browser.md",
"opt_in_alias": "skills/.compat/open-gstack-browser/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/setup-browser-cookies",
"replacement_invocation": "$qa --mode Report --module setup-browser-cookies",
"dispatcher": "qa",
"public_mode": "Report",
"internal_alias": "browser-auth",
"preserved_module": "skills/qa/references/legacy/setup-browser-cookies.md",
"opt_in_alias": "skills/.compat/setup-browser-cookies/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/pair-agent",
"replacement_invocation": "$qa --mode Report --module pair-agent",
"dispatcher": "qa",
"public_mode": "Report",
"internal_alias": "browser-pair",
"preserved_module": "skills/qa/references/legacy/pair-agent.md",
"opt_in_alias": "skills/.compat/pair-agent/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/scrape",
"replacement_invocation": "$qa --mode Report --module scrape",
"dispatcher": "qa",
"public_mode": "Report",
"internal_alias": "scrape",
"preserved_module": "skills/qa/references/legacy/scrape.md",
"opt_in_alias": "skills/.compat/scrape/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/skillify",
"replacement_invocation": "$qa --mode Report --module skillify",
"dispatcher": "qa",
"public_mode": "Report",
"internal_alias": "skillify",
"preserved_module": "skills/qa/references/legacy/skillify.md",
"opt_in_alias": "skills/.compat/skillify/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/benchmark-models",
"replacement_invocation": "$qa --mode Report --module benchmark-models",
"dispatcher": "qa",
"public_mode": "Report",
"internal_alias": "model-benchmark",
"preserved_module": "skills/qa/references/legacy/benchmark-models.md",
"opt_in_alias": "skills/.compat/benchmark-models/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/investigate",
"replacement_invocation": "$debug --mode Diagnose-only --module investigate",
"dispatcher": "debug",
"public_mode": "Diagnose-only",
"internal_alias": "investigate",
"preserved_module": "skills/debug/references/legacy/investigate.md",
"opt_in_alias": "skills/.compat/investigate/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/ios-fix",
"replacement_invocation": "$debug --mode Fix --module ios-fix",
"dispatcher": "debug",
"public_mode": "Fix",
"internal_alias": "ios-fix",
"preserved_module": "skills/debug/references/legacy/ios-fix.md",
"opt_in_alias": "skills/.compat/ios-fix/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/careful",
"replacement_invocation": "$debug --mode Diagnose-only --module careful",
"dispatcher": "debug",
"public_mode": "Diagnose-only",
"internal_alias": "careful",
"preserved_module": "skills/debug/references/legacy/careful.md",
"opt_in_alias": "skills/.compat/careful/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/freeze",
"replacement_invocation": "$debug --mode Diagnose-only --module freeze",
"dispatcher": "debug",
"public_mode": "Diagnose-only",
"internal_alias": "freeze",
"preserved_module": "skills/debug/references/legacy/freeze.md",
"opt_in_alias": "skills/.compat/freeze/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/guard",
"replacement_invocation": "$debug --mode Diagnose-only --module guard",
"dispatcher": "debug",
"public_mode": "Diagnose-only",
"internal_alias": "guard",
"preserved_module": "skills/debug/references/legacy/guard.md",
"opt_in_alias": "skills/.compat/guard/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/unfreeze",
"replacement_invocation": "$debug --mode Diagnose-only --module unfreeze",
"dispatcher": "debug",
"public_mode": "Diagnose-only",
"internal_alias": "unfreeze",
"preserved_module": "skills/debug/references/legacy/unfreeze.md",
"opt_in_alias": "skills/.compat/unfreeze/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/review",
"replacement_invocation": "$review --mode Normal --module review",
"dispatcher": "review",
"public_mode": "Normal",
"internal_alias": "diff",
"preserved_module": "skills/review/references/legacy/review.md",
"opt_in_alias": null,
"alias_required": false,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/cso",
"replacement_invocation": "$review --mode Security --module cso",
"dispatcher": "review",
"public_mode": "Security",
"internal_alias": "security",
"preserved_module": "skills/review/references/legacy/cso.md",
"opt_in_alias": "skills/.compat/cso/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/health",
"replacement_invocation": "$review --mode Deep --module health",
"dispatcher": "review",
"public_mode": "Deep",
"internal_alias": "health",
"preserved_module": "skills/review/references/legacy/health.md",
"opt_in_alias": "skills/.compat/health/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/codex",
"replacement_invocation": "$review --mode Deep --module codex",
"dispatcher": "review",
"public_mode": "Deep",
"internal_alias": "outside-codex",
"preserved_module": "skills/review/references/legacy/codex.md",
"opt_in_alias": "skills/.compat/codex/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/claude",
"replacement_invocation": "$review --mode Deep --module claude",
"dispatcher": "review",
"public_mode": "Deep",
"internal_alias": "outside-claude",
"preserved_module": "skills/review/references/legacy/claude.md",
"opt_in_alias": "skills/.compat/claude/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/ship",
"replacement_invocation": "$ship --mode Prepare --module ship",
"dispatcher": "ship",
"public_mode": "Prepare",
"internal_alias": "ship",
"preserved_module": "skills/ship/references/legacy/ship.md",
"opt_in_alias": null,
"alias_required": false,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/land-and-deploy",
"replacement_invocation": "$ship --mode Land --module land-and-deploy",
"dispatcher": "ship",
"public_mode": "Land",
"internal_alias": "land",
"preserved_module": "skills/ship/references/legacy/land-and-deploy.md",
"opt_in_alias": "skills/.compat/land-and-deploy/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/landing-report",
"replacement_invocation": "$ship --mode Prepare --module landing-report",
"dispatcher": "ship",
"public_mode": "Prepare",
"internal_alias": "queue",
"preserved_module": "skills/ship/references/legacy/landing-report.md",
"opt_in_alias": "skills/.compat/landing-report/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/document-release",
"replacement_invocation": "$ship --mode Prepare --module document-release",
"dispatcher": "ship",
"public_mode": "Prepare",
"internal_alias": "docs",
"preserved_module": "skills/ship/references/legacy/document-release.md",
"opt_in_alias": "skills/.compat/document-release/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/setup-deploy",
"replacement_invocation": "$ship --mode Deploy --module setup-deploy",
"dispatcher": "ship",
"public_mode": "Deploy",
"internal_alias": "setup",
"preserved_module": "skills/ship/references/legacy/setup-deploy.md",
"opt_in_alias": "skills/.compat/setup-deploy/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/document-generate",
"replacement_invocation": "$ship --mode Prepare --module document-generate",
"dispatcher": "ship",
"public_mode": "Prepare",
"internal_alias": "docs-generate",
"preserved_module": "skills/ship/references/legacy/document-generate.md",
"opt_in_alias": "skills/.compat/document-generate/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/gstack-upgrade",
"replacement_invocation": "$ship --mode Prepare --module gstack-upgrade",
"dispatcher": "ship",
"public_mode": "Prepare",
"internal_alias": "upgrade",
"preserved_module": "skills/ship/references/legacy/gstack-upgrade.md",
"opt_in_alias": "skills/.compat/gstack-upgrade/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/ios-clean",
"replacement_invocation": "$ship --mode Prepare --module ios-clean",
"dispatcher": "ship",
"public_mode": "Prepare",
"internal_alias": "ios-clean",
"preserved_module": "skills/ship/references/legacy/ios-clean.md",
"opt_in_alias": "skills/.compat/ios-clean/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
},
{
"legacy_invocation": "/ios-sync",
"replacement_invocation": "$ship --mode Prepare --module ios-sync",
"dispatcher": "ship",
"public_mode": "Prepare",
"internal_alias": "ios-sync",
"preserved_module": "skills/ship/references/legacy/ios-sync.md",
"opt_in_alias": "skills/.compat/ios-sync/SKILL.md",
"alias_required": true,
"default_discoverable": false,
"judgment_copied_into_alias": false
}
]
}
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /office-hours
This is not a public/discoverable skill. Route the legacy invocation to `$plan --mode Discovery --module office-hours`, then read [the preserved module](../skills/plan/references/legacy/office-hours.md) in full.
- Tree: `plan`
- Public mode: `Discovery`
- Legacy internal alias: `product`
- Dispatcher role: `primary`
- Mandatory specialist input: `true`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /open-gstack-browser
This is not a public/discoverable skill. Route the legacy invocation to `$qa --mode Report --module open-gstack-browser`, then read [the preserved module](../skills/qa/references/legacy/open-gstack-browser.md) in full.
- Tree: `qa`
- Public mode: `Report`
- Legacy internal alias: `browser-visible`
- Dispatcher role: `internal`
- Mandatory specialist input: `false`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /pair-agent
This is not a public/discoverable skill. Route the legacy invocation to `$qa --mode Report --module pair-agent`, then read [the preserved module](../skills/qa/references/legacy/pair-agent.md) in full.
- Tree: `qa`
- Public mode: `Report`
- Legacy internal alias: `browser-pair`
- Dispatcher role: `internal`
- Mandatory specialist input: `false`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /plan-ceo-review
This is not a public/discoverable skill. Route the legacy invocation to `$plan --mode Product --module plan-ceo-review`, then read [the preserved module](../skills/plan/references/legacy/plan-ceo-review.md) in full.
- Tree: `plan`
- Public mode: `Product`
- Legacy internal alias: `ceo`
- Dispatcher role: `primary`
- Mandatory specialist input: `true`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /plan-design-review
This is not a public/discoverable skill. Route the legacy invocation to `$design --mode Critique --module plan-design-review`, then read [the preserved module](../skills/design/references/legacy/plan-design-review.md) in full.
- Tree: `design`
- Public mode: `Critique`
- Legacy internal alias: `plan-review`
- Dispatcher role: `primary`
- Mandatory specialist input: `true`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /plan-devex-review
This is not a public/discoverable skill. Route the legacy invocation to `$plan --mode DX --module plan-devex-review`, then read [the preserved module](../skills/plan/references/legacy/plan-devex-review.md) in full.
- Tree: `plan`
- Public mode: `DX`
- Legacy internal alias: `dx`
- Dispatcher role: `primary`
- Mandatory specialist input: `true`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /plan-eng-review
This is not a public/discoverable skill. Route the legacy invocation to `$plan --mode Engineering --module plan-eng-review`, then read [the preserved module](../skills/plan/references/legacy/plan-eng-review.md) in full.
- Tree: `plan`
- Public mode: `Engineering`
- Legacy internal alias: `eng`
- Dispatcher role: `primary`
- Mandatory specialist input: `true`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /plan-tune
This is not a public/discoverable skill. Route the legacy invocation to `$plan --mode Discovery --module plan-tune`, then read [the preserved module](../skills/plan/references/legacy/plan-tune.md) in full.
- Tree: `plan`
- Public mode: `Discovery`
- Legacy internal alias: `preferences`
- Dispatcher role: `primary`
- Mandatory specialist input: `true`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /qa-only
This is not a public/discoverable skill. Route the legacy invocation to `$qa --mode Report --module qa-only`, then read [the preserved module](../skills/qa/references/legacy/qa-only.md) in full.
- Tree: `qa`
- Public mode: `Report`
- Legacy internal alias: `report`
- Dispatcher role: `primary`
- Mandatory specialist input: `true`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /qa
This is not a public/discoverable skill. Route the legacy invocation to `$qa --mode Fix --module qa`, then read [the preserved module](../skills/qa/references/legacy/qa.md) in full.
- Tree: `qa`
- Public mode: `Fix`
- Legacy internal alias: `fix`
- Dispatcher role: `primary`
- Mandatory specialist input: `true`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /retro
This is not a public/discoverable skill. Route the legacy invocation to `$plan --mode Discovery --module retro`, then read [the preserved module](../skills/plan/references/legacy/retro.md) in full.
- Tree: `plan`
- Public mode: `Discovery`
- Legacy internal alias: `retro`
- Dispatcher role: `internal`
- Mandatory specialist input: `false`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /review
This is not a public/discoverable skill. Route the legacy invocation to `$review --mode Normal --module review`, then read [the preserved module](../skills/review/references/legacy/review.md) in full.
- Tree: `review`
- Public mode: `Normal`
- Legacy internal alias: `diff`
- Dispatcher role: `primary`
- Mandatory specialist input: `true`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /scrape
This is not a public/discoverable skill. Route the legacy invocation to `$qa --mode Report --module scrape`, then read [the preserved module](../skills/qa/references/legacy/scrape.md) in full.
- Tree: `qa`
- Public mode: `Report`
- Legacy internal alias: `scrape`
- Dispatcher role: `internal`
- Mandatory specialist input: `false`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /setup-browser-cookies
This is not a public/discoverable skill. Route the legacy invocation to `$qa --mode Report --module setup-browser-cookies`, then read [the preserved module](../skills/qa/references/legacy/setup-browser-cookies.md) in full.
- Tree: `qa`
- Public mode: `Report`
- Legacy internal alias: `browser-auth`
- Dispatcher role: `internal`
- Mandatory specialist input: `false`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /setup-deploy
This is not a public/discoverable skill. Route the legacy invocation to `$ship --mode Deploy --module setup-deploy`, then read [the preserved module](../skills/ship/references/legacy/setup-deploy.md) in full.
- Tree: `ship`
- Public mode: `Deploy`
- Legacy internal alias: `setup`
- Dispatcher role: `primary`
- Mandatory specialist input: `true`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /setup-gbrain
This is not a public/discoverable skill. Route the legacy invocation to `$plan --mode Discovery --module setup-gbrain`, then read [the preserved module](../skills/plan/references/legacy/setup-gbrain.md) in full.
- Tree: `plan`
- Public mode: `Discovery`
- Legacy internal alias: `memory-setup`
- Dispatcher role: `internal`
- Mandatory specialist input: `false`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /ship
This is not a public/discoverable skill. Route the legacy invocation to `$ship --mode Prepare --module ship`, then read [the preserved module](../skills/ship/references/legacy/ship.md) in full.
- Tree: `ship`
- Public mode: `Prepare`
- Legacy internal alias: `ship`
- Dispatcher role: `primary`
- Mandatory specialist input: `true`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /skillify
This is not a public/discoverable skill. Route the legacy invocation to `$qa --mode Report --module skillify`, then read [the preserved module](../skills/qa/references/legacy/skillify.md) in full.
- Tree: `qa`
- Public mode: `Report`
- Legacy internal alias: `skillify`
- Dispatcher role: `internal`
- Mandatory specialist input: `false`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /spec
This is not a public/discoverable skill. Route the legacy invocation to `$plan --mode Specification --module spec`, then read [the preserved module](../skills/plan/references/legacy/spec.md) in full.
- Tree: `plan`
- Public mode: `Specification`
- Legacy internal alias: `spec`
- Dispatcher role: `primary`
- Mandatory specialist input: `true`
+10
View File
@@ -0,0 +1,10 @@
<!-- GENERATED by scripts/gstack2/generate-skill-tree.ts; do not edit. -->
# Compatibility alias: /sync-gbrain
This is not a public/discoverable skill. Route the legacy invocation to `$plan --mode Discovery --module sync-gbrain`, then read [the preserved module](../skills/plan/references/legacy/sync-gbrain.md) in full.
- Tree: `plan`
- Public mode: `Discovery`
- Legacy internal alias: `memory-sync`
- Dispatcher role: `internal`
- Mandatory specialist input: `false`

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