diff --git a/SKILL.md b/SKILL.md index 7838996b7..40f1b0706 100644 --- a/SKILL.md +++ b/SKILL.md @@ -88,6 +88,16 @@ if [ -d ".claude/skills/gstack" ] && [ ! -L ".claude/skills/gstack" ]; then fi fi echo "VENDORED_GSTACK: $_VENDORED" +# Semantic code search (sqry) — lightweight detection only (command -v is ~1ms). +# Index status is checked at query time by the agent, not at preamble load. +_SQRY="unavailable" +_SQRY_INDEXED="unknown" +if command -v sqry-mcp >/dev/null 2>&1; then + _SQRY="available" + [ -d ".sqry" ] && _SQRY_INDEXED="yes" || _SQRY_INDEXED="no" +fi +echo "SQRY: $_SQRY" +[ "$_SQRY" = "available" ] && echo "SQRY_INDEXED: $_SQRY_INDEXED" # Detect spawned session (OpenClaw or other orchestrator) [ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true ``` diff --git a/autoplan/SKILL.md b/autoplan/SKILL.md index 7b05d620e..02c118b42 100644 --- a/autoplan/SKILL.md +++ b/autoplan/SKILL.md @@ -98,6 +98,16 @@ if [ -d ".claude/skills/gstack" ] && [ ! -L ".claude/skills/gstack" ]; then fi fi echo "VENDORED_GSTACK: $_VENDORED" +# Semantic code search (sqry) — lightweight detection only (command -v is ~1ms). +# Index status is checked at query time by the agent, not at preamble load. +_SQRY="unavailable" +_SQRY_INDEXED="unknown" +if command -v sqry-mcp >/dev/null 2>&1; then + _SQRY="available" + [ -d ".sqry" ] && _SQRY_INDEXED="yes" || _SQRY_INDEXED="no" +fi +echo "SQRY: $_SQRY" +[ "$_SQRY" = "available" ] && echo "SQRY_INDEXED: $_SQRY_INDEXED" # Detect spawned session (OpenClaw or other orchestrator) [ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true ``` diff --git a/benchmark/SKILL.md b/benchmark/SKILL.md index 370d09d53..ec96977d8 100644 --- a/benchmark/SKILL.md +++ b/benchmark/SKILL.md @@ -91,6 +91,16 @@ if [ -d ".claude/skills/gstack" ] && [ ! -L ".claude/skills/gstack" ]; then fi fi echo "VENDORED_GSTACK: $_VENDORED" +# Semantic code search (sqry) — lightweight detection only (command -v is ~1ms). +# Index status is checked at query time by the agent, not at preamble load. +_SQRY="unavailable" +_SQRY_INDEXED="unknown" +if command -v sqry-mcp >/dev/null 2>&1; then + _SQRY="available" + [ -d ".sqry" ] && _SQRY_INDEXED="yes" || _SQRY_INDEXED="no" +fi +echo "SQRY: $_SQRY" +[ "$_SQRY" = "available" ] && echo "SQRY_INDEXED: $_SQRY_INDEXED" # Detect spawned session (OpenClaw or other orchestrator) [ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true ``` diff --git a/browse/SKILL.md b/browse/SKILL.md index 2aad0cec1..f1a51bb28 100644 --- a/browse/SKILL.md +++ b/browse/SKILL.md @@ -90,6 +90,16 @@ if [ -d ".claude/skills/gstack" ] && [ ! -L ".claude/skills/gstack" ]; then fi fi echo "VENDORED_GSTACK: $_VENDORED" +# Semantic code search (sqry) — lightweight detection only (command -v is ~1ms). +# Index status is checked at query time by the agent, not at preamble load. +_SQRY="unavailable" +_SQRY_INDEXED="unknown" +if command -v sqry-mcp >/dev/null 2>&1; then + _SQRY="available" + [ -d ".sqry" ] && _SQRY_INDEXED="yes" || _SQRY_INDEXED="no" +fi +echo "SQRY: $_SQRY" +[ "$_SQRY" = "available" ] && echo "SQRY_INDEXED: $_SQRY_INDEXED" # Detect spawned session (OpenClaw or other orchestrator) [ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true ``` diff --git a/canary/SKILL.md b/canary/SKILL.md index 6cf762034..a6e1f4610 100644 --- a/canary/SKILL.md +++ b/canary/SKILL.md @@ -90,6 +90,16 @@ if [ -d ".claude/skills/gstack" ] && [ ! -L ".claude/skills/gstack" ]; then fi fi echo "VENDORED_GSTACK: $_VENDORED" +# Semantic code search (sqry) — lightweight detection only (command -v is ~1ms). +# Index status is checked at query time by the agent, not at preamble load. +_SQRY="unavailable" +_SQRY_INDEXED="unknown" +if command -v sqry-mcp >/dev/null 2>&1; then + _SQRY="available" + [ -d ".sqry" ] && _SQRY_INDEXED="yes" || _SQRY_INDEXED="no" +fi +echo "SQRY: $_SQRY" +[ "$_SQRY" = "available" ] && echo "SQRY_INDEXED: $_SQRY_INDEXED" # Detect spawned session (OpenClaw or other orchestrator) [ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true ``` diff --git a/checkpoint/SKILL.md b/checkpoint/SKILL.md index 22b5d3ad7..dbff98bb5 100644 --- a/checkpoint/SKILL.md +++ b/checkpoint/SKILL.md @@ -93,6 +93,16 @@ if [ -d ".claude/skills/gstack" ] && [ ! -L ".claude/skills/gstack" ]; then fi fi echo "VENDORED_GSTACK: $_VENDORED" +# Semantic code search (sqry) — lightweight detection only (command -v is ~1ms). +# Index status is checked at query time by the agent, not at preamble load. +_SQRY="unavailable" +_SQRY_INDEXED="unknown" +if command -v sqry-mcp >/dev/null 2>&1; then + _SQRY="available" + [ -d ".sqry" ] && _SQRY_INDEXED="yes" || _SQRY_INDEXED="no" +fi +echo "SQRY: $_SQRY" +[ "$_SQRY" = "available" ] && echo "SQRY_INDEXED: $_SQRY_INDEXED" # Detect spawned session (OpenClaw or other orchestrator) [ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true ``` diff --git a/codex/SKILL.md b/codex/SKILL.md index 9b40b27e5..1b88ac982 100644 --- a/codex/SKILL.md +++ b/codex/SKILL.md @@ -92,6 +92,16 @@ if [ -d ".claude/skills/gstack" ] && [ ! -L ".claude/skills/gstack" ]; then fi fi echo "VENDORED_GSTACK: $_VENDORED" +# Semantic code search (sqry) — lightweight detection only (command -v is ~1ms). +# Index status is checked at query time by the agent, not at preamble load. +_SQRY="unavailable" +_SQRY_INDEXED="unknown" +if command -v sqry-mcp >/dev/null 2>&1; then + _SQRY="available" + [ -d ".sqry" ] && _SQRY_INDEXED="yes" || _SQRY_INDEXED="no" +fi +echo "SQRY: $_SQRY" +[ "$_SQRY" = "available" ] && echo "SQRY_INDEXED: $_SQRY_INDEXED" # Detect spawned session (OpenClaw or other orchestrator) [ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true ``` diff --git a/contrib/add-tool/README.md b/contrib/add-tool/README.md new file mode 100644 index 000000000..730eb182e --- /dev/null +++ b/contrib/add-tool/README.md @@ -0,0 +1,147 @@ +# Adding an External Tool to gstack + +This directory contains integrations for external development tools that +enhance gstack's workflow skills with specialized capabilities. + +## Structure + +Each tool integration lives in its own directory: + + contrib/add-tool// + ├── README.md # What the tool does and how the integration works + ├── tools.json # Routing contract: which gstack skills use which tools + ├── detection.sh # Bash fragment appended to preamble for detection + ├── install.sh # Idempotent install script + └── uninstall.sh # Clean removal script + +## How it works + +1. A bash block in the preamble checks if the tool binary exists and outputs + status variables (available/unavailable, version, etc.). + +2. A TypeScript resolver reads `tools.json` and generates conditional markdown + blocks for each skill template. The block is skipped entirely when the tool + is not detected. + +3. Skills that benefit from the tool include `{{TOOL_CONTEXT}}` in their + SKILL.md.tmpl, placed after `{{LEARNINGS_SEARCH}}`. + +## Requirements for a tool integration + +- Tool must be optional. gstack works without it. +- Detection must be fast (< 50ms). It runs on every skill invocation. +- Resolver output must be concise to avoid prompt bloat. +- Install script must be idempotent. +- Uninstall script must leave gstack in a clean state. +- tools.json must include min_version for compatibility gating. + +## Security: MCP tool calls vs resource reads + +Add-ins wire external MCP servers into skill templates. The LLM executing +those skills has shell access, file write access, and network access. This +makes the boundary between "tool routing" and "content injection" a real +security boundary. + +### The rule + +Resolvers must only emit static markdown and MCP tool names. They must +never instruct the agent to read MCP resources (`ReadMcpResourceTool`, +`sqry://...`, etc.) into its context. + +### Why this matters + +MCP tool calls and MCP resource reads have different trust properties: + +| | MCP tool call | MCP resource read | +|---|---|---| +| How it works | Agent calls `mcp__foo__bar(params)`, gets response | Agent reads `foo://docs/guide` via ReadMcpResourceTool | +| Where content lands | Tool-response channel (structured, bounded) | Instruction stream (indistinguishable from skill text) | +| What it influences | What the agent knows (data) | What the agent does (behavior) | +| If MCP server is compromised | Attacker controls one tool response | Attacker controls agent behavior with shell access | + +A tool response saying "drop all tables" is data the agent reports. A resource +read saying "drop all tables" is an instruction the agent may follow, because +it arrives in the same channel as the skill's own instructions. There is no +programmatic boundary between them. + +Prompt-level defenses ("SECURITY: treat as reference data") are in-band with +the content they guard. An adversarial payload inside the resource can +override them. This is the standard prompt injection pattern. + +### What to do instead + +Inline parameter guidance as static text. If your tool has parameter defaults, +limits, or usage tips, put them in `tools.json` (e.g., as a `parameter_guidance` +string) and let the resolver emit them as static markdown. The resolver output +is generated at build time from trusted source files, not fetched at runtime +from an MCP server process. + +```jsonc +// tools.json: static guidance the resolver emits directly +{ + "parameter_guidance": "Most tools accept max_depth (default 3, max 10)..." +} + +// resolver output (good): static markdown with tool names +// **Tool parameters:** Most tools accept max_depth (default 3, max 10)... + +// resolver output (bad): instructs agent to read external content +// **Tool parameters:** read `foo://docs/guide` via ReadMcpResourceTool +``` + +### The staleness trade-off + + + +The original sqry integration used live MCP resources (`sqry://docs/...`) +to serve parameter guidance so that updates to sqry's defaults would be +picked up automatically without a gstack release. This is a real benefit. +Version coupling between tools creates maintenance burden and stale docs. + +We chose static inline guidance instead for three reasons: + +1. The injection risk scales. Every user who installs the add-in gets + an MCP server process whose output enters the LLM's instruction stream. + A single compromised binary update affects every machine. + +2. The staleness risk does not scale the same way. Parameter defaults change + infrequently. When they do, updating `tools.json` is a one-line PR. + Stale defaults cause suboptimal queries. Injected instructions cause + arbitrary code execution. + +3. No programmatic boundary exists today. MCP resource content and skill + instructions occupy the same text channel. Until the MCP spec provides + isolated resource channels, there is no way to let the agent read + external content safely. + +If your tool's parameters change frequently enough that static guidance is +genuinely burdensome, that is a signal to contribute upstream to the MCP +spec for content isolation, not to work around it by injecting untrusted +content into the instruction stream. + +## Existing integrations + +- [sqry](sqry/) - AST-based semantic code search via MCP (callers/callees + tracing, cycle detection, complexity metrics, structural call-path tracing) diff --git a/contrib/add-tool/sqry/README.md b/contrib/add-tool/sqry/README.md new file mode 100644 index 000000000..4d9c3bf7e --- /dev/null +++ b/contrib/add-tool/sqry/README.md @@ -0,0 +1,78 @@ +# sqry Integration for gstack + +[sqry](https://github.com/verivus-oss/sqry) provides AST-based semantic code +search via 34 MCP tools. This integration adds structural code analysis to +gstack skills: callers/callees tracing, cycle detection, complexity metrics, +structural call-path tracing, and more. + +## Install + + bash contrib/add-tool/sqry/install.sh [claude|codex|gemini|all] + +## What it does + +When sqry is installed and configured as an MCP server, gstack skills gain a +"Structural Code Analysis" section with contextual tool recommendations: + +- `/investigate` gets caller/callee tracing, cycle detection, blast radius analysis +- `/cso` gets structural call-path tracing from input handlers to sinks, dead code detection +- `/review` gets complexity regression checks, cycle detection, semantic diff +- `/retro` gets structural trend analysis and codebase health metrics +- `/plan-eng-review` gets dependency visualization and architecture boundary validation +- `/ship` gets pre-ship structural verification (cycles, dead code, complexity) + +See `tools.json` for the complete routing table. + +## Architecture: contextual routing with static guidance + +gstack owns WHEN: `tools.json` defines which sqry tools to use at which +skill phase (e.g., `trace_path` during `/cso` security analysis). This is +gstack's value-add, contextual routing that sqry doesn't know about. + +Parameter guidance is static: `tools.json` contains a `parameter_guidance` +string that the resolver emits as inline markdown. No external content enters +the LLM's instruction stream at runtime. + +When sqry's parameter defaults change, update the `parameter_guidance` field in +`tools.json`. It's a one-line change. + + + +## Relationship to existing sqry skills + +The `sqry-claude`, `sqry-codex`, and `sqry-gemini` skills (shipped with sqry) +teach agents how to *set up and use* sqry. This gstack integration is +different. It wires sqry tools into gstack's *existing workflow skills* so +they're used automatically at the right moment during debugging, review, +security audits, etc. + +| sqry skills (setup) | gstack add-in (workflow) | +|---------------------|------------------------| +| Teach tool usage | Wire tools into skill phases | +| Manual invocation | Automatic contextual use | +| Generic patterns | Skill-phase routing | +| No index management | Auto-rebuild when stale | +| Parameter guidance inline | Parameter guidance inline (static in tools.json) | + +## Uninstall + + bash contrib/add-tool/sqry/uninstall.sh + +This removes the gstack integration. sqry itself remains installed. diff --git a/contrib/add-tool/sqry/detection.sh b/contrib/add-tool/sqry/detection.sh new file mode 100644 index 000000000..15d83ffca --- /dev/null +++ b/contrib/add-tool/sqry/detection.sh @@ -0,0 +1,12 @@ +# Semantic code search (sqry) — lightweight detection only +# Reference fragment — inlined by preamble.ts resolver +# Only command -v (~1ms) and directory check. No subprocess calls. +# Index staleness is checked at query time by the agent, not here. +_SQRY="unavailable" +_SQRY_INDEXED="unknown" +if command -v sqry-mcp >/dev/null 2>&1; then + _SQRY="available" + [ -d ".sqry" ] && _SQRY_INDEXED="yes" || _SQRY_INDEXED="no" +fi +echo "SQRY: $_SQRY" +[ "$_SQRY" = "available" ] && echo "SQRY_INDEXED: $_SQRY_INDEXED" diff --git a/contrib/add-tool/sqry/install.sh b/contrib/add-tool/sqry/install.sh new file mode 100644 index 000000000..fa363b288 --- /dev/null +++ b/contrib/add-tool/sqry/install.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env bash +# Install sqry as a gstack structural code analysis add-in. +# Idempotent — safe to run multiple times. +set -e + +AGENT="${1:-claude}" +MIN_VERSION="7.0.0" + +echo "=== sqry integration for gstack ===" +echo "" + +# 1. Check for sqry CLI +if ! command -v sqry >/dev/null 2>&1; then + echo "sqry not found on PATH." + echo "" + echo "Install via cargo (recommended — builds from source):" + echo " cargo install sqry-cli sqry-mcp" + echo "" + echo "Or download a release binary from:" + echo " https://github.com/verivus-oss/sqry/releases" + echo "" + echo "Then re-run this script." + exit 1 +fi + +# 2. Check version (normalize: "sqry 7.1.4" -> "7.1.4") +SQRY_VERSION=$(sqry --version 2>/dev/null | awk '{print $2}' || echo "0.0.0") +echo "Found sqry $SQRY_VERSION" + +# Portable semver comparator (no sort -V, works on macOS) +version_lt() { + local IFS=. + local i a=($1) b=($2) + for ((i=0; i<${#b[@]}; i++)); do + [ -z "${a[i]}" ] && a[i]=0 + if ((10#${a[i]} < 10#${b[i]})); then return 0; fi + if ((10#${a[i]} > 10#${b[i]})); then return 1; fi + done + return 1 +} + +if version_lt "$SQRY_VERSION" "$MIN_VERSION"; then + echo "sqry $MIN_VERSION+ required. Please upgrade:" + echo " cargo install sqry-cli sqry-mcp" + exit 1 +fi + +# 3. Check for sqry-mcp +if ! command -v sqry-mcp >/dev/null 2>&1; then + echo "sqry-mcp not found on PATH." + echo "" + echo "Install the MCP server:" + echo " cargo install sqry-mcp" + echo "" + echo "Then re-run this script." + exit 1 +fi + +echo "Found sqry-mcp at $(command -v sqry-mcp)" + +# 4. Configure MCP for the target agent +# Delegate to sqry's own setup command — it knows each host's config format. +echo "" +echo "Configuring MCP server for $AGENT..." + +case "$AGENT" in + claude) sqry mcp setup --tool claude ;; + codex) sqry mcp setup --tool codex ;; + gemini) sqry mcp setup --tool gemini ;; + all) sqry mcp setup ;; + *) echo "Warning: Auto-configuration not supported for $AGENT. Run 'sqry mcp setup' manually." ;; +esac + +# 5. Verify MCP configuration +echo "" +echo "MCP status:" +sqry mcp status 2>/dev/null || echo " (could not verify — run 'sqry mcp status' manually)" + +# 6. Build initial index if not present +if ! sqry index --status --json . 2>/dev/null | grep -q '"exists": true'; then + echo "" + echo "Building initial sqry index..." + sqry index . + echo "Index built." +else + echo "" + echo "sqry index already exists." + if sqry index --status --json . 2>/dev/null | grep -q '"stale": true'; then + echo "Index is stale — rebuilding..." + sqry index . + echo "Index rebuilt." + fi +fi + +# 7. Regenerate gstack skills (picks up {{SQRY_CONTEXT}} resolver) +GSTACK_DIR="${GSTACK_ROOT:-$HOME/.claude/skills/gstack}" +if [ -f "$GSTACK_DIR/package.json" ]; then + echo "" + echo "Regenerating gstack skill docs..." + (cd "$GSTACK_DIR" && bun run gen:skill-docs --host all 2>/dev/null) || { + echo "Warning: Could not regenerate skill docs. Run manually:" + echo " cd $GSTACK_DIR && bun run gen:skill-docs --host all" + } +fi + +echo "" +echo "Done. sqry structural code analysis is now available in gstack skills." +echo "" +echo "IMPORTANT: Restart your AI agent session for the MCP tools to appear." diff --git a/contrib/add-tool/sqry/tools.json b/contrib/add-tool/sqry/tools.json new file mode 100644 index 000000000..7c0681d4e --- /dev/null +++ b/contrib/add-tool/sqry/tools.json @@ -0,0 +1,83 @@ +{ + "tool": "sqry", + "homepage": "https://github.com/verivus-oss/sqry", + "mcp_server_name": "sqry", + "detection": { + "binary": "sqry", + "min_version": "7.0.0", + "rebuild_hint": "If you made structural changes this session, call rebuild_index before your next sqry query." + }, + "parameter_guidance": "Most sqry tools accept `max_depth` (default 3, max 10) and `max_results` (default 20, max 100). Scope queries to specific files or directories when possible — full-repo queries on large codebases are expensive. Use `semantic_search` for broad discovery, then `direct_callers`/`direct_callees` for focused tracing.", + "integrations": { + "investigate": { + "phase": "root-cause-investigation", + "context": "structural root cause analysis", + "tools": [ + { "tool": "direct_callers", "when": "immediate callers of suspect function" }, + { "tool": "direct_callees", "when": "immediate callees of suspect function" }, + { "tool": "call_hierarchy", "when": "multi-level caller/callee chains when one-hop insufficient" }, + { "tool": "is_node_in_cycle", "when": "check if bug site is in circular dependency" }, + { "tool": "trace_path", "when": "call path from entry point to bug site" }, + { "tool": "dependency_impact", "when": "blast radius — what else breaks if this symbol is wrong" }, + { "tool": "get_definition", "when": "jump to definition of symbol from stack traces" }, + { "tool": "get_references", "when": "all usages of suspect symbol across codebase" } + ] + }, + "cso": { + "phase": "structural-security-analysis", + "context": "AST-powered security audit", + "tools": [ + { "tool": "trace_path", "when": "structural call paths from input handlers to dangerous sinks (exec, eval, innerHTML, raw SQL)" }, + { "tool": "call_hierarchy", "when": "full call tree from auth/authz entry points to verify coverage" }, + { "tool": "find_cycles", "when": "circular dependencies that could cause infinite loops (DoS vectors)" }, + { "tool": "find_unused", "when": "dead code with old vulnerabilities or stale auth checks" }, + { "tool": "complexity_metrics", "when": "high-complexity functions (cyclomatic >15) for manual security review" }, + { "tool": "direct_callers", "when": "verify security-critical functions only called from trusted contexts" }, + { "tool": "semantic_search", "when": "functions matching security patterns (auth*, sanitize*, validate*, escape*)" }, + { "tool": "cross_language_edges", "when": "FFI/HTTP boundaries where trust assumptions change" } + ] + }, + "review": { + "phase": "structural-diff-analysis", + "context": "structural analysis of changed code", + "tools": [ + { "tool": "complexity_metrics", "when": "cyclomatic complexity of changed files — flag regressions" }, + { "tool": "find_cycles", "when": "check if changed symbols introduced or participate in cycles" }, + { "tool": "dependency_impact", "when": "downstream impact of changed public APIs" }, + { "tool": "find_unused", "when": "newly-dead code after refactors or API changes" }, + { "tool": "semantic_diff", "when": "structural changes between PR branch and base branch" }, + { "tool": "direct_callers", "when": "verify callers of changed functions still work with new signature" } + ] + }, + "retro": { + "phase": "structural-trend-analysis", + "context": "structural code quality analysis for retrospective", + "tools": [ + { "tool": "semantic_diff", "when": "structural changes between this week's HEAD and last week's tag/commit" }, + { "tool": "complexity_metrics", "when": "complexity trends — adding or reducing complexity?" }, + { "tool": "find_cycles", "when": "new cycles introduced this week" }, + { "tool": "get_insights", "when": "overall codebase health metrics for retrospective dashboard" } + ] + }, + "plan-eng-review": { + "phase": "architecture-understanding", + "context": "structural architecture analysis for plan review", + "tools": [ + { "tool": "export_graph", "when": "visualize module dependencies to validate architecture boundaries" }, + { "tool": "subgraph", "when": "dependency neighborhood around components the plan modifies" }, + { "tool": "show_dependencies", "when": "dependency tree of modules the plan touches" }, + { "tool": "find_cycles", "when": "existing cycles the plan should address or avoid" }, + { "tool": "cross_language_edges", "when": "cross-language boundaries the plan must respect" } + ] + }, + "ship": { + "phase": "pre-ship-structural-check", + "context": "structural verification before shipping", + "tools": [ + { "tool": "find_cycles", "when": "no circular dependencies in shipped code" }, + { "tool": "find_unused", "when": "catch dead code being shipped" }, + { "tool": "complexity_metrics", "when": "verify complexity hasn't regressed" } + ] + } + } +} diff --git a/contrib/add-tool/sqry/uninstall.sh b/contrib/add-tool/sqry/uninstall.sh new file mode 100644 index 000000000..aa620aa01 --- /dev/null +++ b/contrib/add-tool/sqry/uninstall.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +# Remove sqry integration from gstack. +# Does NOT uninstall sqry itself — only removes the gstack integration. +set -e + +echo "=== Removing sqry integration from gstack ===" + +# Helper: remove a key from a JSON file using node (portable) +remove_json_key() { + local file="$1" key_path="$2" + [ -f "$file" ] && command -v node >/dev/null 2>&1 || return 0 + node -e " + const fs = require('fs'); + try { + const s = JSON.parse(fs.readFileSync('$file', 'utf-8')); + const parts = '$key_path'.split('.'); + let obj = s; + for (let i = 0; i < parts.length - 1; i++) { + if (!obj[parts[i]]) return; + obj = obj[parts[i]]; + } + const last = parts[parts.length - 1]; + if (obj[last] !== undefined) { + delete obj[last]; + fs.writeFileSync('$file', JSON.stringify(s, null, 2)); + console.log('Removed ' + '$key_path' + ' from $file'); + } + } catch(e) {} + " 2>/dev/null || true +} + +# 1. Claude: global mcpServers.sqry + per-project mcpServers.sqry +for settings in "$HOME/.claude.json" "$HOME/.claude/settings.json"; do + remove_json_key "$settings" "mcpServers.sqry" + # Also clean per-project entries + if [ -f "$settings" ] && command -v node >/dev/null 2>&1; then + node -e " + const fs = require('fs'); + try { + const s = JSON.parse(fs.readFileSync('$settings', 'utf-8')); + if (s.projects) { + let changed = false; + for (const [k, v] of Object.entries(s.projects)) { + if (v && v.mcpServers && v.mcpServers.sqry) { + delete v.mcpServers.sqry; + changed = true; + } + } + if (changed) { + fs.writeFileSync('$settings', JSON.stringify(s, null, 2)); + console.log('Removed per-project sqry MCP entries from $settings'); + } + } + } catch(e) {} + " 2>/dev/null || true + fi +done + +# 2. Codex: [mcp_servers.sqry] section in TOML (portable, no sed -i) +CODEX_CONFIG="$HOME/.codex/config.toml" +if [ -f "$CODEX_CONFIG" ] && grep -q '\[mcp_servers\.sqry\]' "$CODEX_CONFIG" 2>/dev/null; then + node -e " + const fs = require('fs'); + const lines = fs.readFileSync('$CODEX_CONFIG', 'utf-8').split('\n'); + const out = []; + let skip = false; + for (const line of lines) { + if (/^\[mcp_servers\.sqry[\].]/.test(line.trim())) { skip = true; continue; } + if (skip && line.startsWith('[') && !/^\[mcp_servers\.sqry[\].]/.test(line.trim())) { skip = false; } + if (!skip) out.push(line); + } + fs.writeFileSync('$CODEX_CONFIG', out.join('\n')); + console.log('Removed [mcp_servers.sqry] from Codex config'); + " 2>/dev/null || true +fi + +# 3. Gemini: mcpServers.sqry in JSON +GEMINI_CONFIG="$HOME/.gemini/settings.json" +remove_json_key "$GEMINI_CONFIG" "mcpServers.sqry" + +# 4. Regenerate gstack skills ({{SQRY_CONTEXT}} emits nothing without sqry) +GSTACK_DIR="${GSTACK_ROOT:-$HOME/.claude/skills/gstack}" +if [ -f "$GSTACK_DIR/package.json" ]; then + echo "Regenerating gstack skill docs..." + (cd "$GSTACK_DIR" && bun run gen:skill-docs --host all 2>/dev/null) || true +fi + +echo "Done. sqry integration removed. sqry itself is still installed." +echo "To fully uninstall sqry: see https://github.com/verivus-oss/sqry#uninstall" diff --git a/cso/SKILL.md b/cso/SKILL.md index 89f2b13fb..8874d1333 100644 --- a/cso/SKILL.md +++ b/cso/SKILL.md @@ -95,6 +95,16 @@ if [ -d ".claude/skills/gstack" ] && [ ! -L ".claude/skills/gstack" ]; then fi fi echo "VENDORED_GSTACK: $_VENDORED" +# Semantic code search (sqry) — lightweight detection only (command -v is ~1ms). +# Index status is checked at query time by the agent, not at preamble load. +_SQRY="unavailable" +_SQRY_INDEXED="unknown" +if command -v sqry-mcp >/dev/null 2>&1; then + _SQRY="available" + [ -d ".sqry" ] && _SQRY_INDEXED="yes" || _SQRY_INDEXED="no" +fi +echo "SQRY: $_SQRY" +[ "$_SQRY" = "available" ] && echo "SQRY_INDEXED: $_SQRY_INDEXED" # Detect spawned session (OpenClaw or other orchestrator) [ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true ``` @@ -655,6 +665,27 @@ matches a past learning, display: This makes the compounding visible. The user should see that gstack is getting smarter on their codebase over time. +## Structural Code Analysis (sqry) + +If `SQRY: unavailable`: skip this section. +If `SQRY: available` but no `mcp__sqry__` tools visible: tell user to run `sqry mcp setup` and restart session. + +**Index freshness:** if `SQRY_INDEXED: no` → tell user to run `sqry index .` (typically 10-60s), then `mcp__sqry__rebuild_index`. +If you made structural changes this session, call rebuild_index before your next sqry query. + +**AST-powered security audit** — use these `mcp__sqry__` tools: + +- `mcp__sqry__trace_path` — structural call paths from input handlers to dangerous sinks (exec, eval, innerHTML, raw SQL) +- `mcp__sqry__call_hierarchy` — full call tree from auth/authz entry points to verify coverage +- `mcp__sqry__find_cycles` — circular dependencies that could cause infinite loops (DoS vectors) +- `mcp__sqry__find_unused` — dead code with old vulnerabilities or stale auth checks +- `mcp__sqry__complexity_metrics` — high-complexity functions (cyclomatic >15) for manual security review +- `mcp__sqry__direct_callers` — verify security-critical functions only called from trusted contexts +- `mcp__sqry__semantic_search` — functions matching security patterns (auth*, sanitize*, validate*, escape*) +- `mcp__sqry__cross_language_edges` — FFI/HTTP boundaries where trust assumptions change + +**Tool parameters:** Most sqry tools accept `max_depth` (default 3, max 10) and `max_results` (default 20, max 100). Scope queries to specific files or directories when possible — full-repo queries on large codebases are expensive. Use `semantic_search` for broad discovery, then `direct_callers`/`direct_callees` for focused tracing. + ### Phase 1: Attack Surface Census Map what an attacker sees — both code surface and infrastructure surface. diff --git a/cso/SKILL.md.tmpl b/cso/SKILL.md.tmpl index e12a690c2..474d046c3 100644 --- a/cso/SKILL.md.tmpl +++ b/cso/SKILL.md.tmpl @@ -111,6 +111,8 @@ This is NOT a checklist — it's a reasoning phase. The output is understanding, {{LEARNINGS_SEARCH}} +{{SQRY_CONTEXT}} + ### Phase 1: Attack Surface Census Map what an attacker sees — both code surface and infrastructure surface. diff --git a/design-consultation/SKILL.md b/design-consultation/SKILL.md index 68e488793..bd99cf47f 100644 --- a/design-consultation/SKILL.md +++ b/design-consultation/SKILL.md @@ -95,6 +95,16 @@ if [ -d ".claude/skills/gstack" ] && [ ! -L ".claude/skills/gstack" ]; then fi fi echo "VENDORED_GSTACK: $_VENDORED" +# Semantic code search (sqry) — lightweight detection only (command -v is ~1ms). +# Index status is checked at query time by the agent, not at preamble load. +_SQRY="unavailable" +_SQRY_INDEXED="unknown" +if command -v sqry-mcp >/dev/null 2>&1; then + _SQRY="available" + [ -d ".sqry" ] && _SQRY_INDEXED="yes" || _SQRY_INDEXED="no" +fi +echo "SQRY: $_SQRY" +[ "$_SQRY" = "available" ] && echo "SQRY_INDEXED: $_SQRY_INDEXED" # Detect spawned session (OpenClaw or other orchestrator) [ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true ``` diff --git a/design-html/SKILL.md b/design-html/SKILL.md index 10aaece0b..a737183e9 100644 --- a/design-html/SKILL.md +++ b/design-html/SKILL.md @@ -97,6 +97,16 @@ if [ -d ".claude/skills/gstack" ] && [ ! -L ".claude/skills/gstack" ]; then fi fi echo "VENDORED_GSTACK: $_VENDORED" +# Semantic code search (sqry) — lightweight detection only (command -v is ~1ms). +# Index status is checked at query time by the agent, not at preamble load. +_SQRY="unavailable" +_SQRY_INDEXED="unknown" +if command -v sqry-mcp >/dev/null 2>&1; then + _SQRY="available" + [ -d ".sqry" ] && _SQRY_INDEXED="yes" || _SQRY_INDEXED="no" +fi +echo "SQRY: $_SQRY" +[ "$_SQRY" = "available" ] && echo "SQRY_INDEXED: $_SQRY_INDEXED" # Detect spawned session (OpenClaw or other orchestrator) [ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true ``` diff --git a/design-review/SKILL.md b/design-review/SKILL.md index b87c509df..3a6b3c04c 100644 --- a/design-review/SKILL.md +++ b/design-review/SKILL.md @@ -95,6 +95,16 @@ if [ -d ".claude/skills/gstack" ] && [ ! -L ".claude/skills/gstack" ]; then fi fi echo "VENDORED_GSTACK: $_VENDORED" +# Semantic code search (sqry) — lightweight detection only (command -v is ~1ms). +# Index status is checked at query time by the agent, not at preamble load. +_SQRY="unavailable" +_SQRY_INDEXED="unknown" +if command -v sqry-mcp >/dev/null 2>&1; then + _SQRY="available" + [ -d ".sqry" ] && _SQRY_INDEXED="yes" || _SQRY_INDEXED="no" +fi +echo "SQRY: $_SQRY" +[ "$_SQRY" = "available" ] && echo "SQRY_INDEXED: $_SQRY_INDEXED" # Detect spawned session (OpenClaw or other orchestrator) [ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true ``` diff --git a/design-shotgun/SKILL.md b/design-shotgun/SKILL.md index d254d9d22..ac1e367c7 100644 --- a/design-shotgun/SKILL.md +++ b/design-shotgun/SKILL.md @@ -92,6 +92,16 @@ if [ -d ".claude/skills/gstack" ] && [ ! -L ".claude/skills/gstack" ]; then fi fi echo "VENDORED_GSTACK: $_VENDORED" +# Semantic code search (sqry) — lightweight detection only (command -v is ~1ms). +# Index status is checked at query time by the agent, not at preamble load. +_SQRY="unavailable" +_SQRY_INDEXED="unknown" +if command -v sqry-mcp >/dev/null 2>&1; then + _SQRY="available" + [ -d ".sqry" ] && _SQRY_INDEXED="yes" || _SQRY_INDEXED="no" +fi +echo "SQRY: $_SQRY" +[ "$_SQRY" = "available" ] && echo "SQRY_INDEXED: $_SQRY_INDEXED" # Detect spawned session (OpenClaw or other orchestrator) [ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true ``` diff --git a/devex-review/SKILL.md b/devex-review/SKILL.md index 96575feab..571b05d4b 100644 --- a/devex-review/SKILL.md +++ b/devex-review/SKILL.md @@ -95,6 +95,16 @@ if [ -d ".claude/skills/gstack" ] && [ ! -L ".claude/skills/gstack" ]; then fi fi echo "VENDORED_GSTACK: $_VENDORED" +# Semantic code search (sqry) — lightweight detection only (command -v is ~1ms). +# Index status is checked at query time by the agent, not at preamble load. +_SQRY="unavailable" +_SQRY_INDEXED="unknown" +if command -v sqry-mcp >/dev/null 2>&1; then + _SQRY="available" + [ -d ".sqry" ] && _SQRY_INDEXED="yes" || _SQRY_INDEXED="no" +fi +echo "SQRY: $_SQRY" +[ "$_SQRY" = "available" ] && echo "SQRY_INDEXED: $_SQRY_INDEXED" # Detect spawned session (OpenClaw or other orchestrator) [ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true ``` diff --git a/docs/superpowers/plans/2026-04-06-sqry-add-in.dag.toml b/docs/superpowers/plans/2026-04-06-sqry-add-in.dag.toml new file mode 100644 index 000000000..37c11175d --- /dev/null +++ b/docs/superpowers/plans/2026-04-06-sqry-add-in.dag.toml @@ -0,0 +1,140 @@ +# sqry Add-in for gstack — Implementation DAG +# +# Spec: docs/superpowers/specs/2026-04-06-sqry-add-in-design.md +# Approved: 2026-04-06 (Gemini 3 Pro + OpenAI Codex gpt-5.4, 4 rounds) +# +# Nodes are tasks. `depends_on` edges encode ordering constraints. +# Tasks with no shared dependency can run in parallel. + +[meta] +spec = "docs/superpowers/specs/2026-04-06-sqry-add-in-design.md" +branch = "feat/sqry-add-in" +base = "main" + +# ───────────────────────────────────────────────────────────────── +# Phase 0: Scaffolding (no dependencies — all parallelizable) +# ───────────────────────────────────────────────────────────────── + +[tasks.scaffold_tools_json] +description = "Create contrib/add-tool/sqry/tools.json — the routing contract" +files_create = ["contrib/add-tool/sqry/tools.json"] +depends_on = [] + +[tasks.scaffold_detection] +description = "Create contrib/add-tool/sqry/detection.sh — preamble bash fragment" +files_create = ["contrib/add-tool/sqry/detection.sh"] +depends_on = [] + +[tasks.scaffold_install] +description = "Create contrib/add-tool/sqry/install.sh — idempotent installer" +files_create = ["contrib/add-tool/sqry/install.sh"] +depends_on = [] + +[tasks.scaffold_uninstall] +description = "Create contrib/add-tool/sqry/uninstall.sh — clean removal" +files_create = ["contrib/add-tool/sqry/uninstall.sh"] +depends_on = [] + +[tasks.scaffold_readme] +description = "Create contrib/add-tool/sqry/README.md — integration docs" +files_create = ["contrib/add-tool/sqry/README.md"] +depends_on = [] + +# ───────────────────────────────────────────────────────────────── +# Phase 1: Resolver + registration (depends on tools.json) +# ───────────────────────────────────────────────────────────────── + +[tasks.resolver] +description = "Create scripts/resolvers/sqry.ts — reads tools.json, generates conditional markdown per skill" +files_create = ["scripts/resolvers/sqry.ts"] +depends_on = ["scaffold_tools_json"] + +[tasks.register_resolver] +description = "Modify scripts/resolvers/index.ts — import generateSqryContext, add SQRY_CONTEXT to RESOLVERS" +files_modify = ["scripts/resolvers/index.ts"] +depends_on = ["resolver"] + +# ───────────────────────────────────────────────────────────────── +# Phase 2: Preamble detection (depends on detection.sh) +# ───────────────────────────────────────────────────────────────── + +[tasks.preamble_detection] +description = "Modify scripts/resolvers/preamble.ts — inline detection.sh bash after llm-gateway block" +files_modify = ["scripts/resolvers/preamble.ts"] +depends_on = ["scaffold_detection"] + +# ───────────────────────────────────────────────────────────────── +# Phase 3: Template placement (depends on resolver registration) +# All 6 templates are independent of each other. +# ───────────────────────────────────────────────────────────────── + +[tasks.tmpl_investigate] +description = "Add {{SQRY_CONTEXT}} to investigate/SKILL.md.tmpl after {{LLM_GATEWAY_CONTEXT}}" +files_modify = ["investigate/SKILL.md.tmpl"] +depends_on = ["register_resolver"] + +[tasks.tmpl_cso] +description = "Add {{SQRY_CONTEXT}} to cso/SKILL.md.tmpl after {{LEARNINGS_SEARCH}}" +files_modify = ["cso/SKILL.md.tmpl"] +depends_on = ["register_resolver"] + +[tasks.tmpl_review] +description = "Add {{SQRY_CONTEXT}} to review/SKILL.md.tmpl after {{LLM_GATEWAY_CONTEXT}}" +files_modify = ["review/SKILL.md.tmpl"] +depends_on = ["register_resolver"] + +[tasks.tmpl_retro] +description = "Add {{SQRY_CONTEXT}} to retro/SKILL.md.tmpl after {{LLM_GATEWAY_CONTEXT}}" +files_modify = ["retro/SKILL.md.tmpl"] +depends_on = ["register_resolver"] + +[tasks.tmpl_plan_eng_review] +description = "Add {{SQRY_CONTEXT}} to plan-eng-review/SKILL.md.tmpl after {{LLM_GATEWAY_CONTEXT}}" +files_modify = ["plan-eng-review/SKILL.md.tmpl"] +depends_on = ["register_resolver"] + +[tasks.tmpl_ship] +description = "Add {{SQRY_CONTEXT}} to ship/SKILL.md.tmpl after {{LLM_GATEWAY_CONTEXT}}" +files_modify = ["ship/SKILL.md.tmpl"] +depends_on = ["register_resolver"] + +# ───────────────────────────────────────────────────────────────── +# Phase 3b: Update shared contrib README (no code dependency) +# ───────────────────────────────────────────────────────────────── + +[tasks.update_contrib_readme] +description = "Update contrib/add-tool/README.md — add sqry to integrations list, fix placement rule on line 27" +files_modify = ["contrib/add-tool/README.md"] +depends_on = [] + +# ───────────────────────────────────────────────────────────────── +# Phase 4: Regenerate SKILL.md files (depends on all template + resolver work) +# ───────────────────────────────────────────────────────────────── + +[tasks.gen_skill_docs] +description = "Run bun run gen:skill-docs to regenerate all SKILL.md files from templates" +command = "bun run gen:skill-docs" +depends_on = [ + "register_resolver", + "preamble_detection", + "tmpl_investigate", + "tmpl_cso", + "tmpl_review", + "tmpl_retro", + "tmpl_plan_eng_review", + "tmpl_ship", +] + +# ───────────────────────────────────────────────────────────────── +# Phase 5: Tests (depends on resolver + tools.json) +# ───────────────────────────────────────────────────────────────── + +[tasks.test_resolver] +description = "Create test/sqry-resolver.test.ts — resolver + tools.json validation (mirrors llm-gateway test)" +files_create = ["test/sqry-resolver.test.ts"] +depends_on = ["resolver", "scaffold_tools_json"] + +[tasks.run_tests] +description = "Run bun test to validate resolver, skill docs, and existing tests still pass" +command = "bun test" +depends_on = ["gen_skill_docs", "test_resolver"] diff --git a/docs/superpowers/plans/2026-04-06-sqry-v8-alignment.dag.toml b/docs/superpowers/plans/2026-04-06-sqry-v8-alignment.dag.toml new file mode 100644 index 000000000..ab5f13f5c --- /dev/null +++ b/docs/superpowers/plans/2026-04-06-sqry-v8-alignment.dag.toml @@ -0,0 +1,133 @@ +# sqry v8.0.0 Alignment — Resource Delegation Restructure +# +# Problem: Our tools.json + resolver hardcode thick static tool guidance +# (constraints, defaults, tiering) that duplicates and will drift from +# sqry v8's live MCP resources (sqry://docs/capability-map, tool-guide). +# +# Solution: Make gstack's sqry integration "thin" like the v8 skills: +# - Keep: tool routing (which tools for which skill phase), gating, index freshness +# - Remove: hardcoded defaults, per-tool constraints, output limit prose +# - Add: MCP resource delegation (read sqry://docs/* for parameter guidance) +# - Add: manifest check (sqry://meta/manifest for server health) +# +# No template changes needed — {{SQRY_CONTEXT}} placement stays the same. +# No preamble changes needed — detection block is unaffected. +# No install/uninstall/detection.sh changes needed. + +[meta] +spec = "docs/superpowers/specs/2026-04-06-sqry-add-in-design.md" +branch = "feat/sqry-add-in" +base = "main" +reason = "Align with sqry-skills v8.0.0 resource delegation architecture" + +# ───────────────────────────────────────────────────────────────── +# Phase 1: Restructure tools.json (source of truth) +# ───────────────────────────────────────────────────────────────── + +[tasks.thin_tools_json] +description = """Restructure contrib/add-tool/sqry/tools.json: +- Remove top-level `defaults` section (max_depth, max_results, rebuild_hint) +- Remove per-tool `constraint` fields from all integrations +- Add top-level `mcp_resources` object mapping purpose to sqry:// URIs: + "capability_map": "sqry://docs/capability-map" + "tool_guide": "sqry://docs/tool-guide" + "manifest": "sqry://meta/manifest" +- Add `rebuild_hint` as a plain string under detection (gstack-specific, not a sqry default) +- Keep: tool routing (tool name + when clause), phase, context — these are gstack's value-add +- Keep: min_version at 7.0.0 (we use index --status --json which requires it) +- `when` clauses stay as-is — they describe WHEN to use the tool in the skill phase, + not HOW to parameterize it (that's the MCP resource's job) +""" +files_modify = ["contrib/add-tool/sqry/tools.json"] +depends_on = [] + +# ───────────────────────────────────────────────────────────────── +# Phase 2: Restructure resolver (depends on new tools.json shape) +# ───────────────────────────────────────────────────────────────── + +[tasks.thin_resolver] +description = """Restructure scripts/resolvers/sqry.ts to match v8 resource delegation: +- Update ToolsConfig interface: remove `defaults`, add `mcp_resources` record, + move rebuild_hint under detection +- Remove ToolMapping.constraint field from interface +- Remove constraint annotation rendering from tool list +- Remove hardcoded "Output limits" paragraph +- Add MCP resource delegation block after tool list: + "For parameter guidance (max_depth, max_results, scoping), read + `sqry://docs/capability-map` via ReadMcpResourceTool. + For full tool reference, read `sqry://docs/tool-guide`." +- Add manifest health check: "Before first sqry query, read + `sqry://meta/manifest` to confirm the MCP server is connected." +- Keep: SQRY availability gating, MCP runtime gate, index freshness, rebuild_hint +- Keep: tool list with `when` clauses (no constraints) +""" +files_modify = ["scripts/resolvers/sqry.ts"] +depends_on = ["thin_tools_json"] + +# ───────────────────────────────────────────────────────────────── +# Phase 3: Update tests (depends on new resolver output) +# ───────────────────────────────────────────────────────────────── + +[tasks.update_tests] +description = """Update test/sqry-resolver.test.ts to match restructured output: +- Remove test 'has defaults section with output limits' +- Remove test 'constraint annotations appear in tool list' +- Remove test 'tools without constraints have no bold annotation' +- Remove per-tool constraint tests from schema validation loop +- Remove test checking output contains 'max_depth 2' and 'max_results 50' +- Add test: tools.json has mcp_resources with capability_map, tool_guide, manifest URIs +- Add test: tools.json has no `defaults` key +- Add test: no tool in any integration has a `constraint` field +- Add test: resolver output contains 'sqry://docs/capability-map' +- Add test: resolver output contains 'sqry://meta/manifest' +- Add test: resolver output contains 'ReadMcpResourceTool' +- Keep: all tool name validation, when clause validation, gating tests, + index freshness tests, preamble tests, SKILL.md content tests +""" +files_modify = ["test/sqry-resolver.test.ts"] +depends_on = ["thin_resolver"] + +# ───────────────────────────────────────────────────────────────── +# Phase 4: Regenerate + update golden files +# ───────────────────────────────────────────────────────────────── + +[tasks.gen_skill_docs] +description = "Run bun run gen:skill-docs --host all to regenerate all SKILL.md files" +command = "bun run gen:skill-docs --host all" +depends_on = ["thin_resolver"] + +[tasks.update_golden] +description = """Update golden baseline files for ship skill: +- cp ship/SKILL.md test/fixtures/golden/claude-ship-SKILL.md +- cp .agents/skills/gstack-ship/SKILL.md test/fixtures/golden/codex-ship-SKILL.md +- cp .factory/skills/gstack-ship/SKILL.md test/fixtures/golden/factory-ship-SKILL.md +""" +files_modify = [ + "test/fixtures/golden/claude-ship-SKILL.md", + "test/fixtures/golden/codex-ship-SKILL.md", + "test/fixtures/golden/factory-ship-SKILL.md", +] +depends_on = ["gen_skill_docs"] + +# ───────────────────────────────────────────────────────────────── +# Phase 5: Run tests +# ───────────────────────────────────────────────────────────────── + +[tasks.run_tests] +description = "Run bun test to validate everything passes" +command = "bun test" +depends_on = ["update_tests", "update_golden"] + +# ───────────────────────────────────────────────────────────────── +# Phase 6: Update contrib README to mention resource delegation +# ───────────────────────────────────────────────────────────────── + +[tasks.update_sqry_readme] +description = """Update contrib/add-tool/sqry/README.md: +- Add note that parameter guidance is served live via sqry MCP resources +- Mention sqry://docs/capability-map and sqry://docs/tool-guide +- Clarify that tools.json defines WHEN to use tools (skill-phase routing), + while MCP resources define HOW (parameter limits, tiering, scoping) +""" +files_modify = ["contrib/add-tool/sqry/README.md"] +depends_on = [] diff --git a/docs/superpowers/specs/2026-04-06-sqry-add-in-design.md b/docs/superpowers/specs/2026-04-06-sqry-add-in-design.md new file mode 100644 index 000000000..d5ad71196 --- /dev/null +++ b/docs/superpowers/specs/2026-04-06-sqry-add-in-design.md @@ -0,0 +1,823 @@ +# sqry Add-in for gstack + +**Date:** 2026-04-06 +**Status:** Design approved +**Reviewers:** Gemini 3 Pro, OpenAI Codex (gpt-5.4) + +## Summary + +Add sqry (AST-based semantic code search) as a first-class add-in to gstack, +following the established `contrib/add-tool/` pattern. sqry is optional — gstack +works without it. When present, six skills gain structural code analysis +capabilities: `/investigate`, `/cso`, `/review`, `/retro`, `/plan-eng-review`, +and `/ship`. + +## Prior Art: The Add-in Pattern + +gstack has one existing add-in: llm-gateway (`contrib/add-tool/llm-gateway/`). +The pattern is: + +``` +contrib/add-tool// +├── README.md # What the tool does and how the integration works +├── tools.json # Routing contract: which gstack skills use which tools +├── detection.sh # Bash fragment inlined by preamble.ts for detection +├── install.sh # Idempotent install script +└── uninstall.sh # Clean removal script +``` + +Plus: +- A TypeScript resolver (`scripts/resolvers/.ts`) reads `tools.json` and + generates conditional markdown blocks per skill +- Registration in `scripts/resolvers/index.ts` as `{{PLACEHOLDER}}` +- Detection bash inlined into `scripts/resolvers/preamble.ts` +- `{{PLACEHOLDER}}` placed in each skill's `.tmpl` file (after + `{{LLM_GATEWAY_CONTEXT}}` where present; otherwise after `{{LEARNINGS_SEARCH}}`) + +### Requirements from `contrib/add-tool/README.md` + +- Tool MUST be optional — gstack works without it +- Detection MUST be fast (< 50ms) — it runs on every skill invocation +- Resolver output MUST be concise — avoid prompt bloat +- Install script MUST be idempotent +- Uninstall script MUST leave gstack in a clean state +- tools.json MUST include min_version for compatibility gating + +## Design + +### 1. detection.sh — Preamble Fragment + +Uses sqry's own `index --status --json` command (discovered via Codex review: +runs in <1ms, returns structured JSON including a `stale` field computed via +sqry's internal hash-based change detection). This replaces an earlier design +that used `find -newer` which was fragile on monorepos and probed sqry internals +(`.sqry/graph/snapshot.sqry`) instead of using the supported API. + +```bash +# Semantic code search (sqry) +_SQRY="unavailable" +_SQRY_INDEXED="no" +_SQRY_STALE="no" +if command -v sqry >/dev/null 2>&1; then + _SQRY="available" + _SQRY_VERSION=$(sqry --version 2>/dev/null | head -1 || echo "unknown") + _SQRY_STATUS=$(sqry index --status --json . 2>/dev/null || echo '{}') + if echo "$_SQRY_STATUS" | grep -q '"exists": true' 2>/dev/null; then + _SQRY_INDEXED="yes" + fi + if echo "$_SQRY_STATUS" | grep -q '"stale": true' 2>/dev/null; then + _SQRY_STALE="yes" + fi +fi +echo "SQRY: $_SQRY" +[ "$_SQRY" = "available" ] && echo "SQRY_VERSION: $_SQRY_VERSION" +[ "$_SQRY" = "available" ] && echo "SQRY_INDEXED: $_SQRY_INDEXED" +[ "$_SQRY" = "available" ] && echo "SQRY_STALE: $_SQRY_STALE" +``` + +**Performance budget:** `sqry --version` is <1ms. `sqry index --status --json` +is <1ms (reads manifest only, no file scanning). Total: <5ms, well under 50ms. + +**Preamble output variables:** + +| Variable | Values | Meaning | +|----------|--------|---------| +| `SQRY` | `available` / `unavailable` | sqry binary on PATH | +| `SQRY_VERSION` | e.g. `7.1.4` | CLI version | +| `SQRY_INDEXED` | `yes` / `no` | `.sqry/` index exists for this repo | +| `SQRY_STALE` | `yes` / `no` | Index out of date (sqry's own detection) | + +### 2. tools.json — Routing Contract + +Defines which sqry MCP tools are recommended in which gstack skills, at which +phase, with what constraints. This extends the llm-gateway `tools.json` schema +with two additional fields: `defaults` (global output limits) and per-tool +`constraint` (tool-specific parameter guidance). These additions are covered +by `test/sqry-resolver.test.ts` which mirrors `test/llm-gateway-resolver.test.ts`. + +```json +{ + "tool": "sqry", + "homepage": "https://github.com/verivus-oss/sqry", + "mcp_server_name": "sqry", + "detection": { + "binary": "sqry", + "min_version": "7.0.0" + }, + "defaults": { + "max_depth": 2, + "max_results": 50, + "rebuild_hint": "If you made structural changes this session, call rebuild_index before your next sqry query." + }, + "integrations": { + "investigate": { + "phase": "root-cause-investigation", + "context": "structural root cause analysis", + "tools": [ + { + "tool": "direct_callers", + "when": "find immediate callers of the suspect function (one-hop)", + "constraint": "max_results: 50" + }, + { + "tool": "direct_callees", + "when": "find immediate callees of the suspect function (one-hop)", + "constraint": "max_results: 50" + }, + { + "tool": "call_hierarchy", + "when": "trace multi-level caller/callee chains when one-hop is insufficient", + "constraint": "max_depth: 2, direction: incoming or outgoing" + }, + { + "tool": "is_node_in_cycle", + "when": "check if the bug site is in a circular dependency" + }, + { + "tool": "trace_path", + "when": "find the call path from entry point to bug site", + "constraint": "max_hops: 5" + }, + { + "tool": "dependency_impact", + "when": "understand blast radius — what else breaks if this symbol is wrong", + "constraint": "max_depth: 3" + }, + { + "tool": "get_definition", + "when": "jump to the actual definition of a symbol referenced in stack traces" + }, + { + "tool": "get_references", + "when": "find all usages of a suspect symbol across the codebase" + } + ] + }, + "cso": { + "phase": "structural-security-analysis", + "context": "AST-powered security audit", + "tools": [ + { + "tool": "trace_path", + "when": "trace structural call paths from input handlers toward dangerous sinks (exec, eval, innerHTML, raw SQL)", + "constraint": "max_hops: 5" + }, + { + "tool": "call_hierarchy", + "when": "map the full call tree from auth/authz entry points to verify coverage", + "constraint": "max_depth: 2" + }, + { + "tool": "find_cycles", + "when": "detect circular dependencies that could cause infinite loops (DoS vectors)", + "constraint": "scope to files from Phase 1 attack surface" + }, + { + "tool": "find_unused", + "when": "find dead code that may contain old vulnerabilities or stale auth checks", + "constraint": "filter by language detected in Phase 0" + }, + { + "tool": "complexity_metrics", + "when": "flag high-complexity functions (cyclomatic >15) for manual security review", + "constraint": "scope to file_path from Phase 1" + }, + { + "tool": "direct_callers", + "when": "verify that security-critical functions are only called from trusted contexts", + "constraint": "max_results: 50" + }, + { + "tool": "semantic_search", + "when": "find all functions matching security-relevant patterns (auth*, sanitize*, validate*, escape*)" + }, + { + "tool": "cross_language_edges", + "when": "find FFI/HTTP boundaries where trust assumptions change" + } + ] + }, + "review": { + "phase": "structural-diff-analysis", + "context": "structural analysis of changed code", + "tools": [ + { + "tool": "complexity_metrics", + "when": "check cyclomatic complexity of changed files — flag regressions", + "constraint": "scope to changed file paths" + }, + { + "tool": "find_cycles", + "when": "check if changed symbols introduced or participate in cycles", + "constraint": "scope to changed files" + }, + { + "tool": "dependency_impact", + "when": "analyze downstream impact of changed public APIs", + "constraint": "max_depth: 2" + }, + { + "tool": "find_unused", + "when": "catch newly-dead code after refactors or API changes", + "constraint": "scope to changed files" + }, + { + "tool": "semantic_diff", + "when": "compare structural changes between the PR branch and base branch" + }, + { + "tool": "direct_callers", + "when": "verify callers of changed functions still work with the new signature", + "constraint": "max_results: 50" + } + ] + }, + "retro": { + "phase": "structural-trend-analysis", + "context": "structural code quality analysis for retrospective", + "tools": [ + { + "tool": "semantic_diff", + "when": "compare structural changes between this week's HEAD and last week's tag/commit" + }, + { + "tool": "complexity_metrics", + "when": "track complexity trends — are we adding or reducing complexity?", + "constraint": "scope to files changed this week" + }, + { + "tool": "find_cycles", + "when": "check if new cycles were introduced this week" + }, + { + "tool": "get_insights", + "when": "get overall codebase health metrics for the retrospective dashboard" + } + ] + }, + "plan-eng-review": { + "phase": "architecture-understanding", + "context": "structural architecture analysis for plan review", + "tools": [ + { + "tool": "export_graph", + "when": "visualize module dependencies to validate architecture boundaries", + "constraint": "format: mermaid" + }, + { + "tool": "subgraph", + "when": "extract the dependency neighborhood around components the plan modifies" + }, + { + "tool": "show_dependencies", + "when": "verify dependency tree of modules the plan touches" + }, + { + "tool": "find_cycles", + "when": "check for existing cycles the plan should address or avoid" + }, + { + "tool": "cross_language_edges", + "when": "understand cross-language boundaries the plan must respect" + } + ] + }, + "ship": { + "phase": "pre-ship-structural-check", + "context": "structural verification before shipping", + "tools": [ + { + "tool": "find_cycles", + "when": "verify no circular dependencies in shipped code", + "constraint": "scope to changed files" + }, + { + "tool": "find_unused", + "when": "catch dead code being shipped", + "constraint": "scope to changed files" + }, + { + "tool": "complexity_metrics", + "when": "verify complexity hasn't regressed", + "constraint": "scope to changed files" + } + ] + } + } +} +``` + +### 3. scripts/resolvers/sqry.ts — Resolver + +Reads `tools.json` and generates conditional markdown per skill. Follows the +same structure as `llm-gateway.ts` with three additions addressing review +feedback: + +- **Rebuild hint** when index is stale or missing +- **Constraint annotations** per tool (max_depth, max_results, max_hops) +- **Runtime MCP gate** — tells the agent to check for actual `mcp__sqry__*` + tools at runtime, not just the preamble binary detection (Gemini point #3: + PATH check != MCP configured) + +```typescript +import type { TemplateContext, ResolverFn } from './types'; +import { readFileSync } from 'fs'; +import { resolve } from 'path'; + +interface ToolMapping { + tool: string; + when: string; + constraint?: string; +} + +interface SkillIntegration { + phase: string; + context: string; + tools: ToolMapping[]; +} + +interface ToolsConfig { + tool: string; + mcp_server_name: string; + detection: { binary: string; min_version: string }; + defaults: { max_depth: number; max_results: number; rebuild_hint: string }; + integrations: Record; +} + +let cachedConfig: ToolsConfig | null = null; + +function loadToolsConfig(): ToolsConfig { + if (cachedConfig) return cachedConfig; + const configPath = resolve( + import.meta.dir, + '../../contrib/add-tool/sqry/tools.json', + ); + cachedConfig = JSON.parse(readFileSync(configPath, 'utf-8')); + return cachedConfig!; +} + +export const generateSqryContext: ResolverFn = ( + ctx: TemplateContext, +): string => { + let config: ToolsConfig; + try { + config = loadToolsConfig(); + } catch { + return ''; + } + + const integration = config.integrations[ctx.skillName]; + if (!integration) return ''; + + const prefix = `mcp__${config.mcp_server_name}__`; + + const toolList = integration.tools + .map((t) => { + const note = t.constraint ? ` (**${t.constraint}**)` : ''; + return `- \`${prefix}${t.tool}\` — ${t.when}${note}`; + }) + .join('\n'); + + return `## Structural Code Analysis (sqry) + +If preamble shows \`SQRY: unavailable\`: skip this section entirely. + +If preamble shows \`SQRY: available\`: check your available tools for the \`${prefix}\` prefix. +- If you see \`${prefix}\` tools: use them as described below. +- If you do NOT see \`${prefix}\` tools despite \`SQRY: available\`: tell the user + "sqry is installed but not configured as an MCP server. Run \`sqry mcp setup\` + to enable structural code analysis, then restart this session." + +**Index freshness:** +- If \`SQRY_INDEXED: no\`: run \`${prefix}rebuild_index\` before any queries. +- If \`SQRY_STALE: yes\`: run \`${prefix}rebuild_index\` before any queries. +- ${config.defaults.rebuild_hint} + +**During ${integration.context}**, use these sqry MCP tools: + +${toolList} + +**Output limits:** Default to max_depth ${config.defaults.max_depth}, max_results ${config.defaults.max_results}. +Only increase when the narrower result is insufficient — large results exhaust context.`; +}; +``` + +### 4. scripts/resolvers/index.ts — Registration + +Add import and entry: + +```typescript +import { generateSqryContext } from './sqry'; + +// In RESOLVERS record: +SQRY_CONTEXT: generateSqryContext, +``` + +### 5. scripts/resolvers/preamble.ts — Detection Inline + +Add the detection.sh bash fragment after the existing llm-gateway detection +block (around line 106), following the same inline pattern: + +```typescript +// After the LLM_GATEWAY echo lines: +// Semantic code search (sqry) +_SQRY="unavailable" +_SQRY_INDEXED="no" +_SQRY_STALE="no" +if command -v sqry >/dev/null 2>&1; then + _SQRY="available" + _SQRY_VERSION=$(sqry --version 2>/dev/null | head -1 || echo "unknown") + _SQRY_STATUS=$(sqry index --status --json . 2>/dev/null || echo '{}') + if echo "$_SQRY_STATUS" | grep -q '"exists": true' 2>/dev/null; then + _SQRY_INDEXED="yes" + fi + if echo "$_SQRY_STATUS" | grep -q '"stale": true' 2>/dev/null; then + _SQRY_STALE="yes" + fi +fi +echo "SQRY: $_SQRY" +[ "$_SQRY" = "available" ] && echo "SQRY_VERSION: $_SQRY_VERSION" +[ "$_SQRY" = "available" ] && echo "SQRY_INDEXED: $_SQRY_INDEXED" +[ "$_SQRY" = "available" ] && echo "SQRY_STALE: $_SQRY_STALE" +``` + +### 6. Template Placement — `{{SQRY_CONTEXT}}` + +Insert `{{SQRY_CONTEXT}}` immediately after `{{LLM_GATEWAY_CONTEXT}}` where +present; otherwise place it after `{{LEARNINGS_SEARCH}}`. + +| Template | After | +|----------|-------| +| `investigate/SKILL.md.tmpl` | `{{LLM_GATEWAY_CONTEXT}}` | +| `cso/SKILL.md.tmpl` | `{{LEARNINGS_SEARCH}}` (before Phase 1) | +| `review/SKILL.md.tmpl` | `{{LLM_GATEWAY_CONTEXT}}` | +| `retro/SKILL.md.tmpl` | `{{LLM_GATEWAY_CONTEXT}}` | +| `plan-eng-review/SKILL.md.tmpl` | `{{LLM_GATEWAY_CONTEXT}}` | +| `ship/SKILL.md.tmpl` | `{{LLM_GATEWAY_CONTEXT}}` | + +### 7. install.sh + +```bash +#!/usr/bin/env bash +# Install sqry as a gstack structural code analysis add-in. +# Idempotent — safe to run multiple times. +set -e + +AGENT="${1:-claude}" +MIN_VERSION="7.0.0" + +echo "=== sqry integration for gstack ===" +echo "" + +# 1. Check for sqry CLI +if ! command -v sqry >/dev/null 2>&1; then + echo "sqry not found on PATH." + echo "" + echo "Install via the signed installer:" + echo " curl -fsSL https://raw.githubusercontent.com/verivus-oss/sqry/main/scripts/install.sh | bash -s -- --component all" + echo "" + echo "Or via cargo:" + echo " cargo install sqry-cli sqry-mcp" + echo "" + echo "Then re-run this script." + exit 1 +fi + +# 2. Check version (normalize: "sqry 7.1.4" -> "7.1.4") +SQRY_VERSION=$(sqry --version 2>/dev/null | awk '{print $2}' || echo "0.0.0") +echo "Found sqry $SQRY_VERSION" + +# Portable semver comparator (no sort -V, works on macOS) +version_lt() { + local IFS=. + local i a=($1) b=($2) + for ((i=0; i<${#b[@]}; i++)); do + [ -z "${a[i]}" ] && a[i]=0 + if ((10#${a[i]} < 10#${b[i]})); then return 0; fi + if ((10#${a[i]} > 10#${b[i]})); then return 1; fi + done + return 1 +} + +if version_lt "$SQRY_VERSION" "$MIN_VERSION"; then + echo "sqry $MIN_VERSION+ required. Please upgrade:" + echo " curl -fsSL https://raw.githubusercontent.com/verivus-oss/sqry/main/scripts/install.sh | bash -s -- --component all" + exit 1 +fi + +# 3. Check for sqry-mcp +if ! command -v sqry-mcp >/dev/null 2>&1; then + echo "sqry-mcp not found on PATH." + echo "" + echo "Install the MCP server:" + echo " curl -fsSL https://raw.githubusercontent.com/verivus-oss/sqry/main/scripts/install.sh | bash -s -- --component mcp" + echo "" + echo "Or via cargo:" + echo " cargo install sqry-mcp" + echo "" + echo "Then re-run this script." + exit 1 +fi + +echo "Found sqry-mcp at $(command -v sqry-mcp)" + +# 4. Configure MCP for the target agent +# Delegate to sqry's own setup command — it knows each host's config format. +echo "" +echo "Configuring MCP server for $AGENT..." + +case "$AGENT" in + claude) sqry mcp setup --tool claude ;; + codex) sqry mcp setup --tool codex ;; + gemini) sqry mcp setup --tool gemini ;; + all) sqry mcp setup ;; + *) echo "Warning: Auto-configuration not supported for $AGENT. Run 'sqry mcp setup' manually." ;; +esac + +# 5. Verify MCP configuration +echo "" +echo "MCP status:" +sqry mcp status 2>/dev/null || echo " (could not verify — run 'sqry mcp status' manually)" + +# 6. Build initial index if not present +if ! sqry index --status --json . 2>/dev/null | grep -q '"exists": true'; then + echo "" + echo "Building initial sqry index..." + sqry index . + echo "Index built." +else + echo "" + echo "sqry index already exists." + if sqry index --status --json . 2>/dev/null | grep -q '"stale": true'; then + echo "Index is stale — rebuilding..." + sqry index . + echo "Index rebuilt." + fi +fi + +# 7. Regenerate gstack skills (picks up {{SQRY_CONTEXT}} resolver) +GSTACK_DIR="${GSTACK_ROOT:-$HOME/.claude/skills/gstack}" +if [ -f "$GSTACK_DIR/package.json" ]; then + echo "" + echo "Regenerating gstack skill docs..." + (cd "$GSTACK_DIR" && bun run gen:skill-docs --host all 2>/dev/null) || { + echo "Warning: Could not regenerate skill docs. Run manually:" + echo " cd $GSTACK_DIR && bun run gen:skill-docs --host all" + } +fi + +echo "" +echo "Done. sqry structural code analysis is now available in gstack skills." +echo "" +echo "IMPORTANT: Restart your AI agent session for the MCP tools to appear." +``` + +### 8. uninstall.sh + +```bash +#!/usr/bin/env bash +# Remove sqry integration from gstack. +# Does NOT uninstall sqry itself — only removes the gstack integration. +set -e + +echo "=== Removing sqry integration from gstack ===" + +# Helper: remove a key from a JSON file using node (portable) +remove_json_key() { + local file="$1" key_path="$2" + [ -f "$file" ] && command -v node >/dev/null 2>&1 || return 0 + node -e " + const fs = require('fs'); + try { + const s = JSON.parse(fs.readFileSync('$file', 'utf-8')); + const parts = '$key_path'.split('.'); + let obj = s; + for (let i = 0; i < parts.length - 1; i++) { + if (!obj[parts[i]]) return; + obj = obj[parts[i]]; + } + const last = parts[parts.length - 1]; + if (obj[last] !== undefined) { + delete obj[last]; + fs.writeFileSync('$file', JSON.stringify(s, null, 2)); + console.log('Removed ' + '$key_path' + ' from $file'); + } + } catch(e) {} + " 2>/dev/null || true +} + +# 1. Claude: global mcpServers.sqry + per-project mcpServers.sqry +for settings in "$HOME/.claude.json" "$HOME/.claude/settings.json"; do + remove_json_key "$settings" "mcpServers.sqry" + # Also clean per-project entries + if [ -f "$settings" ] && command -v node >/dev/null 2>&1; then + node -e " + const fs = require('fs'); + try { + const s = JSON.parse(fs.readFileSync('$settings', 'utf-8')); + if (s.projects) { + let changed = false; + for (const [k, v] of Object.entries(s.projects)) { + if (v && v.mcpServers && v.mcpServers.sqry) { + delete v.mcpServers.sqry; + changed = true; + } + } + if (changed) { + fs.writeFileSync('$settings', JSON.stringify(s, null, 2)); + console.log('Removed per-project sqry MCP entries from $settings'); + } + } + } catch(e) {} + " 2>/dev/null || true + fi +done + +# 2. Codex: [mcp_servers.sqry] section in TOML (portable, no sed -i) +CODEX_CONFIG="$HOME/.codex/config.toml" +if [ -f "$CODEX_CONFIG" ] && grep -q '\[mcp_servers\.sqry\]' "$CODEX_CONFIG" 2>/dev/null; then + node -e " + const fs = require('fs'); + const lines = fs.readFileSync('$CODEX_CONFIG', 'utf-8').split('\n'); + const out = []; + let skip = false; + for (const line of lines) { + if (/^\[mcp_servers\.sqry[\].]/.test(line.trim())) { skip = true; continue; } + if (skip && line.startsWith('[') && !/^\[mcp_servers\.sqry[\].]/.test(line.trim())) { skip = false; } + if (!skip) out.push(line); + } + fs.writeFileSync('$CODEX_CONFIG', out.join('\n')); + console.log('Removed [mcp_servers.sqry] from Codex config'); + " 2>/dev/null || true +fi + +# 3. Gemini: mcpServers.sqry in JSON +GEMINI_CONFIG="$HOME/.gemini/settings.json" +remove_json_key "$GEMINI_CONFIG" "mcpServers.sqry" + +# 4. Regenerate gstack skills ({{SQRY_CONTEXT}} emits nothing without sqry) +GSTACK_DIR="${GSTACK_ROOT:-$HOME/.claude/skills/gstack}" +if [ -f "$GSTACK_DIR/package.json" ]; then + echo "Regenerating gstack skill docs..." + (cd "$GSTACK_DIR" && bun run gen:skill-docs --host all 2>/dev/null) || true +fi + +echo "Done. sqry integration removed. sqry itself is still installed." +echo "To fully uninstall sqry: see https://github.com/verivus-oss/sqry#uninstall" +``` + +### 9. README.md + +```markdown +# sqry Integration for gstack + +[sqry](https://github.com/verivus-oss/sqry) provides AST-based semantic code +search via 34 MCP tools. This integration adds structural code analysis to +gstack skills — callers/callees tracing, cycle detection, complexity metrics, +structural call-path tracing, and more. + +## Install + + bash contrib/add-tool/sqry/install.sh [claude|codex|gemini|all] + +## What it does + +When sqry is installed and configured as an MCP server, gstack skills gain a +"Structural Code Analysis" section with contextual tool recommendations: + +- `/investigate` gets caller/callee tracing, cycle detection, blast radius analysis +- `/cso` gets structural call-path tracing from input handlers to sinks, dead code detection +- `/review` gets complexity regression checks, cycle detection, semantic diff +- `/retro` gets structural trend analysis and codebase health metrics +- `/plan-eng-review` gets dependency visualization and architecture boundary validation +- `/ship` gets pre-ship structural verification (cycles, dead code, complexity) + +See `tools.json` for the complete routing table. + +## Relationship to existing sqry skills + +The `sqry-claude`, `sqry-codex`, and `sqry-gemini` skills (shipped with sqry) +teach agents how to *set up and use* sqry. This gstack integration is different — +it wires sqry tools into gstack's *existing workflow skills* so they're used +automatically at the right moment during debugging, review, security audits, etc. + +| sqry skills (setup) | gstack add-in (workflow) | +|---------------------|------------------------| +| Teach tool usage | Wire tools into skill phases | +| Manual invocation | Automatic contextual use | +| Generic patterns | Skill-specific constraints | +| No index management | Auto-rebuild when stale | + +## Uninstall + + bash contrib/add-tool/sqry/uninstall.sh + +This removes the gstack integration. sqry itself remains installed. +``` + +## Risks and Mitigations + +| Risk | Mitigation | +|------|-----------| +| Context blowout from large sqry results | `defaults.max_depth: 2, max_results: 50` in tools.json; per-tool constraints | +| Stale index causing hallucinated results | `sqry index --status --json` returns `stale` field; resolver tells agent to rebuild | +| sqry binary on PATH but MCP not configured | Dual gate in resolver: preamble check + runtime tool prefix check; helpful error message | +| `.gstack-worktrees/` polluting the index | sqry respects `.gitignore`; worktrees are gitignored | +| Agent session requires restart after install | install.sh ends with hard-stop message | +| Agent over-calls sqry for simple string searches | Each tool has a prescriptive `when` clause — not "use whenever" | +| Concurrency: file edits during sqry parse | sqry uses snapshot-based reads from its index, not live file handles | +| `sqry index --status` not available in older sqry | min_version 7.0.0 in tools.json; install.sh enforces version check | + +## Review Feedback Incorporated + +### From Gemini 3 Pro + +1. **Detection flawed for multi-host MCP** — Fixed: dual gate (preamble binary + check + runtime `mcp__sqry__*` tool presence check) with helpful setup message. +2. **24h staleness dangerous** — Fixed: using sqry's own `stale` field from + `index --status --json` (hash-based change detection, not time-based). +3. **Context blowout risk** — Fixed: enforced `max_depth`/`max_results` defaults + and per-tool constraint annotations. +4. **Missing `/plan-eng-review`** — Added: `export_graph`, `subgraph`, + `show_dependencies`, `find_cycles`, `cross_language_edges`. +5. **Session restart required** — Fixed: install.sh hard-stops with restart message. +6. **Tool calling aggressiveness** — Fixed: prescriptive `when` clauses per tool. + +### From OpenAI Codex (o3) + +1. **Hand-rolled filesystem probing** — Fixed: replaced `find -newer` with + `sqry index --status --json` which runs in <1ms and returns structured data + including `stale`, `exists`, `symbol_count`, `file_count`. +2. **Hand-rolled MCP config edits** — Fixed: install.sh delegates to + `sqry mcp setup --tool ` instead of writing JSON/TOML manually. +3. **Design thicker than llm-gateway** — Fixed: detection now uses sqry's own + API (3 lines of grep on JSON) instead of filesystem probing. install.sh + delegates MCP setup. Thinner and more aligned with the pattern. +4. **`sqry mcp status --json`** — Codex discovered this returns full MCP config + state for all hosts in <1ms. Used in install.sh to verify after setup. + +### From OpenAI Codex — Round 2 (gpt-5.4) + +1. **BLOCK: Version gate broken** — `sqry --version` returns `sqry 7.1.4` not + `7.1.4`. Fixed: parse with `awk '{print $2}'`. Also replaced non-portable + `sort -V` with arithmetic semver comparator. +2. **BLOCK: uninstall.sh wrong Codex TOML format** — sqry writes + `[mcp_servers.sqry]` not `[[mcp_servers]] name = "sqry"`. Also missing + per-project Claude cleanup and using non-portable `sed -i`. Fixed: rewrote + with `node` for all formats, added per-project cleanup. +3. **BLOCK: Invalid tool constraints** — `direct_callers`/`direct_callees` don't + accept `max_depth` (they're always depth=1). Fixed: changed constraints to + `max_results: 50`, added `call_hierarchy` for multi-level tracing. +4. **WARN: Overclaimed "taint path analysis"** — Fixed: downgraded to + "structural call-path tracing" throughout. +5. **WARN: Placement prose inaccurate** — Fixed: changed to "after + `{{LLM_GATEWAY_CONTEXT}}` where present; otherwise after `{{LEARNINGS_SEARCH}}`." +6. **WARN: Wrong MCP prefix in prose** — Fixed: `mcp_sqry_*` → `mcp__sqry__*`. +7. **WARN: Missing test file** — Fixed: added `test/sqry-resolver.test.ts` to + files list, called out extended schema explicitly. + +### From OpenAI Codex — Round 3 (gpt-5.4) + +1. **BLOCK: TOML subtable orphaning** — uninstall node script only matched + exact `[mcp_servers.sqry]` header, missing nested subtables like + `[mcp_servers.sqry.env]`. Fixed: regex now matches `[mcp_servers.sqry]` + AND `[mcp_servers.sqry.*]` patterns, skipping the entire block. +2. **WARN: Prior art prose inconsistency** — line 34 still said "after + `{{LEARNINGS_SEARCH}}`" while the placement section said the correct rule. + Fixed: made consistent throughout. + +### From OpenAI Codex — Round 4 (gpt-5.4) + +1. **WARN: Stale placement rule in shared README** — `contrib/add-tool/README.md` + line 27 still says "placed after `{{LEARNINGS_SEARCH}}`" but the correct rule + is "after `{{LLM_GATEWAY_CONTEXT}}` where present; otherwise after + `{{LEARNINGS_SEARCH}}`." Fixed: added explicit README update to the modify + table in "Files to Create/Modify". + +## Files to Create/Modify + +### Create + +| File | Purpose | +|------|---------| +| `contrib/add-tool/sqry/README.md` | Integration documentation | +| `contrib/add-tool/sqry/tools.json` | Routing contract | +| `contrib/add-tool/sqry/detection.sh` | Preamble bash fragment | +| `contrib/add-tool/sqry/install.sh` | Idempotent installer | +| `contrib/add-tool/sqry/uninstall.sh` | Clean removal | +| `scripts/resolvers/sqry.ts` | TypeScript resolver | +| `test/sqry-resolver.test.ts` | Resolver + tools.json validation (mirrors llm-gateway test) | + +### Modify + +| File | Change | +|------|--------| +| `scripts/resolvers/index.ts` | Import `generateSqryContext`, add `SQRY_CONTEXT` to RESOLVERS | +| `scripts/resolvers/preamble.ts` | Inline detection.sh bash after llm-gateway block | +| `investigate/SKILL.md.tmpl` | Add `{{SQRY_CONTEXT}}` after `{{LLM_GATEWAY_CONTEXT}}` | +| `cso/SKILL.md.tmpl` | Add `{{SQRY_CONTEXT}}` after `{{LEARNINGS_SEARCH}}` | +| `review/SKILL.md.tmpl` | Add `{{SQRY_CONTEXT}}` after `{{LLM_GATEWAY_CONTEXT}}` | +| `retro/SKILL.md.tmpl` | Add `{{SQRY_CONTEXT}}` after `{{LLM_GATEWAY_CONTEXT}}` | +| `plan-eng-review/SKILL.md.tmpl` | Add `{{SQRY_CONTEXT}}` after `{{LLM_GATEWAY_CONTEXT}}` | +| `ship/SKILL.md.tmpl` | Add `{{SQRY_CONTEXT}}` after `{{LLM_GATEWAY_CONTEXT}}` | +| `contrib/add-tool/README.md` | Add sqry to "Existing integrations" list; update placement rule on line 27 to: "after `{{LLM_GATEWAY_CONTEXT}}` where present; otherwise after `{{LEARNINGS_SEARCH}}`" | diff --git a/document-release/SKILL.md b/document-release/SKILL.md index 90b84d2d2..4f3ffa735 100644 --- a/document-release/SKILL.md +++ b/document-release/SKILL.md @@ -92,6 +92,16 @@ if [ -d ".claude/skills/gstack" ] && [ ! -L ".claude/skills/gstack" ]; then fi fi echo "VENDORED_GSTACK: $_VENDORED" +# Semantic code search (sqry) — lightweight detection only (command -v is ~1ms). +# Index status is checked at query time by the agent, not at preamble load. +_SQRY="unavailable" +_SQRY_INDEXED="unknown" +if command -v sqry-mcp >/dev/null 2>&1; then + _SQRY="available" + [ -d ".sqry" ] && _SQRY_INDEXED="yes" || _SQRY_INDEXED="no" +fi +echo "SQRY: $_SQRY" +[ "$_SQRY" = "available" ] && echo "SQRY_INDEXED: $_SQRY_INDEXED" # Detect spawned session (OpenClaw or other orchestrator) [ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true ``` diff --git a/health/SKILL.md b/health/SKILL.md index f8f7b2ae9..42a2ecf0d 100644 --- a/health/SKILL.md +++ b/health/SKILL.md @@ -92,6 +92,16 @@ if [ -d ".claude/skills/gstack" ] && [ ! -L ".claude/skills/gstack" ]; then fi fi echo "VENDORED_GSTACK: $_VENDORED" +# Semantic code search (sqry) — lightweight detection only (command -v is ~1ms). +# Index status is checked at query time by the agent, not at preamble load. +_SQRY="unavailable" +_SQRY_INDEXED="unknown" +if command -v sqry-mcp >/dev/null 2>&1; then + _SQRY="available" + [ -d ".sqry" ] && _SQRY_INDEXED="yes" || _SQRY_INDEXED="no" +fi +echo "SQRY: $_SQRY" +[ "$_SQRY" = "available" ] && echo "SQRY_INDEXED: $_SQRY_INDEXED" # Detect spawned session (OpenClaw or other orchestrator) [ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true ``` diff --git a/investigate/SKILL.md b/investigate/SKILL.md index 30feccd0e..79fd192c3 100644 --- a/investigate/SKILL.md +++ b/investigate/SKILL.md @@ -107,6 +107,16 @@ if [ -d ".claude/skills/gstack" ] && [ ! -L ".claude/skills/gstack" ]; then fi fi echo "VENDORED_GSTACK: $_VENDORED" +# Semantic code search (sqry) — lightweight detection only (command -v is ~1ms). +# Index status is checked at query time by the agent, not at preamble load. +_SQRY="unavailable" +_SQRY_INDEXED="unknown" +if command -v sqry-mcp >/dev/null 2>&1; then + _SQRY="available" + [ -d ".sqry" ] && _SQRY_INDEXED="yes" || _SQRY_INDEXED="no" +fi +echo "SQRY: $_SQRY" +[ "$_SQRY" = "available" ] && echo "SQRY_INDEXED: $_SQRY_INDEXED" # Detect spawned session (OpenClaw or other orchestrator) [ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true ``` @@ -613,6 +623,27 @@ matches a past learning, display: This makes the compounding visible. The user should see that gstack is getting smarter on their codebase over time. +## Structural Code Analysis (sqry) + +If `SQRY: unavailable`: skip this section. +If `SQRY: available` but no `mcp__sqry__` tools visible: tell user to run `sqry mcp setup` and restart session. + +**Index freshness:** if `SQRY_INDEXED: no` → tell user to run `sqry index .` (typically 10-60s), then `mcp__sqry__rebuild_index`. +If you made structural changes this session, call rebuild_index before your next sqry query. + +**structural root cause analysis** — use these `mcp__sqry__` tools: + +- `mcp__sqry__direct_callers` — immediate callers of suspect function +- `mcp__sqry__direct_callees` — immediate callees of suspect function +- `mcp__sqry__call_hierarchy` — multi-level caller/callee chains when one-hop insufficient +- `mcp__sqry__is_node_in_cycle` — check if bug site is in circular dependency +- `mcp__sqry__trace_path` — call path from entry point to bug site +- `mcp__sqry__dependency_impact` — blast radius — what else breaks if this symbol is wrong +- `mcp__sqry__get_definition` — jump to definition of symbol from stack traces +- `mcp__sqry__get_references` — all usages of suspect symbol across codebase + +**Tool parameters:** Most sqry tools accept `max_depth` (default 3, max 10) and `max_results` (default 20, max 100). Scope queries to specific files or directories when possible — full-repo queries on large codebases are expensive. Use `semantic_search` for broad discovery, then `direct_callers`/`direct_callees` for focused tracing. + Output: **"Root cause hypothesis: ..."** — a specific, testable claim about what is wrong and why. --- diff --git a/investigate/SKILL.md.tmpl b/investigate/SKILL.md.tmpl index 3004300e2..e8f439c92 100644 --- a/investigate/SKILL.md.tmpl +++ b/investigate/SKILL.md.tmpl @@ -63,6 +63,8 @@ Gather context before forming any hypothesis. {{LEARNINGS_SEARCH}} +{{SQRY_CONTEXT}} + Output: **"Root cause hypothesis: ..."** — a specific, testable claim about what is wrong and why. --- diff --git a/land-and-deploy/SKILL.md b/land-and-deploy/SKILL.md index 644020097..bbcd479de 100644 --- a/land-and-deploy/SKILL.md +++ b/land-and-deploy/SKILL.md @@ -89,6 +89,16 @@ if [ -d ".claude/skills/gstack" ] && [ ! -L ".claude/skills/gstack" ]; then fi fi echo "VENDORED_GSTACK: $_VENDORED" +# Semantic code search (sqry) — lightweight detection only (command -v is ~1ms). +# Index status is checked at query time by the agent, not at preamble load. +_SQRY="unavailable" +_SQRY_INDEXED="unknown" +if command -v sqry-mcp >/dev/null 2>&1; then + _SQRY="available" + [ -d ".sqry" ] && _SQRY_INDEXED="yes" || _SQRY_INDEXED="no" +fi +echo "SQRY: $_SQRY" +[ "$_SQRY" = "available" ] && echo "SQRY_INDEXED: $_SQRY_INDEXED" # Detect spawned session (OpenClaw or other orchestrator) [ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true ``` diff --git a/learn/SKILL.md b/learn/SKILL.md index 656ae76b2..98cd77a66 100644 --- a/learn/SKILL.md +++ b/learn/SKILL.md @@ -92,6 +92,16 @@ if [ -d ".claude/skills/gstack" ] && [ ! -L ".claude/skills/gstack" ]; then fi fi echo "VENDORED_GSTACK: $_VENDORED" +# Semantic code search (sqry) — lightweight detection only (command -v is ~1ms). +# Index status is checked at query time by the agent, not at preamble load. +_SQRY="unavailable" +_SQRY_INDEXED="unknown" +if command -v sqry-mcp >/dev/null 2>&1; then + _SQRY="available" + [ -d ".sqry" ] && _SQRY_INDEXED="yes" || _SQRY_INDEXED="no" +fi +echo "SQRY: $_SQRY" +[ "$_SQRY" = "available" ] && echo "SQRY_INDEXED: $_SQRY_INDEXED" # Detect spawned session (OpenClaw or other orchestrator) [ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true ``` diff --git a/office-hours/SKILL.md b/office-hours/SKILL.md index 9795f1e5e..56b583aaa 100644 --- a/office-hours/SKILL.md +++ b/office-hours/SKILL.md @@ -99,6 +99,16 @@ if [ -d ".claude/skills/gstack" ] && [ ! -L ".claude/skills/gstack" ]; then fi fi echo "VENDORED_GSTACK: $_VENDORED" +# Semantic code search (sqry) — lightweight detection only (command -v is ~1ms). +# Index status is checked at query time by the agent, not at preamble load. +_SQRY="unavailable" +_SQRY_INDEXED="unknown" +if command -v sqry-mcp >/dev/null 2>&1; then + _SQRY="available" + [ -d ".sqry" ] && _SQRY_INDEXED="yes" || _SQRY_INDEXED="no" +fi +echo "SQRY: $_SQRY" +[ "$_SQRY" = "available" ] && echo "SQRY_INDEXED: $_SQRY_INDEXED" # Detect spawned session (OpenClaw or other orchestrator) [ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true ``` diff --git a/open-gstack-browser/SKILL.md b/open-gstack-browser/SKILL.md index 126bd5fb7..4d7c3ac17 100644 --- a/open-gstack-browser/SKILL.md +++ b/open-gstack-browser/SKILL.md @@ -89,6 +89,16 @@ if [ -d ".claude/skills/gstack" ] && [ ! -L ".claude/skills/gstack" ]; then fi fi echo "VENDORED_GSTACK: $_VENDORED" +# Semantic code search (sqry) — lightweight detection only (command -v is ~1ms). +# Index status is checked at query time by the agent, not at preamble load. +_SQRY="unavailable" +_SQRY_INDEXED="unknown" +if command -v sqry-mcp >/dev/null 2>&1; then + _SQRY="available" + [ -d ".sqry" ] && _SQRY_INDEXED="yes" || _SQRY_INDEXED="no" +fi +echo "SQRY: $_SQRY" +[ "$_SQRY" = "available" ] && echo "SQRY_INDEXED: $_SQRY_INDEXED" # Detect spawned session (OpenClaw or other orchestrator) [ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true ``` diff --git a/plan-ceo-review/SKILL.md b/plan-ceo-review/SKILL.md index 78e87f4da..e879ba57e 100644 --- a/plan-ceo-review/SKILL.md +++ b/plan-ceo-review/SKILL.md @@ -95,6 +95,16 @@ if [ -d ".claude/skills/gstack" ] && [ ! -L ".claude/skills/gstack" ]; then fi fi echo "VENDORED_GSTACK: $_VENDORED" +# Semantic code search (sqry) — lightweight detection only (command -v is ~1ms). +# Index status is checked at query time by the agent, not at preamble load. +_SQRY="unavailable" +_SQRY_INDEXED="unknown" +if command -v sqry-mcp >/dev/null 2>&1; then + _SQRY="available" + [ -d ".sqry" ] && _SQRY_INDEXED="yes" || _SQRY_INDEXED="no" +fi +echo "SQRY: $_SQRY" +[ "$_SQRY" = "available" ] && echo "SQRY_INDEXED: $_SQRY_INDEXED" # Detect spawned session (OpenClaw or other orchestrator) [ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true ``` diff --git a/plan-design-review/SKILL.md b/plan-design-review/SKILL.md index bc9a1d16a..d9fa2918b 100644 --- a/plan-design-review/SKILL.md +++ b/plan-design-review/SKILL.md @@ -93,6 +93,16 @@ if [ -d ".claude/skills/gstack" ] && [ ! -L ".claude/skills/gstack" ]; then fi fi echo "VENDORED_GSTACK: $_VENDORED" +# Semantic code search (sqry) — lightweight detection only (command -v is ~1ms). +# Index status is checked at query time by the agent, not at preamble load. +_SQRY="unavailable" +_SQRY_INDEXED="unknown" +if command -v sqry-mcp >/dev/null 2>&1; then + _SQRY="available" + [ -d ".sqry" ] && _SQRY_INDEXED="yes" || _SQRY_INDEXED="no" +fi +echo "SQRY: $_SQRY" +[ "$_SQRY" = "available" ] && echo "SQRY_INDEXED: $_SQRY_INDEXED" # Detect spawned session (OpenClaw or other orchestrator) [ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true ``` diff --git a/plan-devex-review/SKILL.md b/plan-devex-review/SKILL.md index 56a51ba2b..292669f90 100644 --- a/plan-devex-review/SKILL.md +++ b/plan-devex-review/SKILL.md @@ -97,6 +97,16 @@ if [ -d ".claude/skills/gstack" ] && [ ! -L ".claude/skills/gstack" ]; then fi fi echo "VENDORED_GSTACK: $_VENDORED" +# Semantic code search (sqry) — lightweight detection only (command -v is ~1ms). +# Index status is checked at query time by the agent, not at preamble load. +_SQRY="unavailable" +_SQRY_INDEXED="unknown" +if command -v sqry-mcp >/dev/null 2>&1; then + _SQRY="available" + [ -d ".sqry" ] && _SQRY_INDEXED="yes" || _SQRY_INDEXED="no" +fi +echo "SQRY: $_SQRY" +[ "$_SQRY" = "available" ] && echo "SQRY_INDEXED: $_SQRY_INDEXED" # Detect spawned session (OpenClaw or other orchestrator) [ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true ``` diff --git a/plan-eng-review/SKILL.md b/plan-eng-review/SKILL.md index 93f71bd7b..95473588a 100644 --- a/plan-eng-review/SKILL.md +++ b/plan-eng-review/SKILL.md @@ -95,6 +95,16 @@ if [ -d ".claude/skills/gstack" ] && [ ! -L ".claude/skills/gstack" ]; then fi fi echo "VENDORED_GSTACK: $_VENDORED" +# Semantic code search (sqry) — lightweight detection only (command -v is ~1ms). +# Index status is checked at query time by the agent, not at preamble load. +_SQRY="unavailable" +_SQRY_INDEXED="unknown" +if command -v sqry-mcp >/dev/null 2>&1; then + _SQRY="available" + [ -d ".sqry" ] && _SQRY_INDEXED="yes" || _SQRY_INDEXED="no" +fi +echo "SQRY: $_SQRY" +[ "$_SQRY" = "available" ] && echo "SQRY_INDEXED: $_SQRY_INDEXED" # Detect spawned session (OpenClaw or other orchestrator) [ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true ``` @@ -738,6 +748,24 @@ matches a past learning, display: This makes the compounding visible. The user should see that gstack is getting smarter on their codebase over time. +## Structural Code Analysis (sqry) + +If `SQRY: unavailable`: skip this section. +If `SQRY: available` but no `mcp__sqry__` tools visible: tell user to run `sqry mcp setup` and restart session. + +**Index freshness:** if `SQRY_INDEXED: no` → tell user to run `sqry index .` (typically 10-60s), then `mcp__sqry__rebuild_index`. +If you made structural changes this session, call rebuild_index before your next sqry query. + +**structural architecture analysis for plan review** — use these `mcp__sqry__` tools: + +- `mcp__sqry__export_graph` — visualize module dependencies to validate architecture boundaries +- `mcp__sqry__subgraph` — dependency neighborhood around components the plan modifies +- `mcp__sqry__show_dependencies` — dependency tree of modules the plan touches +- `mcp__sqry__find_cycles` — existing cycles the plan should address or avoid +- `mcp__sqry__cross_language_edges` — cross-language boundaries the plan must respect + +**Tool parameters:** Most sqry tools accept `max_depth` (default 3, max 10) and `max_results` (default 20, max 100). Scope queries to specific files or directories when possible — full-repo queries on large codebases are expensive. Use `semantic_search` for broad discovery, then `direct_callers`/`direct_callees` for focused tracing. + ### 1. Architecture review Evaluate: * Overall system design and component boundaries. diff --git a/plan-eng-review/SKILL.md.tmpl b/plan-eng-review/SKILL.md.tmpl index 36c9d59e8..9217c3d7e 100644 --- a/plan-eng-review/SKILL.md.tmpl +++ b/plan-eng-review/SKILL.md.tmpl @@ -118,6 +118,8 @@ Always work through the full interactive review: one section at a time (Architec {{LEARNINGS_SEARCH}} +{{SQRY_CONTEXT}} + ### 1. Architecture review Evaluate: * Overall system design and component boundaries. diff --git a/qa-only/SKILL.md b/qa-only/SKILL.md index f1eeedff9..d384a7d95 100644 --- a/qa-only/SKILL.md +++ b/qa-only/SKILL.md @@ -91,6 +91,16 @@ if [ -d ".claude/skills/gstack" ] && [ ! -L ".claude/skills/gstack" ]; then fi fi echo "VENDORED_GSTACK: $_VENDORED" +# Semantic code search (sqry) — lightweight detection only (command -v is ~1ms). +# Index status is checked at query time by the agent, not at preamble load. +_SQRY="unavailable" +_SQRY_INDEXED="unknown" +if command -v sqry-mcp >/dev/null 2>&1; then + _SQRY="available" + [ -d ".sqry" ] && _SQRY_INDEXED="yes" || _SQRY_INDEXED="no" +fi +echo "SQRY: $_SQRY" +[ "$_SQRY" = "available" ] && echo "SQRY_INDEXED: $_SQRY_INDEXED" # Detect spawned session (OpenClaw or other orchestrator) [ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true ``` diff --git a/qa/SKILL.md b/qa/SKILL.md index edb475c90..79842304b 100644 --- a/qa/SKILL.md +++ b/qa/SKILL.md @@ -97,6 +97,16 @@ if [ -d ".claude/skills/gstack" ] && [ ! -L ".claude/skills/gstack" ]; then fi fi echo "VENDORED_GSTACK: $_VENDORED" +# Semantic code search (sqry) — lightweight detection only (command -v is ~1ms). +# Index status is checked at query time by the agent, not at preamble load. +_SQRY="unavailable" +_SQRY_INDEXED="unknown" +if command -v sqry-mcp >/dev/null 2>&1; then + _SQRY="available" + [ -d ".sqry" ] && _SQRY_INDEXED="yes" || _SQRY_INDEXED="no" +fi +echo "SQRY: $_SQRY" +[ "$_SQRY" = "available" ] && echo "SQRY_INDEXED: $_SQRY_INDEXED" # Detect spawned session (OpenClaw or other orchestrator) [ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true ``` diff --git a/retro/SKILL.md b/retro/SKILL.md index b2f434198..839e6cbdd 100644 --- a/retro/SKILL.md +++ b/retro/SKILL.md @@ -90,6 +90,16 @@ if [ -d ".claude/skills/gstack" ] && [ ! -L ".claude/skills/gstack" ]; then fi fi echo "VENDORED_GSTACK: $_VENDORED" +# Semantic code search (sqry) — lightweight detection only (command -v is ~1ms). +# Index status is checked at query time by the agent, not at preamble load. +_SQRY="unavailable" +_SQRY_INDEXED="unknown" +if command -v sqry-mcp >/dev/null 2>&1; then + _SQRY="available" + [ -d ".sqry" ] && _SQRY_INDEXED="yes" || _SQRY_INDEXED="no" +fi +echo "SQRY: $_SQRY" +[ "$_SQRY" = "available" ] && echo "SQRY_INDEXED: $_SQRY_INDEXED" # Detect spawned session (OpenClaw or other orchestrator) [ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true ``` @@ -647,6 +657,23 @@ matches a past learning, display: This makes the compounding visible. The user should see that gstack is getting smarter on their codebase over time. +## Structural Code Analysis (sqry) + +If `SQRY: unavailable`: skip this section. +If `SQRY: available` but no `mcp__sqry__` tools visible: tell user to run `sqry mcp setup` and restart session. + +**Index freshness:** if `SQRY_INDEXED: no` → tell user to run `sqry index .` (typically 10-60s), then `mcp__sqry__rebuild_index`. +If you made structural changes this session, call rebuild_index before your next sqry query. + +**structural code quality analysis for retrospective** — use these `mcp__sqry__` tools: + +- `mcp__sqry__semantic_diff` — structural changes between this week's HEAD and last week's tag/commit +- `mcp__sqry__complexity_metrics` — complexity trends — adding or reducing complexity? +- `mcp__sqry__find_cycles` — new cycles introduced this week +- `mcp__sqry__get_insights` — overall codebase health metrics for retrospective dashboard + +**Tool parameters:** Most sqry tools accept `max_depth` (default 3, max 10) and `max_results` (default 20, max 100). Scope queries to specific files or directories when possible — full-repo queries on large codebases are expensive. Use `semantic_search` for broad discovery, then `direct_callers`/`direct_callees` for focused tracing. + ### Step 1: Gather Raw Data First, fetch origin and identify the current user: diff --git a/retro/SKILL.md.tmpl b/retro/SKILL.md.tmpl index d89cb7175..99e09849a 100644 --- a/retro/SKILL.md.tmpl +++ b/retro/SKILL.md.tmpl @@ -60,6 +60,8 @@ Usage: /retro [window | compare | global] {{LEARNINGS_SEARCH}} +{{SQRY_CONTEXT}} + ### Step 1: Gather Raw Data First, fetch origin and identify the current user: diff --git a/review/SKILL.md b/review/SKILL.md index 9e2965db3..0e56e0fe4 100644 --- a/review/SKILL.md +++ b/review/SKILL.md @@ -93,6 +93,16 @@ if [ -d ".claude/skills/gstack" ] && [ ! -L ".claude/skills/gstack" ]; then fi fi echo "VENDORED_GSTACK: $_VENDORED" +# Semantic code search (sqry) — lightweight detection only (command -v is ~1ms). +# Index status is checked at query time by the agent, not at preamble load. +_SQRY="unavailable" +_SQRY_INDEXED="unknown" +if command -v sqry-mcp >/dev/null 2>&1; then + _SQRY="available" + [ -d ".sqry" ] && _SQRY_INDEXED="yes" || _SQRY_INDEXED="no" +fi +echo "SQRY: $_SQRY" +[ "$_SQRY" = "available" ] && echo "SQRY_INDEXED: $_SQRY_INDEXED" # Detect spawned session (OpenClaw or other orchestrator) [ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true ``` @@ -882,6 +892,25 @@ matches a past learning, display: This makes the compounding visible. The user should see that gstack is getting smarter on their codebase over time. +## Structural Code Analysis (sqry) + +If `SQRY: unavailable`: skip this section. +If `SQRY: available` but no `mcp__sqry__` tools visible: tell user to run `sqry mcp setup` and restart session. + +**Index freshness:** if `SQRY_INDEXED: no` → tell user to run `sqry index .` (typically 10-60s), then `mcp__sqry__rebuild_index`. +If you made structural changes this session, call rebuild_index before your next sqry query. + +**structural analysis of changed code** — use these `mcp__sqry__` tools: + +- `mcp__sqry__complexity_metrics` — cyclomatic complexity of changed files — flag regressions +- `mcp__sqry__find_cycles` — check if changed symbols introduced or participate in cycles +- `mcp__sqry__dependency_impact` — downstream impact of changed public APIs +- `mcp__sqry__find_unused` — newly-dead code after refactors or API changes +- `mcp__sqry__semantic_diff` — structural changes between PR branch and base branch +- `mcp__sqry__direct_callers` — verify callers of changed functions still work with new signature + +**Tool parameters:** Most sqry tools accept `max_depth` (default 3, max 10) and `max_results` (default 20, max 100). Scope queries to specific files or directories when possible — full-repo queries on large codebases are expensive. Use `semantic_search` for broad discovery, then `direct_callers`/`direct_callees` for focused tracing. + ## Step 4: Critical pass (core review) Apply the CRITICAL categories from the checklist against the diff: diff --git a/review/SKILL.md.tmpl b/review/SKILL.md.tmpl index 9ccb1ec23..7da65bd95 100644 --- a/review/SKILL.md.tmpl +++ b/review/SKILL.md.tmpl @@ -73,6 +73,8 @@ Run `git diff origin/` to get the full diff. This includes both committed {{LEARNINGS_SEARCH}} +{{SQRY_CONTEXT}} + ## Step 4: Critical pass (core review) Apply the CRITICAL categories from the checklist against the diff: diff --git a/scripts/resolvers/index.ts b/scripts/resolvers/index.ts index 072b1a3da..f4b01a6f3 100644 --- a/scripts/resolvers/index.ts +++ b/scripts/resolvers/index.ts @@ -18,6 +18,7 @@ import { generateConfidenceCalibration } from './confidence'; import { generateInvokeSkill } from './composition'; import { generateReviewArmy } from './review-army'; import { generateDxFramework } from './dx'; +import { generateSqryContext } from './sqry'; export const RESOLVERS: Record = { SLUG_EVAL: generateSlugEval, @@ -62,4 +63,5 @@ export const RESOLVERS: Record = { REVIEW_ARMY: generateReviewArmy, CROSS_REVIEW_DEDUP: generateCrossReviewDedup, DX_FRAMEWORK: generateDxFramework, + SQRY_CONTEXT: generateSqryContext, }; diff --git a/scripts/resolvers/preamble.ts b/scripts/resolvers/preamble.ts index bacbc0f00..3f921016e 100644 --- a/scripts/resolvers/preamble.ts +++ b/scripts/resolvers/preamble.ts @@ -97,6 +97,16 @@ if [ -d ".claude/skills/gstack" ] && [ ! -L ".claude/skills/gstack" ]; then fi fi echo "VENDORED_GSTACK: $_VENDORED" +# Semantic code search (sqry) — lightweight detection only (command -v is ~1ms). +# Index status is checked at query time by the agent, not at preamble load. +_SQRY="unavailable" +_SQRY_INDEXED="unknown" +if command -v sqry-mcp >/dev/null 2>&1; then + _SQRY="available" + [ -d ".sqry" ] && _SQRY_INDEXED="yes" || _SQRY_INDEXED="no" +fi +echo "SQRY: $_SQRY" +[ "$_SQRY" = "available" ] && echo "SQRY_INDEXED: $_SQRY_INDEXED" # Detect spawned session (OpenClaw or other orchestrator) [ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true \`\`\``; diff --git a/scripts/resolvers/sqry.ts b/scripts/resolvers/sqry.ts new file mode 100644 index 000000000..ac70a8938 --- /dev/null +++ b/scripts/resolvers/sqry.ts @@ -0,0 +1,68 @@ +import type { TemplateContext, ResolverFn } from './types'; +import { readFileSync } from 'fs'; +import { resolve } from 'path'; + +interface ToolMapping { + tool: string; + when: string; +} + +interface SkillIntegration { + phase: string; + context: string; + tools: ToolMapping[]; +} + +interface ToolsConfig { + tool: string; + mcp_server_name: string; + detection: { binary: string; min_version: string; rebuild_hint: string }; + parameter_guidance: string; + integrations: Record; +} + +let cachedConfig: ToolsConfig | null = null; + +function loadToolsConfig(): ToolsConfig { + if (cachedConfig) return cachedConfig; + const configPath = resolve( + import.meta.dir, + '../../contrib/add-tool/sqry/tools.json', + ); + cachedConfig = JSON.parse(readFileSync(configPath, 'utf-8')); + return cachedConfig!; +} + +export const generateSqryContext: ResolverFn = ( + ctx: TemplateContext, +): string => { + let config: ToolsConfig; + try { + config = loadToolsConfig(); + } catch { + return ''; + } + + const integration = config.integrations[ctx.skillName]; + if (!integration) return ''; + + const prefix = `mcp__${config.mcp_server_name}__`; + + const toolList = integration.tools + .map((t) => `- \`${prefix}${t.tool}\` — ${t.when}`) + .join('\n'); + + return `## Structural Code Analysis (sqry) + +If \`SQRY: unavailable\`: skip this section. +If \`SQRY: available\` but no \`${prefix}\` tools visible: tell user to run \`sqry mcp setup\` and restart session. + +**Index freshness:** if \`SQRY_INDEXED: no\` → tell user to run \`sqry index .\` (typically 10-60s), then \`${prefix}rebuild_index\`. +${config.detection.rebuild_hint} + +**${integration.context}** — use these \`${prefix}\` tools: + +${toolList} + +**Tool parameters:** ${config.parameter_guidance}`; +}; diff --git a/setup-browser-cookies/SKILL.md b/setup-browser-cookies/SKILL.md index 8a369d0ee..7a0454dbd 100644 --- a/setup-browser-cookies/SKILL.md +++ b/setup-browser-cookies/SKILL.md @@ -87,6 +87,16 @@ if [ -d ".claude/skills/gstack" ] && [ ! -L ".claude/skills/gstack" ]; then fi fi echo "VENDORED_GSTACK: $_VENDORED" +# Semantic code search (sqry) — lightweight detection only (command -v is ~1ms). +# Index status is checked at query time by the agent, not at preamble load. +_SQRY="unavailable" +_SQRY_INDEXED="unknown" +if command -v sqry-mcp >/dev/null 2>&1; then + _SQRY="available" + [ -d ".sqry" ] && _SQRY_INDEXED="yes" || _SQRY_INDEXED="no" +fi +echo "SQRY: $_SQRY" +[ "$_SQRY" = "available" ] && echo "SQRY_INDEXED: $_SQRY_INDEXED" # Detect spawned session (OpenClaw or other orchestrator) [ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true ``` diff --git a/setup-deploy/SKILL.md b/setup-deploy/SKILL.md index 41ba613ef..f070746c1 100644 --- a/setup-deploy/SKILL.md +++ b/setup-deploy/SKILL.md @@ -93,6 +93,16 @@ if [ -d ".claude/skills/gstack" ] && [ ! -L ".claude/skills/gstack" ]; then fi fi echo "VENDORED_GSTACK: $_VENDORED" +# Semantic code search (sqry) — lightweight detection only (command -v is ~1ms). +# Index status is checked at query time by the agent, not at preamble load. +_SQRY="unavailable" +_SQRY_INDEXED="unknown" +if command -v sqry-mcp >/dev/null 2>&1; then + _SQRY="available" + [ -d ".sqry" ] && _SQRY_INDEXED="yes" || _SQRY_INDEXED="no" +fi +echo "SQRY: $_SQRY" +[ "$_SQRY" = "available" ] && echo "SQRY_INDEXED: $_SQRY_INDEXED" # Detect spawned session (OpenClaw or other orchestrator) [ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true ``` diff --git a/ship/SKILL.md b/ship/SKILL.md index f3bfd6269..ea7231b5b 100644 --- a/ship/SKILL.md +++ b/ship/SKILL.md @@ -94,6 +94,16 @@ if [ -d ".claude/skills/gstack" ] && [ ! -L ".claude/skills/gstack" ]; then fi fi echo "VENDORED_GSTACK: $_VENDORED" +# Semantic code search (sqry) — lightweight detection only (command -v is ~1ms). +# Index status is checked at query time by the agent, not at preamble load. +_SQRY="unavailable" +_SQRY_INDEXED="unknown" +if command -v sqry-mcp >/dev/null 2>&1; then + _SQRY="available" + [ -d ".sqry" ] && _SQRY_INDEXED="yes" || _SQRY_INDEXED="no" +fi +echo "SQRY: $_SQRY" +[ "$_SQRY" = "available" ] && echo "SQRY_INDEXED: $_SQRY_INDEXED" # Detect spawned session (OpenClaw or other orchestrator) [ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true ``` @@ -1576,6 +1586,22 @@ matches a past learning, display: This makes the compounding visible. The user should see that gstack is getting smarter on their codebase over time. +## Structural Code Analysis (sqry) + +If `SQRY: unavailable`: skip this section. +If `SQRY: available` but no `mcp__sqry__` tools visible: tell user to run `sqry mcp setup` and restart session. + +**Index freshness:** if `SQRY_INDEXED: no` → tell user to run `sqry index .` (typically 10-60s), then `mcp__sqry__rebuild_index`. +If you made structural changes this session, call rebuild_index before your next sqry query. + +**structural verification before shipping** — use these `mcp__sqry__` tools: + +- `mcp__sqry__find_cycles` — no circular dependencies in shipped code +- `mcp__sqry__find_unused` — catch dead code being shipped +- `mcp__sqry__complexity_metrics` — verify complexity hasn't regressed + +**Tool parameters:** Most sqry tools accept `max_depth` (default 3, max 10) and `max_results` (default 20, max 100). Scope queries to specific files or directories when possible — full-repo queries on large codebases are expensive. Use `semantic_search` for broad discovery, then `direct_callers`/`direct_callees` for focused tracing. + ## Step 3.48: Scope Drift Detection Before reviewing code quality, check: **did they build what was requested — nothing more, nothing less?** diff --git a/ship/SKILL.md.tmpl b/ship/SKILL.md.tmpl index 76e4873d6..8b11f87fe 100644 --- a/ship/SKILL.md.tmpl +++ b/ship/SKILL.md.tmpl @@ -242,6 +242,8 @@ If multiple suites need to run, run them sequentially (each needs a test lane). {{LEARNINGS_SEARCH}} +{{SQRY_CONTEXT}} + {{SCOPE_DRIFT}} --- diff --git a/test/fixtures/golden/claude-ship-SKILL.md b/test/fixtures/golden/claude-ship-SKILL.md index 34cfaa7b2..dd5881643 100644 --- a/test/fixtures/golden/claude-ship-SKILL.md +++ b/test/fixtures/golden/claude-ship-SKILL.md @@ -86,6 +86,33 @@ fi _ROUTING_DECLINED=$(~/.claude/skills/gstack/bin/gstack-config get routing_declined 2>/dev/null || echo "false") echo "HAS_ROUTING: $_HAS_ROUTING" echo "ROUTING_DECLINED: $_ROUTING_DECLINED" +# Vendoring deprecation: detect if CWD has a vendored gstack copy +_VENDORED="no" +if [ -d ".claude/skills/gstack" ] && [ ! -L ".claude/skills/gstack" ]; then + if [ -f ".claude/skills/gstack/VERSION" ] || [ -d ".claude/skills/gstack/.git" ]; then + _VENDORED="yes" + fi +fi +echo "VENDORED_GSTACK: $_VENDORED" +# Semantic code search (sqry) +_SQRY="unavailable" +_SQRY_INDEXED="no" +_SQRY_STALE="no" +if command -v sqry >/dev/null 2>&1; then + _SQRY="available" + _SQRY_VERSION=$(sqry --version 2>/dev/null | head -1 || echo "unknown") + _SQRY_STATUS=$(sqry index --status --json . 2>/dev/null || echo '{}') + if echo "$_SQRY_STATUS" | grep -q '"exists": true' 2>/dev/null; then + _SQRY_INDEXED="yes" + fi + if echo "$_SQRY_STATUS" | grep -q '"stale": true' 2>/dev/null; then + _SQRY_STALE="yes" + fi +fi +echo "SQRY: $_SQRY" +[ "$_SQRY" = "available" ] && echo "SQRY_VERSION: $_SQRY_VERSION" +[ "$_SQRY" = "available" ] && echo "SQRY_INDEXED: $_SQRY_INDEXED" +[ "$_SQRY" = "available" ] && echo "SQRY_STALE: $_SQRY_STALE" # Detect spawned session (OpenClaw or other orchestrator) [ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true ``` @@ -214,6 +241,38 @@ Say "No problem. You can add routing rules later by running `gstack-config set r This only happens once per project. If `HAS_ROUTING` is `yes` or `ROUTING_DECLINED` is `true`, skip this entirely. +If `VENDORED_GSTACK` is `yes`: This project has a vendored copy of gstack at +`.claude/skills/gstack/`. Vendoring is deprecated. We will not keep vendored copies +up to date, so this project's gstack will fall behind. + +Use AskUserQuestion (one-time per project, check for `~/.gstack/.vendoring-warned-$SLUG` marker): + +> This project has gstack vendored in `.claude/skills/gstack/`. Vendoring is deprecated. +> We won't keep this copy up to date, so you'll fall behind on new features and fixes. +> +> Want to migrate to team mode? It takes about 30 seconds. + +Options: +- A) Yes, migrate to team mode now +- B) No, I'll handle it myself + +If A: +1. Run `git rm -r .claude/skills/gstack/` +2. Run `echo '.claude/skills/gstack/' >> .gitignore` +3. Run `~/.claude/skills/gstack/bin/gstack-team-init required` (or `optional`) +4. Run `git add .claude/ .gitignore CLAUDE.md && git commit -m "chore: migrate gstack from vendored to team mode"` +5. Tell the user: "Done. Each developer now runs: `cd ~/.claude/skills/gstack && ./setup --team`" + +If B: say "OK, you're on your own to keep the vendored copy up to date." + +Always run (regardless of choice): +```bash +eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" 2>/dev/null || true +touch ~/.gstack/.vendoring-warned-${SLUG:-unknown} +``` + +This only happens once per project. If the marker file exists, skip entirely. + If `SPAWNED_SESSION` is `"true"`, you are running inside a session spawned by an AI orchestrator (e.g., OpenClaw). In spawned sessions: - Do NOT use AskUserQuestion for interactive prompts. Auto-choose the recommended option. @@ -580,6 +639,16 @@ You are running the `/ship` workflow. This is a **non-interactive, fully automat - Auto-fixable review findings (dead code, N+1, stale comments — fixed automatically) - Test coverage gaps within target threshold (auto-generate and commit, or flag in PR body) +**Re-run behavior (idempotency):** +Re-running `/ship` means "run the whole checklist again." Every verification step +(tests, coverage audit, plan completion, pre-landing review, adversarial review, +VERSION/CHANGELOG check, TODOS, document-release) runs on every invocation. +Only *actions* are idempotent: +- Step 4: If VERSION already bumped, skip the bump but still read the version +- Step 7: If already pushed, skip the push command +- Step 8: If PR exists, update the body instead of creating a new PR +Never skip a verification step because a prior `/ship` run already performed it. + --- ## Step 1: Pre-flight @@ -1526,6 +1595,22 @@ matches a past learning, display: This makes the compounding visible. The user should see that gstack is getting smarter on their codebase over time. +## Structural Code Analysis (sqry) + +If `SQRY: unavailable`: skip this section. +If `SQRY: available` but no `mcp__sqry__` tools visible: tell user to run `sqry mcp setup` and restart session. + +**Index freshness:** if `SQRY_INDEXED: no` → tell user to run `sqry index .` (typically 10-60s), then `mcp__sqry__rebuild_index`. +If you made structural changes this session, call rebuild_index before your next sqry query. + +**structural verification before shipping** — use these `mcp__sqry__` tools: + +- `mcp__sqry__find_cycles` — no circular dependencies in shipped code +- `mcp__sqry__find_unused` — catch dead code being shipped +- `mcp__sqry__complexity_metrics` — verify complexity hasn't regressed + +**Tool parameters:** Most sqry tools accept `max_depth` (default 3, max 10) and `max_results` (default 20, max 100). Scope queries to specific files or directories when possible — full-repo queries on large codebases are expensive. Use `semantic_search` for broad discovery, then `direct_callers`/`direct_callees` for focused tracing. + ## Step 3.48: Scope Drift Detection Before reviewing code quality, check: **did they build what was requested — nothing more, nothing less?** @@ -1658,7 +1743,244 @@ Present Codex output under a `CODEX (design):` header, merged with the checklist Include any design findings alongside the code review findings. They follow the same Fix-First flow below. -4. **Classify each finding as AUTO-FIX or ASK** per the Fix-First Heuristic in +## Step 3.55: Review Army — Specialist Dispatch + +### Detect stack and scope + +```bash +source <(~/.claude/skills/gstack/bin/gstack-diff-scope 2>/dev/null) || true +# Detect stack for specialist context +STACK="" +[ -f Gemfile ] && STACK="${STACK}ruby " +[ -f package.json ] && STACK="${STACK}node " +[ -f requirements.txt ] || [ -f pyproject.toml ] && STACK="${STACK}python " +[ -f go.mod ] && STACK="${STACK}go " +[ -f Cargo.toml ] && STACK="${STACK}rust " +echo "STACK: ${STACK:-unknown}" +DIFF_INS=$(git diff origin/ --stat | tail -1 | grep -oE '[0-9]+ insertion' | grep -oE '[0-9]+' || echo "0") +DIFF_DEL=$(git diff origin/ --stat | tail -1 | grep -oE '[0-9]+ deletion' | grep -oE '[0-9]+' || echo "0") +DIFF_LINES=$((DIFF_INS + DIFF_DEL)) +echo "DIFF_LINES: $DIFF_LINES" +# Detect test framework for specialist test stub generation +TEST_FW="" +{ [ -f jest.config.ts ] || [ -f jest.config.js ]; } && TEST_FW="jest" +[ -f vitest.config.ts ] && TEST_FW="vitest" +{ [ -f spec/spec_helper.rb ] || [ -f .rspec ]; } && TEST_FW="rspec" +{ [ -f pytest.ini ] || [ -f conftest.py ]; } && TEST_FW="pytest" +[ -f go.mod ] && TEST_FW="go-test" +echo "TEST_FW: ${TEST_FW:-unknown}" +``` + +### Read specialist hit rates (adaptive gating) + +```bash +~/.claude/skills/gstack/bin/gstack-specialist-stats 2>/dev/null || true +``` + +### Select specialists + +Based on the scope signals above, select which specialists to dispatch. + +**Always-on (dispatch on every review with 50+ changed lines):** +1. **Testing** — read `~/.claude/skills/gstack/review/specialists/testing.md` +2. **Maintainability** — read `~/.claude/skills/gstack/review/specialists/maintainability.md` + +**If DIFF_LINES < 50:** Skip all specialists. Print: "Small diff ($DIFF_LINES lines) — specialists skipped." Continue to the Fix-First flow (item 4). + +**Conditional (dispatch if the matching scope signal is true):** +3. **Security** — if SCOPE_AUTH=true, OR if SCOPE_BACKEND=true AND DIFF_LINES > 100. Read `~/.claude/skills/gstack/review/specialists/security.md` +4. **Performance** — if SCOPE_BACKEND=true OR SCOPE_FRONTEND=true. Read `~/.claude/skills/gstack/review/specialists/performance.md` +5. **Data Migration** — if SCOPE_MIGRATIONS=true. Read `~/.claude/skills/gstack/review/specialists/data-migration.md` +6. **API Contract** — if SCOPE_API=true. Read `~/.claude/skills/gstack/review/specialists/api-contract.md` +7. **Design** — if SCOPE_FRONTEND=true. Use the existing design review checklist at `~/.claude/skills/gstack/review/design-checklist.md` + +### Adaptive gating + +After scope-based selection, apply adaptive gating based on specialist hit rates: + +For each conditional specialist that passed scope gating, check the `gstack-specialist-stats` output above: +- If tagged `[GATE_CANDIDATE]` (0 findings in 10+ dispatches): skip it. Print: "[specialist] auto-gated (0 findings in N reviews)." +- If tagged `[NEVER_GATE]`: always dispatch regardless of hit rate. Security and data-migration are insurance policy specialists — they should run even when silent. + +**Force flags:** If the user's prompt includes `--security`, `--performance`, `--testing`, `--maintainability`, `--data-migration`, `--api-contract`, `--design`, or `--all-specialists`, force-include that specialist regardless of gating. + +Note which specialists were selected, gated, and skipped. Print the selection: +"Dispatching N specialists: [names]. Skipped: [names] (scope not detected). Gated: [names] (0 findings in N+ reviews)." + +--- + +### Dispatch specialists in parallel + +For each selected specialist, launch an independent subagent via the Agent tool. +**Launch ALL selected specialists in a single message** (multiple Agent tool calls) +so they run in parallel. Each subagent has fresh context — no prior review bias. + +**Each specialist subagent prompt:** + +Construct the prompt for each specialist. The prompt includes: + +1. The specialist's checklist content (you already read the file above) +2. Stack context: "This is a {STACK} project." +3. Past learnings for this domain (if any exist): + +```bash +~/.claude/skills/gstack/bin/gstack-learnings-search --type pitfall --query "{specialist domain}" --limit 5 2>/dev/null || true +``` + +If learnings are found, include them: "Past learnings for this domain: {learnings}" + +4. Instructions: + +"You are a specialist code reviewer. Read the checklist below, then run +`git diff origin/` to get the full diff. Apply the checklist against the diff. + +For each finding, output a JSON object on its own line: +{\"severity\":\"CRITICAL|INFORMATIONAL\",\"confidence\":N,\"path\":\"file\",\"line\":N,\"category\":\"category\",\"summary\":\"description\",\"fix\":\"recommended fix\",\"fingerprint\":\"path:line:category\",\"specialist\":\"name\"} + +Required fields: severity, confidence, path, category, summary, specialist. +Optional: line, fix, fingerprint, evidence, test_stub. + +If you can write a test that would catch this issue, include it in the `test_stub` field. +Use the detected test framework ({TEST_FW}). Write a minimal skeleton — describe/it/test +blocks with clear intent. Skip test_stub for architectural or design-only findings. + +If no findings: output `NO FINDINGS` and nothing else. +Do not output anything else — no preamble, no summary, no commentary. + +Stack context: {STACK} +Past learnings: {learnings or 'none'} + +CHECKLIST: +{checklist content}" + +**Subagent configuration:** +- Use `subagent_type: "general-purpose"` +- Do NOT use `run_in_background` — all specialists must complete before merge +- If any specialist subagent fails or times out, log the failure and continue with results from successful specialists. Specialists are additive — partial results are better than no results. + +--- + +### Step 3.56: Collect and merge findings + +After all specialist subagents complete, collect their outputs. + +**Parse findings:** +For each specialist's output: +1. If output is "NO FINDINGS" — skip, this specialist found nothing +2. Otherwise, parse each line as a JSON object. Skip lines that are not valid JSON. +3. Collect all parsed findings into a single list, tagged with their specialist name. + +**Fingerprint and deduplicate:** +For each finding, compute its fingerprint: +- If `fingerprint` field is present, use it +- Otherwise: `{path}:{line}:{category}` (if line is present) or `{path}:{category}` + +Group findings by fingerprint. For findings sharing the same fingerprint: +- Keep the finding with the highest confidence score +- Tag it: "MULTI-SPECIALIST CONFIRMED ({specialist1} + {specialist2})" +- Boost confidence by +1 (cap at 10) +- Note the confirming specialists in the output + +**Apply confidence gates:** +- Confidence 7+: show normally in the findings output +- Confidence 5-6: show with caveat "Medium confidence — verify this is actually an issue" +- Confidence 3-4: move to appendix (suppress from main findings) +- Confidence 1-2: suppress entirely + +**Compute PR Quality Score:** +After merging, compute the quality score: +`quality_score = max(0, 10 - (critical_count * 2 + informational_count * 0.5))` +Cap at 10. Log this in the review result at the end. + +**Output merged findings:** +Present the merged findings in the same format as the current review: + +``` +SPECIALIST REVIEW: N findings (X critical, Y informational) from Z specialists + +[For each finding, in order: CRITICAL first, then INFORMATIONAL, sorted by confidence descending] +[SEVERITY] (confidence: N/10, specialist: name) path:line — summary + Fix: recommended fix + [If MULTI-SPECIALIST CONFIRMED: show confirmation note] + +PR Quality Score: X/10 +``` + +These findings flow into the Fix-First flow (item 4) alongside the checklist pass (Step 3.5). +The Fix-First heuristic applies identically — specialist findings follow the same AUTO-FIX vs ASK classification. + +**Compile per-specialist stats:** +After merging findings, compile a `specialists` object for the review-log persist. +For each specialist (testing, maintainability, security, performance, data-migration, api-contract, design, red-team): +- If dispatched: `{"dispatched": true, "findings": N, "critical": N, "informational": N}` +- If skipped by scope: `{"dispatched": false, "reason": "scope"}` +- If skipped by gating: `{"dispatched": false, "reason": "gated"}` +- If not applicable (e.g., red-team not activated): omit from the object + +Include the Design specialist even though it uses `design-checklist.md` instead of the specialist schema files. +Remember these stats — you will need them for the review-log entry in Step 5.8. + +--- + +### Red Team dispatch (conditional) + +**Activation:** Only if DIFF_LINES > 200 OR any specialist produced a CRITICAL finding. + +If activated, dispatch one more subagent via the Agent tool (foreground, not background). + +The Red Team subagent receives: +1. The red-team checklist from `~/.claude/skills/gstack/review/specialists/red-team.md` +2. The merged specialist findings from Step 3.56 (so it knows what was already caught) +3. The git diff command + +Prompt: "You are a red team reviewer. The code has already been reviewed by N specialists +who found the following issues: {merged findings summary}. Your job is to find what they +MISSED. Read the checklist, run `git diff origin/`, and look for gaps. +Output findings as JSON objects (same schema as the specialists). Focus on cross-cutting +concerns, integration boundary issues, and failure modes that specialist checklists +don't cover." + +If the Red Team finds additional issues, merge them into the findings list before +the Fix-First flow (item 4). Red Team findings are tagged with `"specialist":"red-team"`. + +If the Red Team returns NO FINDINGS, note: "Red Team review: no additional issues found." +If the Red Team subagent fails or times out, skip silently and continue. + +### Step 3.57: Cross-review finding dedup + +Before classifying findings, check if any were previously skipped by the user in a prior review on this branch. + +```bash +~/.claude/skills/gstack/bin/gstack-review-read +``` + +Parse the output: only lines BEFORE `---CONFIG---` are JSONL entries (the output also contains `---CONFIG---` and `---HEAD---` footer sections that are not JSONL — ignore those). + +For each JSONL entry that has a `findings` array: +1. Collect all fingerprints where `action: "skipped"` +2. Note the `commit` field from that entry + +If skipped fingerprints exist, get the list of files changed since that review: + +```bash +git diff --name-only HEAD +``` + +For each current finding (from both the checklist pass (Step 3.5) and specialist review (Step 3.55-3.56)), check: +- Does its fingerprint match a previously skipped finding? +- Is the finding's file path NOT in the changed-files set? + +If both conditions are true: suppress the finding. It was intentionally skipped and the relevant code hasn't changed. + +Print: "Suppressed N findings from prior reviews (previously skipped by user)" + +**Only suppress `skipped` findings — never `fixed` or `auto-fixed`** (those might regress and should be re-checked). + +If no prior reviews exist or none have a `findings` array, skip this step silently. + +Output a summary header: `Pre-Landing Review: N issues (X critical, Y informational)` + +4. **Classify each finding from both the checklist pass and specialist review (Step 3.55-3.56) as AUTO-FIX or ASK** per the Fix-First Heuristic in checklist.md. Critical findings lean toward ASK; informational lean toward AUTO-FIX. 5. **Auto-fix all AUTO-FIX items.** Apply each fix. Output one line per fix: @@ -1680,10 +2002,13 @@ Present Codex output under a `CODEX (design):` header, merged with the checklist 9. Persist the review result to the review log: ```bash -~/.claude/skills/gstack/bin/gstack-review-log '{"skill":"review","timestamp":"TIMESTAMP","status":"STATUS","issues_found":N,"critical":N,"informational":N,"commit":"'"$(git rev-parse --short HEAD)"'","via":"ship"}' +~/.claude/skills/gstack/bin/gstack-review-log '{"skill":"review","timestamp":"TIMESTAMP","status":"STATUS","issues_found":N,"critical":N,"informational":N,"quality_score":SCORE,"specialists":SPECIALISTS_JSON,"findings":FINDINGS_JSON,"commit":"'"$(git rev-parse --short HEAD)"'","via":"ship"}' ``` Substitute TIMESTAMP (ISO 8601), STATUS ("clean" if no issues, "issues_found" otherwise), and N values from the summary counts above. The `via:"ship"` distinguishes from standalone `/review` runs. +- `quality_score` = the PR Quality Score computed in Step 3.56 (e.g., 7.5). If specialists were skipped (small diff), use `10.0` +- `specialists` = the per-specialist stats object compiled in Step 3.56. Each specialist that was considered gets an entry: `{"dispatched":true/false,"findings":N,"critical":N,"informational":N}` if dispatched, or `{"dispatched":false,"reason":"scope|gated"}` if skipped. Example: `{"testing":{"dispatched":true,"findings":2,"critical":0,"informational":2},"security":{"dispatched":false,"reason":"scope"}}` +- `findings` = array of per-finding records. For each finding (from checklist pass and specialists), include: `{"fingerprint":"path:line:category","severity":"CRITICAL|INFORMATIONAL","action":"ACTION"}`. ACTION is `"auto-fixed"`, `"fixed"` (user approved), or `"skipped"` (user chose Skip). Save the review output — it goes into the PR body in Step 8. @@ -1889,7 +2214,7 @@ echo "BASE: $BASE_VERSION HEAD: $CURRENT_VERSION" if [ "$CURRENT_VERSION" != "$BASE_VERSION" ]; then echo "ALREADY_BUMPED"; fi ``` -If output shows `ALREADY_BUMPED`, VERSION was already bumped on this branch (prior `/ship` run). Skip the rest of Step 4 and use the current VERSION. Otherwise proceed with the bump. +If output shows `ALREADY_BUMPED`, VERSION was already bumped on this branch (prior `/ship` run). Skip the bump action (do not modify VERSION), but read the current VERSION value — it is needed for CHANGELOG and PR body. Continue to the next step. Otherwise proceed with the bump. 1. Read the current `VERSION` file (4-digit format: `MAJOR.MINOR.PATCH.MICRO`) @@ -2080,7 +2405,7 @@ echo "LOCAL: $LOCAL REMOTE: $REMOTE" [ "$LOCAL" = "$REMOTE" ] && echo "ALREADY_PUSHED" || echo "PUSH_NEEDED" ``` -If `ALREADY_PUSHED`, skip the push. Otherwise push with upstream tracking: +If `ALREADY_PUSHED`, skip the push but continue to Step 8. Otherwise push with upstream tracking: ```bash git push -u origin @@ -2102,7 +2427,7 @@ gh pr view --json url,number,state -q 'if .state == "OPEN" then "PR #\(.number): glab mr view -F json 2>/dev/null | jq -r 'if .state == "opened" then "MR_EXISTS" else "NO_MR" end' 2>/dev/null || echo "NO_MR" ``` -If an **open** PR/MR already exists: **update** the PR body with the latest test results, coverage, and review findings using `gh pr edit --body "..."` (GitHub) or `glab mr update -d "..."` (GitLab). Print the existing URL and continue to Step 8.5. +If an **open** PR/MR already exists: **update** the PR body using `gh pr edit --body "..."` (GitHub) or `glab mr update -d "..."` (GitLab). Always regenerate the PR body from scratch using this run's fresh results (test output, coverage audit, review findings, adversarial review, TODOS summary). Never reuse stale PR body content from a prior run. Print the existing URL and continue to Step 8.5. If no PR/MR exists: create a pull request (GitHub) or merge request (GitLab) using the platform detected in Step 0. @@ -2207,6 +2532,8 @@ execute its full workflow: This step is automatic. Do not ask the user for confirmation. The goal is zero-friction doc updates — the user runs `/ship` and documentation stays current without a separate command. +If Step 8.5 created a docs commit, re-edit the PR/MR body to include the latest commit SHA in the summary. This ensures the PR body reflects the truly final state after document-release. + --- ## Step 8.75: Persist ship metrics diff --git a/test/fixtures/golden/codex-ship-SKILL.md b/test/fixtures/golden/codex-ship-SKILL.md index ec0116f06..20cdaf7ea 100644 --- a/test/fixtures/golden/codex-ship-SKILL.md +++ b/test/fixtures/golden/codex-ship-SKILL.md @@ -80,6 +80,33 @@ fi _ROUTING_DECLINED=$($GSTACK_BIN/gstack-config get routing_declined 2>/dev/null || echo "false") echo "HAS_ROUTING: $_HAS_ROUTING" echo "ROUTING_DECLINED: $_ROUTING_DECLINED" +# Vendoring deprecation: detect if CWD has a vendored gstack copy +_VENDORED="no" +if [ -d ".agents/skills/gstack" ] && [ ! -L ".agents/skills/gstack" ]; then + if [ -f ".agents/skills/gstack/VERSION" ] || [ -d ".agents/skills/gstack/.git" ]; then + _VENDORED="yes" + fi +fi +echo "VENDORED_GSTACK: $_VENDORED" +# Semantic code search (sqry) +_SQRY="unavailable" +_SQRY_INDEXED="no" +_SQRY_STALE="no" +if command -v sqry >/dev/null 2>&1; then + _SQRY="available" + _SQRY_VERSION=$(sqry --version 2>/dev/null | head -1 || echo "unknown") + _SQRY_STATUS=$(sqry index --status --json . 2>/dev/null || echo '{}') + if echo "$_SQRY_STATUS" | grep -q '"exists": true' 2>/dev/null; then + _SQRY_INDEXED="yes" + fi + if echo "$_SQRY_STATUS" | grep -q '"stale": true' 2>/dev/null; then + _SQRY_STALE="yes" + fi +fi +echo "SQRY: $_SQRY" +[ "$_SQRY" = "available" ] && echo "SQRY_VERSION: $_SQRY_VERSION" +[ "$_SQRY" = "available" ] && echo "SQRY_INDEXED: $_SQRY_INDEXED" +[ "$_SQRY" = "available" ] && echo "SQRY_STALE: $_SQRY_STALE" # Detect spawned session (OpenClaw or other orchestrator) [ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true ``` @@ -208,6 +235,38 @@ Say "No problem. You can add routing rules later by running `gstack-config set r This only happens once per project. If `HAS_ROUTING` is `yes` or `ROUTING_DECLINED` is `true`, skip this entirely. +If `VENDORED_GSTACK` is `yes`: This project has a vendored copy of gstack at +`.agents/skills/gstack/`. Vendoring is deprecated. We will not keep vendored copies +up to date, so this project's gstack will fall behind. + +Use AskUserQuestion (one-time per project, check for `~/.gstack/.vendoring-warned-$SLUG` marker): + +> This project has gstack vendored in `.agents/skills/gstack/`. Vendoring is deprecated. +> We won't keep this copy up to date, so you'll fall behind on new features and fixes. +> +> Want to migrate to team mode? It takes about 30 seconds. + +Options: +- A) Yes, migrate to team mode now +- B) No, I'll handle it myself + +If A: +1. Run `git rm -r .agents/skills/gstack/` +2. Run `echo '.agents/skills/gstack/' >> .gitignore` +3. Run `$GSTACK_BIN/gstack-team-init required` (or `optional`) +4. Run `git add .claude/ .gitignore CLAUDE.md && git commit -m "chore: migrate gstack from vendored to team mode"` +5. Tell the user: "Done. Each developer now runs: `cd $GSTACK_ROOT && ./setup --team`" + +If B: say "OK, you're on your own to keep the vendored copy up to date." + +Always run (regardless of choice): +```bash +eval "$($GSTACK_BIN/gstack-slug 2>/dev/null)" 2>/dev/null || true +touch ~/.gstack/.vendoring-warned-${SLUG:-unknown} +``` + +This only happens once per project. If the marker file exists, skip entirely. + If `SPAWNED_SESSION` is `"true"`, you are running inside a session spawned by an AI orchestrator (e.g., OpenClaw). In spawned sessions: - Do NOT use AskUserQuestion for interactive prompts. Auto-choose the recommended option. @@ -574,6 +633,16 @@ You are running the `/ship` workflow. This is a **non-interactive, fully automat - Auto-fixable review findings (dead code, N+1, stale comments — fixed automatically) - Test coverage gaps within target threshold (auto-generate and commit, or flag in PR body) +**Re-run behavior (idempotency):** +Re-running `/ship` means "run the whole checklist again." Every verification step +(tests, coverage audit, plan completion, pre-landing review, adversarial review, +VERSION/CHANGELOG check, TODOS, document-release) runs on every invocation. +Only *actions* are idempotent: +- Step 4: If VERSION already bumped, skip the bump but still read the version +- Step 7: If already pushed, skip the push command +- Step 8: If PR exists, update the body instead of creating a new PR +Never skip a verification step because a prior `/ship` run already performed it. + --- ## Step 1: Pre-flight @@ -1493,6 +1562,22 @@ $GSTACK_BIN/gstack-learnings-search --limit 10 2>/dev/null || true If learnings are found, incorporate them into your analysis. When a review finding matches a past learning, note it: "Prior learning applied: [key] (confidence N, from [date])" +## Structural Code Analysis (sqry) + +If `SQRY: unavailable`: skip this section. +If `SQRY: available` but no `mcp__sqry__` tools visible: tell user to run `sqry mcp setup` and restart session. + +**Index freshness:** if `SQRY_INDEXED: no` → tell user to run `sqry index .` (typically 10-60s), then `mcp__sqry__rebuild_index`. +If you made structural changes this session, call rebuild_index before your next sqry query. + +**structural verification before shipping** — use these `mcp__sqry__` tools: + +- `mcp__sqry__find_cycles` — no circular dependencies in shipped code +- `mcp__sqry__find_unused` — catch dead code being shipped +- `mcp__sqry__complexity_metrics` — verify complexity hasn't regressed + +**Tool parameters:** Most sqry tools accept `max_depth` (default 3, max 10) and `max_results` (default 20, max 100). Scope queries to specific files or directories when possible — full-repo queries on large codebases are expensive. Use `semantic_search` for broad discovery, then `direct_callers`/`direct_callees` for focused tracing. + ## Step 3.48: Scope Drift Detection Before reviewing code quality, check: **did they build what was requested — nothing more, nothing less?** @@ -1602,7 +1687,43 @@ Substitute: TIMESTAMP = ISO 8601 datetime, STATUS = "clean" if 0 findings or "is Include any design findings alongside the code review findings. They follow the same Fix-First flow below. -4. **Classify each finding as AUTO-FIX or ASK** per the Fix-First Heuristic in + + +### Step 3.57: Cross-review finding dedup + +Before classifying findings, check if any were previously skipped by the user in a prior review on this branch. + +```bash +$GSTACK_ROOT/bin/gstack-review-read +``` + +Parse the output: only lines BEFORE `---CONFIG---` are JSONL entries (the output also contains `---CONFIG---` and `---HEAD---` footer sections that are not JSONL — ignore those). + +For each JSONL entry that has a `findings` array: +1. Collect all fingerprints where `action: "skipped"` +2. Note the `commit` field from that entry + +If skipped fingerprints exist, get the list of files changed since that review: + +```bash +git diff --name-only HEAD +``` + +For each current finding (from both the checklist pass (Step 3.5) and specialist review (Step 3.55-3.56)), check: +- Does its fingerprint match a previously skipped finding? +- Is the finding's file path NOT in the changed-files set? + +If both conditions are true: suppress the finding. It was intentionally skipped and the relevant code hasn't changed. + +Print: "Suppressed N findings from prior reviews (previously skipped by user)" + +**Only suppress `skipped` findings — never `fixed` or `auto-fixed`** (those might regress and should be re-checked). + +If no prior reviews exist or none have a `findings` array, skip this step silently. + +Output a summary header: `Pre-Landing Review: N issues (X critical, Y informational)` + +4. **Classify each finding from both the checklist pass and specialist review (Step 3.55-3.56) as AUTO-FIX or ASK** per the Fix-First Heuristic in checklist.md. Critical findings lean toward ASK; informational lean toward AUTO-FIX. 5. **Auto-fix all AUTO-FIX items.** Apply each fix. Output one line per fix: @@ -1624,10 +1745,13 @@ Substitute: TIMESTAMP = ISO 8601 datetime, STATUS = "clean" if 0 findings or "is 9. Persist the review result to the review log: ```bash -$GSTACK_ROOT/bin/gstack-review-log '{"skill":"review","timestamp":"TIMESTAMP","status":"STATUS","issues_found":N,"critical":N,"informational":N,"commit":"'"$(git rev-parse --short HEAD)"'","via":"ship"}' +$GSTACK_ROOT/bin/gstack-review-log '{"skill":"review","timestamp":"TIMESTAMP","status":"STATUS","issues_found":N,"critical":N,"informational":N,"quality_score":SCORE,"specialists":SPECIALISTS_JSON,"findings":FINDINGS_JSON,"commit":"'"$(git rev-parse --short HEAD)"'","via":"ship"}' ``` Substitute TIMESTAMP (ISO 8601), STATUS ("clean" if no issues, "issues_found" otherwise), and N values from the summary counts above. The `via:"ship"` distinguishes from standalone `/review` runs. +- `quality_score` = the PR Quality Score computed in Step 3.56 (e.g., 7.5). If specialists were skipped (small diff), use `10.0` +- `specialists` = the per-specialist stats object compiled in Step 3.56. Each specialist that was considered gets an entry: `{"dispatched":true/false,"findings":N,"critical":N,"informational":N}` if dispatched, or `{"dispatched":false,"reason":"scope|gated"}` if skipped. Example: `{"testing":{"dispatched":true,"findings":2,"critical":0,"informational":2},"security":{"dispatched":false,"reason":"scope"}}` +- `findings` = array of per-finding records. For each finding (from checklist pass and specialists), include: `{"fingerprint":"path:line:category","severity":"CRITICAL|INFORMATIONAL","action":"ACTION"}`. ACTION is `"auto-fixed"`, `"fixed"` (user approved), or `"skipped"` (user chose Skip). Save the review output — it goes into the PR body in Step 8. @@ -1710,7 +1834,7 @@ echo "BASE: $BASE_VERSION HEAD: $CURRENT_VERSION" if [ "$CURRENT_VERSION" != "$BASE_VERSION" ]; then echo "ALREADY_BUMPED"; fi ``` -If output shows `ALREADY_BUMPED`, VERSION was already bumped on this branch (prior `/ship` run). Skip the rest of Step 4 and use the current VERSION. Otherwise proceed with the bump. +If output shows `ALREADY_BUMPED`, VERSION was already bumped on this branch (prior `/ship` run). Skip the bump action (do not modify VERSION), but read the current VERSION value — it is needed for CHANGELOG and PR body. Continue to the next step. Otherwise proceed with the bump. 1. Read the current `VERSION` file (4-digit format: `MAJOR.MINOR.PATCH.MICRO`) @@ -1901,7 +2025,7 @@ echo "LOCAL: $LOCAL REMOTE: $REMOTE" [ "$LOCAL" = "$REMOTE" ] && echo "ALREADY_PUSHED" || echo "PUSH_NEEDED" ``` -If `ALREADY_PUSHED`, skip the push. Otherwise push with upstream tracking: +If `ALREADY_PUSHED`, skip the push but continue to Step 8. Otherwise push with upstream tracking: ```bash git push -u origin @@ -1923,7 +2047,7 @@ gh pr view --json url,number,state -q 'if .state == "OPEN" then "PR #\(.number): glab mr view -F json 2>/dev/null | jq -r 'if .state == "opened" then "MR_EXISTS" else "NO_MR" end' 2>/dev/null || echo "NO_MR" ``` -If an **open** PR/MR already exists: **update** the PR body with the latest test results, coverage, and review findings using `gh pr edit --body "..."` (GitHub) or `glab mr update -d "..."` (GitLab). Print the existing URL and continue to Step 8.5. +If an **open** PR/MR already exists: **update** the PR body using `gh pr edit --body "..."` (GitHub) or `glab mr update -d "..."` (GitLab). Always regenerate the PR body from scratch using this run's fresh results (test output, coverage audit, review findings, adversarial review, TODOS summary). Never reuse stale PR body content from a prior run. Print the existing URL and continue to Step 8.5. If no PR/MR exists: create a pull request (GitHub) or merge request (GitLab) using the platform detected in Step 0. @@ -2028,6 +2152,8 @@ execute its full workflow: This step is automatic. Do not ask the user for confirmation. The goal is zero-friction doc updates — the user runs `/ship` and documentation stays current without a separate command. +If Step 8.5 created a docs commit, re-edit the PR/MR body to include the latest commit SHA in the summary. This ensures the PR body reflects the truly final state after document-release. + --- ## Step 8.75: Persist ship metrics diff --git a/test/fixtures/golden/factory-ship-SKILL.md b/test/fixtures/golden/factory-ship-SKILL.md index 95f051118..7b7e23c38 100644 --- a/test/fixtures/golden/factory-ship-SKILL.md +++ b/test/fixtures/golden/factory-ship-SKILL.md @@ -82,6 +82,33 @@ fi _ROUTING_DECLINED=$($GSTACK_BIN/gstack-config get routing_declined 2>/dev/null || echo "false") echo "HAS_ROUTING: $_HAS_ROUTING" echo "ROUTING_DECLINED: $_ROUTING_DECLINED" +# Vendoring deprecation: detect if CWD has a vendored gstack copy +_VENDORED="no" +if [ -d ".factory/skills/gstack" ] && [ ! -L ".factory/skills/gstack" ]; then + if [ -f ".factory/skills/gstack/VERSION" ] || [ -d ".factory/skills/gstack/.git" ]; then + _VENDORED="yes" + fi +fi +echo "VENDORED_GSTACK: $_VENDORED" +# Semantic code search (sqry) +_SQRY="unavailable" +_SQRY_INDEXED="no" +_SQRY_STALE="no" +if command -v sqry >/dev/null 2>&1; then + _SQRY="available" + _SQRY_VERSION=$(sqry --version 2>/dev/null | head -1 || echo "unknown") + _SQRY_STATUS=$(sqry index --status --json . 2>/dev/null || echo '{}') + if echo "$_SQRY_STATUS" | grep -q '"exists": true' 2>/dev/null; then + _SQRY_INDEXED="yes" + fi + if echo "$_SQRY_STATUS" | grep -q '"stale": true' 2>/dev/null; then + _SQRY_STALE="yes" + fi +fi +echo "SQRY: $_SQRY" +[ "$_SQRY" = "available" ] && echo "SQRY_VERSION: $_SQRY_VERSION" +[ "$_SQRY" = "available" ] && echo "SQRY_INDEXED: $_SQRY_INDEXED" +[ "$_SQRY" = "available" ] && echo "SQRY_STALE: $_SQRY_STALE" # Detect spawned session (OpenClaw or other orchestrator) [ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true ``` @@ -210,6 +237,38 @@ Say "No problem. You can add routing rules later by running `gstack-config set r This only happens once per project. If `HAS_ROUTING` is `yes` or `ROUTING_DECLINED` is `true`, skip this entirely. +If `VENDORED_GSTACK` is `yes`: This project has a vendored copy of gstack at +`.factory/skills/gstack/`. Vendoring is deprecated. We will not keep vendored copies +up to date, so this project's gstack will fall behind. + +Use AskUserQuestion (one-time per project, check for `~/.gstack/.vendoring-warned-$SLUG` marker): + +> This project has gstack vendored in `.factory/skills/gstack/`. Vendoring is deprecated. +> We won't keep this copy up to date, so you'll fall behind on new features and fixes. +> +> Want to migrate to team mode? It takes about 30 seconds. + +Options: +- A) Yes, migrate to team mode now +- B) No, I'll handle it myself + +If A: +1. Run `git rm -r .factory/skills/gstack/` +2. Run `echo '.factory/skills/gstack/' >> .gitignore` +3. Run `$GSTACK_BIN/gstack-team-init required` (or `optional`) +4. Run `git add .claude/ .gitignore CLAUDE.md && git commit -m "chore: migrate gstack from vendored to team mode"` +5. Tell the user: "Done. Each developer now runs: `cd $GSTACK_ROOT && ./setup --team`" + +If B: say "OK, you're on your own to keep the vendored copy up to date." + +Always run (regardless of choice): +```bash +eval "$($GSTACK_BIN/gstack-slug 2>/dev/null)" 2>/dev/null || true +touch ~/.gstack/.vendoring-warned-${SLUG:-unknown} +``` + +This only happens once per project. If the marker file exists, skip entirely. + If `SPAWNED_SESSION` is `"true"`, you are running inside a session spawned by an AI orchestrator (e.g., OpenClaw). In spawned sessions: - Do NOT use AskUserQuestion for interactive prompts. Auto-choose the recommended option. @@ -576,6 +635,16 @@ You are running the `/ship` workflow. This is a **non-interactive, fully automat - Auto-fixable review findings (dead code, N+1, stale comments — fixed automatically) - Test coverage gaps within target threshold (auto-generate and commit, or flag in PR body) +**Re-run behavior (idempotency):** +Re-running `/ship` means "run the whole checklist again." Every verification step +(tests, coverage audit, plan completion, pre-landing review, adversarial review, +VERSION/CHANGELOG check, TODOS, document-release) runs on every invocation. +Only *actions* are idempotent: +- Step 4: If VERSION already bumped, skip the bump but still read the version +- Step 7: If already pushed, skip the push command +- Step 8: If PR exists, update the body instead of creating a new PR +Never skip a verification step because a prior `/ship` run already performed it. + --- ## Step 1: Pre-flight @@ -1522,6 +1591,22 @@ matches a past learning, display: This makes the compounding visible. The user should see that gstack is getting smarter on their codebase over time. +## Structural Code Analysis (sqry) + +If `SQRY: unavailable`: skip this section. +If `SQRY: available` but no `mcp__sqry__` tools visible: tell user to run `sqry mcp setup` and restart session. + +**Index freshness:** if `SQRY_INDEXED: no` → tell user to run `sqry index .` (typically 10-60s), then `mcp__sqry__rebuild_index`. +If you made structural changes this session, call rebuild_index before your next sqry query. + +**structural verification before shipping** — use these `mcp__sqry__` tools: + +- `mcp__sqry__find_cycles` — no circular dependencies in shipped code +- `mcp__sqry__find_unused` — catch dead code being shipped +- `mcp__sqry__complexity_metrics` — verify complexity hasn't regressed + +**Tool parameters:** Most sqry tools accept `max_depth` (default 3, max 10) and `max_results` (default 20, max 100). Scope queries to specific files or directories when possible — full-repo queries on large codebases are expensive. Use `semantic_search` for broad discovery, then `direct_callers`/`direct_callees` for focused tracing. + ## Step 3.48: Scope Drift Detection Before reviewing code quality, check: **did they build what was requested — nothing more, nothing less?** @@ -1654,7 +1739,244 @@ Present Codex output under a `CODEX (design):` header, merged with the checklist Include any design findings alongside the code review findings. They follow the same Fix-First flow below. -4. **Classify each finding as AUTO-FIX or ASK** per the Fix-First Heuristic in +## Step 3.55: Review Army — Specialist Dispatch + +### Detect stack and scope + +```bash +source <($GSTACK_BIN/gstack-diff-scope 2>/dev/null) || true +# Detect stack for specialist context +STACK="" +[ -f Gemfile ] && STACK="${STACK}ruby " +[ -f package.json ] && STACK="${STACK}node " +[ -f requirements.txt ] || [ -f pyproject.toml ] && STACK="${STACK}python " +[ -f go.mod ] && STACK="${STACK}go " +[ -f Cargo.toml ] && STACK="${STACK}rust " +echo "STACK: ${STACK:-unknown}" +DIFF_INS=$(git diff origin/ --stat | tail -1 | grep -oE '[0-9]+ insertion' | grep -oE '[0-9]+' || echo "0") +DIFF_DEL=$(git diff origin/ --stat | tail -1 | grep -oE '[0-9]+ deletion' | grep -oE '[0-9]+' || echo "0") +DIFF_LINES=$((DIFF_INS + DIFF_DEL)) +echo "DIFF_LINES: $DIFF_LINES" +# Detect test framework for specialist test stub generation +TEST_FW="" +{ [ -f jest.config.ts ] || [ -f jest.config.js ]; } && TEST_FW="jest" +[ -f vitest.config.ts ] && TEST_FW="vitest" +{ [ -f spec/spec_helper.rb ] || [ -f .rspec ]; } && TEST_FW="rspec" +{ [ -f pytest.ini ] || [ -f conftest.py ]; } && TEST_FW="pytest" +[ -f go.mod ] && TEST_FW="go-test" +echo "TEST_FW: ${TEST_FW:-unknown}" +``` + +### Read specialist hit rates (adaptive gating) + +```bash +$GSTACK_BIN/gstack-specialist-stats 2>/dev/null || true +``` + +### Select specialists + +Based on the scope signals above, select which specialists to dispatch. + +**Always-on (dispatch on every review with 50+ changed lines):** +1. **Testing** — read `$GSTACK_ROOT/review/specialists/testing.md` +2. **Maintainability** — read `$GSTACK_ROOT/review/specialists/maintainability.md` + +**If DIFF_LINES < 50:** Skip all specialists. Print: "Small diff ($DIFF_LINES lines) — specialists skipped." Continue to the Fix-First flow (item 4). + +**Conditional (dispatch if the matching scope signal is true):** +3. **Security** — if SCOPE_AUTH=true, OR if SCOPE_BACKEND=true AND DIFF_LINES > 100. Read `$GSTACK_ROOT/review/specialists/security.md` +4. **Performance** — if SCOPE_BACKEND=true OR SCOPE_FRONTEND=true. Read `$GSTACK_ROOT/review/specialists/performance.md` +5. **Data Migration** — if SCOPE_MIGRATIONS=true. Read `$GSTACK_ROOT/review/specialists/data-migration.md` +6. **API Contract** — if SCOPE_API=true. Read `$GSTACK_ROOT/review/specialists/api-contract.md` +7. **Design** — if SCOPE_FRONTEND=true. Use the existing design review checklist at `$GSTACK_ROOT/review/design-checklist.md` + +### Adaptive gating + +After scope-based selection, apply adaptive gating based on specialist hit rates: + +For each conditional specialist that passed scope gating, check the `gstack-specialist-stats` output above: +- If tagged `[GATE_CANDIDATE]` (0 findings in 10+ dispatches): skip it. Print: "[specialist] auto-gated (0 findings in N reviews)." +- If tagged `[NEVER_GATE]`: always dispatch regardless of hit rate. Security and data-migration are insurance policy specialists — they should run even when silent. + +**Force flags:** If the user's prompt includes `--security`, `--performance`, `--testing`, `--maintainability`, `--data-migration`, `--api-contract`, `--design`, or `--all-specialists`, force-include that specialist regardless of gating. + +Note which specialists were selected, gated, and skipped. Print the selection: +"Dispatching N specialists: [names]. Skipped: [names] (scope not detected). Gated: [names] (0 findings in N+ reviews)." + +--- + +### Dispatch specialists in parallel + +For each selected specialist, launch an independent subagent via the Agent tool. +**Launch ALL selected specialists in a single message** (multiple Agent tool calls) +so they run in parallel. Each subagent has fresh context — no prior review bias. + +**Each specialist subagent prompt:** + +Construct the prompt for each specialist. The prompt includes: + +1. The specialist's checklist content (you already read the file above) +2. Stack context: "This is a {STACK} project." +3. Past learnings for this domain (if any exist): + +```bash +$GSTACK_BIN/gstack-learnings-search --type pitfall --query "{specialist domain}" --limit 5 2>/dev/null || true +``` + +If learnings are found, include them: "Past learnings for this domain: {learnings}" + +4. Instructions: + +"You are a specialist code reviewer. Read the checklist below, then run +`git diff origin/` to get the full diff. Apply the checklist against the diff. + +For each finding, output a JSON object on its own line: +{\"severity\":\"CRITICAL|INFORMATIONAL\",\"confidence\":N,\"path\":\"file\",\"line\":N,\"category\":\"category\",\"summary\":\"description\",\"fix\":\"recommended fix\",\"fingerprint\":\"path:line:category\",\"specialist\":\"name\"} + +Required fields: severity, confidence, path, category, summary, specialist. +Optional: line, fix, fingerprint, evidence, test_stub. + +If you can write a test that would catch this issue, include it in the `test_stub` field. +Use the detected test framework ({TEST_FW}). Write a minimal skeleton — describe/it/test +blocks with clear intent. Skip test_stub for architectural or design-only findings. + +If no findings: output `NO FINDINGS` and nothing else. +Do not output anything else — no preamble, no summary, no commentary. + +Stack context: {STACK} +Past learnings: {learnings or 'none'} + +CHECKLIST: +{checklist content}" + +**Subagent configuration:** +- Use `subagent_type: "general-purpose"` +- Do NOT use `run_in_background` — all specialists must complete before merge +- If any specialist subagent fails or times out, log the failure and continue with results from successful specialists. Specialists are additive — partial results are better than no results. + +--- + +### Step 3.56: Collect and merge findings + +After all specialist subagents complete, collect their outputs. + +**Parse findings:** +For each specialist's output: +1. If output is "NO FINDINGS" — skip, this specialist found nothing +2. Otherwise, parse each line as a JSON object. Skip lines that are not valid JSON. +3. Collect all parsed findings into a single list, tagged with their specialist name. + +**Fingerprint and deduplicate:** +For each finding, compute its fingerprint: +- If `fingerprint` field is present, use it +- Otherwise: `{path}:{line}:{category}` (if line is present) or `{path}:{category}` + +Group findings by fingerprint. For findings sharing the same fingerprint: +- Keep the finding with the highest confidence score +- Tag it: "MULTI-SPECIALIST CONFIRMED ({specialist1} + {specialist2})" +- Boost confidence by +1 (cap at 10) +- Note the confirming specialists in the output + +**Apply confidence gates:** +- Confidence 7+: show normally in the findings output +- Confidence 5-6: show with caveat "Medium confidence — verify this is actually an issue" +- Confidence 3-4: move to appendix (suppress from main findings) +- Confidence 1-2: suppress entirely + +**Compute PR Quality Score:** +After merging, compute the quality score: +`quality_score = max(0, 10 - (critical_count * 2 + informational_count * 0.5))` +Cap at 10. Log this in the review result at the end. + +**Output merged findings:** +Present the merged findings in the same format as the current review: + +``` +SPECIALIST REVIEW: N findings (X critical, Y informational) from Z specialists + +[For each finding, in order: CRITICAL first, then INFORMATIONAL, sorted by confidence descending] +[SEVERITY] (confidence: N/10, specialist: name) path:line — summary + Fix: recommended fix + [If MULTI-SPECIALIST CONFIRMED: show confirmation note] + +PR Quality Score: X/10 +``` + +These findings flow into the Fix-First flow (item 4) alongside the checklist pass (Step 3.5). +The Fix-First heuristic applies identically — specialist findings follow the same AUTO-FIX vs ASK classification. + +**Compile per-specialist stats:** +After merging findings, compile a `specialists` object for the review-log persist. +For each specialist (testing, maintainability, security, performance, data-migration, api-contract, design, red-team): +- If dispatched: `{"dispatched": true, "findings": N, "critical": N, "informational": N}` +- If skipped by scope: `{"dispatched": false, "reason": "scope"}` +- If skipped by gating: `{"dispatched": false, "reason": "gated"}` +- If not applicable (e.g., red-team not activated): omit from the object + +Include the Design specialist even though it uses `design-checklist.md` instead of the specialist schema files. +Remember these stats — you will need them for the review-log entry in Step 5.8. + +--- + +### Red Team dispatch (conditional) + +**Activation:** Only if DIFF_LINES > 200 OR any specialist produced a CRITICAL finding. + +If activated, dispatch one more subagent via the Agent tool (foreground, not background). + +The Red Team subagent receives: +1. The red-team checklist from `$GSTACK_ROOT/review/specialists/red-team.md` +2. The merged specialist findings from Step 3.56 (so it knows what was already caught) +3. The git diff command + +Prompt: "You are a red team reviewer. The code has already been reviewed by N specialists +who found the following issues: {merged findings summary}. Your job is to find what they +MISSED. Read the checklist, run `git diff origin/`, and look for gaps. +Output findings as JSON objects (same schema as the specialists). Focus on cross-cutting +concerns, integration boundary issues, and failure modes that specialist checklists +don't cover." + +If the Red Team finds additional issues, merge them into the findings list before +the Fix-First flow (item 4). Red Team findings are tagged with `"specialist":"red-team"`. + +If the Red Team returns NO FINDINGS, note: "Red Team review: no additional issues found." +If the Red Team subagent fails or times out, skip silently and continue. + +### Step 3.57: Cross-review finding dedup + +Before classifying findings, check if any were previously skipped by the user in a prior review on this branch. + +```bash +$GSTACK_ROOT/bin/gstack-review-read +``` + +Parse the output: only lines BEFORE `---CONFIG---` are JSONL entries (the output also contains `---CONFIG---` and `---HEAD---` footer sections that are not JSONL — ignore those). + +For each JSONL entry that has a `findings` array: +1. Collect all fingerprints where `action: "skipped"` +2. Note the `commit` field from that entry + +If skipped fingerprints exist, get the list of files changed since that review: + +```bash +git diff --name-only HEAD +``` + +For each current finding (from both the checklist pass (Step 3.5) and specialist review (Step 3.55-3.56)), check: +- Does its fingerprint match a previously skipped finding? +- Is the finding's file path NOT in the changed-files set? + +If both conditions are true: suppress the finding. It was intentionally skipped and the relevant code hasn't changed. + +Print: "Suppressed N findings from prior reviews (previously skipped by user)" + +**Only suppress `skipped` findings — never `fixed` or `auto-fixed`** (those might regress and should be re-checked). + +If no prior reviews exist or none have a `findings` array, skip this step silently. + +Output a summary header: `Pre-Landing Review: N issues (X critical, Y informational)` + +4. **Classify each finding from both the checklist pass and specialist review (Step 3.55-3.56) as AUTO-FIX or ASK** per the Fix-First Heuristic in checklist.md. Critical findings lean toward ASK; informational lean toward AUTO-FIX. 5. **Auto-fix all AUTO-FIX items.** Apply each fix. Output one line per fix: @@ -1676,10 +1998,13 @@ Present Codex output under a `CODEX (design):` header, merged with the checklist 9. Persist the review result to the review log: ```bash -$GSTACK_ROOT/bin/gstack-review-log '{"skill":"review","timestamp":"TIMESTAMP","status":"STATUS","issues_found":N,"critical":N,"informational":N,"commit":"'"$(git rev-parse --short HEAD)"'","via":"ship"}' +$GSTACK_ROOT/bin/gstack-review-log '{"skill":"review","timestamp":"TIMESTAMP","status":"STATUS","issues_found":N,"critical":N,"informational":N,"quality_score":SCORE,"specialists":SPECIALISTS_JSON,"findings":FINDINGS_JSON,"commit":"'"$(git rev-parse --short HEAD)"'","via":"ship"}' ``` Substitute TIMESTAMP (ISO 8601), STATUS ("clean" if no issues, "issues_found" otherwise), and N values from the summary counts above. The `via:"ship"` distinguishes from standalone `/review` runs. +- `quality_score` = the PR Quality Score computed in Step 3.56 (e.g., 7.5). If specialists were skipped (small diff), use `10.0` +- `specialists` = the per-specialist stats object compiled in Step 3.56. Each specialist that was considered gets an entry: `{"dispatched":true/false,"findings":N,"critical":N,"informational":N}` if dispatched, or `{"dispatched":false,"reason":"scope|gated"}` if skipped. Example: `{"testing":{"dispatched":true,"findings":2,"critical":0,"informational":2},"security":{"dispatched":false,"reason":"scope"}}` +- `findings` = array of per-finding records. For each finding (from checklist pass and specialists), include: `{"fingerprint":"path:line:category","severity":"CRITICAL|INFORMATIONAL","action":"ACTION"}`. ACTION is `"auto-fixed"`, `"fixed"` (user approved), or `"skipped"` (user chose Skip). Save the review output — it goes into the PR body in Step 8. @@ -1885,7 +2210,7 @@ echo "BASE: $BASE_VERSION HEAD: $CURRENT_VERSION" if [ "$CURRENT_VERSION" != "$BASE_VERSION" ]; then echo "ALREADY_BUMPED"; fi ``` -If output shows `ALREADY_BUMPED`, VERSION was already bumped on this branch (prior `/ship` run). Skip the rest of Step 4 and use the current VERSION. Otherwise proceed with the bump. +If output shows `ALREADY_BUMPED`, VERSION was already bumped on this branch (prior `/ship` run). Skip the bump action (do not modify VERSION), but read the current VERSION value — it is needed for CHANGELOG and PR body. Continue to the next step. Otherwise proceed with the bump. 1. Read the current `VERSION` file (4-digit format: `MAJOR.MINOR.PATCH.MICRO`) @@ -2076,7 +2401,7 @@ echo "LOCAL: $LOCAL REMOTE: $REMOTE" [ "$LOCAL" = "$REMOTE" ] && echo "ALREADY_PUSHED" || echo "PUSH_NEEDED" ``` -If `ALREADY_PUSHED`, skip the push. Otherwise push with upstream tracking: +If `ALREADY_PUSHED`, skip the push but continue to Step 8. Otherwise push with upstream tracking: ```bash git push -u origin @@ -2098,7 +2423,7 @@ gh pr view --json url,number,state -q 'if .state == "OPEN" then "PR #\(.number): glab mr view -F json 2>/dev/null | jq -r 'if .state == "opened" then "MR_EXISTS" else "NO_MR" end' 2>/dev/null || echo "NO_MR" ``` -If an **open** PR/MR already exists: **update** the PR body with the latest test results, coverage, and review findings using `gh pr edit --body "..."` (GitHub) or `glab mr update -d "..."` (GitLab). Print the existing URL and continue to Step 8.5. +If an **open** PR/MR already exists: **update** the PR body using `gh pr edit --body "..."` (GitHub) or `glab mr update -d "..."` (GitLab). Always regenerate the PR body from scratch using this run's fresh results (test output, coverage audit, review findings, adversarial review, TODOS summary). Never reuse stale PR body content from a prior run. Print the existing URL and continue to Step 8.5. If no PR/MR exists: create a pull request (GitHub) or merge request (GitLab) using the platform detected in Step 0. @@ -2203,6 +2528,8 @@ execute its full workflow: This step is automatic. Do not ask the user for confirmation. The goal is zero-friction doc updates — the user runs `/ship` and documentation stays current without a separate command. +If Step 8.5 created a docs commit, re-edit the PR/MR body to include the latest commit SHA in the summary. This ensures the PR body reflects the truly final state after document-release. + --- ## Step 8.75: Persist ship metrics diff --git a/test/sqry-resolver.test.ts b/test/sqry-resolver.test.ts new file mode 100644 index 000000000..7e248d8ad --- /dev/null +++ b/test/sqry-resolver.test.ts @@ -0,0 +1,208 @@ +import { describe, test, expect } from 'bun:test'; +import { generateSqryContext } from '../scripts/resolvers/sqry'; +import type { TemplateContext, HostPaths } from '../scripts/resolvers/types'; +import * as fs from 'fs'; +import * as path from 'path'; + +const ROOT = path.resolve(import.meta.dir, '..'); + +// Known sqry MCP tool names (from sqry MCP server tool registrations) +const KNOWN_SQRY_TOOLS = [ + 'call_hierarchy', 'complexity_metrics', 'cross_language_edges', + 'dependency_impact', 'direct_callees', 'direct_callers', + 'explain_code', 'export_graph', 'find_cycles', 'find_duplicates', + 'find_unused', 'get_definition', 'get_document_symbols', + 'get_graph_stats', 'get_hover_info', 'get_index_status', + 'get_insights', 'get_references', 'get_workspace_symbols', + 'hierarchical_search', 'is_node_in_cycle', 'list_files', + 'list_symbols', 'pattern_search', 'rebuild_index', + 'relation_query', 'search_similar', 'semantic_diff', + 'semantic_search', 'show_dependencies', 'sqry_ask', + 'subgraph', 'trace_path', +]; + +const claudePaths: HostPaths = { + skillRoot: '~/.claude/skills/gstack', + localSkillRoot: '.claude/skills/gstack', + binDir: '~/.claude/skills/gstack/bin', + browseDir: '~/.claude/skills/gstack/browse/dist', + designDir: '~/.claude/skills/gstack/design/dist', +}; + +function makeCtx(skillName: string, host: string = 'claude'): TemplateContext { + return { + skillName, + tmplPath: path.join(ROOT, skillName, 'SKILL.md.tmpl'), + host: host as any, + paths: claudePaths, + preambleTier: 4, + }; +} + +// Load tools.json for schema validation +const toolsJsonPath = path.join(ROOT, 'contrib/add-tool/sqry/tools.json'); +const toolsConfig = JSON.parse(fs.readFileSync(toolsJsonPath, 'utf-8')); + +describe('sqry tools.json schema validation', () => { + test('has valid top-level structure', () => { + expect(toolsConfig.tool).toBe('sqry'); + expect(toolsConfig.mcp_server_name).toBe('sqry'); + expect(toolsConfig.detection).toBeDefined(); + expect(toolsConfig.detection.binary).toBe('sqry'); + expect(toolsConfig.detection.min_version).toBe('7.0.0'); + expect(toolsConfig.detection.rebuild_hint).toBeTruthy(); + expect(toolsConfig.integrations).toBeDefined(); + }); + + test('has no mcp_resources section (resource reads are prohibited — see contrib/add-tool/README.md Security)', () => { + expect(toolsConfig.mcp_resources).toBeUndefined(); + }); + + test('has static parameter_guidance string', () => { + expect(toolsConfig.parameter_guidance).toBeDefined(); + expect(typeof toolsConfig.parameter_guidance).toBe('string'); + expect(toolsConfig.parameter_guidance.length).toBeGreaterThan(20); + }); + + const integrationNames = Object.keys(toolsConfig.integrations); + + test('has 6 skill integrations', () => { + expect(integrationNames).toEqual([ + 'investigate', 'cso', 'review', 'retro', 'plan-eng-review', 'ship', + ]); + }); + + test('no tool in any integration has a constraint field', () => { + for (const [, integration] of Object.entries(toolsConfig.integrations) as [string, any][]) { + for (const tool of integration.tools) { + expect(tool.constraint).toBeUndefined(); + } + } + }); + + for (const [skillName, integration] of Object.entries(toolsConfig.integrations) as [string, any][]) { + describe(`integration: ${skillName}`, () => { + test('has required fields', () => { + expect(integration.phase).toBeTruthy(); + expect(integration.context).toBeTruthy(); + expect(Array.isArray(integration.tools)).toBe(true); + expect(integration.tools.length).toBeGreaterThan(0); + }); + + for (const tool of integration.tools) { + test(`tool "${tool.tool}" is a known sqry MCP tool`, () => { + expect(KNOWN_SQRY_TOOLS).toContain(tool.tool); + }); + + test(`tool "${tool.tool}" has a when description`, () => { + expect(tool.when).toBeTruthy(); + expect(tool.when.length).toBeGreaterThan(10); + }); + } + }); + } +}); + +describe('SQRY_CONTEXT resolver', () => { + const integratedSkills = Object.keys(toolsConfig.integrations); + + for (const skillName of integratedSkills) { + test(`${skillName}: returns non-empty output`, () => { + const result = generateSqryContext(makeCtx(skillName)); + expect(result.length).toBeGreaterThan(0); + }); + + test(`${skillName}: contains mcp__sqry__ prefix`, () => { + const result = generateSqryContext(makeCtx(skillName)); + expect(result).toContain('mcp__sqry__'); + }); + + test(`${skillName}: contains SQRY availability gating`, () => { + const result = generateSqryContext(makeCtx(skillName)); + expect(result).toContain('SQRY: unavailable'); + expect(result).toContain('SQRY: available'); + }); + + test(`${skillName}: contains MCP runtime gate`, () => { + const result = generateSqryContext(makeCtx(skillName)); + expect(result).toContain('sqry mcp setup'); + }); + + test(`${skillName}: contains index freshness instructions`, () => { + const result = generateSqryContext(makeCtx(skillName)); + expect(result).toContain('SQRY_INDEXED: no'); + expect(result).toContain('rebuild_index'); + }); + + test(`${skillName}: emits static parameter guidance (no MCP resource reads)`, () => { + const result = generateSqryContext(makeCtx(skillName)); + expect(result).toContain('Tool parameters:'); + expect(result).not.toContain('ReadMcpResourceTool'); + expect(result).not.toContain('sqry://'); + }); + + test(`${skillName}: uses context from tools.json`, () => { + const result = generateSqryContext(makeCtx(skillName)); + const expectedContext = toolsConfig.integrations[skillName].context; + expect(result).toContain(expectedContext); + }); + } + + test('returns empty string for unknown skills', () => { + expect(generateSqryContext(makeCtx('browse'))).toBe(''); + expect(generateSqryContext(makeCtx('qa'))).toBe(''); + expect(generateSqryContext(makeCtx('design-review'))).toBe(''); + expect(generateSqryContext(makeCtx('nonexistent-skill'))).toBe(''); + }); +}); + +describe('generated SKILL.md files contain sqry content', () => { + const integratedSkills = ['investigate', 'cso', 'review', 'retro', 'plan-eng-review', 'ship']; + + for (const skill of integratedSkills) { + test(`${skill}/SKILL.md contains Structural Code Analysis section`, () => { + const content = fs.readFileSync(path.join(ROOT, skill, 'SKILL.md'), 'utf-8'); + expect(content).toContain('## Structural Code Analysis (sqry)'); + expect(content).toContain('mcp__sqry__'); + }); + + test(`${skill}/SKILL.md has no unresolved {{SQRY_CONTEXT}} placeholder`, () => { + const content = fs.readFileSync(path.join(ROOT, skill, 'SKILL.md'), 'utf-8'); + expect(content).not.toContain('{{SQRY_CONTEXT}}'); + }); + + test(`${skill}/SKILL.md has static parameter guidance (no MCP resource reads)`, () => { + const content = fs.readFileSync(path.join(ROOT, skill, 'SKILL.md'), 'utf-8'); + expect(content).not.toContain('ReadMcpResourceTool'); + expect(content).not.toContain('sqry://'); + expect(content).toContain('Tool parameters:'); + }); + } + + test('non-integrated skills have no sqry content', () => { + const nonIntegrated = ['browse', 'qa', 'design-review', 'office-hours', 'codex']; + for (const skill of nonIntegrated) { + const skillPath = path.join(ROOT, skill, 'SKILL.md'); + if (fs.existsSync(skillPath)) { + const content = fs.readFileSync(skillPath, 'utf-8'); + expect(content).not.toContain('Structural Code Analysis'); + expect(content).not.toContain('mcp__sqry__'); + } + } + }); +}); + +describe('preamble detection block', () => { + test('preamble.ts contains sqry detection', () => { + const preamble = fs.readFileSync(path.join(ROOT, 'scripts/resolvers/preamble.ts'), 'utf-8'); + expect(preamble).toContain('sqry'); + expect(preamble).toContain('SQRY:'); + expect(preamble).toContain('SQRY_INDEXED'); + }); + + test('generated SKILL.md preamble contains sqry detection output', () => { + const content = fs.readFileSync(path.join(ROOT, 'review/SKILL.md'), 'utf-8'); + expect(content).toContain('SQRY:'); + expect(content).toContain('SQRY_INDEXED:'); + }); +});