mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-20 20:00:45 +02:00
implement six-skill gstack 2 runtime
This commit is contained in:
@@ -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
|
||||||
@@ -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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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
@@ -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 -->
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { main } from "../runtime/cli.js";
|
||||||
|
|
||||||
|
const code = await main();
|
||||||
|
process.exitCode = code;
|
||||||
+106
-83
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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");
|
||||||
|
}
|
||||||
|
|||||||
@@ -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")) {
|
||||||
|
|||||||
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 &
|
||||||
|
|||||||
@@ -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
@@ -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");
|
||||||
|
|||||||
@@ -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
@@ -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"
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Executable
+5
@@ -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" "$@"
|
||||||
@@ -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) ))
|
||||||
|
|||||||
@@ -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,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
@@ -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.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
@@ -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 ────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 &
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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 -->
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,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
@@ -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
@@ -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
@@ -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 -->
|
||||||
|
|||||||
@@ -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` |
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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
Reference in New Issue
Block a user