Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down
10 changes: 10 additions & 0 deletions autoplan/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down
10 changes: 10 additions & 0 deletions benchmark/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down
10 changes: 10 additions & 0 deletions browse/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down
10 changes: 10 additions & 0 deletions canary/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down
10 changes: 10 additions & 0 deletions checkpoint/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down
10 changes: 10 additions & 0 deletions codex/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down
147 changes: 147 additions & 0 deletions contrib/add-tool/README.md
Original file line number Diff line number Diff line change
@@ -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/<tool-name>/
├── 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

<!--
Design history: the sqry integration originally used a "resource delegation"
model where parameter guidance was served live by the sqry MCP server via
sqry://docs/capability-map and sqry://docs/tool-guide. The reasoning was
sound: sqry owns its own parameter limits and cost tiers, and serving them
live prevents version drift. When sqry ships new defaults, gstack agents
pick them up automatically without a gstack release.

We moved away from this because the security cost outweighs the staleness
cost. MCP resource content enters the LLM's instruction stream in-band,
creating a prompt injection vector that scales with every user who installs
the add-in. A compromised or malicious MCP server binary can inject
arbitrary instructions into an LLM with shell access on every machine.

The staleness risk is real but manageable: parameter defaults change
infrequently, and when they do, updating tools.json is a one-line PR.
The injection risk is neither infrequent nor manageable. It is a
persistent attack surface that exists on every session.

If a future MCP specification adds programmatic content isolation (e.g.,
sandboxed resource channels that the LLM cannot treat as instructions),
this guidance should be revisited. Until then: static text only.
-->

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)
78 changes: 78 additions & 0 deletions contrib/add-tool/sqry/README.md
Original file line number Diff line number Diff line change
@@ -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.

<!--
Design history: this integration originally used sqry v8's "resource
delegation" model, where parameter guidance was served live by the sqry
MCP server via sqry://docs/capability-map and sqry://docs/tool-guide.
The benefit was real: no version coupling, automatic updates when sqry
ships new defaults.

We moved to static guidance because MCP resource content enters the LLM's
instruction stream in-band with skill instructions. There is no programmatic
boundary between "read this for reference" and "follow these instructions."
A compromised sqry-mcp binary could inject arbitrary instructions into an
LLM with shell access. The staleness cost of static guidance is low
(parameter defaults change infrequently). The injection risk of live
resource reads is high (persistent attack surface on every session).

See contrib/add-tool/README.md "Security" section for the full rationale
and guidance for future add-ins.
-->

## 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.
12 changes: 12 additions & 0 deletions contrib/add-tool/sqry/detection.sh
Original file line number Diff line number Diff line change
@@ -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"
Loading
Loading