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 name: gstack
preamble-tier: 1 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.
version: 1.2.0 metadata:
description: | internal: true
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
--- ---
{{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 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.
(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`.
+3 -1
View File
@@ -1,5 +1,5 @@
--- ---
name: autoplan name: gstack-1-autoplan
preamble-tier: 3 preamble-tier: 3
version: 1.0.0 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) 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 - Grep
- WebSearch - WebSearch
- AskUserQuestion - AskUserQuestion
metadata:
internal: true
--- ---
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly --> <!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
<!-- Regenerate: bun run gen:skill-docs --> <!-- 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 preamble-tier: 1
version: 1.0.0 version: 1.0.0
description: Cross-model benchmark for gstack skills. (gstack) description: Cross-model benchmark for gstack skills. (gstack)
@@ -12,6 +12,8 @@ allowed-tools:
- Bash - Bash
- Read - Read
- AskUserQuestion - AskUserQuestion
metadata:
internal: true
--- ---
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly --> <!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
<!-- Regenerate: bun run gen:skill-docs --> <!-- Regenerate: bun run gen:skill-docs -->
+3 -1
View File
@@ -1,5 +1,5 @@
--- ---
name: benchmark name: gstack-1-benchmark
preamble-tier: 1 preamble-tier: 1
version: 1.0.0 version: 1.0.0
description: Performance regression detection using the browse daemon. (gstack) description: Performance regression detection using the browse daemon. (gstack)
@@ -13,6 +13,8 @@ allowed-tools:
- Write - Write
- Glob - Glob
- AskUserQuestion - AskUserQuestion
metadata:
internal: true
--- ---
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly --> <!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
<!-- Regenerate: bun run gen:skill-docs --> <!-- 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" ln -s "$REPO_ROOT" "$AGENTS_LINK"
fi 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 # GStack 2 delegates skill placement to the standard Agent Skills installer,
# forwarded pty, so any `read` in setup (skill-prefix prompt, plan-tune hook # and root ./setup installs only the optional per-user runtime. Development can
# consent) would hang the workspace forever. Detaching stdin makes every setup # execute the checked-out binaries directly, so invoking ./setup here would
# prompt take its smart non-interactive default (flat skill names, etc.). # mutate user state without adding any workspace capability.
# #
# `--plan-tune-hooks=prompt` is load-bearing, not redundant: stdin alone only # Brain-aware (gbrain) blocks render into an untracked workspace dir.
# 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.
# #
# The worktree's SKILL.md files stay canonical (the guard above). If gbrain is # The worktree's SKILL.md files stay canonical (the guard above). If gbrain is
# installed, render the :user variant (with GBRAIN_CONTEXT_LOAD + # 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 # 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 # 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 # the global install. Result: this workspace gets the full gbrain experience
# while git stays clean. Other projects pick up blocks via `gstack-config # while git stays clean. Production skill placement and updates remain owned by
# gbrain-refresh` (printed below). # the standard Agent Skills installer.
GBRAIN_DETECT="$REPO_ROOT/bin/gstack-gbrain-detect" GBRAIN_DETECT="$REPO_ROOT/bin/gstack-gbrain-detect"
RENDER_DIR="$REPO_ROOT/.claude/gstack-rendered" RENDER_DIR="$REPO_ROOT/.claude/gstack-rendered"
if [ -x "$GBRAIN_DETECT" ] && "$GBRAIN_DETECT" --is-ok 2>/dev/null; then 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 " .agents/skills/gstack → $REPO_ROOT"
echo "Edit any SKILL.md and test immediately — no copy/deploy needed." echo "Edit any SKILL.md and test immediately — no copy/deploy needed."
echo "" 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 " gstack-config gbrain-refresh"
echo "Then use the standard Agent Skills installer for production skill updates."
echo "" echo ""
echo "To tear down: bin/dev-teardown" 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: * Cache layout:
* ~/.gstack/brain-cache/ ← cross-project (user-profile only) * ~/.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 * Atomic writes via .tmp + rename. Stale-but-usable fallback when brain
* unreachable. Concurrent-refresh dedup is a follow-up commit (T15). * 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 { homedir, hostname } from 'os';
import { spawnSync } from 'child_process'; import { spawnSync } from 'child_process';
import { execGbrainJson, spawnGbrain } from '../lib/gbrain-exec'; import { execGbrainJson, spawnGbrain } from '../lib/gbrain-exec';
import { discoverProjectIdentity } from '../runtime/identity.js';
import { import {
BRAIN_CACHE_ENTITIES, BRAIN_CACHE_ENTITIES,
CACHE_REFRESH_LOCK_TIMEOUT_MS, CACHE_REFRESH_LOCK_TIMEOUT_MS,
@@ -39,6 +40,25 @@ import {
const GSTACK_HOME = process.env.GSTACK_HOME || join(homedir(), '.gstack'); 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 { interface CacheMeta {
/** Version of the schema pack the cache was built against. Mismatch → full rebuild. */ /** Version of the schema pack the cache was built against. Mismatch → full rebuild. */
schema_version: string; schema_version: string;
@@ -51,34 +71,36 @@ interface CacheMeta {
} }
/** Returns the directory holding a given entity's cache file. */ /** 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') { if (entity.scope === 'cross-project') {
return join(GSTACK_HOME, 'brain-cache'); 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}`); 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. */ /** 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]; const entity = BRAIN_CACHE_ENTITIES[entityName];
if (!entity) throw new Error(`Unknown brain cache entity: ${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. */ /** 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') { if (scope === 'cross-project') {
return join(GSTACK_HOME, 'brain-cache', '_meta.json'); return join(GSTACK_HOME, 'brain-cache', '_meta.json');
} }
if (!projectSlug) throw new Error('Per-project meta needs a project slug'); const stateId = projectStateId(project);
return join(GSTACK_HOME, 'projects', projectSlug, 'brain-cache', '_meta.json'); 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 { function loadMeta(scope: 'cross-project' | 'per-project', project: ProjectRef): CacheMeta {
const path = metaPath(scope, projectSlug); const path = metaPath(scope, project);
if (!existsSync(path)) { if (!existsSync(path)) {
return { schema_version: GSTACK_SCHEMA_PACK_VERSION, endpoint_hash: detectEndpointHash(), last_refresh: {}, last_attempt: {} }; 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 { function saveMeta(scope: 'cross-project' | 'per-project', project: ProjectRef, meta: CacheMeta): void {
const path = metaPath(scope, projectSlug); const path = metaPath(scope, project);
mkdirSync(dirname(path), { recursive: true }); mkdirSync(dirname(path), { recursive: true });
atomicWrite(path, JSON.stringify(meta, null, 2)); atomicWrite(path, JSON.stringify(meta, null, 2));
} }
@@ -128,17 +150,8 @@ function sha8(input: string): string {
* (different endpoint → different cache). * (different endpoint → different cache).
*/ */
export function detectEndpointHash(): string { export function detectEndpointHash(): string {
const claudeJsonPath = join(homedir(), '.claude.json'); const endpoint = process.env.GSTACK_GBRAIN_ENDPOINT || process.env.GBRAIN_URL;
if (existsSync(claudeJsonPath)) { if (typeof endpoint === 'string' && endpoint.length > 0) return sha8(endpoint);
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 */ }
}
// Local engine — no endpoint URL; use a stable literal hash. // Local engine — no endpoint URL; use a stable literal hash.
return 'local'; return 'local';
} }
@@ -168,8 +181,8 @@ function isStale(entityName: string, meta: CacheMeta): boolean {
} }
/** Returns true if the cache file exists on disk. */ /** Returns true if the cache file exists on disk. */
function hasFile(entityName: string, projectSlug: string | null): boolean { function hasFile(entityName: string, project: ProjectRef): boolean {
return existsSync(entityPath(entityName, projectSlug)); return existsSync(entityPath(entityName, project));
} }
/** Returns true if schema version recorded in meta differs from current pack version. */ /** Returns true if schema version recorded in meta differs from current pack version. */
@@ -195,39 +208,39 @@ interface GetResult {
message?: string; 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]; const entity = BRAIN_CACHE_ENTITIES[entityName];
if (!entity) throw new Error(`Unknown entity: ${entityName}`); if (!entity) throw new Error(`Unknown entity: ${entityName}`);
const scope = entity.scope; const scope = entity.scope;
const meta = loadMeta(scope, projectSlug); const meta = loadMeta(scope, project);
// Schema-version mismatch → full rebuild (D4 A4). // Schema-version mismatch → full rebuild (D4 A4).
if (schemaVersionMismatch(meta) || endpointSwitched(meta)) { if (schemaVersionMismatch(meta) || endpointSwitched(meta)) {
rebuildAllForScope(scope, projectSlug); rebuildAllForScope(scope, project);
// After rebuild, meta is fresh; fall through to warm path. // After rebuild, meta is fresh; fall through to warm path.
const newMeta = loadMeta(scope, projectSlug); const newMeta = loadMeta(scope, project);
if (hasFile(entityName, projectSlug) && !isStale(entityName, newMeta)) { if (hasFile(entityName, project) && !isStale(entityName, newMeta)) {
return { path: entityPath(entityName, projectSlug), state: 'warm' }; return { path: entityPath(entityName, project), state: 'warm' };
} }
// Rebuild may have failed for this entity specifically. // 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)) { if (hasFile(entityName, project) && !isStale(entityName, meta)) {
return { path: entityPath(entityName, projectSlug), state: 'warm' }; return { path: entityPath(entityName, project), state: 'warm' };
} }
// Stale or missing — try cold refresh. // Stale or missing — try cold refresh.
const refreshed = refreshEntity(entityName, projectSlug); const refreshed = refreshEntity(entityName, project);
if (refreshed) { 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. // Refresh failed. Use stale-but-usable if file exists.
if (hasFile(entityName, projectSlug)) { if (hasFile(entityName, project)) {
return { path: entityPath(entityName, projectSlug), state: 'stale-fallback', message: 'brain unreachable; using stale cache' }; return { path: entityPath(entityName, project), state: 'stale-fallback', message: 'brain unreachable; using stale cache' };
} }
// No cache and no refresh = missing. // 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 * concurrent attempts from different projects on cross-project entities
* serialize naturally because they're rare and the lock window is short. * serialize naturally because they're rare and the lock window is short.
*/ */
function lockPath(projectSlug: string | null): string { function lockPath(project: ProjectRef): string {
const dir = projectSlug const stateId = projectStateId(project);
? join(GSTACK_HOME, 'projects', projectSlug, 'brain-cache') const dir = stateId
? join(GSTACK_HOME, 'projects', stateId, 'brain-cache')
: join(GSTACK_HOME, 'brain-cache'); : join(GSTACK_HOME, 'brain-cache');
return join(dir, '.refresh.lock'); return join(dir, '.refresh.lock');
} }
@@ -261,8 +275,8 @@ interface LockHandle {
* (and the lock is fresh). Stale locks (process dead OR older than the * (and the lock is fresh). Stale locks (process dead OR older than the
* timeout) are taken over. * timeout) are taken over.
*/ */
function tryAcquireLock(projectSlug: string | null): LockHandle | null { function tryAcquireLock(project: ProjectRef): LockHandle | null {
const path = lockPath(projectSlug); const path = lockPath(project);
mkdirSync(dirname(path), { recursive: true }); mkdirSync(dirname(path), { recursive: true });
// If a lock exists, see if it's stale // 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 * (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. * (process dead, or older than CACHE_REFRESH_LOCK_TIMEOUT_MS) are taken over.
*/ */
export function withRefreshLock<T>(projectSlug: string | null, fn: () => T): T | 'dedup' { export function withRefreshLock<T>(project: ProjectRef, fn: () => T): T | 'dedup' {
const handle = tryAcquireLock(projectSlug); const handle = tryAcquireLock(project);
if (!handle) return 'dedup'; if (!handle) return 'dedup';
try { try {
return fn(); 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. */ /** 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]; const entity = BRAIN_CACHE_ENTITIES[entityName];
if (!entity) return false; if (!entity) return false;
// Mark attempt // Mark attempt
const meta = loadMeta(entity.scope, projectSlug); const meta = loadMeta(entity.scope, project);
meta.last_attempt = meta.last_attempt || {}; meta.last_attempt = meta.last_attempt || {};
meta.last_attempt[entityName] = Date.now(); 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. // (recent-decisions, salience) need different queries from direct page reads.
// For T2a we implement the direct-page path; derived digests get filled in by // For T2a we implement the direct-page path; derived digests get filled in by
// the resolver / write-back paths in later commits. // the resolver / write-back paths in later commits.
const digestContent = fetchAndCompressEntity(entityName, projectSlug); const digestContent = fetchAndCompressEntity(entityName, project);
if (digestContent === null) { if (digestContent === null) {
saveMeta(entity.scope, projectSlug, meta); saveMeta(entity.scope, project, meta);
return false; return false;
} }
@@ -363,12 +377,12 @@ export function refreshEntity(entityName: string, projectSlug: string | null): b
final = truncateToBudget(final, entity.budget_bytes); final = truncateToBudget(final, entity.budget_bytes);
} }
atomicWrite(entityPath(entityName, projectSlug), final); atomicWrite(entityPath(entityName, project), final);
meta.last_refresh[entityName] = Date.now(); meta.last_refresh[entityName] = Date.now();
// Keep schema/endpoint identity fresh. // Keep schema/endpoint identity fresh.
meta.schema_version = GSTACK_SCHEMA_PACK_VERSION; meta.schema_version = GSTACK_SCHEMA_PACK_VERSION;
meta.endpoint_hash = detectEndpointHash(); meta.endpoint_hash = detectEndpointHash();
saveMeta(entity.scope, projectSlug, meta); saveMeta(entity.scope, project, meta);
return true; 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). * Refresh all entities for a scope (per-project or cross-project).
* Used by --full and by schema/endpoint-change rebuilds. * 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 success = 0;
let failed = 0; let failed = 0;
for (const [name, entity] of Object.entries(BRAIN_CACHE_ENTITIES)) { for (const [name, entity] of Object.entries(BRAIN_CACHE_ENTITIES)) {
// Cross-project entities only refresh when explicitly targeted via no-slug calls // Cross-project entities only refresh when explicitly targeted via no-slug calls
if (entity.scope === 'cross-project' && projectSlug) continue; if (entity.scope === 'cross-project' && project) continue;
if (entity.scope === 'per-project' && !projectSlug) continue; if (entity.scope === 'per-project' && !project) continue;
if (refreshEntity(name, projectSlug)) success++; else failed++; if (refreshEntity(name, project)) success++; else failed++;
} }
return { success, failed }; return { success, failed };
} }
/** Rebuild on schema-version mismatch or endpoint switch. Wipes affected scope first. */ /** 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. // Wipe files but preserve dir; meta gets fully rewritten by refreshes below.
for (const [name, entity] of Object.entries(BRAIN_CACHE_ENTITIES)) { for (const [name, entity] of Object.entries(BRAIN_CACHE_ENTITIES)) {
if (entity.scope !== scope) continue; if (entity.scope !== scope) continue;
const p = entityPath(name, projectSlug); const p = entityPath(name, project);
if (existsSync(p)) { if (existsSync(p)) {
try { unlinkSync(p); } catch { /* best effort */ } try { unlinkSync(p); } catch { /* best effort */ }
} }
@@ -405,11 +419,11 @@ function rebuildAllForScope(scope: 'cross-project' | 'per-project', projectSlug:
last_refresh: {}, last_refresh: {},
last_attempt: {}, last_attempt: {},
}; };
saveMeta(scope, projectSlug, fresh); saveMeta(scope, project, fresh);
// Refresh all entities in this scope // Refresh all entities in this scope
for (const [name, entity] of Object.entries(BRAIN_CACHE_ENTITIES)) { for (const [name, entity] of Object.entries(BRAIN_CACHE_ENTITIES)) {
if (entity.scope !== scope) continue; 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 // 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]; const entity = BRAIN_CACHE_ENTITIES[entityName];
if (!entity) throw new Error(`Unknown entity: ${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]; 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. * For T2a we implement the entity → page-slug mapping for the simple cases.
* Derived digests (recent-decisions, salience) get specialized paths. * 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) { switch (entityName) {
case 'user-profile': case 'user-profile':
return fetchUserProfile(); return fetchUserProfile();
@@ -541,8 +556,7 @@ export function getSalienceAllowlist(): ReadonlyArray<string> {
// Shell out to gstack-config with a tight timeout. Falls back to defaults // Shell out to gstack-config with a tight timeout. Falls back to defaults
// on any failure (config script missing, command non-zero, parse error). // on any failure (config script missing, command non-zero, parse error).
try { try {
const skillRoot = join(homedir(), '.claude', 'skills', 'gstack'); const bin = join(process.env.GSTACK_BIN || join(GSTACK_HOME, 'bin'), 'gstack-config');
const bin = join(skillRoot, 'bin', 'gstack-config');
if (!existsSync(bin)) return SALIENCE_DEFAULT_ALLOWLIST; if (!existsSync(bin)) return SALIENCE_DEFAULT_ALLOWLIST;
const result = spawnSync(bin, ['get', 'salience_allowlist'], { timeout: 2000, encoding: 'utf-8' }); const result = spawnSync(bin, ['get', 'salience_allowlist'], { timeout: 2000, encoding: 'utf-8' });
if (result.status !== 0 || !result.stdout) return SALIENCE_DEFAULT_ALLOWLIST; if (result.status !== 0 || !result.stdout) return SALIENCE_DEFAULT_ALLOWLIST;
@@ -639,8 +653,8 @@ export function cmdDigest(slug: string): string | null {
// Subcommand: meta // Subcommand: meta
// ────────────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────────────
export function cmdMeta(projectSlug: string | null): CacheMeta { export function cmdMeta(project: ProjectRef): CacheMeta {
if (projectSlug) return loadMeta('per-project', projectSlug); if (project) return loadMeta('per-project', project);
return loadMeta('cross-project', null); return loadMeta('cross-project', null);
} }
@@ -665,8 +679,10 @@ export interface BootstrapDraft {
competitive_intel?: { slug: string; title: string; body: string }; 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 draft: BootstrapDraft = {};
const projectSlug = projectNamespace(project) as string;
const stateId = projectStateId(project) as string;
const repoRoot = process.env.GSTACK_REPO_ROOT || process.cwd(); const repoRoot = process.env.GSTACK_REPO_ROOT || process.cwd();
// Product synthesis: CLAUDE.md headline + README first paragraph // 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" // 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); const goalsHints = synthesizeGoalsHints(learningsPath, repoRoot);
if (goalsHints.length > 0) { if (goalsHints.length > 0) {
draft.goals = goalsHints.slice(0, 3).map((hint, idx) => ({ 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 * 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. * 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. // 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 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 }> = []; 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 }; 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; 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 { function printUsage(): void {
@@ -849,14 +872,14 @@ Subcommands:
async function main(): Promise<number> { async function main(): Promise<number> {
const { cmd, positional, flags } = parseArgs(process.argv); const { cmd, positional, flags } = parseArgs(process.argv);
const projectSlug = projectSlugFromFlag(flags); const project = await projectTargetFromFlag(flags);
try { try {
switch (cmd) { switch (cmd) {
case 'get': { case 'get': {
const entityName = positional[0]; const entityName = positional[0];
if (!entityName) { printUsage(); return 1; } if (!entityName) { printUsage(); return 1; }
const result = cmdGet(entityName, projectSlug); const result = cmdGet(entityName, project);
if (result.state === 'missing') { if (result.state === 'missing') {
process.stderr.write(`(${result.state}: ${result.message ?? 'no cache'})\n`); process.stderr.write(`(${result.state}: ${result.message ?? 'no cache'})\n`);
return 2; return 2;
@@ -872,7 +895,7 @@ async function main(): Promise<number> {
// another process is already mid-refresh on the same project. // another process is already mid-refresh on the same project.
if (flags.entity) { if (flags.entity) {
const entityName = String(flags.entity); const entityName = String(flags.entity);
const result = withRefreshLock(projectSlug, () => refreshEntity(entityName, projectSlug)); const result = withRefreshLock(project, () => refreshEntity(entityName, project));
if (result === 'dedup') { if (result === 'dedup') {
process.stderr.write(`(dedup: another refresh in flight)\n`); process.stderr.write(`(dedup: another refresh in flight)\n`);
return 3; return 3;
@@ -880,7 +903,7 @@ async function main(): Promise<number> {
process.stdout.write(result ? `refreshed ${entityName}\n` : `failed to refresh ${entityName}\n`); process.stdout.write(result ? `refreshed ${entityName}\n` : `failed to refresh ${entityName}\n`);
return result ? 0 : 1; return result ? 0 : 1;
} }
const allResult = withRefreshLock(projectSlug, () => refreshAll(projectSlug)); const allResult = withRefreshLock(project, () => refreshAll(project));
if (allResult === 'dedup') { if (allResult === 'dedup') {
process.stderr.write(`(dedup: another refresh in flight)\n`); process.stderr.write(`(dedup: another refresh in flight)\n`);
return 3; return 3;
@@ -891,7 +914,7 @@ async function main(): Promise<number> {
case 'invalidate': { case 'invalidate': {
const entityName = positional[0]; const entityName = positional[0];
if (!entityName) { printUsage(); return 1; } if (!entityName) { printUsage(); return 1; }
cmdInvalidate(entityName, projectSlug); cmdInvalidate(entityName, project);
process.stdout.write(`invalidated ${entityName}\n`); process.stdout.write(`invalidated ${entityName}\n`);
return 0; return 0;
} }
@@ -907,21 +930,21 @@ async function main(): Promise<number> {
return 0; return 0;
} }
case 'meta': { case 'meta': {
const meta = cmdMeta(projectSlug); const meta = cmdMeta(project);
process.stdout.write(JSON.stringify(meta, null, 2) + '\n'); process.stdout.write(JSON.stringify(meta, null, 2) + '\n');
return 0; return 0;
} }
case 'bootstrap': { case 'bootstrap': {
if (!projectSlug) { if (!project) {
process.stderr.write('bootstrap requires --project <slug>\n'); process.stderr.write('bootstrap requires --project <slug>\n');
return 1; return 1;
} }
const draft = cmdBootstrap(projectSlug); const draft = cmdBootstrap(project);
process.stdout.write(JSON.stringify(draft, null, 2) + '\n'); process.stdout.write(JSON.stringify(draft, null, 2) + '\n');
return 0; return 0;
} }
case 'list': { case 'list': {
const pages = cmdList(projectSlug); const pages = cmdList(project);
if (flags.json) { if (flags.json) {
process.stdout.write(JSON.stringify(pages, null, 2) + '\n'); process.stdout.write(JSON.stringify(pages, null, 2) + '\n');
} else { } else {
+7 -3
View File
@@ -78,12 +78,13 @@ _gstack_codex_log_event() {
local _event="$1" local _event="$1"
local _duration="${2:-0}" local _duration="${2:-0}"
[ "${_TEL:-off}" = "off" ] && return 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 local _ts
_ts=$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo unknown) _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' \ printf '{"skill":"codex","event":"%s","duration_s":"%s","ts":"%s"}\n' \
"$_event" "$_duration" "$_ts" \ "$_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 -------------------------------------------------- # --- Learnings log on hang --------------------------------------------------
@@ -94,7 +95,10 @@ _gstack_codex_log_hang() {
# Best-effort: errors swallowed. # Best-effort: errors swallowed.
local _mode="${1:-unknown}" local _mode="${1:-unknown}"
local _prompt_size="${2:-0}" 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 [ -x "$_log_bin" ] || return 0
local _key="codex-hang-$(date +%s 2>/dev/null || echo unknown)" 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")" \ "$_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 #!/usr/bin/env node
# gstack-config — read/write ~/.gstack/config.yaml // Compatibility adapter for preserved specialist modules.
# // config.json is the only writable config authority. A legacy config.yaml may
# Usage: // be read as a migration fallback, but this command never writes YAML.
# 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
STATE_DIR="${GSTACK_STATE_ROOT:-${GSTACK_HOME:-${GSTACK_STATE_DIR:-$HOME/.gstack}}}" import fs from "node:fs/promises";
CONFIG_FILE="$STATE_DIR/config.yaml" 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`. const scriptDir = path.dirname(fileURLToPath(import.meta.url));
# Default semantics: DEFAULTS table below is the canonical source. Header text const env = process.env.GSTACK_HOME
# is documentation that must stay in sync with DEFAULTS. ? process.env
CONFIG_HEADER='# gstack configuration — edit freely, changes take effect on next skill run. : process.env.GSTACK_STATE_ROOT
# Docs: https://github.com/garrytan/gstack ? { ...process.env, GSTACK_HOME: process.env.GSTACK_STATE_ROOT }
# : process.env.GSTACK_STATE_DIR
# ─── Behavior ──────────────────────────────────────────────────────── ? { ...process.env, GSTACK_HOME: process.env.GSTACK_STATE_DIR }
# proactive: true # Auto-invoke skills when your request matches one. : process.env;
# # Set to false to only run skills you type explicitly. const home = path.resolve(env.GSTACK_HOME || path.join(os.homedir(), ".gstack"));
# const legacyConfig = path.join(home, "config.yaml");
# routing_declined: false # Set to true to skip the CLAUDE.md routing injection const defaults = Object.freeze({
# # prompt. Set back to false to be asked again. proactive: true,
# routing_declined: false,
# ─── Telemetry ─────────────────────────────────────────────────────── telemetry: "off",
# telemetry: off # off | anonymous | community auto_upgrade: false,
# # off — no data sent, no local analytics (default) update_check: true,
# # anonymous — counter only, no device ID skill_prefix: false,
# # community — usage data + stable device ID checkpoint_mode: "explicit",
# checkpoint_push: false,
# ─── Updates ───────────────────────────────────────────────────────── explain_level: "default",
# auto_upgrade: false # true = silently upgrade on session start codex_reviews: "enabled",
# update_check: true # false = suppress version check notifications gstack_contributor: false,
# skip_eng_review: false,
# ─── Skill naming ──────────────────────────────────────────────────── workspace_root: path.join(os.homedir(), "conductor", "workspaces"),
# skill_prefix: false # true = namespace skills as /gstack-qa, /gstack-ship cross_project_learnings: "",
# # false = short names /qa, /ship artifacts_sync_mode: "off",
# artifacts_sync_mode_prompted: false,
# ─── Checkpoint ────────────────────────────────────────────────────── plan_tune_hooks: "prompt",
# checkpoint_mode: explicit # explicit | continuous redact_repo_visibility: "",
# # explicit — commit only when you run /ship or /checkpoint redact_prepush_hook: false,
# # continuous — auto-commit after each significant change salience_allowlist: "",
# # 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.
#
'
# DEFAULTS table — canonical default values for known keys. const [command, ...args] = process.argv.slice(2);
# `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
redact_repo_visibility) echo "" ;; # empty → fall through to gh/glab detection try {
redact_prepush_hook) echo "false" ;; switch (command) {
# Brain-aware planning (v1.48 / T5+T10+T16). Defaults documented inline: case "get":
# brain_trust_policy@<hash> — unset on fresh install; setup-gbrain await getCommand(args);
# writes 'personal' for local engines, break;
# asks the user for remote-ambiguous. case "set":
# salience_allowlist — empty falls through to await setCommand(args);
# SALIENCE_DEFAULT_ALLOWLIST (D9). break;
# user_slug_at_<hash> — empty triggers resolve-user-slug case "list":
# fallback chain (D4 A3) on first call. await listCommand();
brain_trust_policy*) echo "unset" ;; break;
salience_allowlist) echo "" ;; case "defaults":
user_slug_at_*) echo "" ;; printEntries(defaults);
*) echo "" ;; break;
esac 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;
} }
# ────────────────────────────────────────────────────────────────────── async function getCommand(args) {
# Brain-integration helpers (T5+T10+T16) if (args.length !== 1) throw new Error("Usage: gstack-config get <key>");
# ────────────────────────────────────────────────────────────────────── const key = validateKey(args[0]);
let value;
# Compute sha8 of a string. Used for endpoint hashing. if (await exists(path.join(home, "config.json"))) value = await configGet(home, key);
sha8_of() { if (value === undefined && await exists(legacyConfig)) value = await readLegacyValue(key);
printf '%s' "$1" | shasum -a 256 | cut -c1-8 if (value === undefined) value = defaultFor(key);
process.stdout.write(formatValue(value));
} }
# Detect the active brain endpoint hash. Reads ~/.claude.json for the gbrain async function setCommand(args) {
# MCP server URL. Falls back to the literal 'local' when no MCP is configured. if (args.length !== 2) throw new Error("Usage: gstack-config set <key> <value>");
endpoint_hash() { const key = validateKey(args[0]);
_claude_json="$HOME/.claude.json" const raw = validateClosedValue(key, args[1]);
if [ -f "$_claude_json" ] && command -v jq >/dev/null 2>&1; then await mutateConfigHome(() => configSet(home, key, parseConfigValue(raw)));
_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"
} }
# Detect endpoint hash collisions. When two distinct endpoints share the same async function listCommand() {
# sha8 prefix (rare but possible), escalate to sha16 by emitting the longer const stored = await exists(path.join(home, "config.json"))
# hash. Detection: scan config file for existing brain_trust_policy@<hash> or ? await loadConfig(home)
# user_slug_at_<hash> keys; if any non-active hash equals the active sha8 but : await readLegacyConfig(home);
# would differ at sha16, the active endpoint needs sha16. const flattened = { ...defaults, ...flatten(stored) };
endpoint_hash_with_collision_check() { printEntries(flattened);
_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"
} }
# Resolve the user-slug per D4 A3 chain: function defaultFor(key) {
# 1. mcp__gbrain__whoami.client_name (best effort via gbrain CLI shell-out) if (/^brain_trust_policy(?:@|$)/.test(key)) return "unset";
# 2. $USER env return Object.hasOwn(defaults, key) ? defaults[key] : "";
# 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"
} }
case "${1:-}" in function validateKey(key) {
get) if (typeof key !== "string" || !/^[a-zA-Z0-9_]+(?:@[a-f0-9]+)?$/.test(key)) {
KEY="${2:?Usage: gstack-config get <key>}" throw new Error("key must contain only alphanumeric characters, underscores, and an optional @<hex-hash> suffix");
# Validate key (alphanumeric + underscore + optional @<hash> suffix for }
# endpoint-namespaced keys introduced by the brain-aware planning layer) return key;
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"
# Summarize for the user. Use python (already required elsewhere) to function validateClosedValue(key, value) {
# parse the JSON portably; fall back to grep if python is unavailable. const domains = [
PYTHON_CMD=$(command -v python3 || command -v python || true) [/^brain_trust_policy(?:@|$)/, ["personal", "shared", "unset"], "unset"],
if [ -n "$PYTHON_CMD" ]; then [/^explain_level$/, ["default", "terse"], "default"],
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) [/^artifacts_sync_mode$/, ["off", "artifacts-only", "full"], "off"],
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) [/^redact_repo_visibility$/, ["public", "private", "unknown"], "unknown"],
else [/^redact_prepush_hook$/, ["true", "false"], "false"],
STATUS=$(grep -o '"gbrain_local_status":[[:space:]]*"[^"]*"' "$DETECTION_FILE" | sed 's/.*"\([^"]*\)"$/\1/') [/^plan_tune_hooks$/, ["prompt", "yes", "no"], "prompt"],
VERSION=$(grep -o '"gbrain_version":[[:space:]]*"[^"]*"' "$DETECTION_FILE" | sed 's/.*"\([^"]*\)"$/\1/') ];
[ -z "$STATUS" ] && STATUS=unknown if (key === "codex_reviews" && !["enabled", "disabled"].includes(value)) {
[ -z "$VERSION" ] && VERSION=unknown throw new Error(`codex_reviews '${value}' not recognized. Valid values: enabled, disabled. Existing value left unchanged.`);
fi }
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 async function readLegacyValue(key) {
ok|timeout) const content = await fs.readFile(legacyConfig, "utf8");
# "timeout" = slow-but-healthy engine (#1964) — same treatment as let found;
# "ok", matching gstack-gbrain-detect --is-ok and gen-skill-docs. for (const line of content.split(/\r?\n/)) {
echo "Detected gbrain v$VERSION (local-status: $STATUS)." const match = line.match(/^([A-Za-z0-9_]+(?:@[a-f0-9]+)?):\s*(.*?)\s*(?:#.*)?$/);
# Render brain-aware blocks INTO the global install so EVERY project's if (match?.[1] === key) found = parseConfigValue(unquote(match[2]));
# Claude sessions get them (other projects read SKILL.md + sections from }
# ~/.claude/skills/gstack via absolute paths baked at gen time). Guards return found;
# (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. function unquote(value) {
INSTALL_DIR="$HOME/.claude/skills/gstack" if (value.length >= 2 && ((value.startsWith('"') && value.endsWith('"')) ||
if [ ! -d "$INSTALL_DIR" ]; then (value.startsWith("'") && value.endsWith("'")))) return value.slice(1, -1);
echo "No global install at $INSTALL_DIR — nothing to render. (Dev workspaces get blocks via bin/dev-setup.)" return value;
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 function endpointHash() {
echo "Skip: $INSTALL_DIR doesn't look like a gstack clone (missing VERSION/package.json) — refusing to modify it." const endpoint = env.GSTACK_GBRAIN_ENDPOINT || env.GBRAIN_URL || "";
elif ! command -v bun >/dev/null 2>&1; then return endpoint ? createHash("sha256").update(endpoint).digest("hex").slice(0, 8) : "local";
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." async function resolveUserSlug() {
echo "Note: this dirties the install's git tree (generated blocks differ from main, by design)." const key = `user_slug_at_${endpointHash()}`;
echo " A 'git reset --hard origin/main' there reverts them; re-run 'gstack-config gbrain-refresh' to restore." if (await exists(path.join(home, "config.json"))) {
else const stored = await configGet(home, key);
echo "Warning: render failed. Run 'cd $INSTALL_DIR && bun run gen:skill-docs:user --host claude' manually to see the error." if (typeof stored === "string" && stored) {
fi process.stdout.write(stored);
;; return;
*) }
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." const user = sanitizeSlug(env.USER || "");
;; const email = spawnSync("git", ["config", "user.email"], { encoding: "utf8" }).stdout?.trim();
esac const fallback = email
;; ? `email-${sha8(email)}`
*) : `anonymous-${sha8(os.hostname() || "unknown")}`;
echo "Usage: gstack-config {get|set|list|defaults|endpoint-hash|resolve-user-slug|gbrain-refresh} [key] [value]" const slug = user || fallback;
exit 1 await mutateConfigHome(() => configSet(home, key, slug));
;; process.stdout.write(slug);
esac }
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, compact,
type DecisionEvent, type DecisionEvent,
} from "../lib/gstack-decision"; } 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 HERE = import.meta.dir;
const args = process.argv.slice(2); const args = process.argv.slice(2);
const slug = resolveSlug(`${HERE}/gstack-slug`); const project = await discoverProjectIdentity();
const paths = decisionPaths(slug); const paths = decisionPaths(project.projectId);
mkdirSync(dirname(paths.log), { recursive: true }); mkdirSync(dirname(paths.log), { recursive: true });
function enqueue(): void { function enqueue(): void {
// Fire-and-forget cross-machine sync (no-op when artifacts_sync is off). // 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")) { if (args.includes("--compact")) {
+4 -3
View File
@@ -28,13 +28,14 @@ import {
datamark, datamark,
type ActiveDecision, type ActiveDecision,
} from "../lib/gstack-decision"; } 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 HERE = import.meta.dir;
const args = process.argv.slice(2); const args = process.argv.slice(2);
const slug = resolveSlug(`${HERE}/gstack-slug`); const projectId = (await discoverProjectIdentity()).projectId;
const paths = decisionPaths(slug); const paths = decisionPaths(projectId);
const queryRaw = flagValue(args, "--query"); const queryRaw = flagValue(args, "--query");
const query = queryRaw?.toLowerCase(); const query = queryRaw?.toLowerCase();
const scope = flagValue(args, "--scope"); 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).""" Returns the held fd (kept open for the process lifetime)."""
import fcntl 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) os.makedirs(d, exist_ok=True)
fd = open(os.path.join(d, f"{name}.lock"), "w") fd = open(os.path.join(d, f"{name}.lock"), "w")
try: try:
+5 -5
View File
@@ -22,7 +22,7 @@
# date, mode. Silent skip on invalid input. # date, mode. Silent skip on invalid input.
# #
# Profile file: ~/.gstack/developer-profile.json (unified schema — see # 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. # question-events.jsonl.
set -euo pipefail set -euo pipefail
@@ -32,8 +32,8 @@ ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
GSTACK_HOME="${GSTACK_STATE_ROOT:-${GSTACK_HOME:-$HOME/.gstack}}" GSTACK_HOME="${GSTACK_STATE_ROOT:-${GSTACK_HOME:-$HOME/.gstack}}"
PROFILE_FILE="$GSTACK_HOME/developer-profile.json" PROFILE_FILE="$GSTACK_HOME/developer-profile.json"
LEGACY_FILE="$GSTACK_HOME/builder-profile.jsonl" LEGACY_FILE="$GSTACK_HOME/builder-profile.jsonl"
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_ID="${PROJECT_ID:?gstack project identity unavailable}"
CMD="${1:---read}" CMD="${1:---read}"
shift || true shift || true
@@ -308,7 +308,7 @@ do_gap() {
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
do_derive() { do_derive() {
ensure_profile 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 REGISTRY="$ROOT_DIR/scripts/question-registry.ts"
local SIGNALS="$ROOT_DIR/scripts/psychographic-signals.ts" local SIGNALS="$ROOT_DIR/scripts/psychographic-signals.ts"
if [ ! -f "$REGISTRY" ] || [ ! -f "$SIGNALS" ]; then if [ ! -f "$REGISTRY" ] || [ ! -f "$SIGNALS" ]; then
@@ -394,7 +394,7 @@ do_trace() {
echo "TRACE: missing dimension argument" >&2 echo "TRACE: missing dimension argument" >&2
exit 1 exit 1
fi fi
local EVENTS="$GSTACK_HOME/projects/$SLUG/question-log.jsonl" local EVENTS="$GSTACK_HOME/projects/$PROJECT_ID/question-log.jsonl"
if [ ! -f "$EVENTS" ]; then if [ ! -f "$EVENTS" ]; then
echo "TRACE: no events for this project" echo "TRACE: no events for this project"
return 0 return 0
+3 -3
View File
@@ -25,9 +25,9 @@
set -euo pipefail set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
GSTACK_HOME="${GSTACK_STATE_ROOT:-${GSTACK_HOME:-$HOME/.gstack}}" 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_ID="${PROJECT_ID:?gstack project identity unavailable}"
PROJECT_DIR="$GSTACK_HOME/projects/$SLUG" PROJECT_DIR="$GSTACK_HOME/projects/$PROJECT_ID"
PROPOSAL_FILE="$PROJECT_DIR/distillation-proposals.json" PROPOSAL_FILE="$PROJECT_DIR/distillation-proposals.json"
MEMORY_FILE="$GSTACK_HOME/free-text-memory.json" MEMORY_FILE="$GSTACK_HOME/free-text-memory.json"
PROFILE_FILE="$GSTACK_HOME/developer-profile.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)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
GSTACK_HOME="${GSTACK_STATE_ROOT:-${GSTACK_HOME:-$HOME/.gstack}}" 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}" 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" LOG_FILE="$PROJECT_DIR/question-log.jsonl"
PROPOSAL_FILE="$PROJECT_DIR/distillation-proposals.json" PROPOSAL_FILE="$PROJECT_DIR/distillation-proposals.json"
COST_LOG="$GSTACK_HOME/distill-cost.jsonl" COST_LOG="$GSTACK_HOME/distill-cost.jsonl"
@@ -47,14 +48,14 @@ esac
# --- Status subcommand -------------------------------------------------- # --- Status subcommand --------------------------------------------------
if [ "$MODE" = "status" ]; then 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 fs = require("fs");
const slug = process.env.SLUG_PATH; const projectId = process.env.PROJECT_ID_PATH;
const path = process.env.COST_LOG_PATH; const path = process.env.COST_LOG_PATH;
if (!fs.existsSync(path)) { console.log("no distill runs yet"); process.exit(0); } 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 lines = fs.readFileSync(path, "utf-8").trim().split("\n").filter(Boolean);
const mine = lines.map((l) => JSON.parse(l)).filter((e) => e.slug === slug); 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 slug=" + slug); process.exit(0); } 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 totalUsd = mine.reduce((a, e) => a + (e.cost_usd_est || 0), 0);
const todayIso = new Date().toISOString().slice(0, 10); const todayIso = new Date().toISOString().slice(0, 10);
const today = mine.filter((e) => (e.ts || "").startsWith(todayIso)); 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. # Append cost log line.
TS=$(date -u +%Y-%m-%dT%H:%M:%SZ) 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 "DISTILL_COMPLETE:"
echo " proposals_file: $PROPOSAL_FILE" echo " proposals_file: $PROPOSAL_FILE"
+6 -4
View File
@@ -13,9 +13,11 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
case "$(uname -s)" in case "$(uname -s)" in
MINGW*|MSYS*|CYGWIN*) command -v cygpath >/dev/null 2>&1 && SCRIPT_DIR="$(cygpath -m "$SCRIPT_DIR")" ;; MINGW*|MSYS*|CYGWIN*) command -v cygpath >/dev/null 2>&1 && SCRIPT_DIR="$(cygpath -m "$SCRIPT_DIR")" ;;
esac esac
eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null)"
GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}" 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" INPUT="$1"
@@ -85,7 +87,7 @@ if [ $VALIDATE_RC -ne 0 ] || [ -z "$VALIDATED" ]; then
exit 1 exit 1
fi 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). # 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 # gstack-learnings-search — read and filter project learnings
# Usage: gstack-learnings-search [--type TYPE] [--query KEYWORD] [--limit N] [--cross-project] # 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. # resolves duplicates (latest winner per key+type), and outputs formatted text.
# Exit 0 silently if no learnings file exists. # Exit 0 silently if no learnings file exists.
set -euo pipefail set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null)"
GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}" 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="" TYPE=""
QUERY="" QUERY=""
@@ -25,7 +26,7 @@ while [[ $# -gt 0 ]]; do
esac esac
done 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 # Collect cross-project JSONL files separately so the trust gate can distinguish
# current-project rows from rows loaded from other projects. # current-project rows from rows loaded from other projects.
@@ -36,7 +37,7 @@ if [ "$CROSS_PROJECT" = true ]; then
while IFS= read -r f; do while IFS= read -r f; do
CROSS_FILES+=("$f") CROSS_FILES+=("$f")
[ ${#CROSS_FILES[@]} -ge 5 ] && break [ ${#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 fi
if [ ! -f "$LEARNINGS_FILE" ] && [ ${#CROSS_FILES[@]} -eq 0 ]; then 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 * ~/.claude/projects/<encoded-cwd>/<uuid>.jsonl — Claude Code sessions
* ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl — Codex CLI sessions * ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl — Codex CLI sessions
* ~/Library/Application Support/Cursor/User/*.vscdb — Cursor (V1.0.1 follow-up) * ~/Library/Application Support/Cursor/User/*.vscdb — Cursor (V1.0.1 follow-up)
* ~/.gstack/projects/<slug>/learnings.jsonl — typed: learning * $GSTACK_HOME/projects/<project-id>/learnings.jsonl — typed: learning
* ~/.gstack/projects/<slug>/timeline.jsonl — typed: timeline * $GSTACK_HOME/projects/<project-id>/timeline.jsonl — typed: timeline
* ~/.gstack/projects/<slug>/ceo-plans/*.md — typed: ceo-plan * $GSTACK_HOME/projects/<project-id>/ceo-plans/*.md — typed: ceo-plan
* ~/.gstack/projects/<slug>/*-design-*.md — typed: design-doc * $GSTACK_HOME/projects/<project-id>/*-design-*.md — typed: design-doc
* ~/.gstack/analytics/eureka.jsonl — typed: eureka * ~/.gstack/analytics/eureka.jsonl — typed: eureka
* ~/.gstack/builder-profile.jsonl — typed: builder-profile-entry * ~/.gstack/builder-profile.jsonl — typed: builder-profile-entry
* *
@@ -53,7 +53,7 @@ import {
closeSync, closeSync,
rmSync, rmSync,
} from "fs"; } 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 { execFileSync, spawnSync, spawn, type ChildProcess } from "child_process";
import { homedir } from "os"; import { homedir } from "os";
import { createHash } from "crypto"; import { createHash } from "crypto";
@@ -438,14 +438,14 @@ function* walkGstackArtifacts(ctx: WalkContext): Generator<{ path: string; type:
} }
if (!existsSync(projectsRoot)) return; if (!existsSync(projectsRoot)) return;
let slugs: string[]; let projectIds: string[];
try { try {
slugs = readdirSync(projectsRoot); projectIds = readdirSync(projectsRoot);
} catch { } catch {
return; return;
} }
for (const slug of slugs) { for (const projectId of projectIds) {
const projDir = join(projectsRoot, slug); const projDir = join(projectsRoot, projectId);
let st; let st;
try { try {
st = statSync(projDir); st = statSync(projDir);
@@ -758,10 +758,11 @@ function buildArtifactPage(path: string, type: MemoryType): PageRecord {
const sha = fileSha256(path); const sha = fileSha256(path);
const raw = readFileSync(path, "utf-8"); 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"; let slug_repo = "_unattributed";
const m = path.match(/\/\.gstack\/projects\/([^/]+)\//); const relative = pathRelative(GSTACK_HOME, path).split(/[\\/]/);
if (m) slug_repo = m[1]; if (relative[0] === "projects" && relative[1]) slug_repo = relative[1];
const date = new Date(stats.mtimeMs).toISOString().slice(0, 10); const date = new Date(stats.mtimeMs).toISOString().slice(0, 10);
const baseName = basename(path, path.endsWith(".jsonl") ? ".jsonl" : ".md"); const baseName = basename(path, path.endsWith(".jsonl") ? ".jsonl" : ".md");
+5 -5
View File
@@ -27,10 +27,10 @@
import '../lib/conductor-env-shim'; import '../lib/conductor-env-shim';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { runBenchmark, formatTable, formatJson, formatMarkdown, type BenchmarkInput } from '../test/helpers/benchmark-runner'; import { runBenchmark, formatTable, formatJson, formatMarkdown, type BenchmarkInput } from '../lib/model-benchmark/runner';
import { ClaudeAdapter } from '../test/helpers/providers/claude'; import { ClaudeAdapter } from '../lib/model-benchmark/providers/claude';
import { GptAdapter } from '../test/helpers/providers/gpt'; import { GptAdapter } from '../lib/model-benchmark/providers/gpt';
import { GeminiAdapter } from '../test/helpers/providers/gemini'; import { GeminiAdapter } from '../lib/model-benchmark/providers/gemini';
const ADAPTER_FACTORIES = { const ADAPTER_FACTORIES = {
claude: () => new ClaudeAdapter(), claude: () => new ClaudeAdapter(),
@@ -130,7 +130,7 @@ async function main(): Promise<void> {
if (doJudge) { if (doJudge) {
try { try {
const { judgeEntries } = await import('../test/helpers/benchmark-judge'); const { judgeEntries } = await import('../lib/model-benchmark/judge');
await judgeEntries(report); await judgeEntries(report);
} catch (err) { } catch (err) {
console.error(`WARN: judge unavailable: ${(err as Error).message}`); console.error(`WARN: judge unavailable: ${(err as Error).message}`);
+4 -63
View File
@@ -1,65 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# gstack-paths — output portable state-root paths for skill bash blocks # Compatibility adapter. Path decisions live only in runtime/paths.js.
# Usage: eval "$(gstack-paths)" → sets GSTACK_STATE_ROOT, PLAN_ROOT, TMP_ROOT set -eu
# 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
# State root: where gstack writes projects/, sessions/, analytics/. ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
if [ -n "${GSTACK_HOME:-}" ]; then exec "$ROOT/bin/gstack" paths --shell
_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"
+5 -3
View File
@@ -33,10 +33,12 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
case "$(uname -s)" in case "$(uname -s)" in
MINGW*|MSYS*|CYGWIN*) command -v cygpath >/dev/null 2>&1 && SCRIPT_DIR="$(cygpath -m "$SCRIPT_DIR")" ;; MINGW*|MSYS*|CYGWIN*) command -v cygpath >/dev/null 2>&1 && SCRIPT_DIR="$(cygpath -m "$SCRIPT_DIR")" ;;
esac esac
eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null)"
# GSTACK_STATE_ROOT takes precedence over GSTACK_HOME (test isolation per D16). # GSTACK_STATE_ROOT takes precedence over GSTACK_HOME (test isolation per D16).
GSTACK_HOME="${GSTACK_STATE_ROOT:-${GSTACK_HOME:-$HOME/.gstack}}" 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" INPUT="$1"
@@ -197,7 +199,7 @@ if [ $VALIDATE_RC -ne 0 ] || [ -z "$VALIDATED" ]; then
exit 1 exit 1
fi 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) # Cathedral T5: composite-source dedup. If this exact (source, tool_use_id)
# was already logged within the last 100 lines, skip — protects against # was already logged within the last 100 lines, skip — protects against
+7 -6
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# gstack-question-preference — read/write/check explicit per-question preferences. # 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" } # Schema: { "<question_id>": "always-ask" | "never-ask" | "ask-only-for-one-way" }
# #
# Subcommands: # Subcommands:
@@ -25,11 +25,12 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
# GSTACK_STATE_ROOT takes precedence over GSTACK_HOME (test isolation per D16). # GSTACK_STATE_ROOT takes precedence over GSTACK_HOME (test isolation per D16).
GSTACK_HOME="${GSTACK_STATE_ROOT:-${GSTACK_HOME:-$HOME/.gstack}}" 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_ID="${PROJECT_ID:?gstack project identity unavailable}"
PREF_FILE="$GSTACK_HOME/projects/$SLUG/question-preferences.json" PROJECT_DIR="$GSTACK_HOME/projects/$PROJECT_ID"
EVENT_FILE="$GSTACK_HOME/projects/$SLUG/question-events.jsonl" PREF_FILE="$PROJECT_DIR/question-preferences.json"
mkdir -p "$GSTACK_HOME/projects/$SLUG" EVENT_FILE="$PROJECT_DIR/question-events.jsonl"
mkdir -p "$PROJECT_DIR"
CMD="${1:-}" CMD="${1:-}"
shift || true 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% # Collaborative: top author < 80%
# #
# Override: gstack-config set repo_mode solo|collaborative # 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 set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" 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) REMOTE_URL=$(git remote get-url origin 2>/dev/null || true)
if [ -z "$REMOTE_URL" ]; then if [ -z "$REMOTE_URL" ]; then
echo "REPO_MODE=unknown" echo "REPO_MODE=unknown"
exit 0 exit 0
fi fi
SLUG=$(echo "$REMOTE_URL" | sed 's|.*[:/]\([^/]*/[^/]*\)\.git$|\1|;s|.*[:/]\([^/]*/[^/]*\)$|\1|' | tr '/' '-') # gstack-slug emits only eval-safe values and derives PROJECT_ID from Git's
[ -z "${SLUG:-}" ] && { echo "REPO_MODE=unknown"; exit 0; } # 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: only allow known values (prevent shell injection via source <(...))
validate_mode() { validate_mode() {
@@ -34,7 +36,7 @@ if [ -n "$OVERRIDE" ] && [ "$OVERRIDE" != "null" ]; then
fi fi
# Check cache (7-day TTL) # 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" CACHE_FILE="$CACHE_DIR/repo-mode.json"
if [ -f "$CACHE_FILE" ]; then 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) )) 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":"..."}' # Usage: gstack-review-log '{"skill":"...","timestamp":"...","status":"..."}'
set -euo pipefail set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null)"
GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}" 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) # Validate: input must be parseable JSON (reject malformed or injection attempts)
INPUT="$1" INPUT="$1"
@@ -15,7 +17,7 @@ if ! printf '%s' "$INPUT" | bun -e "JSON.parse(await Bun.stdin.text())" 2>/dev/n
exit 1 exit 1
fi 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). # 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 # Usage: gstack-review-read
set -euo pipefail set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null)"
GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}" 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---" echo "---CONFIG---"
"$SCRIPT_DIR/gstack-config" get skip_eng_review 2>/dev/null || echo "false" "$SCRIPT_DIR/gstack-config" get skip_eng_review 2>/dev/null || echo "false"
echo "---HEAD---" echo "---HEAD---"
+86 -48
View File
@@ -1,55 +1,93 @@
#!/usr/bin/env bash #!/usr/bin/env node
# gstack-slug — output project slug and sanitized branch name /**
# Usage: eval "$(gstack-slug)" → sets SLUG and BRANCH variables * gstack-slug — emit human and local project identities for shell callers.
# Or: gstack-slug → prints SLUG=... and BRANCH=... lines *
# * Usage: eval "$(gstack-slug)"
# Security: output is sanitized to [a-zA-Z0-9._-] only, preventing *
# shell injection when consumed via source or eval. * SLUG remains the sanitized, human-facing repository slug used by remote
set -euo pipefail * 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" import fs from "node:fs/promises";
PROJECT_DIR="$(pwd)" import path from "node:path";
# Encode absolute path as cache key: /Users/j/foo → _Users_j_foo import { execFile as execFileCallback } from "node:child_process";
CACHE_KEY=$(printf '%s' "$PROJECT_DIR" | tr '/' '_') import { promisify } from "node:util";
CACHE_FILE="${CACHE_DIR}/${CACHE_KEY}" import { discoverProjectIdentity } from "../runtime/identity.js";
import { resolveGstackHome } from "../runtime/paths.js";
# 1. Try cached slug first (guarantees consistency across sessions) const execFile = promisify(execFileCallback);
if [[ -f "$CACHE_FILE" ]]; then const cwd = await canonicalPath(process.cwd());
SLUG=$(cat "$CACHE_FILE") const identity = await discoverProjectIdentity(cwd);
fi const home = resolveGstackHome({ cwd });
const cacheDir = path.join(home, "slug-cache");
# 2. If no cache, compute from git remote (separated from pipeline to avoid // The canonical key is a safe, worktree-stable ID. Read the 1.x path-derived
# pipefail swallowing the error and producing an empty slug) // key once as a compatibility fallback (it was not valid on native Windows).
if [[ -z "${SLUG:-}" ]]; then const cacheFile = path.join(cacheDir, identity.worktreeId);
REMOTE_URL=$(git remote get-url origin 2>/dev/null) || REMOTE_URL="" const legacyCacheFile = path.join(cacheDir, cwd.replace(/[\\/]/g, "_"));
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
# 3. Fallback to basename only when there's truly no git remote configured let slug = sanitize(await fs.readFile(cacheFile, "utf8").catch(() => ""));
SLUG="${SLUG:-$(basename "$PWD" | tr -cd 'a-zA-Z0-9._-')}" 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` await writeCache(cacheDir, cacheFile, slug);
# 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._-')
# 4. Cache the slug for future sessions (atomic write, fail silently) const rawBranch = await git(["rev-parse", "--abbrev-ref", "HEAD"], cwd).catch(() => "");
if [[ -n "$SLUG" ]]; then const branch = sanitize(rawBranch === "HEAD" ? "" : rawBranch) || "unknown";
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
RAW_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null) || RAW_BRANCH="" for (const [name, value] of [
BRANCH=$(printf '%s' "${RAW_BRANCH:-}" | tr -cd 'a-zA-Z0-9._-') ["SLUG", slug],
BRANCH="${BRANCH:-unknown}" ["BRANCH", branch],
echo "SLUG=$SLUG" ["PROJECT_ID", identity.projectId],
echo "BRANCH=$BRANCH" ["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). # dispatches) or NEVER_GATE (security, data-migration — insurance policy).
set -euo pipefail set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null)"
GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}" 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 if [ ! -d "$PROJECT_DIR" ]; then
echo "SPECIALIST_STATS: 0 reviews analyzed" echo "SPECIALIST_STATS: 0 reviews analyzed"
+24 -28
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bun #!/usr/bin/env bun
// gstack-taste-update — update the persistent taste profile at // gstack-taste-update — update the persistent taste profile at
// ~/.gstack/projects/$SLUG/taste-profile.json // $GSTACK_HOME/projects/$PROJECT_ID/taste-profile.json
// //
// Usage: // Usage:
// gstack-taste-update approved <variant-path> [--reason "<why>"] // gstack-taste-update approved <variant-path> [--reason "<why>"]
@@ -8,7 +8,7 @@
// gstack-taste-update show — print current profile summary // gstack-taste-update show — print current profile summary
// gstack-taste-update migrate — upgrade legacy approved.json to v1 // 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, // "version": 1,
@@ -31,12 +31,13 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; 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 SCHEMA_VERSION = 1;
const SESSION_CAP = 50; const SESSION_CAP = 50;
const DECAY_PER_WEEK = 0.05; const DECAY_PER_WEEK = 0.05;
const CURRENT_PROJECT_ID = (await discoverProjectIdentity()).projectId;
type Dimension = 'fonts' | 'colors' | 'layouts' | 'aesthetics'; type Dimension = 'fonts' | 'colors' | 'layouts' | 'aesthetics';
const DIMENSIONS: Dimension[] = ['fonts', 'colors', 'layouts', 'aesthetics']; const DIMENSIONS: Dimension[] = ['fonts', 'colors', 'layouts', 'aesthetics'];
@@ -63,17 +64,12 @@ interface TasteProfile {
sessions: SessionRecord[]; sessions: SessionRecord[];
} }
function getSlug(): string { function getProjectId(): string {
try { return CURRENT_PROJECT_ID;
const output = execSync('git rev-parse --show-toplevel', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
return path.basename(output);
} catch {
return 'unknown';
}
} }
function profilePath(slug: string): string { function profilePath(projectId: string): string {
return path.join(STATE_DIR, 'projects', slug, 'taste-profile.json'); return path.join(STATE_DIR, 'projects', projectId, 'taste-profile.json');
} }
function emptyProfile(): TasteProfile { function emptyProfile(): TasteProfile {
@@ -90,8 +86,8 @@ function emptyProfile(): TasteProfile {
}; };
} }
function load(slug: string): TasteProfile { function load(projectId: string): TasteProfile {
const p = profilePath(slug); const p = profilePath(projectId);
if (!fs.existsSync(p)) return emptyProfile(); if (!fs.existsSync(p)) return emptyProfile();
try { try {
const raw = JSON.parse(fs.readFileSync(p, 'utf-8')); 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 { function save(projectId: string, profile: TasteProfile): void {
const p = profilePath(slug); const p = profilePath(projectId);
fs.mkdirSync(path.dirname(p), { recursive: true }); fs.mkdirSync(path.dirname(p), { recursive: true });
profile.updated_at = new Date().toISOString(); profile.updated_at = new Date().toISOString();
fs.writeFileSync(p, JSON.stringify(profile, null, 2) + '\n'); 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 { function cmdUpdate(action: 'approved' | 'rejected', variant: string, reason?: string): void {
const slug = getSlug(); const projectId = getProjectId();
const profile = load(slug); const profile = load(projectId);
const signals = extractSignals(reason); const signals = extractSignals(reason);
for (const dim of DIMENSIONS) { 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); profile.sessions = profile.sessions.slice(-SESSION_CAP);
} }
save(slug, profile); save(projectId, profile);
console.log(`${action}: ${variant} → ${profilePath(slug)}`); console.log(`${action}: ${variant} → ${profilePath(projectId)}`);
} }
function cmdShow(): void { function cmdShow(): void {
const slug = getSlug(); const projectId = getProjectId();
const profile = applyDecay(load(slug)); const profile = applyDecay(load(projectId));
console.log(`taste-profile.json (slug: ${slug}, sessions: ${profile.sessions.length})`); console.log(`taste-profile.json (project: ${projectId}, sessions: ${profile.sessions.length})`);
for (const dim of DIMENSIONS) { for (const dim of DIMENSIONS) {
const top = [...profile.dimensions[dim].approved] const top = [...profile.dimensions[dim].approved]
.sort((a, b) => b.confidence * b.approved_count - a.confidence * a.approved_count) .sort((a, b) => b.confidence * b.approved_count - a.confidence * a.approved_count)
@@ -257,10 +253,10 @@ function cmdShow(): void {
} }
function cmdMigrate(): void { function cmdMigrate(): void {
const slug = getSlug(); const projectId = getProjectId();
const profile = load(slug); const profile = load(projectId);
save(slug, profile); save(projectId, profile);
console.log(`migrated taste profile to v${SCHEMA_VERSION} at ${profilePath(slug)}`); console.log(`migrated taste profile to v${SCHEMA_VERSION} at ${profilePath(projectId)}`);
} }
// ─── CLI entry ──────────────────────────────────────────────── // ─── CLI entry ────────────────────────────────────────────────
+4 -3
View File
@@ -11,7 +11,8 @@
# --used-browse true --session-id "12345-1710756600" # --used-browse true --session-id "12345-1710756600"
# #
# Env overrides (for testing): # 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 # GSTACK_DIR — override auto-detected gstack root
# #
# NOTE: Uses set -uo pipefail (no -e) — telemetry must never exit non-zero # 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 case "$(uname -s)" in
MINGW*|MSYS*|CYGWIN*) command -v cygpath >/dev/null 2>&1 && SCRIPT_DIR="$(cygpath -m "$SCRIPT_DIR")" ;; MINGW*|MSYS*|CYGWIN*) command -v cygpath >/dev/null 2>&1 && SCRIPT_DIR="$(cygpath -m "$SCRIPT_DIR")" ;;
esac esac
STATE_DIR="${GSTACK_STATE_DIR:-$HOME/.gstack}" STATE_DIR="${GSTACK_HOME:-${GSTACK_STATE_DIR:-$HOME/.gstack}}"
ANALYTICS_DIR="$STATE_DIR/analytics" ANALYTICS_DIR="$STATE_DIR/analytics"
JSONL_FILE="$ANALYTICS_DIR/skill-usage.jsonl" JSONL_FILE="$ANALYTICS_DIR/skill-usage.jsonl"
PENDING_DIR="$ANALYTICS_DIR" # .pending-* files live here 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. # can't be guessed or correlated by someone who knows your machine identity.
INSTALL_ID="" INSTALL_ID=""
if [ "$TIER" = "community" ]; then if [ "$TIER" = "community" ]; then
ID_FILE="$HOME/.gstack/installation-id" ID_FILE="$STATE_DIR/installation-id"
if [ -f "$ID_FILE" ]; then if [ -f "$ID_FILE" ]; then
INSTALL_ID="$(cat "$ID_FILE" 2>/dev/null)" INSTALL_ID="$(cat "$ID_FILE" 2>/dev/null)"
fi fi
+6 -4
View File
@@ -11,9 +11,11 @@
# Validation failure → skip silently (non-blocking). # Validation failure → skip silently (non-blocking).
set -euo pipefail set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null)"
GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}" 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" INPUT="$1"
@@ -34,7 +36,7 @@ if ! printf '%s' "$INPUT" | bun -e "const j=JSON.parse(await Bun.stdin.text());
" 2>/dev/null) || true " 2>/dev/null) || true
fi 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). # 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] # Usage: gstack-timeline-read [--since "7 days ago"] [--limit N] [--branch NAME]
# #
# Session timeline: local-only, never sent anywhere. # 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. # Exit 0 silently if no timeline file exists.
set -euo pipefail set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null)"
GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}" 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="" SINCE=""
LIMIT=20 LIMIT=20
@@ -23,7 +24,7 @@ while [[ $# -gt 0 ]]; do
esac esac
done done
TIMELINE_FILE="$GSTACK_HOME/projects/$SLUG/timeline.jsonl" TIMELINE_FILE="$GSTACK_HOME/projects/$PROJECT_ID/timeline.jsonl"
if [ ! -f "$TIMELINE_FILE" ]; then if [ ! -f "$TIMELINE_FILE" ]; then
exit 0 exit 0
+3 -2
View File
@@ -10,11 +10,12 @@
# GSTACK_DIR — override auto-detected gstack root # GSTACK_DIR — override auto-detected gstack root
# GSTACK_REMOTE_URL — override remote VERSION URL (branch-pinned fallback) # GSTACK_REMOTE_URL — override remote VERSION URL (branch-pinned fallback)
# GSTACK_REMOTE_REPO — override remote git URL for ls-remote SHA resolution # 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 set -euo pipefail
GSTACK_DIR="${GSTACK_DIR:-$(cd "$(dirname "$0")/.." && pwd)}" 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" CACHE_FILE="$STATE_DIR/last-update-check"
MARKER_FILE="$STATE_DIR/just-upgraded-from" MARKER_FILE="$STATE_DIR/just-upgraded-from"
SNOOZE_FILE="$STATE_DIR/update-snoozed" SNOOZE_FILE="$STATE_DIR/update-snoozed"
+3 -1
View File
@@ -1,5 +1,5 @@
--- ---
name: browse name: gstack-1-browse
preamble-tier: 1 preamble-tier: 1
version: 1.1.0 version: 1.1.0
description: Fast headless browser for QA testing and site dogfooding. (gstack) description: Fast headless browser for QA testing and site dogfooding. (gstack)
@@ -12,6 +12,8 @@ allowed-tools:
- Read - Read
- AskUserQuestion - AskUserQuestion
metadata:
internal: true
--- ---
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly --> <!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
<!-- Regenerate: bun run gen:skill-docs --> <!-- Regenerate: bun run gen:skill-docs -->
+16 -5
View File
@@ -722,7 +722,7 @@ export class BrowserManager {
this.consecutiveFailures = 0; this.consecutiveFailures = 0;
} }
async close() { async close(timeoutMs = 5000) {
if (this.browser || (this.connectionMode === 'headed' && this.context)) { if (this.browser || (this.connectionMode === 'headed' && this.context)) {
if (this.connectionMode === 'headed') { if (this.connectionMode === 'headed') {
// Headed/persistent context mode: close the context (which closes the browser) // 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'); if (this.browser) this.browser.removeAllListeners('disconnected');
await Promise.race([ await Promise.race([
this.context ? this.context.close() : Promise.resolve(), this.context ? this.context.close() : Promise.resolve(),
new Promise(resolve => setTimeout(resolve, 5000)), new Promise(resolve => setTimeout(resolve, timeoutMs)),
]).catch(() => {}); ]).catch(() => {});
} else { } else {
// Launched mode: close the browser we spawned // Launched mode: close the browser we spawned
this.browser.removeAllListeners('disconnected'); this.browser.removeAllListeners('disconnected');
await Promise.race([ await Promise.race([
this.browser.close(), this.browser.close(),
new Promise(resolve => setTimeout(resolve, 5000)), new Promise(resolve => setTimeout(resolve, timeoutMs)),
]).catch(() => {}); ]).catch(() => {});
} }
this.browser = null; this.browser = null;
@@ -797,6 +797,11 @@ export class BrowserManager {
const tabId = id ?? this.activeTabId; const tabId = id ?? this.activeTabId;
const page = this.pages.get(tabId); const page = this.pages.get(tabId);
if (!page) throw new Error(`Tab ${tabId} not found`); 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(); await page.close();
this.pages.delete(tabId); this.pages.delete(tabId);
@@ -804,7 +809,7 @@ export class BrowserManager {
this.tabOwnership.delete(tabId); this.tabOwnership.delete(tabId);
// Switch to another tab if we closed the active one // Switch to another tab if we closed the active one
if (tabId === this.activeTabId) { if (wasActive) {
const remaining = [...this.pages.keys()]; const remaining = [...this.pages.keys()];
if (remaining.length > 0) { if (remaining.length > 0) {
this.activeTabId = remaining[remaining.length - 1]; 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'); 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 }); 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(). // T1: same automation-tell-stripping defaults as launchHeaded().
// The handoff path (headless → headed re-launch) takes the same // The handoff path (headless → headed re-launch) takes the same
// anti-detection posture. // anti-detection posture.
+7 -13
View File
@@ -10,11 +10,15 @@
* 1. Prefer node on PATH + a bundled JS entry at * 1. Prefer node on PATH + a bundled JS entry at
* browse/dist/security-sidecar.js (built by package.json's * browse/dist/security-sidecar.js (built by package.json's
* build:security-sidecar script). * build:security-sidecar script).
* 2. Dev fallback: node + browse/src/security-sidecar-entry.ts via tsx * 2. If Node is missing or no compiled entry resolves, return null. The
* (only available in the source checkout, not the compiled install). * /pty-inject-scan
* 3. If Node is missing or no entry resolves, return null. The /pty-inject-scan
* endpoint then responds with l4 { available: false } and the extension * endpoint then responds with l4 { available: false } and the extension
* degrades to WARN+confirm (D7). * 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"; import { existsSync } from "fs";
@@ -46,9 +50,6 @@ function browseRoot(): string {
if (existsSync(join(candidate, "browse", "dist", "security-sidecar.js"))) { if (existsSync(join(candidate, "browse", "dist", "security-sidecar.js"))) {
return candidate; return candidate;
} }
if (existsSync(join(candidate, "src", "security-sidecar-entry.ts"))) {
return candidate;
}
const next = dirname(candidate); const next = dirname(candidate);
if (next === candidate) break; if (next === candidate) break;
candidate = next; candidate = next;
@@ -67,12 +68,5 @@ export function findSecuritySidecar(): SidecarLocation | null {
return { node, entry: compiled, mode: "compiled" }; 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; return null;
} }
+5 -2
View File
@@ -421,14 +421,17 @@ export async function handleMetaCommand(
} }
case 'stop': { 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'; return 'Server stopped';
} }
case 'restart': { case 'restart': {
// Signal that we want a restart — the CLI will detect exit and restart // Signal that we want a restart — the CLI will detect exit and restart
console.log('[browse] Restart requested. Exiting for CLI to restart.'); console.log('[browse] Restart requested. Exiting for CLI to restart.');
await shutdown(); setTimeout(() => { void shutdown(); }, 25).unref?.();
return 'Restarting...'; return 'Restarting...';
} }
+5 -4
View File
@@ -5,20 +5,21 @@
"": { "": {
"name": "gstack", "name": "gstack",
"dependencies": { "dependencies": {
"@huggingface/transformers": "^4.1.0", "@anthropic-ai/sdk": "^0.78.0",
"@ngrok/ngrok": "^1.7.0", "@ngrok/ngrok": "^1.7.0",
"diff": "^9.0.0", "diff": "^9.0.0",
"html-to-docx": "1.8.0", "html-to-docx": "1.8.0",
"marked": "^18.0.2", "marked": "^18.0.2",
"playwright": "^1.58.2", "playwright": "^1.58.2",
"puppeteer-core": "^24.40.0", "puppeteer-core": "^24.40.0",
"sharp": "^0.34.5",
"socks": "^2.8.8", "socks": "^2.8.8",
"xterm": "5",
"xterm-addon-fit": "^0.8.0",
}, },
"devDependencies": { "devDependencies": {
"@anthropic-ai/claude-agent-sdk": "0.2.117", "@anthropic-ai/claude-agent-sdk": "0.2.117",
"@anthropic-ai/sdk": "^0.78.0", "@huggingface/transformers": "^4.1.0",
"xterm": "5",
"xterm-addon-fit": "^0.8.0",
}, },
}, },
}, },
+3 -1
View File
@@ -1,5 +1,5 @@
--- ---
name: canary name: gstack-1-canary
preamble-tier: 2 preamble-tier: 2
version: 1.0.0 version: 1.0.0
description: Post-deploy canary monitoring. (gstack) description: Post-deploy canary monitoring. (gstack)
@@ -13,6 +13,8 @@ triggers:
- monitor after deploy - monitor after deploy
- canary check - canary check
- watch for errors post-deploy - watch for errors post-deploy
metadata:
internal: true
--- ---
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly --> <!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
<!-- Regenerate: bun run gen:skill-docs --> <!-- Regenerate: bun run gen:skill-docs -->
+3 -1
View File
@@ -1,5 +1,5 @@
--- ---
name: careful name: gstack-1-careful
version: 0.1.0 version: 0.1.0
description: Safety guardrails for destructive commands. (gstack) description: Safety guardrails for destructive commands. (gstack)
triggers: triggers:
@@ -16,6 +16,8 @@ hooks:
- type: command - type: command
command: "bash $HOME/.claude/skills/gstack/careful/bin/check-careful.sh" command: "bash $HOME/.claude/skills/gstack/careful/bin/check-careful.sh"
statusMessage: "Checking for destructive commands..." statusMessage: "Checking for destructive commands..."
metadata:
internal: true
--- ---
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly --> <!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
<!-- Regenerate: bun run gen:skill-docs --> <!-- Regenerate: bun run gen:skill-docs -->
+3 -1
View File
@@ -1,5 +1,5 @@
--- ---
name: codex name: gstack-1-codex
preamble-tier: 3 preamble-tier: 3
version: 1.0.0 version: 1.0.0
description: OpenAI Codex CLI wrapper — three modes. (gstack) description: OpenAI Codex CLI wrapper — three modes. (gstack)
@@ -14,6 +14,8 @@ allowed-tools:
- Glob - Glob
- Grep - Grep
- AskUserQuestion - AskUserQuestion
metadata:
internal: true
--- ---
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly --> <!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
<!-- Regenerate: bun run gen:skill-docs --> <!-- 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