Compare commits

Author SHA1 Message Date
Dark-Alex-17 1ee6b4d7c7 docs: Documentation for the RESTful API POC 2026-05-01 14:45:13 -06:00
500 changed files with 33468 additions and 151325 deletions

No files matched your search

-7
View File
@@ -1,7 +0,0 @@
assets/config-template.yaml text eol=lf
assets/hooks/*.sh text eol=lf
assets/agents/** text eol=lf
assets/functions/tools/* text eol=lf
assets/functions/scripts/* text eol=lf
assets/functions/utils/*.sh text eol=lf
scripts/*.sh text eol=lf
+13 -13
View File
@@ -21,25 +21,25 @@ body:
value: | value: |
I tried this: I tried this:
1. `coyote` 1. `loki`
I expected this to happen: I expected this to happen:
Instead, this happened: Instead, this happened:
- type: textarea - type: textarea
id: coyote-log id: loki-log
attributes: attributes:
label: Coyote log label: Loki log
description: Include the Coyote log file to help diagnose the issue. (`coyote --info` to see the log_path) description: Include the Loki log file to help diagnose the issue. (`loki --info` to see the log_path)
value: | value: |
| OS | Log file location | | OS | Log file location |
| ------- | ----------------------------------------------------- | | ------- | ----------------------------------------------------- |
| Linux | `~/.cache/coyote/coyote.log` | | Linux | `~/.cache/loki/loki.log` |
| Mac | `~/Library/Logs/coyote/coyote.log` | | Mac | `~/Library/Logs/loki/loki.log` |
| Windows | `C:\Users\<User>\AppData\Local\coyote\coyote.log` | | Windows | `C:\Users\<User>\AppData\Local\loki\loki.log` |
``` ```
please provide a copy of your coyote log file here if possible; you may need to redact some of the lines please provide a copy of your loki log file here if possible; you may need to redact some of the lines
``` ```
- type: input - type: input
@@ -57,13 +57,13 @@ body:
validations: validations:
required: true required: true
- type: input - type: input
id: coyote-version id: loki-version
attributes: attributes:
label: Coyote Version label: Loki Version
description: > description: >
Coyote version (`coyote --version` if using a release, `git describe` if building Loki version (`loki --version` if using a release, `git describe` if building
from main). from main).
**Make sure that you are using the [latest coyote release](https://github.com/Dark-Alex-17/coyote/releases) or a newer main build** **Make sure that you are using the [latest loki release](https://github.com/Dark-Alex-17/loki/releases) or a newer main build**
placeholder: "coyote 0.1.0" placeholder: "loki 0.1.0"
validations: validations:
required: true required: true
@@ -1,18 +1,11 @@
### Description ### AI assistance (if any):
<!-- What problem is this PR solving? --> - List tools here and files touched by them
### Alternatives Considered (Optional)
<!-- What alternatives did you consider? Why did you choose this approach? -->
### AI assistance (if any)
<!-- Required if AI assistance was used to write or modify code in this PR. -->
<!-- List tools here -->
### Authorship & Understanding ### Authorship & Understanding
- [ ] I wrote or heavily modified this code myself. If I didn't, I gave proper attribution to AI assistance above. - [ ] I wrote or heavily modified this code myself
- [ ] I understand how it works end-to-end - [ ] I understand how it works end-to-end
- [ ] I can maintain this code in the future - [ ] I can maintain this code in the future
- [ ] No undisclosed AI-generated code was used - [ ] No undisclosed AI-generated code was used
- [ ] If AI assistance was used, it is documented above - [ ] If AI assistance was used, it is documented below
+1 -12
View File
@@ -3,7 +3,7 @@ name: CI
on: on:
pull_request: pull_request:
branches: branches:
- '**' - '*'
push: push:
branches: branches:
- main - main
@@ -36,17 +36,6 @@ jobs:
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- name: Cache DuckDB Extensions
id: duckdb-extensions
uses: actions/cache@v4
with:
path: ~/.duckdb/extensions
key: duckdb-ext-${{ matrix.os }}-${{ hashFiles('Cargo.lock') }}
- name: Install DuckDB Extensions
if: steps.duckdb-extensions.outputs.cache-hit != 'true'
run: cargo test --all duckdb
- name: Test - name: Test
run: cargo test --all run: cargo test --all
+31 -96
View File
@@ -8,9 +8,9 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
bump_type: bump_type:
description: 'Specify the type of version bump' description: "Specify the type of version bump"
required: true required: true
default: 'patch' default: "patch"
type: choice type: choice
options: options:
- patch - patch
@@ -46,7 +46,7 @@ jobs:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
python-version: '3.10' python-version: "3.10"
- name: Install Commitizen - name: Install Commitizen
run: | run: |
@@ -98,9 +98,9 @@ jobs:
# Ignore Act's local artifact dir noise # Ignore Act's local artifact dir noise
echo artifacts/ >> .git/info/exclude || true echo artifacts/ >> .git/info/exclude || true
# Edit the version line right after name="coyote" # Edit the version line right after name="loki"
sed -E -i ' sed -E -i '
/^[[:space:]]*name[[:space:]]*=[[:space:]]*"coyote"[[:space:]]*$/ { /^[[:space:]]*name[[:space:]]*=[[:space:]]*"loki"[[:space:]]*$/ {
n n
s|^[[:space:]]*version[[:space:]]*=[[:space:]]*"[^"]*"|version = "'"$VERSION"'"| s|^[[:space:]]*version[[:space:]]*=[[:space:]]*"[^"]*"|version = "'"$VERSION"'"|
} }
@@ -108,19 +108,17 @@ jobs:
cargo update || true cargo update || true
sed -i "s|image: 'darkalex17/coyote:v[^']*'|image: 'darkalex17/coyote:v${VERSION}'|" assets/sbx-kit/spec.yaml
# Git config that helps in Act # Git config that helps in Act
git config user.name "github-actions[bot]" git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com" git config user.email "github-actions[bot]@users.noreply.github.com"
git config --global --add safe.directory "$GITHUB_WORKSPACE" git config --global --add safe.directory "$GITHUB_WORKSPACE"
git status --porcelain git status --porcelain
git diff --name-only -- Cargo.toml Cargo.lock assets/sbx-kit/spec.yaml || true git diff --name-only -- Cargo.toml Cargo.lock || true
if ! git diff --quiet -- Cargo.toml Cargo.lock assets/sbx-kit/spec.yaml; then if ! git diff --quiet -- Cargo.toml Cargo.lock; then
git add -u -- Cargo.toml Cargo.lock assets/sbx-kit/spec.yaml git add -u -- Cargo.toml Cargo.lock
git commit -m "chore: bump Cargo.toml and sandbox image to $VERSION" git commit -m "chore: bump Cargo.toml to $VERSION"
else else
echo "No changes to commit (already at $VERSION)" echo "No changes to commit (already at $VERSION)"
fi fi
@@ -165,31 +163,28 @@ jobs:
- target: aarch64-unknown-linux-musl - target: aarch64-unknown-linux-musl
os: ubuntu-latest os: ubuntu-latest
use-cross: true use-cross: true
cargo-flags: '' cargo-flags: ""
- target: aarch64-unknown-linux-gnu
os: ubuntu-24.04-arm
cargo-flags: ''
- target: aarch64-apple-darwin - target: aarch64-apple-darwin
os: macos-latest os: macos-latest
use-cross: true use-cross: true
cargo-flags: '' cargo-flags: ""
- target: aarch64-pc-windows-msvc - target: aarch64-pc-windows-msvc
os: windows-latest os: windows-latest
use-cross: true use-cross: true
cargo-flags: '' cargo-flags: ""
- target: x86_64-apple-darwin - target: x86_64-apple-darwin
os: macos-latest os: macos-latest
cargo-flags: '' cargo-flags: ""
- target: x86_64-pc-windows-msvc - target: x86_64-pc-windows-msvc
os: windows-latest os: windows-latest
cargo-flags: '' cargo-flags: ""
- target: x86_64-unknown-linux-musl - target: x86_64-unknown-linux-musl
os: ubuntu-latest os: ubuntu-latest
use-cross: true use-cross: true
cargo-flags: '' cargo-flags: ""
- target: x86_64-unknown-linux-gnu - target: x86_64-unknown-linux-gnu
os: ubuntu-latest os: ubuntu-latest
cargo-flags: '' cargo-flags: ""
steps: steps:
- name: Check if actor is repository owner - name: Check if actor is repository owner
@@ -256,7 +251,7 @@ jobs:
run: echo "BUILD_CMD=cross" >> $GITHUB_ENV run: echo "BUILD_CMD=cross" >> $GITHUB_ENV
- name: Install latest LLVM/Clang - name: Install latest LLVM/Clang
if: startsWith(matrix.os, 'ubuntu') if: matrix.os == 'ubuntu-latest'
run: | run: |
wget https://apt.llvm.org/llvm.sh wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh chmod +x llvm.sh
@@ -283,7 +278,7 @@ jobs:
- name: Verify file - name: Verify file
shell: bash shell: bash
run: | run: |
file target/${{ matrix.target }}/release/coyote file target/${{ matrix.target }}/release/loki
- name: Test - name: Test
if: matrix.target != 'aarch64-apple-darwin' && matrix.target != 'aarch64-pc-windows-msvc' if: matrix.target != 'aarch64-apple-darwin' && matrix.target != 'aarch64-pc-windows-msvc'
@@ -343,7 +338,7 @@ jobs:
${{ steps.package.outputs.archive }} ${{ steps.package.outputs.archive }}
${{ steps.package.outputs.sha }} ${{ steps.package.outputs.sha }}
tag_name: v${{ env.RELEASE_VERSION }} tag_name: v${{ env.RELEASE_VERSION }}
name: 'v${{ env.RELEASE_VERSION }}' name: "v${{ env.RELEASE_VERSION }}"
body_path: artifacts/changelog.md body_path: artifacts/changelog.md
prerelease: false prerelease: false
@@ -387,11 +382,11 @@ jobs:
shell: bash shell: bash
run: | run: |
# Set environment variables # Set environment variables
macos_sha="$(cat ./artifacts/coyote-x86_64-apple-darwin.sha256 | awk '{print $1}')" macos_sha="$(cat ./artifacts/loki-x86_64-apple-darwin.sha256 | awk '{print $1}')"
echo "MACOS_SHA=$macos_sha" >> $GITHUB_ENV echo "MACOS_SHA=$macos_sha" >> $GITHUB_ENV
macos_sha_arm="$(cat ./artifacts/coyote-aarch64-apple-darwin.sha256 | awk '{print $1}')" macos_sha_arm="$(cat ./artifacts/loki-aarch64-apple-darwin.sha256 | awk '{print $1}')"
echo "MACOS_SHA_ARM=$macos_sha_arm" >> $GITHUB_ENV echo "MACOS_SHA_ARM=$macos_sha_arm" >> $GITHUB_ENV
linux_sha="$(cat ./artifacts/coyote-x86_64-unknown-linux-musl.sha256 | awk '{print $1}')" linux_sha="$(cat ./artifacts/loki-x86_64-unknown-linux-musl.sha256 | awk '{print $1}')"
echo "LINUX_SHA=$linux_sha" >> $GITHUB_ENV echo "LINUX_SHA=$linux_sha" >> $GITHUB_ENV
release_version="$(cat ./artifacts/release-version)" release_version="$(cat ./artifacts/release-version)"
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
@@ -407,23 +402,23 @@ jobs:
if: env.ACT != 'true' if: env.ACT != 'true'
run: | run: |
# run packaging script # run packaging script
python "./deployment/homebrew/packager.py" ${{ env.RELEASE_VERSION }} "./deployment/homebrew/coyote.rb.template" "./coyote.rb" ${{ env.MACOS_SHA }} ${{ env.MACOS_SHA_ARM }} ${{ env.LINUX_SHA }} python "./deployment/homebrew/packager.py" ${{ env.RELEASE_VERSION }} "./deployment/homebrew/loki.rb.template" "./loki.rb" ${{ env.MACOS_SHA }} ${{ env.MACOS_SHA_ARM }} ${{ env.LINUX_SHA }}
- name: Push changes to Homebrew tap - name: Push changes to Homebrew tap
if: env.ACT != 'true' if: env.ACT != 'true'
env: env:
TOKEN: ${{ secrets.COYOTE_GITHUB_TOKEN }} TOKEN: ${{ secrets.LOKI_GITHUB_TOKEN }}
run: | run: |
# push to Git # push to Git
git config --global user.name "Dark-Alex-17" git config --global user.name "Dark-Alex-17"
git config --global user.email "alex.j.tusa@gmail.com" git config --global user.email "alex.j.tusa@gmail.com"
git clone https://Dark-Alex-17:${{ secrets.COYOTE_GITHUB_TOKEN }}@github.com/Dark-Alex-17/homebrew-coyote.git git clone https://Dark-Alex-17:${{ secrets.LOKI_GITHUB_TOKEN }}@github.com/Dark-Alex-17/homebrew-loki.git
rm homebrew-coyote/Formula/coyote.rb rm homebrew-loki/Formula/loki.rb
cp coyote.rb homebrew-coyote/Formula cp loki.rb homebrew-loki/Formula
cd homebrew-coyote cd homebrew-loki
git add . git add .
git diff-index --quiet HEAD || git commit -am "Update formula for Coyote release ${{ env.RELEASE_VERSION }}" git diff-index --quiet HEAD || git commit -am "Update formula for Loki release ${{ env.RELEASE_VERSION }}"
git push https://$TOKEN@github.com/Dark-Alex-17/homebrew-coyote.git git push https://$TOKEN@github.com/Dark-Alex-17/homebrew-loki.git
publish-crate: publish-crate:
needs: publish-github-release needs: publish-github-release
@@ -460,64 +455,4 @@ jobs:
- uses: katyo/publish-crates@v2 - uses: katyo/publish-crates@v2
if: env.ACT != 'true' if: env.ACT != 'true'
with: with:
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
publish-sandbox-image:
needs: [publish-github-release]
name: Publish Sandbox Docker Image
runs-on: ubuntu-latest
steps:
- name: Check if actor is repository owner
if: ${{ github.actor != github.repository_owner && env.ACT != 'true' }}
run: |
echo "You are not authorized to run this workflow."
exit 1
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Ensure repository is up-to-date
if: env.ACT != 'true'
run: |
git fetch --all
git pull
- name: Get release artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true
- name: Set version variable
run: |
version="$(cat artifacts/release-version)"
echo "version=$version" >> $GITHUB_ENV
- name: Validate release environment variables
run: |
echo "Release version: ${{ env.version }}"
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
if: env.ACT != 'true'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Push to Docker Hub
uses: docker/build-push-action@v5
with:
context: .
file: Dockerfile
platforms: linux/amd64,linux/arm64
push: ${{ env.ACT != 'true' }}
tags: darkalex17/coyote:latest, darkalex17/coyote:v${{ env.version }}
build-args: COYOTE_VERSION=${{ env.version }}
-90
View File
@@ -1,90 +0,0 @@
name: Update models
on:
schedule:
- cron: '0 */12 * * *'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: update-models
cancel-in-progress: false
defaults:
run:
shell: bash
jobs:
update:
name: Update models
runs-on: ubuntu-latest
env:
RUSTFLAGS: --deny warnings
steps:
- uses: actions/checkout@v4
with:
ssh-key: ${{ secrets.RELEASE_BOT_SSH_KEY }}
persist-credentials: true
- name: Install Rust Toolchain Components
uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Cache DuckDB Extensions
id: duckdb-extensions
uses: actions/cache@v4
with:
path: ~/.duckdb/extensions
key: duckdb-ext-ubuntu-latest-${{ hashFiles('Cargo.lock') }}
- name: Install DuckDB Extensions
if: steps.duckdb-extensions.outputs.cache-hit != 'true'
run: cargo test --all duckdb
- name: Write stub config
run: |
COYOTE_CONFIG_DIR="$(mktemp -d)"
printf 'clients:\n - type: openai\n - type: claude\n' > "$COYOTE_CONFIG_DIR/config.yaml"
echo "COYOTE_CONFIG_DIR=$COYOTE_CONFIG_DIR" >> "$GITHUB_ENV"
- name: Regenerate model catalog
run: cargo run --release -- --generate-models
- name: Detect catalog changes
id: diff
run: echo "changed=$(git diff --quiet models.yaml && echo false || echo true)" >> "$GITHUB_OUTPUT"
# The binary built by the regeneration step embedded the OLD models.yaml (the file
# was rewritten after that build), so rebuild before smoke-testing: cargo tracks
# include_str! inputs, making this a cheap incremental rebuild of the final crate.
- name: Rebuild with the refreshed catalog
if: steps.diff.outputs.changed == 'true'
run: cargo build --release
- name: Test
if: steps.diff.outputs.changed == 'true'
run: cargo test --all
- name: Smoke test the refreshed catalog
if: steps.diff.outputs.changed == 'true'
run: |
COUNT=$(./target/release/coyote --list-models | wc -l)
echo "Listed $COUNT chat models"
test "$COUNT" -ge 40
- name: Commit and push
if: steps.diff.outputs.changed == 'true'
run: |
git add models.yaml
git checkout -- .
git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
git commit -m 'chore: refresh generated model catalog [skip ci]'
git pull --rebase origin main
git push origin main
+1 -5
View File
@@ -3,9 +3,5 @@
/.env /.env
!cli/** !cli/**
.idea/ .idea/
/coyote.iml /loki.iml
/.idea/ /.idea/
.coyote/**
.sisyphus/**
.coyote-project.json
.coyote/memory/
+1
View File
@@ -0,0 +1 @@
{"type":"rust","build":"cargo build","test":"cargo test","check":"cargo check","_detected_by":"heuristic","_cached_at":"2026-04-13T13:36:33-06:00"}
+4 -817
View File
@@ -1,816 +1,3 @@
## v0.10.3 (2026-09-15)
### Feat
- create a config alias for default_reasoning_level (to match some wording and make the config flexible)
- support custom resource definitions for openai-compatible Oauth customization
- Support specifying the compression model to use when compressing a session for greater cost controls
- Added full cost tracking to Coyote and added it to the default right prompt
- added support for input/output token cache cost tracking
- Support persisting paused todo lists to sessions so users can exit and resume paused todo sessions; Also fixed a bug so todo list pausing is only supported when being used in a session since it typically requires multi-turn communication
- Support token caching to reduce LLM costs in Anthropic models (not typically handled automatically by APIs)
- **TASK-030**: callers escalate on GAUNTLET_REVIEW_INCOMPLETE instead of findings-routing
- **TASK-032**: review-gauntlet incomplete-review machine line + ×N attempt details
- **TASK-029**: review-gauntlet fault-only lane re-run loop
- **TASK-028**: escalation_timeout 0 waits indefinitely and is the default
- **graph**: stream llm-node completions so node timeout governs long generations
- **TASK-023**: deep-research hardening — fail-safe scripts, fault fallbacks, pipeline notes
- **TASK-020**: finding-verifier + step-runner fail-closed hardening
- **TASK-019**: code-reviewer fail-closed review pipeline
- **TASK-018**: review-gauntlet lane degradation + verification passthrough
- **TASK-017**: adversary execution evidence + fail-closed routing
- **TASK-016**: engine + test fixes from the Phase-1 re-review
- **TASK-011**: traverse agent fallback edges in map peer-discovery BFS
- **TASK-010**: add max_attempts to read-only agent lanes in bundled graphs
- **TASK-009**: add fallback routing to agent nodes
- **TASK-008**: add shared transient matcher and retry support to agent nodes
- Fixed graph agents with new agent variable passing functionality
- **graph**: warn when a declared variable is shadowed by an initial_state key
- **agents**: advertise declared agent variables in agent__list_available
- **agents**: allow passing agent variables to agent__spawn and agent__task_create
- seed variables into initial graph state
- Significant code review suite rewrite with improved graph agent features
- let install commands remove files that have been removed from bundles, both internal and external
- update the web_search_coyote tool to attempt to use the current model first then fallback to the configured model for web searches
- expose the current coyote model to tools
- removed temperature specification in deep-research and finding-verifier agents to make them more univerally usable
- **graph**: thread the graph abort into llm nodes and tool execution
- **graph**: abort a graph run on Ctrl-C from anywhere
- **graph**: warn when an llm node's turn cap is disabled
- **graph**: treat llm max_iterations: 0 as no turn cap
- **graph**: warn when the visit cap is disabled
- **graph**: treat max_loop_iterations: 0 as no visit cap
- **graph**: warn when a timeout is disabled
- **graph**: treat timeout: 0 as no wall-clock bound
- **graph**: validate agent node inputs at load time
- **graph**: seed a child graph agent's state from an agent node's inputs map
- **graph**: validate map branch subgraphs instead of single branch nodes
- **graph**: run map branches as per-item subgraph chains
- **graph**: accept a `{{template}}` for a map node's `max_concurrency`
- created the finding-verifier and whetstone agents for continuous code quality improvements
- teammates flag enabling peer messaging between concurrent graph agent nodes
- support explicit can_spawn_agents, max_concurrent_agents, and max_agent_depth keys in graph.yaml
- support setting the tool_timeout in the main config file instead of only via an environment variable
- Added new environment variable to force showing tool call logging when not in a tty
### Fix
- empty tool results can't cause errors in bedrock, claude, openai, or vertexai clients anymore
- proper support for reasoning/thinking in bedrock models
- **TASK-032**: enforce incomplete⇒BLOCKED structurally; anchor adversary degraded detection
- **TASK-028**: pin example-doc wording; make not-found escalation hint match queue semantics
- **TASK-028**: pin global-template exclusion, tighten timeout boundary test, reword stale hint
- **adversary**: red verification runs block CONFORMS; re-derive timeout envelope; robust runner
- **review-suite**: detect nested finding-verifier faults; harden render/fault markers
- **review-gauntlet**: summary-line critical count, nested-fault hook, fail-closed lane fallback
- **graph**: typed ApiStatusError transient classification; warn on bare output_schema
- **review-suite**: mark example verification commands as placeholders; pin variable description and prompt note
- **graph**: auto-merge only schema-declared keys from llm output
- **review-suite**: pass verification_commands as a declared variable, never LLM-extracted from prose
- **agents**: scrub GRAPH_STATE* from adversary verification commands; raise review-suite max_agent_depth
- **adversary**: narrow died-marker predicate, degraded banner for died criteria, hermetic script tests
- **adversary**: fail closed on PIPELINE-FAULT evidence regardless of status; pin bail-chain and validator shape
- **adversary**: fail closed per criterion when check_criterion dies
- **engine**: carry full error chain in llm node fault text
- **TASK-020**: skip POSIX bash script tests on Windows
- **TASK-020**: surface step-runner LLM-fault captures where the fallback lands
- **TASK-019**: carry PIPELINE-FAULT prefix through verdict.py's crash guard
- **TASK-018**: harden verdict_gate fault routing and widen default_lanes fallback
- **TASK-017**: bound run_checks runtime and fail-close its node-level death
- **tests**: collision-proof parser test temp files to deflake macOS CI
- Don't infinitely compress a session once it passes a threshold when the threshold is close to the max token count
- fix spinner ctrl-c early exit not clearing message and restoring cursor
- **graph**: build peer retirement guards before spawning their tasks
- **graph**: reject agents that ship both config.yaml and graph.yaml at load
- **graph**: bound concurrency caps by the semaphore permit limit
- **graph**: make turn-cap and template checks consistent across validation modes
- **graph**: leave output absent after state_updates when it was absent before
- **graph**: validate the whole max_concurrency template string
- **graph**: reject as == output_key on map nodes
- **graph**: inherit the session abort in agent-node children
- **graph**: retire the teammate identity before structured extraction
- **graph**: reject a literal max_concurrency of 0 instead of clamping it
- **graph**: clear peer identity fields after every map chain step
- **graph**: pick the map teammate identity name by BFS from the branch entry
- **graph**: name the offending node type when a map branch reaches a disallowed node
- **function**: build bundled bash tools without writing back to the source tree
- **agents**: name the map-branch output key in deep-research and finding-verifier
- **graph**: honor an explicit `output` key in state_updates
- Fixed the bad formatting in the finding-verifier agent
- properly anchored the role frontmatter regex so it doesn't catch in-instruction separators
- support non-object tool result outputs for bedrock
- exclude null bytes from fs_grep output on binaries
- suppress subagent error and warning output to stderr/stdout
### Refactor
- **graph**: keep the ordered visit log test-only
- **graph**: move the cfg(test) OUTPUT_KEY import into llm's test module
- **graph**: drop the duplicated locator prefix from map chain step errors
## v0.10.2 (2026-09-04)
### Feat
- added a clear line separator for the end of LLM turns to make it more clear where they are
- sort models by release date
- Improved the architect, explore, and sisyphus agents to have a team mode so they can all work in parallel as a team and updated the parallel-research skill to make explicit mention of this functionality
- support auto continuation in subagents and prevent them from calling todo__pause
- apply provider quirks to models created from a name
- refresh the model catalog from live API data at runtime
- generate the model catalog from live API data
- add model catalog source adapters and merge engine
- add hand-owned model quirks overlay
- apply provider quirks to models created from a name
- refresh the model catalog from live API data at runtime
- generate the model catalog from live API data
- add model catalog source adapters and merge engine
- add hand-owned model quirks overlay
- Improved the architect, explore, and sisyphus agents to have a team mode so they can all work in parallel as a team and updated the parallel-research skill to make explicit mention of this functionality
- added tab completions to --update-bundle flag
- added a dedicated config_dir output to coyote --info/.info
### Fix
- children subagents couldn't properly send messages to parent inboxes
- --generate-models parsed before vault initialization
- drive tool rounds iteratively instead of recursively to prevent stack overflow on long agentic turns to fix latent stack overflow recursion bug
- session replays now include mid-turn messages so the full session gets replayed
- resolve duplicate model ids by requested type
- let ctrl-c interrupt blocking job and agent collects
- skip terminal color-scheme queries in test builds
- memoize quirk glob patterns and isolate concurrent override writes
- memoize quirk glob patterns and isolate concurrent override writes
- children subagents couldn't properly send messages to parent inboxes
### Perf
- probe models-override staleness in the background refresh task
## v0.10.1 (2026-08-31)
### Fix
- make binary shims for custom tools cross-device compatible so users can't accidentally break sandboxes via `sbx cp ~/.config/coyote <sbx-name>:/home/agent/.config`
## v0.10.0 (2026-08-29)
### BREAKING CHANGE
- invocations using --prompt <text> must switch to
--temp-role <text>; clap rejects the old flag loudly.
### Feat
- Support Linux ARM64 GNU
- Note that duckdb RAG driver is unavailable on MUSL Linux builds when initializing a RAG so users know when using the binary, not just based on docs
- updated coyote installation scripts to detect when gnu is installable
- upgraded to v2 mixin schema
- improved first-time run experience and included a templated configuration file that now has comments like the config.example.yaml so users don't have to go to the repo to see all the knobs
- **TASK-005**: carry the quality bar through sisyphus and architect
- **TASK-004**: reviewer routing — code-reviewer quality-bar resolution, domain linter pass, surface-skill routing, rigor folding; file-reviewer surface-skill whitelist + convention/correctness marker
- **TASK-003**: add surface skills batch B — worker-review, iac-review, migration-review, cicd-review
- **TASK-002**: add surface skills batch A — rest-api-review, cli-review, library-review
- **TASK-001**: add rigor/surfaces declaration layer to planning skills
- **repl**: color .info mcp-server verdicts and complete only running servers
- **cli**: rename mcp_config asset category to mcp-config with snake_case alias
- create a new probe agent to probe code verifications using usage-pattern testing
- Add in the web_search_coyote tool where helpful
- **mcp**: add .info mcp-server, .set mcp_tools, and filtered-server listing to the REPL
- **mcp**: enforce per-server tool allowlists across runtime, jobs, agents, and graph nodes
- **mcp**: add mcp_tools allowlist config surfaces across roles, sessions, agents, graphs, and skills
- **mcp**: add per-server tool allowlist policy module and allowedTools config field
- support mcp.json in the root of bundle repos as well as in the legacy functions directory
- prefer ~/.config/coyote/mcp.json for the user-scope MCP config
- claude and openai native web search via web_search_coyote
- **jobs**: node-local job ownership and capability-gated job__* visibility
- **graph**: support max_concurrent_jobs at the graph level
- **jobs**: allow uncapped collect via full_result
- **jobs**: exempt polling tools from loop tracker and hint on unchanged checks
- **supervisor**: push agent completion notifications to the spawning context
- **jobs**: push background-job completion notifications via per-context queue
- inject background-jobs prompt guidance when jobs are enabled
- add background job runner, job__* handlers, and start gates
- generalize supervisor registry to TaskHandle enum with job scaffolding, kill discipline, and max_concurrent_jobs config
- run an advisory observability pass after implementation in sisyphus
- add an observability-review skill for post-implementation monitoring analysis
- check under- and over-logging in the code review gate
- calibrate logging registers in the code-writing agents
- add a logging-discipline skill for calibrating log output to repo conventions
- **cli**: rename --prompt to --temp-role
- complete --filter and --force on the first .install argument
- **mcp**: bound tool-result passthrough and surface resource audience annotations
- **mcp**: add mcp_prompt meta-tool and harden prompt display rendering
- **repl**: add .prompt command with live staged tab-completion
- **mcp**: add mcp_read meta-tool for resource reads
- **mcp**: gate meta-function emission on advertised server capabilities
- **mcp**: add render.rs content policy (text paging, pattern filter, blob spill)
- **mcp**: extend the server catalog to resources, templates, and prompts
- complete --filter and --force on the first .install argument
- run design interviews as grilling frontier rounds across the planning agents
- add a grilling skill for frontier-round design interviews
- add an on-demand architecture-reviewer agent for deepening scans
- add a codebase-design skill with the deep-module design vocabulary
- add a feedback-loop-first diagnosing-bugs skill to the coding suite
- add a Fowler code-smell baseline to the code-review skill
- flag duplicate helpers in code reviews with a repo-wide DRY check
- add a transactional-integrity review skill to the code review gate
- add an operational-history prior-art lane to the code-reviewer agent
- support name=value macro arguments with variable tab completion
- add --help guides to the .install and .uninstall REPL commands
- expand owner/repo shorthand for --install with a --git-host flag
- remove the deprecated --install-from flag and .install remote form
- rename install flags and unify .install dispatch
- add --uninstall and .uninstall for installed bundles
- add --update-bundle with provenance-aware conflict handling
- add --list-bundles and .list bundles with drift detection
- record bundle provenance when installing from remote repos
- add bundle provenance store
- parse bundle manifests and capture resolved SHAs for remote installs
- created a new comment-discipline skill for the built in sisyphus suite
- created a dedicated git_command tool to tighten tool calling permissions in the git-master skill
- Added a new security review step to the code writing quality gates
- addressed review comments
- add --no-workspace-macros opt-out for workspace macro loading
- execute non-isolated macros on the live REPL context
- surface macros as first-class custom commands in the REPL
- add lazy macro resolver with two-dir discovery and per-macro states
- add enabled_macros config field at global, role, agent, and session levels
- add description and isolated fields to Macro struct
- Dynamically detect RAG embedding model dimension for any given model
- Support auto confirmation for gatekeeper agents
- retry LLM API calls once after 401 by force-refreshing the OAuth token
- identity-aware rejected-token marker for LLM OAuth cache
- typed ApiStatusError carrying HTTP status through LLM client errors
- per-request OAuth token injection with mid-session refresh for HTTP MCP servers
- reason-specific warnings for MCP servers that fail OAuth at startup
- Installed duckdb into the coyote image
- Support managing MCP servers from the CLI directly
- append new built-in rag__query function to RAG contexts to allow further querying by LLMs
- support static file bundling with sbx-mixins
- **rag**: offer the storage driver when an agent initializes its RAG
- simplified the duckdb selection prompt
- **rag**: let several Coyote processes query one duckdb RAG at once
- **rag**: support string and UUID Qdrant point IDs
- **rag**: create the API key secret inline in the attach wizard
- let workflow rag nodes select a RAG driver
- improved wording and heuristic detection for sisyphus suite of agents
- upgraded to sbx kit v2 spec for improved integration
- **rag**: add attach-only Qdrant provider, attach wizard and sandbox wiring
- **rag**: add DuckDB provider behind the RAG driver abstraction
- **rag**: add driver/attached fields, validation floors and force-reingest
- **mcp**: send RFC 8707 resource indicator in OAuth flows
- Added loaded indicators to .list tools/mcp-servers/skills
- hide .recover from tab completions when no session is active
- add a new .recover command for sessions to recover from errors
- integrated the git-ssh-sign kit into the coyote sandbox kit
### Fix
- Harden install scripts: detect libssl3 without ldconfig on PATH, survive noexec tmp dirs, and guard against partial curl|bash execution
- ldconfig on Debian-based distros lives in /usr/sbin
- Prefer GNU linux builds in install scripts for duckdb support, and gate duckdb support on linux hosts that are MUSL until MUSL support is added
- **mcp**: omit declared-but-empty prompt/resource capabilities from .info mcp-server
- **mcp**: annotate declared-but-empty prompt/resource capabilities in .info mcp-server
- also support the .continue edge case for session crashing checkpointing
- checkpoint sessions that crash for easy resuming
- job__check ring buffer also collects file output for LLM_OUTPUT as well as stdout
- **tools**: interactive-shell semantics and stderr capture in execute_command
- **function**: gate test-only declaration appender behind cfg(test)
- **graph**: gate unix-only test imports behind cfg(unix)
- harden job runner lifecycle and whitelist conformance
- **function**: floor tool-output truncation cut to a UTF-8 char boundary
- **supervisor**: make agent__check a pure status probe that never consumes the handle
- **supervisor**: surface finished-but-uncollected tasks in turn-end guardrail
- fix grep "binary" errors when searching UTF-8 files with unicode characters like some of the Coyote source
- **bundles**: harden the install pipeline for cross-platform correctness
- **mcp**: harden the spill path for cross-platform correctness
- **repl**: offer prompts in .list tab completion and rename its listing helpers
- **mcp**: gate unix-only spill permission APIs for windows builds
- harden the bundle lifecycle per code review
- address code review findings on the bundle lifecycle
- reserve category names, confirm fork-name collisions, report secrets on uninstall
- make bundle provenance portable to Windows
- user__ask should have been renamed to user__select in graph agent user interaction invocations
- support tab completions for graph-based agents with variables as well as standard agents
- surface macro parse errors on top-level invocation
- render .list macros as a comfy-table instead of fixed-width columns
- drain crossterm characters in zellij in kitty contexts to prevent DA1 responses from entering prompt
- drain tty input when displaying inquire prompts to prevent unintentional escapes
- breaking iwe MCP server changes with newest version
- improved handling of non service-specific secrets using sbx custom-secrets
- include tool output to LLM_OUTPUT in errors as well as stderr
- prevent tmp-overwriting
- Corrected a rare edge case on how tool files are generated during parallel executions
- cosmetic fix after improved bash tool handling
- Improved subagent escalation handling
- latent parsing bugs in fs_patch and argc
- Prevent infinite hangs in coder agent and implement timeouts for LLM API calls and interactive tools
- allow nested italics inside bold spans in markdown renderer
- properly handle OAuth refreshes
- correct newline removal from fs_write and fs_patch
- detect duplicate tool call IDs client-side before sending to Claude
- **rag**: warn when a duckdb store is empty but files are indexed
- **rag**: keep a local Qdrant off an ambient proxy
- keep loopback and LAN traffic off an ambient proxy
- **rag**: stop routing Qdrant requests through an ambient proxy
- **rag**: treat a zero min_score as no floor on Qdrant searches
- **rag**: address Copilot review findings on the driver abstraction
- **rag**: delete the DuckDB write-ahead log alongside the store
- **sandbox**: discover agent-scoped RAG mixin sidecars
- **rag**: stop the attach wizard from silently accepting an empty collection
- **rag**: fail loudly when a RAG's vault secret is missing
- **rag**: serialize DuckDB extension installs to stop a Windows race
- **rag**: emit an sbx kit v2 mixin and declare RAG credentials to the proxy
- **rag**: install DuckDB vss and fts extensions when they are missing
- don't output thinking blocks for claude-based models
- additional edge case fix for duplicate tool call IDs in anthropic API calls
- removed temperature modifier in librarian agent to mitigate invisible errors
- strip reasoning blocks for structured LLM output in graph agents
- prevent rare duplicate tool call IDs in long running claude prompts
- agents inherit global reasoning effort if unset
### Refactor
- **function**: finish supervisor-to-agent vocabulary migration
- **function**: rename agent-tool symbols out of supervisor vocabulary
- Modified the naming of several generalized supervisor values
- pulled out some imports to clean up the MCP render module a bit
- **mcp**: centralize meta-function prefix predicates and fix list_tools pagination
- Refactored some bundle const locations
- drop the .install remote migration hint
- drop the --install-from tombstone entirely
- render .list agents and .list skills as comfy-tables
- **rag**: discover driver_config secrets by grammar, not field name
- **rag**: drop the hardcoded embedding model hint from attach
- **rag**: interpolate every driver_config value, not just api_key
- **rag**: extract RagProvider trait and add YamlProvider
## v0.8.3 (2026-08-03)
### Fix
- infinite loop bug when attempting to interrupt a prompt exchange right before a session compression
- ctrl-c inside of an auto-continue loop created an infinite loop
## v0.8.2 (2026-07-31)
### Fix
- sbx update doesn't allow undefined fields in sbx spec
## v0.8.1 (2026-07-30)
### Feat
- ctrl-c interrupts ongoing prompt in a session, but lets the user inject more instructions mid-stream
- improved function calling performance by allowing parallel tool calling
- created the architect and gatekeeper agents for dramatically improved coding performance
- Improved readability of session message exchange replays
- apply --agent/--role/--rag/--model flags in --acp-server mode
- add headless profile to sbx-kit spec
- implement ACP user-interaction to request_permission bridge
- implement ACP session/load and session/cancel
- implement ACP session/prompt
- add ACP server skeleton with stdout-purity test
- add --headless flag for unattended operation
### Fix
- ctrl-c interruption doesn't discard session messages when throbber is showing
- improper handling of fd-style globbing for directories in fs_glob
- properly templated architect design doc path in starter commands
- .copy works when sessions are resumed
- ACP session/prompt now drives the full tool-execution loop
- ACP spec conformance — ContentBlock prompt params and protocolVersion type
- restore stdout output for standalone --headless mode
- suppress tool-call display in headless mode; initialize session on session/new
- skip stdin drain and set silent render mode when --acp-server is active
- include graph-agent descriptions in .agent <TAB> completions
### Refactor
- move ACP server dispatch into run() for shared flag setup
## v0.8.0 (2026-07-25)
### Feat
- Dynamically detect if a selected client in the sandbox first run wizard supports oauth
- Add support for the --fresh flag again with host environment configuration injection
- force overwrite global sbx secrets
- add sbx secrets globally
- only create secrets local to a sandbox
- Improved credentials management for docker sandboxes
- renamed --contents arg for fs_write/patch to --content since most models attempt that first and error otherwise
- Used improved theme selections for tool call highlighting colors
- Improved theme-derived syntax highlighting for LLM tool call logging
- Improved coloring of LLM tool invocation outputs to make LLM output more readable and cohesive
- renamed the user__ask to user__select and improved descriptions to improve model usage
- Improved coloring/highlighting of tool calls to make LLM invocation logs easier to read
- long-running session improvements
- Made sisyphus suite of agents all auto-approved for tool usage
- added ast_grep tool to Sisyphus suite agents
- created the adversay agent and adversarial-review skill
- new spawnable_agents field in agents to let users restrict what agents can be spawned by a parent agent
- replay pre-compressed messages as well when resuming sessions for users to see
- created a new builtin function for agents who can spawn other agents to list available agents via agent__list_available
- **render**: hanging-indent line wrapping for lists and blockquotes
- **render**: wire table state machine and finalize hook
- **render**: render markdown tables with comfy-table
- **render**: parse table cells and column alignments
- **render**: detect markdown table rows and separators
- **render**: add comfy-table dependency and table border style
- **render**: activate rich markdown renderer as default
- **render**: rich block-level markdown rendering (headings, quotes, lists, hr)
- **render**: rich inline markdown rendering (bold, italic, code, links)
- **render**: detect markdown block-level line types
- **render**: precompute markdown scope styles for rich rendering
- Created the raw_markdown configuration flag
- added a .fork command to fork a new session from a running conversation
- **oauth**: enable browser-paste PKCE flow for OpenAI-compatible providers
- copy host OAuth tokens into sandbox at launch
- implement OAuth 2.0 Device Authorization Grant (RFC 8628)
- hint that browser 'paste code' pages can be ignored during callback capture
- openai-compatible wizard offers OAuth when provider has bundled oauth defaults
- validate unique client names at config load
- bundle xAI OAuth defaults in models.yaml
- OAuth branch in openai_compatible prepare_* fns
- get_oauth_provider_for_client dispatcher + client_config_info update
- OpenAICompatibleOAuthProvider (config-driven OAuthProvider impl)
- add auth + oauth fields to OpenAICompatibleConfig
- add oauth field to ProviderModels
- add client_credentials support to prepare_oauth_access_token
- add OAuthConfig + OAuthFlow types to oauth.rs
- Added reasoning effort to the right prompt
- Improved support for Anthropic's extended thinking
- Also support GEMINI.md workspace instructions
- Improved workspace instructions support
- also detect .mcp.json configurations at workspace roots
- Created new .tool enable/disable and .mcp enable/disable aliases to make REPL usage more egonomic
- Created a new .list <kind> REPL command to make discoveribility easier in the REPL
- Support claude-style hidden workspace MCP configuration files via .mcp.json
- Allow users to customize the workspace-specific configuration directory name so they can use Coyote with other CLI clients like .claude
- Add reasoning_effort validation for the main configuration file
- Add validation for reasoning_effort settings to prevent users from specifying erroneous values
- Added support for modifying the reasoning effort of reasoning models
- Explicitly Prevent .undo usage in graph agents
- Added an .undo command to the REPL to let users have more control over the conversation
- Improve sandbox startup time by using the prebuilt Coyote image
- Make coyote available as a docker image
- Made fs_patch more flexible for different model preferences of patch formats
- Installed nano into the sandbox so that users can edit config files in the sandbox directly
- Support workspace-local skill definitions and MCP configurations
- fully functional graph-based RAG
- Implemented graph-based RAG
- Added a --dangerously-skip-permissions flag to skip permission prompts for tool invocations
- Remove the temperature hyperparameter from the diagnose role
- Added a new oauth.redirectHost field to make it possible to further extend MCP support
- Updated the REPL mcp auth path to use the prettified error messaging
- Improved error messaging for failed MCP starts because of auth issues
- Added support for specifying the oauth port and client ID in MCP server configs
- Implemented OAuth support for OpenAI models via Codex endpoints
- merge MCP config when installing bundled mcp config
- Implemented durable state for sisyphus
- Installed ast-grep for the explore agent to use for better code exploration
- Created the step-runner graph agent for more deterministic coding workflows to produce even more reliable and higher-quality results
- Improved oracle and sisyphus agents with skill integrations for the new skills
- Created new sisyphus family skills to improve performance
- Created new diagnostic role and skill for use in other contexts
- Added new memory functions for deleting and renaming memory files, as well as new lints for memory expiration dates and staleness of memories to improve the memory system
- Created a new iwe skill and installed the iwe MCP server for utilizing large knowledgebases
- Session-specific, file-backed history in the REPL
- Replay session output when a user re-enters a session so all output can be seen again
- Added confirmation message after MCP Oauth succeeds when invoked from --auth-mcp
- Created the --auth-mcp CLI flag to let users auth with remote MCP servers without needing to be in the REPL
- add OAuth authentication support for remote MCP servers
- Added mixin for sisyphus so the ddg MCP server can search arbitrary domains
- added improved error messaging on MCP server initialization
- prefer musl versions for linux when running --update/.update
### Fix
- fresh wizard openai-compatible support
- config existence check for --fresh sandboxes
- Improve coyote sandbox startup time
- bypass forgotten sandbox mode check for MCP secret interpolation
- properly wrap sub-style changes in markdown rendering
- removed accidental duplicate ast_grep tool in explore agent
- npm and npx need the /usr/local/share/npm-global/lib directory to exist to run properly so I've added it to the dockerfile
- added executable bit to adversary agent tools script
- fetch descriptions from graph agent configs as well when listing agents
- **render**: suppress blank lines above rendered table
- chown the full sandbox cache dir, not just the coyote subdir
- chown the whole coyote cache dir not just the oauth dir in the sandbox
- fix typo in Gemini's generation_config property to use camelCase exclusively
- **oauth**: treat missing expires_in as non-expiring device_code token
- **oauth**: send Accept: application/json in device flow requests
- OAuth callback listener skips speculative/malformed browser connections
- resolve reasoning effort for the prompt for global defaults as well
- Account for default model reasoning_effort when supplying that value for the REPL prompts
- model narration included in history and between tool calls to prevent repetition
- Don't terminate agent loops early for null tool output
- reduce code duplication by reusing the new concrete_tool_names function in .list tools
- Agent tools can only be modified via .tool enable/disable using tools in the allowed whitelist in the agent
- re-render agent sessions when entering agents with either pre-configured agent_session or when entering an agent directly into a session
- Per RFC 9728, enable dynamic discovery of OAuth endpoints in MCP using path-aware discovery
- hot-attach to MCP servers that require auth after running .mcp auth <name>
- Correctly inherit graph-global model for extractor model if none is defined
- no cursor timeout when user scrolls away from ongoing streaming output
- default to the nano or notepad when a configured editor is not found
- When EDITOR, VISUAL, or config.editor is defined, don't verify via which
- Added a loop exit condition for the diagnostics skill
- Added directness clause to the diganose role to improve prompt
- fs tools now output better error handling to guide the model more effectively
- Make fs_read more tolerant of various arg invocation formats.
- todo functions are injected properly to roles when roles have auto_continue: true and the REPL is started directly into the role
- updated the redirect URI for OAuth MCP to use localhost since that's what is whitelisted, not 127.0.0.1
- allow MCP OAuth refresh_token to be absent from initial token exchanges
- Overrode the default JSON content-type for MCP OAuth so its properly application/x-www-form-urlencoded
- typo in mcp file name
- Added uvx wrapper for macos-based sandboxes
### Refactor
- Standardized paths module function names to not use 'path' in the name and to just always be either 'dir' or 'file'
- main.rs resolve_oauth_client uses new dispatcher
- split run_oauth_flow into pkce + client_credentials dispatchers
### Perf
- updated the memory injection warning so it only logs once, rather than after each keystroke
## v0.7.4 (2026-07-02)
### Feat
- Pin specific usql version to sbx kit
- recursively take ownership over the copied in coyote config for the sbx
- explicitly specify the COYOTE_CONFIG_DIR in the sbx kit
- --tail-logs can track log rollovers and incoporates a sleep timer to minimize idle CPU cycles
- Added support for log rolling so log files don't just blow up over time
### Fix
- Added back in --kit specification for the running of the sbx
- sbx isn't copying base files in their respective directories
- Update deprecated sbx kit config
- Properly chown the coyote config recursively and password file in the sbx
## v0.7.3 (2026-06-24)
### Fix
- apply bootstrapping of functions at startup to fix edge case
## v0.7.2 (2026-06-19)
### Fix
- usql version upgrade
## v0.7.1 (2026-06-19)
### Fix
- sbx mixins must be passed in directories, not as files and the files must be named spec.yaml per new sbx version
## v0.7.0 (2026-06-18)
### Feat
- added configurable cache path via the COYOTE_CACHE_PATH environment variable
- added a memory option to .set tab completions
- Added a diagnostic .info tools subcommand to make it easier to see what tools are enabled in all contexts
- Added additional info outputs for enabled skills and sbx directories
- directly execute shell commands from within the REPL
- created mixin kit for built-in functions and MCP servers
- Added sbx mixins for the secrets providers so users can also bootstrap those as well.
- added support for loading sbx mixins that are dynamically discovered in the users workspace and config directory
- Added a --fresh flag to let users create a truly bare bones sandbox without bootstrapping their config
- initial built-in sandboxing support powered by Docker sbx
- Added the ability to auto-bootstrap workspace memory when in git repos
- Added explicit guardrail handling for pending agents
- auto-append memory to memory index and don't necessarily require the LLM to remember to do it after a write
- Added an --init-memory [global|workspace] flag to easily and quickly enable memory
- added memory global configuration settings to the output of --info and .info
- added .set memory REPL commands to control memory injection and applied formatting
- Create the built-in memory management tools
- Append the memory system prompts (readonly or r/w) to the system prompt when applicable
- Created the --no-memory CLI flag to disable memory for this invocation
- Added the memory configuration properties and storage to the main app config, roles, sessions, and agents.
- initial scaffolding of a memory system
### Fix
- rebuild the tool scope after dynamically updating the skills_enabled value in the REPL
- properly resolve Windows-based local vault password file locations and bootstrap them into the sandbox when possible
- auto-translation of user-prefixed Mac and Linux paths for the vault password file when running inside a sandbox
- don't attempt to auto complete .vault list in the REPL; that's the end of the command
- buffer tool stdout as well as stderr so that any tools that error to stdout are captured and included in the response to the model, enabling the model to see what went wrong and to reason about how to fix it.
- auto-bootstrapped memory was accidentally putting the MEMORY.md directly in the repo root rather than .coyote/memory/MEMORY.md
- improved the fs_patch script description and added improved error handling to it.
- added in forgotten require_max_tokens to the fable model
- append memory functions to non-graph based agents on init
- when auto_continue is disabled via the .set auto_continue false command, it should strip the todo functions from the list of functions
- use rawPredict for non-streaming Claude requests
### Refactor
- Migrated the .skills command completion to use StateFlags and updated the help messages
## v0.6.0 (2026-06-05)
### Feat
- added skill hint prompt injection and configuration
- Fallthrough on missing secrets during mcp.json merging
- validate visible_skills field at config load time
- implemented reflexion (sorta) in sisyphus for significant code changes to delegate to the code-reviewer agent
- improved explore agent
- removed conditional fallback of LLM_*_RAW_JSON from built-ins
- updated enabled_skills handling to support both list and comma-separated strings
- added new REPL set commands for toggling skills and changing what skills are enabled
- upgraded to the latest version of mcp-remote
- fs_grep now works with both files and directories
- improved code reviewer agents with skills
- added round trip validation for vault providers to ensure permissions and authentication
- created new first-time run wizard for secrets provider
- vault_password_file or nothing at all is shorthand for just using the local gman provider for secret management
- refactored gman usage to be generic and work with various vault providers and use the SupportedProvider enum directly for configurations
- created initial parity gman generalization for vault provider
- Refactored the sisyhpus agent system to utilize the new skills system to improve performance and reliability
- llm graph nodes support skills
- updated sisyphus and coder tools
- removed potentially confusing tab completions for .skill
- .edit skill <name> support from within the REPL
- Added skills_dir to the info output of Coyote
- Created a few auto built-in skills
- Added support for auto_unload skills during chat
- cleaned up skill implementation
- support multiple skill flags to load multiple skills at CLI startup
- Modified --skill CLI to allow users to specify skills to start the REPL or CLI with.
- added CLI --skill flag for modifying skills easily
- REPL integration with skills
- dynamic loading/unloading of skill tools and MCP servers whenever load_skill/unload_skill are invoked
- created built-in functions for listing, loading, and unloading skills
- implemented the skills policy to track available skills per context
- added remote install and install support for skills
- created the skill registry
- decided to make skills persist to disk like agents and not in-memory like built-in roles
- scaffold skill module
### Fix
- disable skills for specific built-in roles
- redirect stderr into user's /dev/tty for guards
- azure doesn't support underscores in key vault
- accidental regression on enabled_skills being empty = all
- greedy secrets regex caused multiple secrets on one line to fail
- add agent context check to skill visibility validation
- enforced global visible_skills in llm node validation and improved skill loading error handling across the project
- restore agent skill policy on error during effective policy calculation
- apply the same validation for skill filenames on list_skills as happens everywhere else
- the vault's init_bare should try to load the provisioned secret_provider from the config file without also interpolating any of the rest of the configuration file. It should only fail if the user has not yet created a configuration file; i.e. done a first-time run.
- the vault roundtrip test used characters that are unsupported by some major secrets providers
- fixed tool filtering logic for skills and user functions in agents
- privilege leak when unloading skills and leaving tool scope untouched
- When bootstrapping an app config to interpolate secrets, clone the secrets provider configuration as well so config secrets stored in remote vaults can be used properly
- forgot to move back up the vault probe value error to be before the delete
- don't silently fail on skill role composition extraction in llm nodes
- set -euo pipefail for the temp script in execute_command.sh tool
- added forgotten skill name validation to has_skill to prevent side-channel attacks
- use unique values for the secrets round trip verification
- stop interpolating a line if any errors occur
- added path validation for skill names
- effective_policy unconditionally overwrote skill values for role-like structs
- updated execute_command to not mangle heredocs and also added explicit instructions to the coder and sisyphus agents to use fs_write and fs_patch over execute_command when writing files
- llm nodes accidentally skipped skill_registry::effective_role because I was passing an inline role instead
- updated temperature values for all agents and roles
- added back in require_max_tokens for new Claude models
- skill support also requires function calling to be enabled
- non_tty tests break on some TTY terminals
- skill loading on agents
- forgot to bootstrap skills on REPL startup
- remove now deprecated .skill edit command
### Refactor
- removed redundant skill name validation from has_skill function
- support both CSV and list formats for enabled_tools
- Support both CSV and list formats for enabled_mcp_servers
## v0.5.0 (2026-05-27)
### Feat
- rename Loki to Coyote
### Fix
- bash-based user interactions in agents accidentally regressed in graph implementation
- Claude function calling in agent contexts
- Claude code rate limit error per new Claude changes
## v0.4.0 (2026-05-23)
### Feat
- LLM node failures propgate up
- Added .install remote tab completions to the REPL
- feature complete install remote with category selection
- Support to interactively add secrets to Coyote that are missing from MCP configs when merging
- Added MCP config merging support for remote asset installations
- install remote now writes files to disk
- Created basic install_remote functions
- Created a more comprehensive and immediately useful default config for first runs
- Created an example graph-based agent called deep-research
- Improved coder agent that is now a graph-based agent
- Removed indicatif spinners. The UX just won't stop clobbering for parallel graph nodes
- Added agent variables support for graph agents and improved script executor to use the same environment variables as normal agent tool calling for further flexibility
- Improved UX with colored spinners for parallel graph agents and no clobbering outputs for sub-agents
- created new graph-based deep-research agent
- improved UX for parallel graph execution
- added branch progress tracker for better visualization of parallel graph super-steps
- Removed the jira-helper agent and replaced it with the atlassian role
- created the RenderMode enum to suppress stdout streaming during parallel graph super-steps
- Full support for map node types
- implemented the frontier-based scheduling for the graph executor with simplified state management (gotta love .clone)
- validation support for parallel graph execution; restricted map nodes to only run for nodes without next targets and not supporting chained map nodes
- created the staging area for state merges per super-step and created the built-in reducers (and their application) for the state merge phase of a super step
- scaffolding work for fan-out nodes for parallel branch execution support and stubbed out Map node types
- Coyote can now update itself via .update and --update commands
- added a .edit command for editing the MCP configuration file
- Created a new .install command to install bundled assets on-demand
- migrated llm node validation to graph loading time instead of graph runtime
- ripped out user input timeout scaffolding for approval and input node types; implementation can't be done cleanly
- added additional support for all RAG-configuration fields in RAG nodes
- initial support for RAG nodes in the graph execution system
- implemented structured logging for graph execution
- merged normal agent config and graph agent configs into one file (either/or)
- added structured-output extraction for llm and agent nodes
- created full llm node runtime implementation
- scaffolded together the initial llm node type and its executor
- wired together graph execution and agent graph dispatch
- implemented support for the graph executor
- created the approval node executor and the input node executor for user interaction
- Added initial support for native Coyote agent nodes in the graph-based agent system
- Added direct script invocation support for graph-based agents
- Added graph validation
- Implemented state management for agent graphs
- initial agent graph scaffolding
- add auto-continue support to all contexts
- dynamic tab completions now show the sessions for a given agent instead of only listing global sessions
- legacy SSE support for MCP server configurations
- support http/sse transport types for MCP server configurations so it fully supports claude desktop-style MCP configs
- 99% complete migration to new state structs to get away from God-Config struct; i.e. AppConfig, AppState, and RequestContext
- Automatic runtime customization using shebangs
- Created a demo TypeScript tool and a get_current_weather function in TypeScript
- Updated the Python demo tool to show all possible parameter types and variations
- Added TypeScript tool support using the refactored common ScriptedLanguage trait
### Fix
- Generified the functions usage of script detection for an executable bit on unix systems
- merge required claude code system prompt into instructions
- updated argc argument passing in run-tool and run-agent scripts
- Added additional graph validation for parallel reads and writes with dependencies between nodes states
- bug in next_single method and improved outcome handling for LLM node execution
- inline RAG bug when globbing files by extension without subdirectory globbing
- update the estimate_token_length function to use the standard word count method
- removed unnecessary regenerate logic for sessions and use the same logic for all contexts; prevents a panic on empty message list
- error when users try to start a session on a graph agent
- added on_other field for approval nodes so users can specify an alternative free-text target when none of the options match what they want
- accidentally added back in full agent tools on LLM nodes
- Improve the coder agent's usage of tools
- make the agent__collect escalation-aware so it doesn't freeze on sub-agent escalations
- check for an existing session before starting up MCP servers when switching to a role
- do not switch to agent if a session is active.
- Do not append todo instructions when function calling is disabled
- a bug in the dynamic completions because the crate name is coyote-ai but the binary is named coyote
- bug found by copilot that would create a lock on the PollSender for sse-based MCP servers
- Accidental shadow of temp_file function for Windows function calling
- upgraded to newer rmcp version to get native-tls support
- RagCache was not being used for agent and sub-agent instantiation
- TypeScript function args were being passed as objects rather than direct parameters
- Added in forgotten wrapper scripts for TypeScript tools
- don't shadow variables in binary path handling for Windows
- Tool call improvements for Windows systems
### Refactor
- migrated llm nodes to use Roles to simplify instructions handling and to function like inline roles
- migrated the next_node and apply_state_updates logic for LLM nodes into the LlmExecutor
- fully complete state re-architecting
- Fully ripped out the god Config struct
- Deprecated old Config struct initialization logic
- migrate functions and MCP servers to AppConfig
- Migrate the vault/bare_init logic
- created a single install_builtins free function to remove from Config::init
- partial migration to init in AppConfig
- Extracted common Python parser logic into a common.rs module
- python tools now use tree-sitter queries instead of AST
## v0.3.0 (2026-04-02) ## v0.3.0 (2026-04-02)
### Feat ### Feat
@@ -834,7 +21,7 @@
- Created a CodeRabbit-style code-reviewer agent - Created a CodeRabbit-style code-reviewer agent
- Added configuration option in agents to indicate the timeout for user input before proceeding (defaults to 5 minutes) - Added configuration option in agents to indicate the timeout for user input before proceeding (defaults to 5 minutes)
- Added support for sub-agents to escalate user interaction requests from any depth to the parent agents for user interactions - Added support for sub-agents to escalate user interaction requests from any depth to the parent agents for user interactions
- built-in user interaction tools to remove the need for the list/confirm/etc prompts in prompt tools and to enhance user interactions in Coyote - built-in user interaction tools to remove the need for the list/confirm/etc prompts in prompt tools and to enhance user interactions in Loki
- Experimental update to sisyphus to use the new parallel agent spawning system - Experimental update to sisyphus to use the new parallel agent spawning system
- Added an agent configuration property that allows auto-injecting sub-agent spawning instructions (when using the built-in sub-agent spawning system) - Added an agent configuration property that allows auto-injecting sub-agent spawning instructions (when using the built-in sub-agent spawning system)
- Auto-dispatch support of sub-agents and support for the teammate pattern between subagents - Auto-dispatch support of sub-agents and support for the teammate pattern between subagents
@@ -888,7 +75,7 @@
- Simplified sisyphus prompt to improve functionality - Simplified sisyphus prompt to improve functionality
- Supported the injection of RAG sources into the prompt, not just via the `.sources rag` command in the REPL so models can directly reference the documents that supported their responses - Supported the injection of RAG sources into the prompt, not just via the `.sources rag` command in the REPL so models can directly reference the documents that supported their responses
- Created the Sisyphus agent to make Coyote function like Claude Code, Gemini, Codex, etc. - Created the Sisyphus agent to make Loki function like Claude Code, Gemini, Codex, etc.
- Created the Oracle agent to handle high-level architectural decisions and design questions about a given codebase - Created the Oracle agent to handle high-level architectural decisions and design questions about a given codebase
- Updated the coder agent to be much more task-focused and to be delegated to by Sisyphus - Updated the coder agent to be much more task-focused and to be delegated to by Sisyphus
- Created the explore agent for exploring codebases to help answer questions - Created the explore agent for exploring codebases to help answer questions
@@ -948,8 +135,8 @@
- Support for secret injection into the global config file (API keys, for example) - Support for secret injection into the global config file (API keys, for example)
- Improved MCP handling toggle handling - Improved MCP handling toggle handling
- Secret injection into the MCP configuration - Secret injection into the MCP configuration
- added REPL support for interacting with the Coyote vault - added REPL support for interacting with the Loki vault
- Integrated gman with Coyote to create a vault and added flags to configure the Coyote vault - Integrated gman with Loki to create a vault and added flags to configure the Loki vault
- Added a default session to the jira helper to make interaction more natural - Added a default session to the jira helper to make interaction more natural
- Created the repo-analyzer role - Created the repo-analyzer role
- Created the coder and sql agents - Created the coder and sql agents
+4 -5
View File
@@ -2,7 +2,7 @@
Contributors are very welcome! **No contribution is too small and all contributions are valued.** Contributors are very welcome! **No contribution is too small and all contributions are valued.**
## Rust ## Rust
You'll need to have the stable Rust toolchain installed in order to develop Coyote. You'll need to have the stable Rust toolchain installed in order to develop Loki.
The Rust toolchain (stable) can be installed via rustup using the following command: The Rust toolchain (stable) can be installed via rustup using the following command:
@@ -78,12 +78,11 @@ act -W .github/workflows/release.yml --input_type bump=minor
## Authorship Policy ## Authorship Policy
All code in this repository is written and reviewed by humans, or AI-generated where explicitly disclosed. All code in this repository is written and reviewed by humans. AI-generated code (e.g., Copilot, ChatGPT,
AI-generated code (e.g., Copilot, ChatGPT, Claude, etc.) is permitted only when explicitly disclosed and Claude, etc.) is not permitted unless explicitly disclosed and approved.
approved. For example: disclosing the use of AI-generated code in a PR description.
Submissions must certify that the contributor understands and can maintain the code they submit. Submissions must certify that the contributor understands and can maintain the code they submit.
## Questions? Reach out to me! ## Questions? Reach out to me!
If you encounter any questions while developing Coyote, please don't hesitate to reach out to me at If you encounter any questions while developing Loki, please don't hesitate to reach out to me at
alex.j.tusa@gmail.com. I'm happy to help contributors in any way I can, regardless of if they're new or experienced! alex.j.tusa@gmail.com. I'm happy to help contributors in any way I can, regardless of if they're new or experienced!
+7 -27
View File
@@ -1,30 +1,19 @@
# Credits # Credits
## Matt Pocock's Skills
The bundled `diagnosing-bugs`, `codebase-design`, and `grilling` skills, the
`architecture-reviewer` agent, and the code smell baseline in the bundled
`code-review` skill are adapted from
[mattpocock/skills](https://github.com/mattpocock/skills) by Matt Pocock,
licensed under the MIT License. The smell definitions trace back to Martin
Fowler's *Refactoring* (ch. 3); the deep-module vocabulary builds on John
Ousterhout's *A Philosophy of Software Design* and Michael Feathers'
*Working Effectively with Legacy Code*.
## AIChat ## AIChat
Coyote originally started as a fork of the fantastic Loki originally started as a fork of the fantastic
[AIChat CLI](https://github.com/sigoden/aichat). The initial goal was simply [AIChat CLI](https://github.com/sigoden/aichat). The initial goal was simply
to fix a bug in how MCP servers worked with AIChat, allowing different MCP to fix a bug in how MCP servers worked with AIChat, allowing different MCP
servers to be specified per agent. Since then, Coyote has evolved far beyond servers to be specified per agent. Since then, Loki has evolved far beyond
its original scope and grown into a passion project with a life of its own. its original scope and grown into a passion project with a life of its own.
Today, Coyote includes first-class MCP server support (for both local and remote Today, Loki includes first-class MCP server support (for both local and remote
servers), a built-in vault for interpolating secrets in configuration files, servers), a built-in vault for interpolating secrets in configuration files,
built-in agents and macros, dynamic tab completions, integrated custom built-in agents and macros, dynamic tab completions, integrated custom
functions (no external `argc` dependency), improved documentation, and much functions (no external `argc` dependency), improved documentation, and much
more with many more ideas planned for the future. more with many more ideas planned for the future.
Coyote is now developed and maintained as an independent project. Full credit Loki is now developed and maintained as an independent project. Full credit
for the original foundation goes to the developers of the wonderful for the original foundation goes to the developers of the wonderful
AIChat project. AIChat project.
@@ -32,20 +21,11 @@ This project is not affiliated with or endorsed by the AIChat maintainers.
## AIChat ## AIChat
Coyote originally began as a fork of [AIChat CLI](https://github.com/sigoden/aichat), Loki originally began as a fork of [AIChat CLI](https://github.com/sigoden/aichat),
created and maintained by the AIChat contributors. created and maintained by the AIChat contributors.
While Coyote has since diverged significantly and is now developed as an While Loki has since diverged significantly and is now developed as an
independent project, its early foundation and inspiration came from the independent project, its early foundation and inspiration came from the
AIChat project. AIChat project.
AIChat is licensed under the MIT License. The MIT license text and its AIChat is licensed under the MIT License.
copyright notice are preserved in the [LICENSE-MIT](./LICENSE-MIT) file.
## Licensing
Coyote as a whole is licensed under the GNU Affero General Public License
v3.0 only (AGPL-3.0-only); see [LICENSE](./LICENSE). Substantial portions
derived from AIChat remain under the MIT License (Copyright (c) sigoden),
preserved in [LICENSE-MIT](./LICENSE-MIT). See [NOTICE](./NOTICE) for the
combined-licensing summary.
Generated
+1184 -2195
View File
File diff suppressed because it is too large. Load diff
+32 -41
View File
@@ -1,30 +1,28 @@
[package] [package]
name = "coyote-ai" name = "loki-ai"
version = "0.10.3" version = "0.3.0"
edition = "2024" edition = "2024"
authors = ["Alex Clarke <alex.j.tusa@gmail.com>"] authors = ["Alex Clarke <alex.j.tusa@gmail.com>"]
description = "The batteries-included runtime for LLMs" description = "An all-in-one, batteries included LLM CLI Tool"
keywords = ["chatgpt", "llm", "cli", "ai", "repl"] keywords = ["chatgpt", "llm", "cli", "ai", "repl"]
homepage = "https://github.com/Dark-Alex-17/coyote" homepage = "https://github.com/Dark-Alex-17/loki"
repository = "https://github.com/Dark-Alex-17/coyote" repository = "https://github.com/Dark-Alex-17/loki"
categories = ["command-line-utilities"] categories = ["command-line-utilities"]
readme = "README.md" readme = "README.md"
license = "AGPL-3.0-only" license = "MIT"
rust-version = "1.95.0" rust-version = "1.89.0"
exclude = [".github", "CONTRIBUTING.md"] exclude = [".github", "CONTRIBUTING.md"]
[dependencies] [dependencies]
anyhow = "1.0.69" anyhow = "1.0.69"
bytes = "1.4.0" bytes = "1.4.0"
clap = { version = "4.5.40", features = ["cargo", "derive", "wrap_help"] } clap = { version = "4.5.40", features = ["cargo", "derive", "wrap_help"] }
comfy-table = { version = "7.1.4", features = ["custom_styling"] }
dirs = "6.0.0" dirs = "6.0.0"
duckdb = { version = "1.10505.0", features = ["bundled"] }
dunce = "1.0.5" dunce = "1.0.5"
futures-util = "0.3.29" futures-util = "0.3.29"
inquire = "0.9.4" inquire = "0.9.4"
is-terminal = "0.4.9" is-terminal = "0.4.9"
reedline = "0.47.0" reedline = "0.46.0"
serde = { version = "1.0.152", features = ["derive"] } serde = { version = "1.0.152", features = ["derive"] }
serde_json = { version = "1.0.93", features = ["preserve_order"] } serde_json = { version = "1.0.93", features = ["preserve_order"] }
serde_yaml = "0.9.17" serde_yaml = "0.9.17"
@@ -36,6 +34,10 @@ tokio = { version = "1.34.0", features = [
"rt-multi-thread", "rt-multi-thread",
"full", "full",
] } ] }
tokio-graceful = "0.2.2"
tokio-stream = { version = "0.1.15", default-features = false, features = [
"sync",
] }
crossterm = "0.29.0" crossterm = "0.29.0"
chrono = "0.4.23" chrono = "0.4.23"
bincode = { version = "2.0.0", features = [ bincode = { version = "2.0.0", features = [
@@ -49,30 +51,28 @@ nu-ansi-term = "0.50.0"
async-trait = "0.1.74" async-trait = "0.1.74"
textwrap = "0.16.0" textwrap = "0.16.0"
ansi_colours = "1.2.2" ansi_colours = "1.2.2"
eventsource-stream = "0.2.3" reqwest-eventsource = "0.6.0"
log = "0.4.28" log = "0.4.28"
log4rs = { version = "1.4.0", features = [ log4rs = { version = "1.4.0", features = ["file_appender"] }
"file_appender",
"rolling_file_appender",
"compound_policy",
"fixed_window_roller",
"size_trigger",
] }
shell-words = "1.1.0" shell-words = "1.1.0"
sha2 = "0.10.8" sha2 = "0.10.8"
unicode-width = "0.2.0" unicode-width = "0.2.0"
async-recursion = "1.1.1" async-recursion = "1.1.1"
http = "1.1.0" http = "1.1.0"
http-body-util = "0.1"
hyper = { version = "1.0", features = ["full"] }
hyper-util = { version = "0.1", features = ["server-auto", "client-legacy"] }
time = { version = "0.3.36", features = ["macros"] }
indexmap = { version = "2.2.6", features = ["serde"] } indexmap = { version = "2.2.6", features = ["serde"] }
hmac = "0.12.1" hmac = "0.12.1"
aws-smithy-eventstream = "0.60.4" aws-smithy-eventstream = "0.60.4"
aws-smithy-types = "=1.4.9"
time = "=0.3.47"
urlencoding = "2.1.3" urlencoding = "2.1.3"
unicode-segmentation = "1.11.0"
json-patch = { version = "4.0.0", default-features = false } json-patch = { version = "4.0.0", default-features = false }
bitflags = "2.5.0" bitflags = "2.5.0"
path-absolutize = "3.1.1" path-absolutize = "3.1.1"
hnsw_rs = "0.3.0" hnsw_rs = "0.3.0"
rayon = "1.10.0"
uuid = { version = "1.9.1", features = ["v4"] } uuid = { version = "1.9.1", features = ["v4"] }
scraper = { version = "0.23.1", default-features = false, features = [ scraper = { version = "0.23.1", default-features = false, features = [
"deterministic", "deterministic",
@@ -82,7 +82,6 @@ html_to_markdown = "0.1.0"
rust-embed = "8.5.0" rust-embed = "8.5.0"
os_info = { version = "3.8.2", default-features = false } os_info = { version = "3.8.2", default-features = false }
bm25 = { version = "2.0.1", features = ["parallelism"] } bm25 = { version = "2.0.1", features = ["parallelism"] }
petgraph = { version = "0.7", features = ["serde-1"] }
which = "8.0.0" which = "8.0.0"
fuzzy-matcher = "0.3.7" fuzzy-matcher = "0.3.7"
terminal-colorsaurus = "0.4.8" terminal-colorsaurus = "0.4.8"
@@ -90,7 +89,7 @@ duct = "1.0.0"
argc = "1.23.0" argc = "1.23.0"
strum_macros = "0.27.2" strum_macros = "0.27.2"
indoc = "2.0.6" indoc = "2.0.6"
rmcp = { version = "3.1.2", features = [ rmcp = { version = "1.5.0", features = [
"client", "client",
"transport-child-process", "transport-child-process",
"transport-streamable-http-client-reqwest", "transport-streamable-http-client-reqwest",
@@ -98,28 +97,26 @@ rmcp = { version = "3.1.2", features = [
] } ] }
num_cpus = "1.17.0" num_cpus = "1.17.0"
tree-sitter = "0.26.8" tree-sitter = "0.26.8"
tree-sitter-language = "0.1"
tree-sitter-python = "0.25.0" tree-sitter-python = "0.25.0"
tree-sitter-typescript = "0.23" tree-sitter-typescript = "0.23"
colored = "3.0.0" colored = "3.0.0"
clap_complete = { version = "4.5.58", features = ["unstable-dynamic"] } clap_complete = { version = "4.5.58", features = ["unstable-dynamic"] }
gman = "0.5.0" gman = "0.4.1"
clap_complete_nushell = "4.5.9" clap_complete_nushell = "4.5.9"
open = "5" open = "5"
rand = { version = "0.10.0", features = ["default"] } rand = { version = "0.10.0", features = ["default"] }
url = "2.5.8" url = "2.5.8"
self_update = { version = "0.44", default-features = false, features = [
"reqwest",
"rustls",
"archive-tar",
"compression-flate2",
"archive-zip",
"compression-zip-deflate",
] }
qrcode = "0.14"
[dependencies.reqwest] [dependencies.reqwest]
version = "0.13.3" version = "0.12.0"
features = ["json", "multipart", "stream", "form", "socks", "rustls"] features = [
"json",
"multipart",
"socks",
"rustls-tls",
"rustls-tls-native-roots",
]
default-features = false default-features = false
[dependencies.syntect] [dependencies.syntect]
@@ -138,18 +135,12 @@ arboard = { version = "3.3.0", default-features = false, features = [
[target.'cfg(not(any(target_os = "linux", target_os = "android", target_os = "emscripten")))'.dependencies] [target.'cfg(not(any(target_os = "linux", target_os = "android", target_os = "emscripten")))'.dependencies]
arboard = { version = "3.3.0", default-features = false } arboard = { version = "3.3.0", default-features = false }
[target.'cfg(unix)'.dependencies]
libc = "0.2"
[dev-dependencies] [dev-dependencies]
ctor = "1.0.13"
pretty_assertions = "1.4.0" pretty_assertions = "1.4.0"
rmcp = { version = "3.1.2", features = ["server"] }
serial_test = "3" serial_test = "3"
tokio = { version = "1.34.0", features = ["test-util"] }
[[bin]] [[bin]]
name = "coyote" name = "loki"
path = "src/main.rs" path = "src/main.rs"
[profile.release] [profile.release]
-115
View File
@@ -1,115 +0,0 @@
ARG COYOTE_VERSION
FROM docker/sandbox-templates:shell-docker AS build
ARG COYOTE_VERSION
ARG TARGETARCH
ENV PATH="/home/agent/.cargo/bin:/home/agent/.local/bin:${PATH}"
USER root
RUN apt-get update && \
apt-get install -y --no-install-recommends \
jq curl git \
build-essential pkg-config \
cmake \
clang libclang-dev \
musl-tools \
libssl-dev \
pandoc \
bzip2 \
nano && \
rm -rf /var/lib/apt/lists/*
RUN set -euo pipefail; \
USQL_VERSION=0.21.4; \
case "${TARGETARCH}" in \
amd64) USQL_ARCH=amd64 ;; \
arm64) USQL_ARCH=arm64 ;; \
*) echo "Unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \
esac; \
TMPDIR=$(mktemp -d); \
curl -fsSL --retry 3 \
"https://github.com/xo/usql/releases/download/v${USQL_VERSION}/usql_static-${USQL_VERSION}-linux-${USQL_ARCH}.tar.bz2" \
-o "$TMPDIR/usql.tar.bz2"; \
tar -xjf "$TMPDIR/usql.tar.bz2" -C "$TMPDIR"; \
install -m 0755 "$TMPDIR/usql_static" /usr/local/bin/usql; \
rm -rf "$TMPDIR"
RUN set -euo pipefail; \
DUCKDB_VERSION=1.5.5; \
case "${TARGETARCH}" in \
amd64) DUCKDB_ARCH=amd64 ;; \
arm64) DUCKDB_ARCH=arm64 ;; \
*) echo "Unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \
esac; \
TMPDIR=$(mktemp -d); \
curl -fsSL --retry 3 \
"https://github.com/duckdb/duckdb/releases/download/v${DUCKDB_VERSION}/duckdb_cli-linux-${DUCKDB_ARCH}.gz" \
-o "$TMPDIR/duckdb.gz"; \
gunzip "$TMPDIR/duckdb.gz"; \
install -m 0755 "$TMPDIR/duckdb" /usr/local/bin/duckdb; \
rm -rf "$TMPDIR"
USER 1000
RUN curl -LsSf https://astral.sh/uv/install.sh | sh && \
printf '#!/bin/sh\nexec uv tool run "$@"\n' > "$HOME/.local/bin/uvx" && \
chmod +x "$HOME/.local/bin/uvx"
RUN mkdir -p /usr/local/share/npm-global/lib
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
sh -s -- -y --default-toolchain stable --profile minimal && \
. "$HOME/.cargo/env" && \
cargo install --locked iwec && \
cargo install --locked ast-grep
USER root
RUN set -euo pipefail; \
case "${TARGETARCH}" in \
amd64) MUSL_TARGET=x86_64-unknown-linux-musl ;; \
arm64) MUSL_TARGET=aarch64-unknown-linux-musl ;; \
*) echo "Unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \
esac; \
TMPDIR=$(mktemp -d); \
curl -fsSL --retry 3 \
"https://github.com/Dark-Alex-17/coyote/releases/download/v${COYOTE_VERSION}/coyote-${MUSL_TARGET}.tar.gz" \
-o "$TMPDIR/coyote.tar.gz"; \
tar -xzf "$TMPDIR/coyote.tar.gz" -C "$TMPDIR"; \
install -m 0755 "$TMPDIR/coyote" /home/agent/.cargo/bin/coyote; \
chown 1000:1000 /home/agent/.cargo/bin/coyote; \
rm -rf "$TMPDIR"
COPY --chmod=0755 scripts/docker-entrypoint.sh /usr/local/bin/coyote-entrypoint
FROM scratch
ARG COYOTE_VERSION
COPY --from=build / /
ENV PATH="/home/agent/.cargo/bin:/home/agent/.local/bin:/usr/local/share/npm-global/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" \
NPM_CONFIG_PREFIX="/usr/local/share/npm-global" \
NO_PROXY="localhost,127.0.0.1,::1,172.17.0.0/16" \
no_proxy="localhost,127.0.0.1,::1,172.17.0.0/16" \
BASH_ENV="/etc/sandbox-persistent.sh"
LABEL com.docker.sandboxes="templates" \
com.docker.sandboxes.base="ubuntu:questing" \
com.docker.sandboxes.flavor="shell-docker" \
com.docker.sandboxes.start-docker="true" \
org.opencontainers.image.title="coyote" \
org.opencontainers.image.description="The batteries-included runtime for LLMs: Shell Assistant, CLI & REPL mode, RAG, AI tools & agents, MCP servers, skills, and macros." \
org.opencontainers.image.source="https://github.com/Dark-Alex-17/coyote" \
org.opencontainers.image.version="${COYOTE_VERSION}"
WORKDIR /home/agent/workspace
USER 1000
# tini as PID 1 (as in the sandbox-templates base) reaps orphans and forwards
# signals; coyote-entrypoint runs coyote by default, or the given command
# (e.g. the Docker Sandboxes keep-alive) when the first arg is sh/bash/a path.
ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/coyote-entrypoint"]
+22 -661
View File
@@ -1,661 +1,22 @@
GNU AFFERO GENERAL PUBLIC LICENSE The MIT License (MIT)
Version 3, 19 November 2007
Copyright (c) 2025 sigoden
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> Copyright (c) 2025 Alexander J. Clarke
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed. Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Preamble in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
The GNU Affero General Public License is a free, copyleft license for copies of the Software, and to permit persons to whom the Software is
software and other kinds of works, specifically designed to ensure furnished to do so, subject to the following conditions:
cooperation with the community in the case of network server software.
The above copyright notice and this permission notice shall be included in all
The licenses for most software and other practical works are designed copies or substantial portions of the Software.
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
share and change all versions of a program--to make sure it remains free IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
software for all its users. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
When we speak of free software, we are referring to freedom, not LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
price. Our General Public Licenses are designed to make sure that you OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
have the freedom to distribute copies of free software (and charge for SOFTWARE.
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.
-22
View File
@@ -1,22 +0,0 @@
The MIT License (MIT)
Copyright (c) 2025 sigoden
Copyright (c) 2025 Alexander J. Clarke
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-24
View File
@@ -1,24 +0,0 @@
Coyote
Copyright (c) 2025 Alexander J. Clarke
This project as a whole is licensed under the GNU Affero General Public
License, version 3.0 only (AGPL-3.0-only). The full text of that license is
provided in the LICENSE file.
--------------------------------------------------------------------------------
Upstream / third-party notices
--------------------------------------------------------------------------------
Coyote began as a fork of AIChat (https://github.com/sigoden/aichat),
Copyright (c) sigoden, which is distributed under the MIT License. Substantial
portions of Coyote are derived from AIChat and remain available under the terms
of the MIT License. The MIT License text and its required copyright and
permission notices are preserved in the LICENSE-MIT file.
As permitted by the MIT License, these portions have been incorporated into a
larger work that is distributed under the AGPL-3.0-only license. When you
receive Coyote as a combined work, your rights and obligations for the work as
a whole are governed by the AGPL-3.0-only license; the MIT notice is retained
to satisfy the attribution requirements of the MIT-licensed portions.
See CREDITS.md for additional background and attribution.
+98 -188
View File
@@ -1,175 +1,121 @@
# Coyote: The batteries-included runtime for LLMs # Loki: All-in-one, batteries-included LLM CLI Tool
![Test](https://github.com/Dark-Alex-17/coyote/actions/workflows/ci.yaml/badge.svg) ![Test](https://github.com/Dark-Alex-17/loki/actions/workflows/ci.yaml/badge.svg)
[![crates.io link](https://img.shields.io/crates/v/coyote-ai.svg)](https://crates.io/crates/coyote-ai) ![LOC](https://tokei.rs/b1/github/Dark-Alex-17/loki?category=code)
![Release](https://img.shields.io/github/v/release/Dark-Alex-17/coyote?color=%23c694ff) [![crates.io link](https://img.shields.io/crates/v/loki-ai.svg)](https://crates.io/crates/loki-ai)
![Crate.io downloads](https://img.shields.io/crates/d/coyote-ai?label=Crate%20downloads) ![Release](https://img.shields.io/github/v/release/Dark-Alex-17/loki?color=%23c694ff)
[![GitHub Downloads](https://img.shields.io/github/downloads/Dark-Alex-17/coyote/total.svg?label=GitHub%20downloads)](https://github.com/Dark-Alex-17/coyote/releases) ![Crate.io downloads](https://img.shields.io/crates/d/loki-ai?label=Crate%20downloads)
![Docker pulls](https://img.shields.io/docker/pulls/darkalex17/coyote?label=Docker%20downloads) [![GitHub Downloads](https://img.shields.io/github/downloads/Dark-Alex-17/loki/total.svg?label=GitHub%20downloads)](https://github.com/Dark-Alex-17/loki/releases)
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](https://github.com/Dark-Alex-17/coyote/blob/main/LICENSE)
Coyote is an **all-in-one, batteries-included LLM runtime** for building, running, and interacting with AI from your terminal. Loki is an all-in-one, batteries-included, LLM CLI tool featuring Shell Assistant, CLI & REPL Mode, RAG, AI Tools &
It brings together a Shell Assistant, CLI & REPL modes, RAG, tools, agents, MCP, skills, sandboxes, multi-agent workflows, and Agents, and More.
more in a single runtime.
Coyote comes ready to use with built-in agents, roles, macros, and tools, so you can get started without assembling an AI It is designed to include a number of useful agents, roles, macros, and more so users can get up and running with Loki
stack from scratch. When you want to extend it, entire bundles of agents, roles, macros, tools, MCP servers, and other in as little time as possible.
configurations can be installed directly from any Git repository.
See [Bundles](https://github.com/Dark-Alex-17/coyote/wiki/Bundles) to learn how to create, install, and share Coyote bundles. ![Agent example](./docs/images/agents/sql.gif)
Coming from [AIChat](https://github.com/sigoden/aichat)? Follow the [migration guide](./docs/AICHAT-MIGRATION.md) to get started.
![Agent example](https://raw.githubusercontent.com/wiki/Dark-Alex-17/coyote/images/agents/sql.gif)
Coming from [AIChat](https://github.com/sigoden/aichat)? Follow the [migration guide](https://github.com/Dark-Alex-17/coyote/wiki/AIChat-Migration) to get started.
## Quick Links ## Quick Links
* [AIChat Migration Guide](https://github.com/Dark-Alex-17/coyote/wiki/AIChat-Migration): Coming from AIChat? Follow the migration guide to get started. * [AIChat Migration Guide](./docs/AICHAT-MIGRATION.md): Coming from AIChat? Follow the migration guide to get started.
* [Installation](#install): Install Coyote * [Installation](#install): Install Loki
* [Getting Started](#getting-started): Get started with Coyote by doing first-run setup steps. * [Getting Started](#getting-started): Get started with Loki by doing first-run setup steps.
* [Bundles](https://github.com/Dark-Alex-17/coyote/wiki/Bundles): Install bundles of agents, roles, skills, macros, tools, and MCP servers from any git repo, and share your own. Bundles are Coyote's equivalent of plugins in other CLI agents. * [REPL](./docs/REPL.md): Interactive Read-Eval-Print Loop for conversational interactions with LLMs and Loki.
* [REPL](https://github.com/Dark-Alex-17/coyote/wiki/REPL): Interactive Read-Eval-Print Loop for conversational interactions with LLMs and Coyote. * [Custom REPL Prompt](./docs/REPL-PROMPT.md): Customize the REPL prompt to provide useful contextual information.
* [Custom REPL Prompt](https://github.com/Dark-Alex-17/coyote/wiki/REPL-Prompt): Customize the REPL prompt to provide useful contextual information. * [Vault](./docs/VAULT.md): Securely store and manage sensitive information such as API keys and credentials.
* [Vault](https://github.com/Dark-Alex-17/coyote/wiki/Vault): Securely store and manage sensitive information such as API keys and credentials. * [Shell Integrations](./docs/SHELL-INTEGRATIONS.md): Seamlessly integrate Loki with your shell environment for enhanced command-line assistance.
* [Sandboxes](https://github.com/Dark-Alex-17/coyote/wiki/Sandboxes): Launch Coyote inside an isolated [Docker Sandbox](https://docs.docker.com/ai/sandboxes/) with one command. Host config and vault credentials are projected in automatically; everything else is delegated to the `sbx` CLI. * [Function Calling](./docs/function-calling/TOOLS.md#Tools): Leverage function calling capabilities to extend Loki's functionality with custom tools
* [Shell Integrations](https://github.com/Dark-Alex-17/coyote/wiki/Shell-Integrations): Seamlessly integrate Coyote with your shell environment for enhanced command-line assistance. * [Creating Custom Tools](./docs/function-calling/CUSTOM-TOOLS.md): You can create your own custom tools to enhance Loki's capabilities.
* [Function Calling](https://github.com/Dark-Alex-17/coyote/wiki/Tools): Leverage function calling capabilities to extend Coyote's functionality with custom tools * [Create Custom Python Tools](./docs/function-calling/CUSTOM-TOOLS.md#custom-python-based-tools)
* [Creating Custom Tools](https://github.com/Dark-Alex-17/coyote/wiki/Custom-Tools): You can create your own custom tools to enhance Coyote's capabilities. * [Create Custom TypeScript Tools](./docs/function-calling/CUSTOM-TOOLS.md#custom-typescript-based-tools)
* [Create Custom Python Tools](https://github.com/Dark-Alex-17/coyote/wiki/Custom-Tools#custom-python-based-tools) * [Create Custom Bash Tools](./docs/function-calling/CUSTOM-BASH-TOOLS.md)
* [Create Custom TypeScript Tools](https://github.com/Dark-Alex-17/coyote/wiki/Custom-Tools#custom-typescript-based-tools) * [Bash Prompt Utilities](./docs/function-calling/BASH-PROMPT-HELPERS.md)
* [Create Custom Bash Tools](https://github.com/Dark-Alex-17/coyote/wiki/Custom-Bash-Tools) * [First-Class MCP Server Support](./docs/function-calling/MCP-SERVERS.md): Easily connect and interact with MCP servers for advanced functionality.
* [Bash Prompt Utilities](https://github.com/Dark-Alex-17/coyote/wiki/Bash-Prompt-Helpers) * [Macros](./docs/MACROS.md): Automate repetitive tasks and workflows with Loki "scripts" (macros).
* [First-Class MCP Server Support](https://github.com/Dark-Alex-17/coyote/wiki/MCP-Servers): Easily connect and interact with MCP servers for advanced functionality. Coyote supports all three MCP capabilities: tools, resources, and prompts. * [RAG](./docs/RAG.md): Retrieval-Augmented Generation for enhanced information retrieval and generation.
* Models interact with each server through a compact set of capability-gated meta-tools: `mcp_search`/`mcp_describe` for discovery across tools, resources, and prompts, `mcp_invoke` for tool calls, `mcp_read` for paged and regex-filterable resource reads, and `mcp_prompt` for server-defined prompts. Binary content is spilled to disk instead of inlined, and oversized tool results are bounded before they reach the model. * [Sessions](/docs/SESSIONS.md): Manage and persist conversational contexts and settings across multiple interactions.
* Invoke server prompts yourself with `.prompt <server> <name> [key=value ...]` in the REPL, with live staged tab-completion (servers, then prompt names, then `key=` arguments), and discover them with `.list prompts`. * [Roles](./docs/ROLES.md): Customize model behavior for specific tasks or domains.
* [Macros](https://github.com/Dark-Alex-17/coyote/wiki/Macros): Automate repetitive tasks and workflows with Coyote "scripts" (macros). Macros are Coyote's custom commands: invoke any macro directly by name (e.g. `.review main`), with tab-completion, right alongside the built-in REPL commands. * [Agents](/docs/AGENTS.md): Leverage AI agents to perform complex tasks and workflows, including sub-agent spawning, teammate messaging, and user interaction tools.
* Give a macro a `description` (shown in `.list macros` and completions) and set `isolated: false` to run its steps on the live session, exactly as if you typed them. Note that non-isolated steps are recorded in the session, and mutating steps (`.role`, `.model`, ...) persist after the macro ends, by design. Steps are fail-fast: an error aborts the remaining steps, but completed steps' effects remain. A non-isolated macro step cannot invoke another macro, and a `.exit` step never exits the REPL. * [Todo System](./docs/TODO-SYSTEM.md): Built-in task tracking for improved agent reliability with smaller models.
* Commit project-specific macros to `.coyote/macros/` in your repo. They shadow same-named global macros (opt out with `--no-workspace-macros`). * [Environment Variables](./docs/ENVIRONMENT-VARIABLES.md): Override and customize your Loki configuration at runtime with environment variables.
* Pass variables positionally or by name: leading `name=value` args set declared variables directly (letting earlier variables keep their defaults), and remaining args fill the rest in order. Tab completion after a macro name lists each variable with its description and default. * [Client Configurations](./docs/clients/CLIENTS.md): Configuration instructions for various LLM providers.
* Scope which macros are invocable with `enabled_macros` in the global config, a role, an agent, or a session (most specific wins; an empty list disables all macros), and toggle at runtime with `.macro enable|disable <name>`. * [Authentication (API Key & OAuth)](./docs/clients/CLIENTS.md#authentication): Authenticate with API keys or OAuth for subscription-based access.
* [RAG](https://github.com/Dark-Alex-17/coyote/wiki/RAG): Retrieval-Augmented Generation for enhanced information retrieval and generation. * [Patching API Requests](./docs/clients/PATCHES.md): Learn how to patch API requests for advanced customization.
* [Sessions](https://github.com/Dark-Alex-17/coyote/wiki/Sessions): Manage and persist conversational contexts and settings across multiple interactions. * [Custom Themes](./docs/THEMES.md): Change the look and feel of Loki to your preferences with custom themes.
* [Memory](https://github.com/Dark-Alex-17/coyote/wiki/Memory): Persistent file-based memory that survives across sessions. Bootstrap with `coyote --init-memory [global|workspace]`. * [History](#history): A history of how Loki came to be.
* [Workspace Instructions](https://github.com/Dark-Alex-17/coyote/wiki/Workspace-Instructions): Human-curated project instructions (`COYOTE.md`) injected into every prompt, with `AGENTS.md`/`CLAUDE.md`/`GEMINI.md` fallbacks for cross-tool compatibility. Scaffold with `coyote --init-instructions`.
* [Roles](https://github.com/Dark-Alex-17/coyote/wiki/Roles): Customize model behavior for specific tasks or domains.
* [Skills](https://github.com/Dark-Alex-17/coyote/wiki/Skills): Modular knowledge or capability packs the LLM can load and unload mid-conversation. Multiple skills compose; instructions stack, tools and MCPs union.
* [Agents](https://github.com/Dark-Alex-17/coyote/wiki/Agents): Leverage AI agents to perform complex tasks and workflows, including sub-agent spawning, teammate messaging, and user interaction tools.
* [Graph Agents](https://github.com/Dark-Alex-17/coyote/wiki/Graph-Agents): Define an agent as a declarative, YAML-driven workflow. A directed graph of typed nodes (LLM calls, scripts, approvals, user input, RAG retrieval, sub-agent spawns).
* [Background Jobs](https://github.com/Dark-Alex-17/coyote/wiki/Background-Jobs): Run long tool calls (builds, test suites, slow MCP calls) in the background with the `job__*` tools while the model keeps working, and completion arrives as a push notification.
* [Hooks](https://github.com/Dark-Alex-17/coyote/wiki/Hooks): Run your own fire-and-forget commands when lifecycle events fire. Turns, tools, LLM requests, agents, sub-agent escalations, and more (26 events). Perfect for desktop notifications, activity logging, and downstream automation; hooks never block Coyote or change its behavior.
* [Todo System](https://github.com/Dark-Alex-17/coyote/wiki/TODO-System): Built-in task tracking for improved LLM reliability with smaller models.
* [Environment Variables](https://github.com/Dark-Alex-17/coyote/wiki/Environment-Variables): Override and customize your Coyote configuration at runtime with environment variables.
* [Client Configurations](https://github.com/Dark-Alex-17/coyote/wiki/Clients): Configuration instructions for various LLM providers.
* [Authentication (API Key & OAuth)](https://github.com/Dark-Alex-17/coyote/wiki/Clients#authentication): Authenticate with API keys or OAuth for subscription-based access.
* [Patching API Requests](https://github.com/Dark-Alex-17/coyote/wiki/Patches): Learn how to patch API requests for advanced customization.
* [Dynamic Model Catalog](https://github.com/Dark-Alex-17/coyote/wiki/Model-Catalog): Coyote's model catalog (context windows, pricing, capabilities) is generated from live API data, refreshed automatically in the background, and mirrorable for air-gapped setups. New models show up without waiting for a release.
* [Custom Themes](https://github.com/Dark-Alex-17/coyote/wiki/Themes): Change the look and feel of Coyote to your preferences with custom themes.
* [History](#history): A history of how Coyote came to be.
## Prerequisites ## Prerequisites
Coyote requires the following tools to be installed on your system: Loki requires the following tools to be installed on your system:
* [jq](https://github.com/jqlang/jq) * [jq](https://github.com/jqlang/jq)
* `brew install jq` * `brew install jq`
* [jira (optional)](https://github.com/ankitpokhrel/jira-cli/wiki/Installation) (For the `query_jira_issues` tool)
* `brew tap ankitpokhrel/jira-cli && brew install jira-cli`
* You'll need to [create a JIRA API token](https://id.atlassian.com/manage-profile/security/api-tokens) for authentication
* Then, save it as an environment variable to your shell profile:
```sh
# ~/.bashrc or ~/.zshrc
export JIRA_API_TOKEN="your_jira_api_token_here"
```
* Then run `jira init`, select installation type as `cloud`, and provide the required details to generate a config
file for the Jira CLI.
* [usql](https://github.com/xo/usql) (For the `sql` agent) * [usql](https://github.com/xo/usql) (For the `sql` agent)
* `brew install xo/xo/usql` * `brew install xo/xo/usql`
* [docker](https://docs.docker.com/engine/install/) * [docker](https://docs.docker.com/engine/install/)
* [uv](https://docs.astral.sh/uv/getting-started/installation/) * [uv](https://docs.astral.sh/uv/getting-started/installation/)
* `curl -LsSf https://astral.sh/uv/install.sh | sh` * `curl -LsSf https://astral.sh/uv/install.sh | sh`
* [iwe](https://github.com/iwe-org/iwe) (`iwec`, for the built-in `iwe` MCP server that navigates large markdown knowledgebases)
* **Homebrew:** `brew tap iwe-org/iwe && brew trust --formula iwe-org/iwe/iwe && brew install iwe`
* **Cargo:** `cargo install iwec`
* [ast-grep](https://ast-grep.github.io/) (for the built-in `ast_grep` structural code search tool, used by the `explore` agent)
* **Homebrew:** `brew install ast-grep`
* **Cargo:** `cargo install ast-grep --locked`
* **npm:** `npm i -g @ast-grep/cli`
* Optional: if `ast-grep` is not installed, the `ast_grep` tool reports it and agents fall back to `fs_grep`
* [duckdb](https://duckdb.org/) (for fast, local RAGs)
* `curl https://install.duckdb.org | bash`
These tools are used to provide various functionalities within Coyote, such as document processing, JSON manipulation, These tools are used to provide various functionalities within Loki, such as document processing, JSON manipulation,
etc., and they are used within agents and tools. interaction with Jira, and they are used within agents and tools.
## Install ## Install
### Cargo ### Cargo
If you have Cargo installed, then you can install `coyote` from Crates.io: If you have Cargo installed, then you can install `loki` from Crates.io:
```shell ```shell
cargo install coyote-ai # Binary name is `coyote` cargo install loki-ai # Binary name is `loki`
# If you encounter issues installing, try installing with '--locked' # If you encounter issues installing, try installing with '--locked'
cargo install --locked coyote-ai cargo install --locked loki-ai
``` ```
### Homebrew (Mac/Linux) ### Homebrew (Mac/Linux)
To install Coyote from Homebrew, install the `coyote` tap. Then you'll be able to install `coyote`: To install Loki from Homebrew, install the `loki` tap. Then you'll be able to install `loki`:
```shell ```shell
brew tap Dark-Alex-17/coyote brew tap Dark-Alex-17/loki
brew install coyote brew install loki
# If you need to be more specific, use: # If you need to be more specific, use:
brew install Dark-Alex-17/coyote/coyote brew install Dark-Alex-17/loki/loki
``` ```
To upgrade `coyote` using Homebrew: To upgrade `loki` using Homebrew:
```shell ```shell
brew upgrade coyote brew upgrade loki
```
### Docker
Coyote is available as a Docker image on Docker Hub (`darkalex17/coyote`) for Linux amd64 and arm64.
Useful for CI, ephemeral environments, or anywhere you prefer not to install it natively.
```bash
docker pull darkalex17/coyote
docker run --rm -it darkalex17/coyote
```
To persist your configuration across container runs, mount your existing config directory:
```bash
docker run --rm -it \
-v ~/.config/coyote:/home/agent/.config/coyote \
darkalex17/coyote
```
If you use the local vault provider and want your vault credentials available in the container, also mount the password file:
```bash
docker run --rm -it \
-v ~/.config/coyote:/home/agent/.config/coyote \
-v ~/.coyote_password:/home/agent/.coyote_password:ro \
darkalex17/coyote
```
Arguments are passed to `coyote`. If the first argument is `sh`, `bash`, or an absolute path, that command runs instead,
e.g. to get a shell inside the image:
```bash
docker run --rm -it darkalex17/coyote bash
``` ```
### Scripts ### Scripts
#### Linux/MacOS (`bash`) #### Linux/MacOS (`bash`)
You can use the following command to run a bash script that downloads and installs the latest version of `coyote` for your You can use the following command to run a bash script that downloads and installs the latest version of `loki` for your
OS (Linux/MacOS) and architecture (x86_64/arm64): OS (Linux/MacOS) and architecture (x86_64/arm64):
```shell ```shell
curl -fsSL https://raw.githubusercontent.com/Dark-Alex-17/coyote/refs/heads/main/scripts/install_coyote.sh | bash curl -fsSL https://raw.githubusercontent.com/Dark-Alex-17/loki/main/install_loki.sh | bash
``` ```
#### Windows/Linux/MacOS (`PowerShell`) #### Windows/Linux/MacOS (`PowerShell`)
You can use the following command to run a PowerShell script that downloads and installs the latest version of `coyote` You can use the following command to run a PowerShell script that downloads and installs the latest version of `loki`
for your OS (Windows/Linux/MacOS) and architecture (x86_64/arm64): for your OS (Windows/Linux/MacOS) and architecture (x86_64/arm64):
```powershell ```powershell
powershell -NoProfile -ExecutionPolicy Bypass -Command "iwr -useb https://raw.githubusercontent.com/Dark-Alex-17/coyote/refs/heads/main/scripts/install_coyote.ps1 | iex" powershell -NoProfile -ExecutionPolicy Bypass -Command "iwr -useb https://raw.githubusercontent.com/Dark-Alex-17/loki/main/scripts/install_loki.ps1 | iex"
``` ```
### Manual ### Manual
Binaries are available on the [releases](https://github.com/Dark-Alex-17/coyote/releases) page for the following platforms: Binaries are available on the [releases](https://github.com/Dark-Alex-17/loki/releases) page for the following platforms:
| Platform | Architecture(s) | | Platform | Architecture(s) |
|----------------|-----------------| |----------------|-----------------|
@@ -180,58 +126,35 @@ Binaries are available on the [releases](https://github.com/Dark-Alex-17/coyote/
#### Windows Instructions #### Windows Instructions
To use a binary from the releases page on Windows, do the following: To use a binary from the releases page on Windows, do the following:
1. Download the latest [binary](https://github.com/Dark-Alex-17/coyote/releases) for your OS. 1. Download the latest [binary](https://github.com/Dark-Alex-17/loki/releases) for your OS.
2. Use 7-Zip or TarTool to unpack the Tar file. 2. Use 7-Zip or TarTool to unpack the Tar file.
3. Run the executable `coyote.exe`! 3. Run the executable `loki.exe`!
#### Linux/MacOS Instructions #### Linux/MacOS Instructions
To use a binary from the releases page on Linux/MacOS, do the following: To use a binary from the releases page on Linux/MacOS, do the following:
1. Download the latest [binary](https://github.com/Dark-Alex-17/coyote/releases) for your OS. 1. Download the latest [binary](https://github.com/Dark-Alex-17/loki/releases) for your OS.
2. `cd` to the directory where you downloaded the binary. 2. `cd` to the directory where you downloaded the binary.
3. Extract the binary with `tar -C /usr/local/bin -xzf coyote-<arch>.tar.gz` (Note: This may require `sudo`) 3. Extract the binary with `tar -C /usr/local/bin -xzf loki-<arch>.tar.gz` (Note: This may require `sudo`)
4. Now you can run `coyote`! 4. Now you can run `loki`!
## Updating
Coyote can update itself in place to the latest GitHub release. Run `coyote --update`
for the newest release, or `coyote --update v0.4.0` for a specific version:
```shell
coyote --update
coyote --update v0.4.0
```
The same is available from within the REPL via `.update` and `.update v0.4.0`.
If Coyote was installed with a package manager, prefer that package manager so its
records stay in sync with the binary on disk; i.e. `brew upgrade coyote` for Homebrew,
or `cargo install --locked coyote-ai` for Cargo.
When Coyote detects a package-manager install it prints a warning and asks for
confirmation. In a non-interactive shell (no TTY), pass `--force` to update
anyway:
```shell
coyote --update --force
```
## Getting Started ## Getting Started
After installation, you can generate the configuration files and directories by simply running: After installation, you can generate the configuration files and directories by simply running:
```sh ```sh
coyote --info loki --info
``` ```
Then, you need to set up the Coyote vault by creating a vault password file. Coyote will do this for you automatically and Then, you need to set up the Loki vault by creating a vault password file. Loki will do this for you automatically and
guide you through the process when you first attempt to access the vault. So, to get started, you can run: guide you through the process when you first attempt to access the vault. So, to get started, you can run:
```sh ```sh
coyote --list-secrets loki --list-secrets
``` ```
### Authentication ### Authentication
Each client in your configuration needs authentication (with a few exceptions; e.g. ollama). Most clients use an API key Each client in your configuration needs authentication (with a few exceptions; e.g. ollama). Most clients use an API key
(set via `api_key` in the config or through the [vault](https://github.com/Dark-Alex-17/coyote/wiki/Vault)). For providers that support OAuth (e.g. Claude Pro/Max (set via `api_key` in the config or through the [vault](./docs/VAULT.md)). For providers that support OAuth (e.g. Claude Pro/Max
subscribers, Google Gemini), you can authenticate with your existing subscription instead: subscribers, Google Gemini), you can authenticate with your existing subscription instead:
```yaml ```yaml
@@ -243,40 +166,40 @@ clients:
``` ```
```sh ```sh
coyote --authenticate my-claude-oauth loki --authenticate my-claude-oauth
# Or via the REPL: .authenticate # Or via the REPL: .authenticate
``` ```
For full details, see the [authentication documentation](https://github.com/Dark-Alex-17/coyote/wiki/Clients#authentication). For full details, see the [authentication documentation](./docs/clients/CLIENTS.md#authentication).
### Tab-Completions ### Tab-Completions
You can also enable tab completions to make using Coyote easier. To do so, add the following to your shell profile: You can also enable tab completions to make using Loki easier. To do so, add the following to your shell profile:
```shell ```shell
# Bash # Bash
# (add to: `~/.bashrc`) # (add to: `~/.bashrc`)
source <(COMPLETE=bash coyote) source <(COMPLETE=bash loki)
# Zsh # Zsh
# (add to: `~/.zshrc`) # (add to: `~/.zshrc`)
source <(COMPLETE=zsh coyote) source <(COMPLETE=zsh loki)
# Fish # Fish
# (add to: `~/.config/fish/config.fish`) # (add to: `~/.config/fish/config.fish`)
source <(COMPLETE=fish coyote | psub) source <(COMPLETE=fish loki | psub)
# Elvish # Elvish
# (add to: `~/.elvish/rc.elv`) # (add to: `~/.elvish/rc.elv`)
eval (E:COMPLETE=elvish coyote | slurp) eval (E:COMPLETE=elvish loki | slurp)
# PowerShell # PowerShell
# (add to: `$PROFILE`) # (add to: `$PROFILE`)
$env:COMPLETE = "powershell" $env:COMPLETE = "powershell"
coyote | Out-String | Invoke-Expression loki | Out-String | Invoke-Expression
``` ```
### Shell Integration ### Shell Integration
You can integrate Coyote's Shell Assistant into your shell for enhanced command-line assistance. Add the code in the You can integrate Loki's Shell Assistant into your shell for enhanced command-line assistance. Add the code in the
corresponding [shell integration script](https://github.com/Dark-Alex-17/coyote/tree/main/scripts/shell-integration) to your shell. Then, you can invoke Coyote to convert natural language to corresponding [shell integration script](./scripts/shell-integration) to your shell. Then, you can invoke Loki to convert natural language to
shell commands by pressing `Alt-e`. For example: shell commands by pressing `Alt-e`. For example:
```shell ```shell
@@ -286,18 +209,18 @@ find . -name "*.md"
``` ```
## Configuration ## Configuration
The location of the global Coyote configuration varies between systems, so you can use the following command to find your The location of the global Loki configuration varies between systems, so you can use the following command to find your
`config.yaml` file: `config.yaml` file:
```shell ```shell
coyote --info | grep 'config_file' | awk '{print $2}' loki --info | grep 'config_file' | awk '{print $2}'
``` ```
The configuration file consists of a number of settings. To see a full example configuration file with every setting The configuration file consists of a number of settings. To see a full example configuration file with every setting
defined, refer to the [example configuration file](https://github.com/Dark-Alex-17/coyote/blob/main/config.example.yaml). defined, refer to the [example configuration file](./config.example.yaml).
### Default LLM ### Default LLM
The following settings are available to configure the default LLM that is used when you start Coyote, and its The following settings are available to configure the default LLM that is used when you start Loki, and its
hyperparameters: hyperparameters:
| Setting | Description | | Setting | Description |
@@ -307,34 +230,34 @@ hyperparameters:
| `top_p` | The default `top_p` hyperparameter value to use for all models, with a range of (0,1) (or (0,2) for some models); <br>Used unless explicitly overridden | | `top_p` | The default `top_p` hyperparameter value to use for all models, with a range of (0,1) (or (0,2) for some models); <br>Used unless explicitly overridden |
### CLI Behavior ### CLI Behavior
You can use the following settings to modify the behavior of Coyote: You can use the following settings to modify the behavior of Loki:
| Setting | Default Value | Description | | Setting | Default Value | Description |
|---------------|---------------|-------------------------------------------------------------------------------------------------------------------------------------| |---------------|---------------|-------------------------------------------------------------------------------------------------------------------------------------|
| `stream` | `true` | Controls whether to use stream-style APIs when querying for completions from LLM providers | | `stream` | `true` | Controls whether to use stream-style APIs when querying for completions from LLM providers |
| `save` | `true` | Controls whether to save each query/response to every model to `messages.md` for posterity; Useful for debugging | | `save` | `true` | Controls whether to save each query/response to every model to `messages.md` for posterity; Useful for debugging |
| `keybindings` | `emacs` | Specifies which keybinding schema to use; can either be `emacs` or `vi` | | `keybindings` | `emacs` | Specifies which keybinding schema to use; can either be `emacs` or `vi` |
| `editor` | `null` | What text editor Coyote should use to edit the input buffer or session (e.g. `vim`, `emacs`, `nano`, `hx`); <br>Defaults to `$EDITOR` | | `editor` | `null` | What text editor Loki should use to edit the input buffer or session (e.g. `vim`, `emacs`, `nano`, `hx`); <br>Defaults to `$EDITOR` |
| `wrap` | `no` | Controls whether text is wrapped (can be `no`, `auto`, or some `<max_width>` | | `wrap` | `no` | Controls whether text is wrapped (can be `no`, `auto`, or some `<max_width>` |
| `wrap_code` | `false` | Enables or disables the wrapping of code blocks | | `wrap_code` | `false` | Enables or disables the wrapping of code blocks |
### Preludes ### Preludes
Preludes let you define the default behavior for the different operating modes of Coyote. The available settings are Preludes let you define the default behavior for the different operating modes of Loki. The available settings are
shown below: shown below:
| Setting | Description | | Setting | Description |
|-----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |-----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `repl_prelude` | This setting lets you specify a default `session` or `role` to use when starting Coyote in [REPL](https://github.com/Dark-Alex-17/coyote/wiki/REPL) mode. <br>Values can be <ul><li>`role:<name>` to define a role</li><li>`session:<name>` to define a session</li><li>`<session>:<role>` to define both a session and a role to use</li></ul> | | `repl_prelude` | This setting lets you specify a default `session` or `role` to use when starting Loki in [REPL](./docs/REPL.md) mode. <br>Values can be <ul><li>`role:<name>` to define a role</li><li>`session:<name>` to define a session</li><li>`<session>:<role>` to define both a session and a role to use</li></ul> |
| `cmd_prelude` | This setting lets you specify a default `session` or `role` to use when running one-off queries in Coyote via the CLI. <br>Values can be <ul><li>`role:<name>` to define a role</li><li>`session:<name>` to define a session</li><li>`<session>:<role>` to define both a session and a role to use</li></ul> | | `cmd_prelude` | This setting lets you specify a default `session` or `role` to use when running one-off queries in Loki via the CLI. <br>Values can be <ul><li>`role:<name>` to define a role</li><li>`session:<name>` to define a session</li><li>`<session>:<role>` to define both a session and a role to use</li></ul> |
| `agent_session` | This setting is used to specify a default session that all agents should start into, unless otherwise specified in the agent configuration. (e.g. `temp`, `default`) | | `agent_session` | This setting is used to specify a default session that all agents should start into, unless otherwise specified in the agent configuration. (e.g. `temp`, `default`) |
### Appearance ### Appearance
The appearance of Coyote can be modified using the following settings: The appearance of Loki can be modified using the following settings:
| Setting | Default Value | Description | | Setting | Default Value | Description |
|---------------|---------------|------------------------------------------------------| |---------------|---------------|------------------------------------------------------|
| `highlight` | `true` | This setting enables or disables syntax highlighting | | `highlight` | `true` | This setting enables or disables syntax highlighting |
| `light_theme` | `false` | This setting toggles light mode in Coyote | | `light_theme` | `false` | This setting toggles light mode in Loki |
### Miscellaneous Settings ### Miscellaneous Settings
| Setting | Default Value | Description | | Setting | Default Value | Description |
@@ -346,24 +269,11 @@ The appearance of Coyote can be modified using the following settings:
## History ## History
Coyote began as a fork of [AIChat CLI](https://github.com/sigoden/aichat) and has since evolved into an independent project. Loki began as a fork of [AIChat CLI](https://github.com/sigoden/aichat) and has since evolved into an independent project.
See [CREDITS.md](https://github.com/Dark-Alex-17/coyote/blob/main/CREDITS.md) for full attribution and background. See [CREDITS.md](./CREDITS.md) for full attribution and background.
--- ---
## Creator ## Creator
* [Alex Clarke](https://github.com/Dark-Alex-17) * [Alex Clarke](https://github.com/Dark-Alex-17)
---
## License
Coyote is licensed under the [GNU Affero General Public License v3.0](https://github.com/Dark-Alex-17/coyote/blob/main/LICENSE)
(AGPL-3.0-only).
Coyote began as a fork of [AIChat](https://github.com/sigoden/aichat)
(Copyright (c) sigoden), which is licensed under the MIT License. Substantial
portions of Coyote are derived from AIChat and remain available under the MIT
License, preserved in [LICENSE-MIT](https://github.com/Dark-Alex-17/coyote/blob/main/LICENSE-MIT). See [NOTICE](https://github.com/Dark-Alex-17/coyote/blob/main/NOTICE) and
[CREDITS.md](https://github.com/Dark-Alex-17/coyote/blob/main/CREDITS.md) for details.
+25 -101
View File
@@ -7,14 +7,14 @@ set -euo pipefail
####################### #######################
# Cache file name for detected project info # Cache file name for detected project info
_COYOTE_PROJECT_CACHE=".coyote-project.json" _LOKI_PROJECT_CACHE=".loki-project.json"
# Read cached project detection if valid # Read cached project detection if valid
# Usage: _read_project_cache "/path/to/project" # Usage: _read_project_cache "/path/to/project"
# Returns: cached JSON on stdout (exit 0) or nothing (exit 1) # Returns: cached JSON on stdout (exit 0) or nothing (exit 1)
_read_project_cache() { _read_project_cache() {
local dir="$1" local dir="$1"
local cache_file="${dir}/${_COYOTE_PROJECT_CACHE}" local cache_file="${dir}/${_LOKI_PROJECT_CACHE}"
if [[ -f "${cache_file}" ]]; then if [[ -f "${cache_file}" ]]; then
local cached local cached
@@ -32,7 +32,7 @@ _read_project_cache() {
_write_project_cache() { _write_project_cache() {
local dir="$1" local dir="$1"
local json="$2" local json="$2"
local cache_file="${dir}/${_COYOTE_PROJECT_CACHE}" local cache_file="${dir}/${_LOKI_PROJECT_CACHE}"
echo "${json}" > "${cache_file}" 2>/dev/null || true echo "${json}" > "${cache_file}" 2>/dev/null || true
} }
@@ -40,57 +40,15 @@ _write_project_cache() {
_detect_heuristic() { _detect_heuristic() {
local dir="$1" local dir="$1"
local runner="" runner_type="" runner_targets=""
if [[ -f "${dir}/Taskfile.yml" || -f "${dir}/Taskfile.yaml" || -f "${dir}/taskfile.yml" || -f "${dir}/taskfile.yaml" ]]; then
runner="task" runner_type="taskfile"
runner_targets=$( (cd "${dir}" && task --list-all 2>/dev/null | sed -n 's/^\* \([^:[:space:]]*\):.*/\1/p') || true)
elif [[ -f "${dir}/justfile" || -f "${dir}/Justfile" ]]; then
runner="just" runner_type="just"
runner_targets=$( (cd "${dir}" && just --summary 2>/dev/null | tr ' ' '\n') || true)
elif [[ -f "${dir}/Makefile" || -f "${dir}/makefile" || -f "${dir}/GNUmakefile" ]]; then
runner="make" runner_type="make"
local mk mkfiles=()
for mk in Makefile makefile GNUmakefile; do
[[ -f "${dir}/${mk}" ]] && mkfiles+=("${dir}/${mk}")
done
runner_targets=$(sed -n 's/^\([A-Za-z0-9_][A-Za-z0-9_.-]*\):\([^=].*\|\)$/\1/p' "${mkfiles[@]}" 2>/dev/null | sort -u || true)
fi
if [[ -n "${runner}" && -n "${runner_targets}" ]]; then
_pick_target() {
local c
for c in "$@"; do
if grep -qx "${c}" <<<"${runner_targets}"; then
echo "${runner} ${c}"
return 0
fi
done
echo ""
}
local r_build r_test r_check r_lint r_fmt
r_build=$(_pick_target build compile)
r_test=$(_pick_target test tests unit)
r_check=$(_pick_target check vet typecheck build)
r_lint=$(_pick_target lint fmt-check)
r_fmt=$(_pick_target fmt format)
if [[ -n "${r_build}${r_test}${r_check}${r_lint}${r_fmt}" ]]; then
echo "{\"type\":\"${runner_type}\",\"build\":\"${r_build}\",\"test\":\"${r_test}\",\"check\":\"${r_check}\",\"lint\":\"${r_lint}\",\"fmt\":\"${r_fmt}\"}"
return 0
fi
fi
# Rust # Rust
if [[ -f "${dir}/Cargo.toml" ]]; then if [[ -f "${dir}/Cargo.toml" ]]; then
echo '{"type":"rust","build":"cargo build","test":"cargo test","check":"cargo check","lint":"cargo clippy --no-deps -- -D warnings","fmt":"cargo fmt"}' echo '{"type":"rust","build":"cargo build","test":"cargo test","check":"cargo check"}'
return 0 return 0
fi fi
# Go # Go
if [[ -f "${dir}/go.mod" ]]; then if [[ -f "${dir}/go.mod" ]]; then
local go_lint="" echo '{"type":"go","build":"go build ./...","test":"go test ./...","check":"go vet ./..."}'
if compgen -G "${dir}/.golangci.*" &>/dev/null && command -v golangci-lint &>/dev/null; then
go_lint="golangci-lint run"
fi
echo "{\"type\":\"go\",\"build\":\"go build ./...\",\"test\":\"go test ./...\",\"check\":\"go vet ./...\",\"lint\":\"${go_lint}\",\"fmt\":\"gofmt -w .\"}"
return 0 return 0
fi fi
@@ -107,25 +65,7 @@ _detect_heuristic() {
[[ -f "${dir}/pnpm-lock.yaml" ]] && pm="pnpm" [[ -f "${dir}/pnpm-lock.yaml" ]] && pm="pnpm"
[[ -f "${dir}/yarn.lock" ]] && pm="yarn" [[ -f "${dir}/yarn.lock" ]] && pm="yarn"
# Emit only scripts the manifest actually declares (same introspection echo "{\"type\":\"nodejs\",\"build\":\"${pm} run build\",\"test\":\"${pm} test\",\"check\":\"${pm} run lint\"}"
# contract as the runner tier: never guess a target into existence).
_pkg_script() {
local s
for s in "$@"; do
if jq -e --arg s "$s" '.scripts[$s] // empty' "${dir}/package.json" &>/dev/null; then
echo "${pm} run ${s}"
return 0
fi
done
echo ""
}
local p_build p_test p_check p_lint p_fmt
p_build=$(_pkg_script build compile)
p_test=$(_pkg_script test)
p_check=$(_pkg_script check typecheck tsc)
p_lint=$(_pkg_script lint)
p_fmt=$(_pkg_script fmt format prettier)
echo "{\"type\":\"nodejs\",\"build\":\"${p_build}\",\"test\":\"${p_test}\",\"check\":\"${p_check}\",\"lint\":\"${p_lint}\",\"fmt\":\"${p_fmt}\"}"
return 0 return 0
fi fi
@@ -142,7 +82,7 @@ _detect_heuristic() {
check_cmd="uv run ruff check ." check_cmd="uv run ruff check ."
fi fi
echo "{\"type\":\"python\",\"build\":\"\",\"test\":\"${test_cmd}\",\"check\":\"${check_cmd}\",\"lint\":\"${check_cmd}\",\"fmt\":\"ruff format .\"}" echo "{\"type\":\"python\",\"build\":\"\",\"test\":\"${test_cmd}\",\"check\":\"${check_cmd}\"}"
return 0 return 0
fi fi
@@ -204,6 +144,17 @@ _detect_heuristic() {
return 0 return 0
fi fi
# Generic build systems (last resort before LLM)
if [[ -f "${dir}/justfile" ]] || [[ -f "${dir}/Justfile" ]]; then
echo '{"type":"just","build":"just build","test":"just test","check":"just lint"}'
return 0
fi
if [[ -f "${dir}/Makefile" ]] || [[ -f "${dir}/makefile" ]] || [[ -f "${dir}/GNUmakefile" ]]; then
echo '{"type":"make","build":"make build","test":"make test","check":"make lint"}'
return 0
fi
return 1 return 1
} }
@@ -267,9 +218,7 @@ _detect_with_llm() {
local prompt local prompt
prompt=$(cat <<-EOF prompt=$(cat <<-EOF
Analyze this project directory and determine the project type, primary language, and the correct shell commands to build, test, check (typecheck/vet), lint, and format it. Analyze this project directory and determine the project type, primary language, and the correct shell commands to build, test, and check (lint/typecheck) it.
PRIORITY RULE: if the project declares its own task-runner interface (a Taskfile, justfile, Makefile, package.json scripts, or similar), those declared targets ARE the correct commands — prefer them over generic ecosystem defaults, and never invent a target the interface does not declare.
EOF EOF
) )
@@ -277,25 +226,25 @@ _detect_with_llm() {
prompt+=$(cat <<-EOF prompt+=$(cat <<-EOF
Respond with ONLY a valid JSON object. No markdown fences, no explanation, no extra text. Respond with ONLY a valid JSON object. No markdown fences, no explanation, no extra text.
The JSON must have exactly these 6 keys: The JSON must have exactly these 4 keys:
{"type":"<language>","build":"<build command>","test":"<test command>","check":"<typecheck/vet command>","lint":"<lint command>","fmt":"<format command>"} {"type":"<language>","build":"<build command>","test":"<test command>","check":"<lint or typecheck command>"}
Rules: Rules:
- "type" must be a single lowercase word (e.g. rust, go, python, nodejs, java, ruby, elixir, cpp, c, zig, haskell, scala, kotlin, dart, swift, php, dotnet, etc.) - "type" must be a single lowercase word (e.g. rust, go, python, nodejs, java, ruby, elixir, cpp, c, zig, haskell, scala, kotlin, dart, swift, php, dotnet, etc.)
- If a command doesn't apply to this project, use an empty string, "" — NEVER guess a command that might not exist; a wrongly-guessed command is worse than an empty one - If a command doesn't apply to this project, use an empty string, ""
- Use the most standard/common commands for the detected ecosystem - Use the most standard/common commands for the detected ecosystem
- If you detect a package manager lockfile, use that package manager (e.g. pnpm over npm) - If you detect a package manager lockfile, use that package manager (e.g. pnpm over npm)
EOF EOF
) )
local llm_response local llm_response
llm_response=$(coyote --no-stream "${prompt}" 2>/dev/null) || return 1 llm_response=$(loki --no-stream "${prompt}" 2>/dev/null) || return 1
llm_response=$(echo "${llm_response}" | sed 's/^```json//;s/^```//;s/```$//' | tr -d '\n' | sed 's/^[[:space:]]*//') llm_response=$(echo "${llm_response}" | sed 's/^```json//;s/^```//;s/```$//' | tr -d '\n' | sed 's/^[[:space:]]*//')
llm_response=$(echo "${llm_response}" | grep -o '{[^}]*}' | head -1) llm_response=$(echo "${llm_response}" | grep -o '{[^}]*}' | head -1)
if echo "${llm_response}" | jq -e '.type and .build != null and .test != null and .check != null' &>/dev/null; then if echo "${llm_response}" | jq -e '.type and .build != null and .test != null and .check != null' &>/dev/null; then
echo "${llm_response}" | jq -c '{type: (.type // "unknown"), build: (.build // ""), test: (.test // ""), check: (.check // ""), lint: (.lint // ""), fmt: (.fmt // "")}' echo "${llm_response}" | jq -c '{type: (.type // "unknown"), build: (.build // ""), test: (.test // ""), check: (.check // "")}'
return 0 return 0
fi fi
@@ -309,7 +258,7 @@ detect_project() {
local cached local cached
if cached=$(_read_project_cache "${dir}"); then if cached=$(_read_project_cache "${dir}"); then
echo "${cached}" | jq -c '{type, build, test, check, lint: (.lint // ""), fmt: (.fmt // "")}' echo "${cached}" | jq -c '{type, build, test, check}'
return 0 return 0
fi fi
@@ -337,31 +286,6 @@ detect_project() {
echo '{"type":"unknown","build":"","test":"","check":""}' echo '{"type":"unknown","build":"","test":"","check":""}'
} }
# resolve_gate_dir maps a workspace root to the directory verification gates
# must run in. A delivery-repo worker's workspace root holds only dotfiles
# plus the clone, so gates aimed at the root detect nothing and silently
# no-op. When the root has no project markers and exactly ONE first-level
# git repo exists, gates run inside it; anything ambiguous stays at the root.
resolve_gate_dir() {
local dir="${1:-.}"
local m
for m in Taskfile.yml Taskfile.yaml taskfile.yml Cargo.toml go.mod package.json pyproject.toml setup.py pom.xml build.gradle mix.exs Gemfile composer.json Makefile justfile Justfile CMakeLists.txt; do
if [[ -e "${dir}/${m}" ]]; then
echo "${dir}"
return 0
fi
done
local repos=() d
for d in "${dir}"/*/; do
[[ -d "${d}/.git" ]] && repos+=("${d}")
done
if [[ ${#repos[@]} -eq 1 ]]; then
echo "${repos[0]%/}"
return 0
fi
echo "${dir}"
}
########################### ###########################
## FILE SEARCH UTILITIES ## ## FILE SEARCH UTILITIES ##
########################### ###########################
-159
View File
@@ -1,159 +0,0 @@
# Adversary
> [!IMPORTANT]
> **v2: now a GRAPH agent.** The plan's acceptance criteria are extracted into a list and the graph
> fans out ONE fresh-context verification branch per criterion — partial coverage ("checked 8 of 12")
> is structurally impossible. Each verdict passes a deterministic contract gate (MET requires
> evidence including a proving test; no test = at best PARTIAL), a holistic ADDITIVE-only pass hunts
> absence/scope-drift/substitution/gamed-tests, and the CONFORMS/DIVERGES sentinel is computed by a
> script (missing plan = DIVERGES, fail-closed). The conformance doctrine below is structurally
> enforced: CONFORMS requires ALL MET + zero holistic complaints + zero pipeline faults +
> zero red verification runs (exit≠0/timeout/skipped, or a runner error when commands were declared).
An **adversarial plan-conformance reviewer**. Where [`code-reviewer`](../code-reviewer/README.md)
asks *"is this code good?"*, `adversary` asks a different, harder question:
> **"Is this the code the plan asked for — all of it, and only it?"**
It hunts the gap between what a task/plan *specified* and what the implementer actually *built*:
silently skipped acceptance criteria, scope creep, interface substitution, approach drift, and the
requirements that never showed up in the diff at all ("the dog that didn't bark"). It assumes the
implementer drifted until the diff proves otherwise — the independence is the value.
## Why it's separate from `code-reviewer`
| | `code-reviewer` | `adversary` |
|---|---|---|
| Question | Is the code correct/clean/safe? | Does the code match the plan? |
| Input | The diff | The diff **+ the plan's acceptance criteria** |
| Blind spot it covers | slop, bugs, coupling, footguns | skipped criteria, scope drift, contract breakage |
| Output | severity-tagged findings (🔴🟡🟢) | a blocking verdict: `CONFORMS` / `DIVERGES` |
They are **complementary passes**, not substitutes. `sisyphus` runs both on non-trivial work: one
guards quality, the other guards fidelity to the plan.
## Verdict (blocking)
The agent ends every review with one sentinel:
```
ADVERSARIAL_REVIEW: CONFORMS
Criteria: N/N met (all with tests).
```
CONFORMS is deterministic: ALL MET + zero holistic complaints + zero pipeline faults +
zero red verification runs (exit≠0/timeout/skipped, or a runner error when commands were declared).
```
ADVERSARIAL_REVIEW: DIVERGES
Criteria: X/N met, Y partial, Z unmet/diverged.
Complaints:
1. Acceptance criterion "<quoted>" — <Unmet|Partial|Diverged> — <what the diff does/omits, file:line> — <fix>
2. ...
```
Every verdict — CONFORMS or DIVERGES — ends with a `Verification runs:` section: the recorded
PASS/FAIL results (command, exit code, duration, failure tail) of the caller-declared verification
commands, or an explicit marker when none were declared or the run never reached that stage. The
one exception is the verdict script's own crash guard, which emits a bare DIVERGES with a
"verdict computation error" line — and no section — when verdict computation itself dies.
A `DIVERGES` verdict **blocks** completion. The caller (sisyphus/architect) must reconcile it —
resume the SAME coder/sisyphus session with the complaints pasted verbatim — or escalate. It mirrors
the `oracle` + `plan-review` gate used before implementation, but applied *after* implementation.
Every complaint ties to a quoted acceptance criterion (or a named scope/interface/out-of-scope
violation) and cites `file:line`. Vague complaints are not emitted.
## How it reviews
```mermaid
flowchart TD
A["parse (llm): extract acceptance criteria"] --> B["facts (diff_facts.py): resolve + bound the diff"]
V["verification_commands (declared graph variable — never parsed from the prompt)"] --> R
B --> R["run_checks (script): run the declared verification commands ONCE, record results"]
R --> C["map over criteria: one fresh-context branch each (parallel)"]
C --> D["check_criterion (llm + fs/ast tools)"]
D --> E["crit_gate (script): MET needs change+test evidence"]
E -. "reject-retry once; exhausted = PARTIAL" .-> D
E --> F["holistic (llm): ADDITIVE-only hunt - absence, scope drift, substitution, gamed tests"]
F --> G["verdict (script): CONFORMS iff ALL MET + zero extras + zero faults + zero red runs; no criteria = DIVERGES"]
G --> H(["ADVERSARIAL_REVIEW: CONFORMS | DIVERGES"])
A -. "fallback" .-> P["pipeline_fault (script): record PIPELINE-FAULT marker"]
B -. "fallback" .-> P
R -. "fallback" .-> P
D -. "fallback" .-> CF["criterion_fault (script): UNMET + PIPELINE-FAULT evidence for that ONE criterion"]
CF -. "map join" .-> F
F -. "fallback" .-> G
P --> G
```
The conformance doctrine is baked into the graph's own nodes:
1. **Every** acceptance criterion gets its own fresh-context verification branch — MET / PARTIAL / UNMET / DIVERGED, machine-gated (MET requires the satisfying change AND the proving test, cited). No test ⇒ at best PARTIAL. Partial coverage of the criteria list is structurally impossible.
2. Ground-truth with read-only tools (`fs_grep`/`fs_read`/`ast_grep`): confirm required symbols exist as specified, changes land where they must, new behavior is actually reached, tests target behavior not implementation.
3. Execution evidence is recorded **once, deterministically**: `run_checks` runs ONLY the caller-declared verification commands (never invented or auto-detected), sequentially under a 3300s total deadline — commands the deadline leaves no budget for are recorded as SKIPPED. Criterion branches have no execution tools by design: "tests pass"-style criteria are judged against the record — a recorded green run is MET citing it; no declared command ⇒ at best PARTIAL. Independently of the criterion judgments, the verdict script blocks CONFORMS on any red run (exit≠0/timeout/skipped) and on an ENVIRONMENT runner-error marker when commands were declared — the declared commands never ran, so the run is unproven. The commands arrive through the declared `verification_commands` graph variable (see [Declaring verification commands](#declaring-verification-commands)) — `parse` never extracts them from the prompt, so the prompt is no longer an extraction source for the commands.
4. Hunt adversarially for the **absent**: skipped criteria, scope creep, interface/approach substitution, out-of-scope touches, downstream contract breakage.
5. **Fail closed on pipeline faults**: if parse, diff resolution, or the verification runner dies, its fallback routes to `pipeline_fault`, which records a `PIPELINE-FAULT:` marker and continues to the verdict (a failed holistic pass falls back straight to the verdict, which synthesizes the same marker). Any recorded fault forces DIVERGES with the fault as complaint #1 — degraded is never passing, and the sentinel is always emitted: the graph never dies without one.
6. **Fail closed per criterion**: a criterion check that dies (max_iterations exhausted, API failure) falls back to `criterion_fault`, which records a schema-shaped UNMET verdict carrying a `PIPELINE-FAULT: criterion ` evidence marker for that one criterion. The other criteria still get verdicts, the report says the check DIED rather than judged, and the run is DIVERGES. That exact marker prefix (the one `criterion_fault` emits) forces the criterion into unmet regardless of the status it carries, so a verdict that echoes it with `MET` can never count toward CONFORMS; `check_criterion` is told never to begin its own evidence with `PIPELINE-FAULT:`.
It is **read-only** — it produces a verdict, never a fix.
## Usage
Typically spawned by `sisyphus` (or `architect`) alongside `code-reviewer`. The spawn prompt IS its
entire context, so it must include the diff (or a base ref to fetch) **and** the acceptance criteria.
Verification commands travel separately, as a declared variable:
```sh
agent__spawn --agent adversary --prompt "
## TASK
Adversarially review the recent changes for TASK-NNN against its plan. Return CONFORMS/DIVERGES.
## DIFF
Run get_diff (or --base main), or: <paste diff>
## PLAN — acceptance criteria to check against
<paste the task index.md body + the relevant PLAN-*.md section, verbatim>
" --variables {"verification_commands": "[\"cargo test --all\", \"cargo clippy -- -D warnings\"]"}
```
Direct invocation for ad-hoc use:
```sh
coyote -a adversary --agent-variable project_dir /path/to/repo \
--agent-variable verification_commands '["cargo test --all"]' \
"Review staged changes against these criteria: <paste criteria>"
```
### Declaring verification commands
`verification_commands` is a declared graph variable (default `'[]'`): a JSON array of shell
commands, passed as a string because agent variables always arrive as strings. `run_checks` runs
exactly those commands with a shell — so the declaration is a **trust boundary**. The prompt is the
wrong side of it: it also carries plan and diff text pasted from the repo under review, and an LLM
`parse` node extracting commands from that prose would let anything in the repo's text run under
the reviewer's shell. Hence the commands are never in the prompt and never in `parse`'s output
schema; `review-gauntlet` forwards its own `verification_commands` variable to the adversary lane
as a structured `inputs:` passthrough, not as prompt text.
- `'[]'` (or omitted) ⇒ the "none declared" marker; execution criteria are at best PARTIAL.
- Anything that is not a JSON array of strings (unparsable JSON, an object, non-string items) is
an invalid declaration: `run_checks` executes **nothing** and records a
`PIPELINE-FAULT: verification_commands declaration invalid — …` marker, which forces DIVERGES:
it fails closed as a PIPELINE-FAULT (complaint #1) — it never degrades to an ENVIRONMENT marker.
- The engine merges only the keys declared under `parse`'s `output_schema.properties` into state,
so an injected instruction that makes the parse LLM emit an extra `verification_commands` key
cannot reach state: the undeclared key is dropped and the declared variable stands.
### Tools
- The graph's `diff_facts` script resolves the diff itself (staged → unstaged → `HEAD~1`, or an explicit ref/range named in the prompt) — there is no `get_diff` tool anymore.
- The `run_checks` script executes the commands declared via the `verification_commands` variable and records the results — criterion branches cite the record and never run anything themselves.
- Criterion branches carry read-only `fs_read`/`fs_cat`/`fs_grep`/`ast_grep` for ground-truth checks.
## Related
- [`code-reviewer`](../code-reviewer/README.md) — the quality reviewer it runs alongside.
- [`plan-review`](../../skills/plan-review/SKILL.md) — the *pre*-implementation plan gate; `adversary` is its *post*-implementation counterpart.
-405
View File
@@ -1,405 +0,0 @@
name: adversary
description: |
Graph-based adversarial plan-conformance reviewer. The plan's acceptance
criteria are extracted into a list and the graph mechanically fans out ONE
fresh-context verification branch per criterion — "checked 8 of 12 and waved
the rest through" becomes structurally impossible. Each criterion verdict
(MET / PARTIAL / UNMET / DIVERGED) passes a deterministic contract gate
(MET requires evidence including a test proving the behavior — no test means
at best PARTIAL). A holistic pass then hunts what per-criterion checks cannot
see: absence (the dog that didn't bark), scope drift, interface substitution,
silently-overridden recorded decisions, out-of-scope touches, and gamed
tests — ADDITIVE-only: it can add complaints, never flip a criterion verdict.
The final CONFORMS/DIVERGES sentinel is computed by a script: CONFORMS
requires every criterion MET, zero holistic complaints, zero pipeline faults,
and zero red verification runs (exit≠0/timeout/skipped, or a runner error
when commands were declared). Missing plan = DIVERGES (fail-closed) naming
the missing spec. Caller-declared verification
commands are executed ONCE by a deterministic stage and recorded — criterion
branches cite the record and never run anything themselves. Any pipeline
fault (parse, diff resolution, verification runner, or holistic failure)
lands a PIPELINE-FAULT marker and fails closed into DIVERGES: degraded is
never passing.
version: '1.0'
global_tools:
- ast_grep.sh
- fs_read.sh
- fs_cat.sh
- fs_grep.sh
variables:
- name: project_dir
description: Project directory containing the changes under review
default: '.'
- name: verification_commands
description: |
JSON array of shell commands the CALLER declares for the deterministic
run_checks stage — e.g. '["cargo test --all"]'. Never inferred from the
prompt; empty array = none declared. Must not boot shared stacks or
services (compose projects, fixed ports) — those collide with the probe
lane running in parallel.
default: '[]'
settings:
max_loop_iterations: 10
log_state_snapshots: false
# 3600 (run_checks node timeout, worst case) + 5400 (the previous
# whole-graph budget, kept as the envelope for every stage other than
# run_checks: parse/facts before it, criteria map/holistic/verdict after)
# = 9000. A graph timeout kills from outside — no fallback fires and the
# sentinel is lost.
timeout: 9000
initial_state:
project_dir_in: ''
parse_retries: 0
criteria: []
diff_spec: auto
plan_notes: ''
scope_notes: ''
extra_context: ''
changed_files: []
diff_text: ''
diff_note: ''
exec_results: ''
crit_verdicts: []
extra_complaints: []
observations: ''
adv_report: ''
gate_feedback: ''
gate_attempts: 0
parse_failure: ''
holistic_failure: ''
pipeline_faults: []
start: parse
nodes:
parse:
id: parse
type: llm
description: Extract the acceptance criteria and plan facts from the spawn prompt. Extraction only — no judging, no criteria invented.
instructions: |
You extract a plan-conformance review request into structured fields.
EXTRACTION ONLY — never invent criteria, never guess intent.
- `criteria`: every acceptance criterion from the plan/task, one entry each,
with a short id (c1, c2, …) and SELF-CONTAINED text (what observable
behavior is promised, including any test expectations the plan states).
Split compound criteria ("A and B") into separate entries when they are
independently verifiable. When the caller supplied NO plan/criteria at
all, emit an empty array — downstream fails closed.
An empty array is ONLY correct when the prompt genuinely contains no
plan/criteria section — when the prompt carries one (a "Plan /
acceptance criteria:" or "PLAN —" block), returning zero criteria is
an extraction FAILURE, never a valid result.
The caller's explicit enumeration is AUTHORITATIVE: when the caller
declares "these N are the (only) criteria" or supplies a numbered
criteria list, extract EXACTLY those N — never add entries they did
not enumerate. NEVER mine criteria from sections the caller labels as
context ("FACTS", "Quality bar", "context, not criteria", background
material) — route those into plan_notes / scope_notes / extra_context
instead. Splitting applies only WITHIN a declared criterion; when in
doubt, FEWER criteria, verbatim.
- `diff_spec`: the git diff basis the caller named — a range, a ref/SHA,
"staged", "worktree", or "auto" when unnamed (a caller phrase like
"get_diff --base X" means diff_spec = "X").
- `project_dir_in`: the directory/worktree path the caller named as
holding the code under review; empty string when the caller named none.
- `plan_notes`: recorded decisions the plan states ("do X not Y because Z"),
verbatim — the holistic pass checks the diff didn't silently override them.
- `scope_notes`: what the plan says NOT to touch / explicit boundaries.
- `extra_context`: anything else conformance-relevant, condensed.
prompt: |
Extract the conformance-review facts from this request:
{{initial_prompt}}
tools: []
output_schema:
type: object
properties:
criteria:
type: array
items:
type: object
properties:
id:
type: string
text:
type: string
required: [id, text]
diff_spec:
type: string
plan_notes:
type: string
scope_notes:
type: string
extra_context:
type: string
project_dir_in:
type: string
required: [criteria, diff_spec, plan_notes, scope_notes, extra_context, project_dir_in]
max_iterations: 3
max_attempts: 2
state_updates:
parse_failure: '{{output}}'
fallback: pipeline_fault
next: facts
facts:
id: facts
type: script
description: Deterministic diff resolution — changed files and the (bounded) diff text every criterion branch verifies against.
script: scripts/diff_facts.py
timeout: 120
fallback: pipeline_fault
next: run_checks
run_checks:
id: run_checks
type: script
description: Deterministic execution evidence — runs ONLY the commands declared via the verification_commands variable in the project dir and records exec_results; criterion branches cite the record and never execute anything. Undeclared commands degrade to a "none declared" marker, runner errors to an ENVIRONMENT marker (criterion branches judge it PARTIAL/unproven; the verdict additionally blocks CONFORMS when commands were declared); a declaration that is not a JSON array of strings executes nothing and records a PIPELINE-FAULT (fail-closed); a 3300s total deadline keeps the script under the node timeout (commands past it are recorded as skipped), and if the node is killed anyway the fallback records a PIPELINE-FAULT.
script: scripts/run_checks.py
timeout: 3600
fallback: pipeline_fault
next: map_criteria
map_criteria:
id: map_criteria
type: map
over: '{{criteria}}'
as: criterion
branch: check_criterion
output_key: crit_verdict
collect_into: crit_verdicts
max_concurrency: 6
next: holistic
check_criterion:
id: check_criterion
type: llm
description: Fresh-context verification of ONE acceptance criterion against the diff and the working tree (ENVIRONMENT/SKIPPED runs judge PARTIAL here; the deterministic verdict blocks CONFORMS when declared commands did not run).
instructions: |
You verify ONE acceptance criterion against a code change. Fresh context,
one criterion, full suspicion: assume the implementer drifted, cut a
corner, or misread the plan until the diff proves otherwise.
Classification (exactly one):
- `MET` — the diff satisfies the criterion AND a test proves the promised
behavior. Evidence MUST cite both: the satisfying change (file:line) and
the test targeting the behavior (file:line). No test ⇒ NOT MET — at best
PARTIAL (a criterion is a promise of observable behavior; unproven
behavior is unverified behavior).
- `PARTIAL` — partially satisfied, or satisfied but unproven (missing/weak
test), or you could not establish it either way (say why — doubt lands
here, never in MET).
- `UNMET` — the plan requires it and the diff does not do it. Absence is a
finding: review what SHOULD be there, not only what IS.
- `DIVERGED` — the diff does something ELSE instead (interface substituted,
approach swapped, criterion reinterpreted).
Ground-truth discipline: fs_grep the symbols the criterion names (confirm
they exist spelled as specified), fs_read around the relevant hunks to
confirm the change makes the criterion true, confirm the test targets
BEHAVIOR (not implementation echo), use ast_grep for structural claims.
Watch for gamed evidence: a test weakened/deleted so it passes, hardcoded
values shaped to the criterion's example, TODO/stub presented as done, a
mock expectation whose count is relaxed (`AnyTimes`, `MinTimes`, ignored
request body) AND whose matcher is permissive (`gomock.Any()`,
`mock.Anything`) — it proves a call happened, never that it carried the
criterion's values, so it is not a proving test —
those are PARTIAL or DIVERGED with the gaming named in the complaint.
Retry / redelivery / idempotency demands: before you mark a criterion PARTIAL
or UNMET for lacking a retry, re-run, or "second attempt emits nothing" spec,
classify the handler. STATEFUL — it carries something across attempts (emits
events, calls an external system, performs an unconditional write, keeps an
in-memory or ledger record) — a retry spec is a legitimate demand. STATELESS —
each attempt is a pure function of the request plus a conditional store
predicate (`WHERE status = 'pending'`, `ended_at IS NULL`, `INSERT … ON
CONFLICT DO NOTHING`) and nothing is emitted or ledgered outside that
predicate — then "retry produces no event" is a tautology of the mechanism,
not a missing behavior: demand at most one test of the predicate itself,
never a retry spec. Name the classification and its file:line evidence in
the complaint either way.
Execution criteria ("tests pass", "lint clean", "build succeeds") are
judged ONLY against the recorded verification runs in your prompt — you
have no execution tools BY DESIGN and must never claim to have run
anything yourself:
- a recorded green run (exit 0) of a command covering the criterion is
the proving evidence — MET, citing that run (the command + exit 0).
- a recorded failing run covering the criterion ⇒ UNMET, citing the
recorded exit code and output tail.
- "none declared" or an ENVIRONMENT marker ⇒ at best PARTIAL, with an
ENVIRONMENT note in the complaint naming the exact command the caller
should declare via the `verification_commands` variable (structured
input, e.g. "cargo test --all") — commands are never declared in the
prompt.
- a SKIPPED record (the runner's total deadline expired before that
command ran) ⇒ at best PARTIAL for the criteria it covers, same
treatment as an ENVIRONMENT marker: unproven at the criterion level.
The deterministic verdict additionally blocks CONFORMS when declared
commands did not run — that is its job, not yours.
Output format — respond with ONLY this JSON object, no prose around it:
{"id": "<the criterion's id, echoed>",
"status": "MET" | "PARTIAL" | "UNMET" | "DIVERGED",
"evidence": "<for MET: the satisfying change file:line AND the proving test
file:line, quoted decisively; otherwise the closest relevant citations or empty>",
"complaint": "<required for non-MET: what the diff does/omits (file:line) and
what would make it conform; empty string for MET>"}
Your response is machine-validated: a malformed response is rejected back
to you exactly once with the reason, then recorded as PARTIAL (unproven).
The id is stamped from the criterion itself — your echo is informative only.
You have a bounded tool budget — prefer fs_grep and line-ranged fs_read over
whole-file reads, and conclude as soon as the evidence is decisive.
Never begin `evidence` with the literal `PIPELINE-FAULT:` — that prefix is
reserved for the graph's own fault markers.
prompt: |
Repository: {{project_dir}}
Changed files: {{changed_files}}
Criterion to verify (JSON):
{{criterion}}
Plan-recorded decisions (context): {{plan_notes}}
{{extra_context}}
The diff{{diff_note}}:
{{diff_text}}
Verification runs (recorded by the deterministic run_checks stage):
{{exec_results}}
{{gate_feedback}}
tools:
- fs_read
- fs_cat
- fs_grep
- ast_grep
max_iterations: 30
max_attempts: 2
state_updates:
crit_verdict: '{{output}}'
fallback: criterion_fault
next: crit_gate
crit_gate:
id: crit_gate
type: script
description: Deterministic contract gate per criterion verdict — status enum, MET requires evidence, non-MET requires a complaint; reject-and-retry once, then record PARTIAL (unproven). Never sinks the map.
script: scripts/crit_gate.py
timeout: 30
criterion_fault:
id: criterion_fault
type: script
description: Fail-closed per-criterion fault marker (reached only via check_criterion's fallback) — converts the captured "LLM node failed" text into a schema-shaped UNMET verdict carrying a PIPELINE-FAULT evidence marker, so one dead criterion check can never sink the map or read as met.
script: scripts/criterion_fault.py
timeout: 30
holistic:
id: holistic
type: llm
description: Cross-criterion adversarial pass — hunts what per-criterion checks cannot see. ADDITIVE-only; cannot flip criterion verdicts.
instructions: |
You run the holistic adversarial pass over a whole change AFTER each
acceptance criterion was independently verified. You hunt what
per-criterion checks cannot see. You are ADDITIVE-ONLY: you may add
complaints; you may NOT flip, soften, or re-judge any criterion verdict.
The checklist (each hit becomes one complaint with kind, file:line
location, the violation, and the fix):
- **Absence** — something the PLAN requires that maps to no criterion and
never showed up in the diff (the dog that didn't bark). A missing
retry/redelivery spec is an absence ONLY for a handler that carries state
or side effects across attempts; a handler that is a pure function of the
request plus a conditional store predicate owes none.
- **Scope drift / out-of-scope** — the diff touches what the plan's scope
notes say not to touch, or quietly does extra unplanned work.
- **Interface/approach substitution** — the plan named an interface,
library, or approach; the diff substituted another without the plan
recording that decision.
- **Silently overridden recorded decision** — plan says "do X not Y
because Z"; diff does Y.
- **Downstream contract breakage** — the diff changes a contract the plan
promised to preserve.
- **Gamed tests** — tests weakened, deleted, or shaped (hardcoded
expectations, skipped assertions) so the criteria APPEAR met.
- **The plan itself is the culprit** — impossible or self-contradictory
requirements: that is a complaint naming the PLAN as root cause; never
silently judge against a corrected version of it.
Be terse and decisive: three real complaints beat fifteen weak ones. A
complaint with no location and no criterion/scope reference is noise — do
not emit it. When everything is a nitpick, emit zero complaints and put
1-3 non-blocking notes in `observations`.
prompt: |
Repository: {{project_dir}}
Changed files: {{changed_files}}
Acceptance criteria: {{criteria}}
Per-criterion verdicts (already final): {{crit_verdicts}}
Plan-recorded decisions: {{plan_notes}}
Scope boundaries: {{scope_notes}}
{{extra_context}}
The diff{{diff_note}}:
{{diff_text}}
tools:
- fs_read
- fs_cat
- fs_grep
- ast_grep
output_schema:
type: object
properties:
extra_complaints:
type: array
items:
type: object
properties:
kind:
type: string
location:
type: string
violation:
type: string
fix:
type: string
required: [kind, location, violation, fix]
observations:
type: string
required: [extra_complaints, observations]
max_iterations: 45
max_attempts: 2
state_updates:
holistic_failure: '{{output}}'
fallback: verdict
next: verdict
pipeline_fault:
id: pipeline_fault
type: script
description: Fail-closed fault marker (reached only via fallback routes from parse/facts/run_checks) — records a PIPELINE-FAULT in state and continues to verdict, which turns any fault into DIVERGES. The graph never dies without its sentinel.
script: scripts/pipeline_fault.py
timeout: 30
next: verdict
verdict:
id: verdict
type: script
description: Deterministic verdict — CONFORMS requires every criterion MET, zero holistic complaints, zero pipeline faults, and zero recorded red verification runs (exit≠0/timeout/skipped, or an ENVIRONMENT runner-error marker when commands were declared); no criteria at all fails closed and any recorded PIPELINE-FAULT forces DIVERGES (degraded is never passing). Assembles the exact sentinel format.
script: scripts/verdict.py
timeout: 30
next: done
done:
id: done
type: end
output: |
{{adv_report}}
@@ -1,148 +0,0 @@
#!/usr/bin/env python3
"""Contract gate for the adversary's per-criterion verify chain.
Validates each criterion verdict against the contract:
- a single JSON object with status in {MET, PARTIAL, UNMET, DIVERGED}
- MET requires non-empty evidence (the change AND the proving test)
- non-MET requires a non-empty complaint
The authoritative id is stamped from the criterion item — the model's echo is
never trusted. Malformed output is rejected back to check_criterion exactly
once with the reason; a second failure records PARTIAL (unproven) so a
malformed item can never sink the map or masquerade as MET.
"""
import json
import os
import re
STATUSES = {"MET", "PARTIAL", "UNMET", "DIVERGED"}
MAX_RETRIES = 1
def load_state():
if path := os.environ.get("GRAPH_STATE_FILE"):
with open(path) as f:
return json.load(f)
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
def criterion_fields(state):
c = state.get("criterion")
if isinstance(c, dict):
cid = c.get("id")
text = c.get("text")
return (
cid.strip() if isinstance(cid, str) and cid.strip() else "unknown",
text.strip() if isinstance(text, str) else "",
)
return "unknown", ""
def extract_json(text):
if not isinstance(text, str) or not text.strip():
raise ValueError("criterion-check output is empty or not text")
text = re.sub(r"```[a-zA-Z]*", "", text).replace("```", "")
start, end = text.find("{"), text.rfind("}")
if start == -1 or end <= start:
raise ValueError("no JSON object found in criterion-check output")
return json.loads(text[start : end + 1])
def validate(obj):
if not isinstance(obj, dict):
raise ValueError("criterion-check output is not a JSON object")
status = obj.get("status")
if status not in STATUSES:
raise ValueError(f"status must be one of {sorted(STATUSES)}, got {status!r}")
evidence = obj.get("evidence") if isinstance(obj.get("evidence"), str) else ""
complaint = obj.get("complaint") if isinstance(obj.get("complaint"), str) else ""
if status == "MET" and not evidence.strip():
raise ValueError(
"MET requires non-empty evidence citing the satisfying change AND the "
"test proving the behavior (file:line)"
)
if status != "MET" and not complaint.strip():
raise ValueError(
f"{status} requires a non-empty complaint (what the diff does/omits at "
"file:line, and what would make it conform)"
)
return status, evidence, complaint
def main():
state = load_state()
cid, ctext = criterion_fields(state)
attempts = state.get("gate_attempts")
if not isinstance(attempts, int):
attempts = 0
try:
status, evidence, complaint = validate(extract_json(state.get("crit_verdict")))
print(
json.dumps(
{
"crit_verdict": {
"id": cid,
"text": ctext,
"status": status,
"evidence": evidence,
"complaint": complaint,
}
}
)
)
return
except Exception as e: # noqa: BLE001
reason = str(e)
if attempts < MAX_RETRIES:
print(
json.dumps(
{
"_next": "check_criterion",
"gate_attempts": attempts + 1,
"gate_feedback": (
"PREVIOUS RESPONSE REJECTED by the verdict gate: " + reason +
". Respond with ONLY the JSON object described in your instructions."
),
}
)
)
return
print(
json.dumps(
{
"crit_verdict": {
"id": cid,
"text": ctext,
"status": "PARTIAL",
"evidence": "",
"complaint": (
"criterion check output failed machine validation after retry: "
+ reason
+ " — treat this criterion as unproven"
),
}
}
)
)
try:
main()
except Exception as e: # noqa: BLE001 — the gate must never crash the chain
print(
json.dumps(
{
"crit_verdict": {
"id": "unknown",
"text": "",
"status": "PARTIAL",
"evidence": "",
"complaint": f"criterion gate error: {e} — treat as unproven",
}
}
)
)
@@ -1,94 +0,0 @@
#!/usr/bin/env python3
"""Fail-closed per-criterion fault marker for the adversary graph.
Reached ONLY via check_criterion's `fallback:` route, after the engine has
written its "LLM node …failed: <chain>" text into `crit_verdict` through
state_updates. Converts that text into a schema-shaped UNMET verdict whose
evidence carries a "PIPELINE-FAULT:" marker, so one criterion check that dies
(max_iterations exhausted, API failure) is recorded as DIED rather than
judged, the other criteria still get verdicts, and the run is DIVERGES. The
map collects it like any other verdict — the branch never sinks the map. The
engine applies state_updates before routing to the fallback (src/graph/llm.rs:
`apply_state_updates_with_output`, then `outcome_from`), so the "no failure
text captured" branch is belt-and-braces rather than an expected path.
"""
import json
import os
MAX_DETAIL_CHARS = 500
def load_state():
if path := os.environ.get("GRAPH_STATE_FILE"):
with open(path) as f:
return json.load(f)
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
def criterion_fields(state):
c = state.get("criterion")
if isinstance(c, dict):
cid = c.get("id")
text = c.get("text")
return (
cid.strip() if isinstance(cid, str) and cid.strip() else "unknown",
text.strip() if isinstance(text, str) else "",
)
return "unknown", ""
def failure_detail(state):
captured = state.get("crit_verdict")
detail = captured.strip() if isinstance(captured, str) else ""
if not detail.startswith("LLM node"):
return "no failure text captured"
detail = detail.replace("\n", " ")
if len(detail) > MAX_DETAIL_CHARS:
detail = detail[: MAX_DETAIL_CHARS - 1] + "…"
return detail
def main():
state = load_state()
cid, ctext = criterion_fields(state)
detail = failure_detail(state)
print(
json.dumps(
{
"crit_verdict": {
"id": cid,
"text": ctext,
"status": "UNMET",
"evidence": f"PIPELINE-FAULT: criterion check failed — {detail}",
"complaint": (
f"criterion check DIED (pipeline fault) — {detail}; the "
"criterion was NOT verified and is treated as unmet "
"(fail-closed)"
),
}
}
)
)
try:
main()
except Exception as e: # noqa: BLE001 — the fault marker itself must never crash
print(
json.dumps(
{
"crit_verdict": {
"id": "unknown",
"text": "",
"status": "UNMET",
"evidence": f"PIPELINE-FAULT: criterion fault-marker script error: {e}",
"complaint": (
f"criterion check DIED (pipeline fault) — fault-marker script "
f"error: {e}; the criterion was NOT verified and is treated as "
"unmet (fail-closed)"
),
}
}
)
)
@@ -1,93 +0,0 @@
#!/usr/bin/env python3
"""Deterministic diff resolution for the adversary graph.
Resolves the diff spec (get_diff semantics: staged → unstaged → HEAD~1 for
"auto"), lists changed files, and captures the bounded diff text every
criterion branch verifies against. Failures degrade into diff_note — the
review proceeds fail-closed rather than crashing.
"""
import json
import re
import os
import subprocess
MAX_DIFF_CHARS = 60000
def load_state():
if path := os.environ.get("GRAPH_STATE_FILE"):
with open(path) as f:
return json.load(f)
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
state = load_state()
# Zero criteria from a prompt that visibly carries a plan is an extraction
# failure (observed non-deterministically live), not a fail-closed no-plan
# run — re-run the parse ONCE before the verdict fails closed on it.
_crit = state.get("criteria") or []
_prompt = state.get("initial_prompt") or ""
if not _crit and isinstance(_prompt, str) and re.search(
r"acceptance criteria|PLAN\s*—|Plan / acceptance", _prompt, re.I
):
try:
_pr = int(state.get("parse_retries") or 0)
except (TypeError, ValueError):
_pr = 0
if _pr < 1:
print(json.dumps({"_next": "parse", "parse_retries": _pr + 1}))
raise SystemExit(0)
proj = os.path.expanduser(
(state.get("project_dir_in") or "").strip() or state.get("project_dir") or "."
)
spec = (state.get("diff_spec") or "auto").strip()
def git(*args):
r = subprocess.run(["git", "-C", proj, *args], capture_output=True, text=True, timeout=90)
if r.returncode != 0:
raise RuntimeError((r.stderr or r.stdout).strip()[:300])
return r.stdout
out = {"diff_note": "", "project_dir": proj}
try:
if spec in ("auto", ""):
text = git("diff", "--cached")
if not text.strip():
text = git("diff")
if not text.strip():
text = git("diff", "HEAD~1")
elif spec == "staged":
text = git("diff", "--cached")
elif spec == "worktree":
text = git("diff", "HEAD")
else:
text = git("diff", spec)
files = sorted({
line[6:].strip()
for line in text.splitlines()
if line.startswith("+++ b/")
})
out["changed_files"] = files[:300]
if len(files) > 300:
out["diff_note"] += (
f" (file list capped at 300 of {len(files)} changed files — use the fs "
"tools for the rest)"
)
if len(text) > MAX_DIFF_CHARS:
out["diff_note"] += (
f" (truncated to {MAX_DIFF_CHARS} chars of {len(text)} — use the fs tools "
"to inspect files beyond the cut)"
)
text = text[:MAX_DIFF_CHARS]
out["diff_text"] = text if text.strip() else "(empty diff — nothing changed)"
except Exception as e: # noqa: BLE001
out["changed_files"] = []
out["diff_text"] = "(diff unavailable)"
out["diff_note"] = f" (diff resolution failed for spec {spec!r}: {e})"
print(json.dumps(out))
@@ -1,74 +0,0 @@
#!/usr/bin/env python3
"""Fail-closed fault marker for the adversary graph.
Reached ONLY via `fallback:` routes (parse, facts, run_checks). Appends a
machine-readable "PIPELINE-FAULT:" marker to `pipeline_faults` and continues
to `verdict`, which turns any recorded fault into DIVERGES — the graph never
dies without its sentinel, and a degraded run can never read as CONFORMS.
Stage attribution — grounded in engine behavior: a SCRIPT node's fallback
route writes NOTHING into state (src/graph/executor.rs script arm routes to
the fallback without recording the error), so the two script stages are
discriminated by which stage outputs are present:
- parse (llm): its `state_updates` wrote an "LLM node …failed: <chain>"
string into `parse_failure` — attribute to parse.
- run_checks vs facts (scripts): diff_facts.py unconditionally writes a
NONEMPTY `diff_text` (even its internal-failure path writes
"(diff unavailable)"), while the graph's initial value is ''. A nonempty
`diff_text` proves facts completed, so the fault is the verification
runner (run_checks killed at the node level, e.g. by the node timeout);
an empty `diff_text` means facts itself died.
"""
import json
import os
MAX_DETAIL_CHARS = 500
def load_state():
if path := os.environ.get("GRAPH_STATE_FILE"):
with open(path) as f:
return json.load(f)
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
def main():
state = load_state()
faults = [f for f in (state.get("pipeline_faults") or []) if isinstance(f, str)]
parse_failure = state.get("parse_failure")
detail = parse_failure.strip() if isinstance(parse_failure, str) else ""
diff_text = state.get("diff_text")
facts_completed = isinstance(diff_text, str) and bool(diff_text.strip())
if detail.startswith("LLM node"):
detail = detail.replace("\n", " ")
if len(detail) > MAX_DETAIL_CHARS:
detail = detail[: MAX_DETAIL_CHARS - 1] + "…"
faults.append(f"PIPELINE-FAULT: parse failed — cannot review: {detail}")
elif facts_completed:
faults.append(
"PIPELINE-FAULT: verification runner killed — the run_checks stage "
"died at the node level before recording exec_results; execution "
"criteria are unproven"
)
else:
faults.append(
"PIPELINE-FAULT: diff resolution failed — changed files and diff text "
"could not be resolved; criterion verification never ran"
)
print(json.dumps({"pipeline_faults": faults}))
try:
main()
except Exception as e: # noqa: BLE001 — the fault marker itself must never crash
print(
json.dumps(
{
"pipeline_faults": [
f"PIPELINE-FAULT: fault-marker script error: {e} — treat the "
"run as degraded"
]
}
)
)
@@ -1,252 +0,0 @@
#!/usr/bin/env python3
"""Deterministic execution-evidence recorder for the adversary graph.
Runs ONLY the caller-declared verification commands (verification_commands)
in the project dir and records the outcomes as `exec_results` — a list of
{cmd, exit, duration_s, tail} records. Criterion branches cite this record;
they never execute anything themselves.
NO auto-detection: undeclared commands mean a "none declared" marker
(fail-visible, not fail-guessed). Any runner error degrades into an
ENVIRONMENT marker — this script never fails the node.
Trust boundary: `verification_commands` is a DECLARED graph variable (or the
review-gauntlet's structured `inputs:` passthrough), never a field an LLM
extracts from the spawn prompt — that prose also carries plan/diff text pasted
from the repo under review, and whatever is declared here runs with
shell=True. Variables land in state as strings, so a JSON-encoded list is
accepted alongside a real list. A declaration that is not a JSON array of
strings is a caller contract violation, not a runner hiccup: it executes
NOTHING and fails closed as a PIPELINE-FAULT (complaint #1) — it never
degrades to an ENVIRONMENT marker. The engine merges only the keys an
llm node's `output_schema.properties` declares, so a parse LLM coaxed into
emitting an extra `verification_commands` key cannot overwrite the declared
value: the undeclared key is dropped before it reaches state.
Budget: commands run sequentially, so TOTAL runtime is bounded by a deadline
(TOTAL_DEADLINE_SECS = 3300s, the run_checks node's 3600s timeout minus
margin). Without it, a handful of hanging commands (4 x 900s) would blow the
NODE timeout and the executor would kill this script from OUTSIDE — the
in-script degradation would never fire. Each command gets
min(PER_COMMAND_TIMEOUT_SECS, remaining budget); commands the deadline
leaves no budget for are recorded as {"cmd", "skipped": "deadline"} entries,
so the in-script handling stays authoritative. The env var
ADVERSARY_RUN_CHECKS_DEADLINE_SECS overrides the deadline (test seam), and
ADVERSARY_STACK_LOCK_PATH overrides the review-stack lock file location
(isolation seam — see acquire_stack_lock).
Process hygiene: commands run through the shell. On POSIX each command gets
its own session (start_new_session) and a timeout SIGKILLs the whole process
group, so grandchildren of a hung command cannot linger. The post-kill
reap/drain is itself bounded by DRAIN_TIMEOUT_SECS: a grandchild that escaped
the session (its own start_new_session) but still holds the stdout pipe
would otherwise block the drain until it exits. On non-POSIX platforms only
the direct shell child is killed — grandchildren of a timed-out command may
survive (documented limitation).
Verification commands run with GRAPH_STATE* (GRAPH_STATE, GRAPH_STATE_FILE)
scrubbed from their environment: the reviewed repo's own tests may exec these very
scripts with their own GRAPH_STATE, and load_state() prefers
GRAPH_STATE_FILE, so an inherited live state file would silently win.
"""
import json
import os
import signal
import subprocess
import sys
import time
try:
import fcntl
except ImportError: # non-POSIX (Windows): advisory flock unavailable
fcntl = None
PER_COMMAND_TIMEOUT_SECS = 900
TOTAL_DEADLINE_SECS = float(os.environ.get("ADVERSARY_RUN_CHECKS_DEADLINE_SECS") or 3300)
STACK_LOCK_PATH = os.environ.get("ADVERSARY_STACK_LOCK_PATH") or os.path.expanduser(
"~/.cache/coyote/review-stack.lock"
)
STACK_LOCK_WAIT_SECS = float(os.environ.get("ADVERSARY_STACK_LOCK_WAIT_SECS") or 1200)
def acquire_stack_lock():
"""Serialize with the probe lane's shared-stack work (advisory flock on a
well-known path; the probe is instructed to flock the same file around
stack boot/teardown). Bounded wait so a hogged lock can never turn into a
false red verification: on timeout we proceed WITHOUT the lock and note it
on stderr. The fd is held for the process lifetime (released on exit).
On non-POSIX platforms (no fcntl) the lock is skipped entirely."""
if fcntl is None:
sys.stderr.write("WARN: review-stack lock unsupported on this platform; running unlocked\n")
return None
try:
os.makedirs(os.path.dirname(STACK_LOCK_PATH), exist_ok=True)
fd = os.open(STACK_LOCK_PATH, os.O_CREAT | os.O_RDWR)
except OSError as e:
sys.stderr.write(f"WARN: review-stack lock unavailable ({e}); running unlocked\n")
return None
limit = time.monotonic() + STACK_LOCK_WAIT_SECS
while True:
try:
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
return fd
except OSError:
if time.monotonic() >= limit:
os.close(fd)
sys.stderr.write(
"WARN: review-stack lock still held after bounded wait — proceeding "
"WITHOUT it; shared-stack collisions with the probe lane are possible\n"
)
return None
time.sleep(5)
DRAIN_TIMEOUT_SECS = 10
TAIL_LINES = 50
MAX_TAIL_CHARS = 8000
def load_state():
if path := os.environ.get("GRAPH_STATE_FILE"):
with open(path) as f:
return json.load(f)
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
class InvalidDeclaration(ValueError):
pass
def declared_commands(declared):
"""Normalize the verification_commands declaration to a list of commands.
Accepts a JSON list or a JSON-encoded string of one; missing/None means
none declared. Anything else raises InvalidDeclaration.
"""
if declared is None:
return []
if isinstance(declared, str):
if not declared.strip():
raise InvalidDeclaration("empty string (declare '[]' for none)")
try:
declared = json.loads(declared)
except json.JSONDecodeError as e:
raise InvalidDeclaration(f"not valid JSON: {e}") from e
if not isinstance(declared, list):
raise InvalidDeclaration(
f"expected a JSON array of strings, got {type(declared).__name__}"
)
bad = [c for c in declared if not isinstance(c, str)]
if bad:
raise InvalidDeclaration(f"non-string item(s): {json.dumps(bad)}")
return [c.strip() for c in declared if c.strip()]
def tail_of(text):
tail = "\n".join(text.splitlines()[-TAIL_LINES:])
if len(tail) > MAX_TAIL_CHARS:
tail = "…" + tail[-MAX_TAIL_CHARS:]
return tail
def run_one(cmd, proj, budget):
"""Run one command, hard-bounded by `budget` seconds; return its record."""
start = time.monotonic()
popen_kwargs = {"start_new_session": True} if os.name == "posix" else {}
env = {k: v for k, v in os.environ.items() if not k.startswith("GRAPH_STATE")}
p = subprocess.Popen(
cmd,
shell=True,
cwd=proj,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
env=env,
**popen_kwargs,
)
try:
out_s, err_s = p.communicate(timeout=budget)
except subprocess.TimeoutExpired:
if os.name == "posix":
try:
os.killpg(os.getpgid(p.pid), signal.SIGKILL)
except (ProcessLookupError, PermissionError):
pass
else:
p.kill()
try:
p.communicate(timeout=DRAIN_TIMEOUT_SECS)
except subprocess.TimeoutExpired:
pass
return {
"cmd": cmd,
"exit": -1,
"duration_s": round(time.monotonic() - start, 1),
"tail": f"TIMEOUT: no exit within {budget:.0f}s — recorded as a failing run",
}
combined = (out_s or "") + (("\n" + err_s) if err_s else "")
return {
"cmd": cmd,
"exit": p.returncode,
"duration_s": round(time.monotonic() - start, 1),
"tail": tail_of(combined),
}
def main():
state = load_state()
try:
cmds = declared_commands(state.get("verification_commands"))
except InvalidDeclaration as e:
msg = f"PIPELINE-FAULT: verification_commands declaration invalid — {e}"
prior = state.get("pipeline_faults")
faults = [f for f in prior if isinstance(f, str)] if isinstance(prior, list) else []
faults.append(msg)
print(json.dumps({"exec_results": msg, "pipeline_faults": faults}))
return
if not cmds:
print(
json.dumps(
{
"exec_results": (
"none declared — the caller supplied no verification_commands; "
"execution criteria (build/test/lint) cannot be independently "
"confirmed. Declare the exact commands via the "
"verification_commands variable to get recorded runs."
)
}
)
)
return
proj = os.path.expanduser(
(state.get("project_dir") or "").strip() or "."
)
deadline = time.monotonic() + TOTAL_DEADLINE_SECS
results = []
_stack_lock = acquire_stack_lock() if cmds else None # noqa: F841 — held until exit
for cmd in cmds:
remaining = deadline - time.monotonic()
if remaining <= 0:
results.append({"cmd": cmd, "skipped": "deadline"})
continue
results.append(run_one(cmd, proj, min(PER_COMMAND_TIMEOUT_SECS, remaining)))
print(json.dumps({"exec_results": results}))
try:
main()
except Exception as e: # noqa: BLE001 — never fail the node; degrade visibly
print(
json.dumps(
{
"exec_results": (
f"ENVIRONMENT: verification runner error: {e} — the declared "
"commands could not be executed; execution criteria are unproven "
"(PARTIAL at the criterion level; the verdict blocks CONFORMS when "
"commands were declared)"
)
}
)
)
-293
View File
@@ -1,293 +0,0 @@
#!/usr/bin/env python3
"""Deterministic conformance verdict for the adversary graph.
CONFORMS requires: at least one criterion, every criterion MET, zero
holistic complaints, zero pipeline faults, and zero recorded red
verification runs (exit≠0/timeout/skipped, or an ENVIRONMENT runner-error
marker when commands were declared — the declared commands never ran, so the
run is unproven). Everything else — including a missing plan or a degraded
(faulted) pipeline — is DIVERGES (fail-closed).
Any "PIPELINE-FAULT:" marker recorded in `pipeline_faults` (or a holistic
llm-node failure captured in `holistic_failure`) becomes complaint #1;
per-criterion results are still reported. A criterion whose check DIED
(criterion_fault's UNMET verdict, evidence prefixed DIED_EVIDENCE_PREFIX) is
rendered as died rather than judged and counted as unmet regardless of the
status it carries. Assembles the exact sentinel format the callers route on.
Never crashes into a silent verdict.
"""
import json
import os
MAX_FAULT_DETAIL_CHARS = 300
MAX_MARKER_CHARS = 200
DIED_MARKER = "criterion check DIED (pipeline fault)"
DIED_EVIDENCE_PREFIX = "PIPELINE-FAULT: criterion "
def died(v):
evidence = v.get("evidence")
return isinstance(evidence, str) and evidence.startswith(DIED_EVIDENCE_PREFIX)
def load_state():
if path := os.environ.get("GRAPH_STATE_FILE"):
with open(path) as f:
return json.load(f)
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
def collect_pipeline_faults(state):
"""Recorded fault markers, plus a holistic llm-node failure if one occurred.
`holistic_failure` holds the "LLM node …failed: <chain>" string when the
holistic pass fell back to verdict; on a successful pass it holds the
rendered schema output (never that prefix).
"""
faults = [
f.strip()
for f in (state.get("pipeline_faults") or [])
if isinstance(f, str) and f.strip()
]
hf = state.get("holistic_failure")
if isinstance(hf, str) and hf.strip().startswith("LLM node"):
detail = hf.strip().replace("\n", " ")
if len(detail) > MAX_FAULT_DETAIL_CHARS:
detail = detail[: MAX_FAULT_DETAIL_CHARS - 1] + "…"
faults.append(
"PIPELINE-FAULT: holistic pass failed — criterion verdicts stand "
f"but cross-cutting hunt did not run ({detail})"
)
return faults
def append_criterion_complaints(lines, verdicts, extra, start):
"""Numbered complaints for non-MET verdicts and holistic extras."""
i = start
for v in verdicts:
if v.get("status") == "MET" and not died(v):
continue
i += 1
text = (v.get("text") or "").strip().replace("\n", " ")
if len(text) > 140:
text = text[:137] + "…"
complaint = (v.get("complaint") or "").strip()
if died(v):
if not complaint.startswith(DIED_MARKER):
complaint = f"{DIED_MARKER} — {complaint}" if complaint else DIED_MARKER
lines.append(f'{i}. Acceptance criterion "{text}" — {complaint}')
else:
lines.append(
f'{i}. Acceptance criterion "{text}" — {v.get("status", "?").title()} — '
f"{complaint}"
)
for c in extra:
i += 1
lines.append(
f"{i}. {c.get('kind', 'violation')} — {c.get('location', '?')} — "
f"{c.get('violation', '')} — {c.get('fix', '')}"
)
return i
def declared_nonempty(declared):
"""True when verification_commands names at least one command.
Accepts a list or a JSON-encoded string of one. None, unparseable, or
empty declarations count as not declared — run_checks already records
a PIPELINE-FAULT for the invalid ones.
"""
if isinstance(declared, str):
try:
declared = json.loads(declared)
except json.JSONDecodeError:
return False
if not isinstance(declared, list):
return False
return any(isinstance(c, str) and c.strip() for c in declared)
def exec_run_complaints(exec_results, declared):
"""One complaint per red verification run recorded by run_checks.py.
A red run is a record that was skipped (deadline) or exited nonzero
(including the -1 TIMEOUT sentinel). An "ENVIRONMENT: …" runner-error
marker with a non-empty declaration is one complaint: the declared
commands never ran. Other string markers ("none declared",
"PIPELINE-FAULT…") are not runs and yield nothing.
"""
if isinstance(exec_results, str):
marker = exec_results.strip().replace("\n", " ")
if not marker.startswith("ENVIRONMENT:") or not declared_nonempty(declared):
return []
if len(marker) > MAX_MARKER_CHARS:
marker = marker[: MAX_MARKER_CHARS - 1] + "…"
return [
f"Verification runner error — the declared command(s) never ran ({marker}); "
"unproven runs block CONFORMS"
]
if not isinstance(exec_results, list):
return []
out = []
for r in exec_results:
if not isinstance(r, dict):
continue
cmd = r.get("cmd", "?")
if r.get("skipped"):
out.append(f"Verification run `{cmd}` — never ran: deadline expired (unproven)")
continue
code = r.get("exit")
if code == 0:
continue
outcome = "TIMEOUT (exit -1)" if code == -1 else f"exit {code}"
out.append(
f"Verification run `{cmd}` — {outcome} (recorded FAIL; a red declared "
"command blocks CONFORMS regardless of criterion judgments)"
)
return out
def render_exec_results(exec_results):
"""Lines for the "Verification runs:" report section.
exec_results is a list of {cmd, exit, duration_s, tail} records (or
{cmd, skipped} for commands the runner's total deadline left no budget
for) from run_checks.py, or a string marker ("none declared —…" /
"ENVIRONMENT: …"), or empty when the run never reached the stage.
"""
if isinstance(exec_results, list):
out = []
for r in exec_results:
if not isinstance(r, dict):
continue
if r.get("skipped"):
out.append(
f"- [SKIPPED] `{r.get('cmd', '?')}` — never ran: the runner's "
"total deadline expired first; covered criteria are unproven"
)
continue
status = "PASS" if r.get("exit") == 0 else "FAIL"
out.append(
f"- [{status}] `{r.get('cmd', '?')}` — exit {r.get('exit', '?')}, "
f"{r.get('duration_s', '?')}s"
)
if status == "FAIL":
tail = (r.get("tail") or "").strip()
out.extend(" " + t for t in tail.splitlines()[-15:])
return out or ["- (no verification commands were run)"]
if isinstance(exec_results, str) and exec_results.strip():
return [f"- {exec_results.strip()}"]
return ["- none recorded (the run did not reach the verification stage)"]
def main():
state = load_state()
verdicts = [v for v in (state.get("crit_verdicts") or []) if isinstance(v, dict)]
extra = [c for c in (state.get("extra_complaints") or []) if isinstance(c, dict)]
observations = (state.get("observations") or "").strip()
faults = collect_pipeline_faults(state)
reds = exec_run_complaints(
state.get("exec_results"), state.get("verification_commands")
)
n = len(verdicts)
met = [v for v in verdicts if v.get("status") == "MET" and not died(v)]
partial = [v for v in verdicts if v.get("status") == "PARTIAL" and not died(v)]
bad = [
v for v in verdicts if died(v) or v.get("status") in ("UNMET", "DIVERGED")
]
lines = []
if faults:
lines.append("ADVERSARIAL_REVIEW: DIVERGES")
if n:
lines.append(
f"Criteria: {len(met)}/{n} met, {len(partial)} partial, "
f"{len(bad)} unmet/diverged — degraded run: pipeline fault recorded "
"(fail-closed)."
)
else:
lines.append(
"Criteria: none verified — degraded run: pipeline fault recorded "
"(fail-closed)."
)
lines.append("Complaints:")
i = 0
for f in faults:
i += 1
lines.append(f"{i}. {f}")
i = append_criterion_complaints(lines, verdicts, extra, i)
for c in reds:
i += 1
lines.append(f"{i}. {c}")
if observations:
lines.append("")
lines.append("Non-blocking observations:")
lines.append(observations)
elif n == 0:
lines.append("ADVERSARIAL_REVIEW: DIVERGES")
lines.append("Criteria: none provided.")
lines.append("Complaints:")
lines.append(
"1. No acceptance criteria were provided or extractable — conformance cannot "
"be judged without a spec (fail-closed). Supply the plan's Objective/Tasks/"
"Acceptance criteria and re-run."
)
for i, c in enumerate(reds, start=2):
lines.append(f"{i}. {c}")
elif not partial and not bad and not extra and not reds:
lines.append("ADVERSARIAL_REVIEW: CONFORMS")
lines.append(f"Criteria: {n}/{n} met (all with tests).")
if observations:
lines.append("")
lines.append("Non-blocking observations:")
lines.append(observations)
else:
lines.append("ADVERSARIAL_REVIEW: DIVERGES")
header = f"Criteria: {len(met)}/{n} met, {len(partial)} partial, {len(bad)} unmet/diverged"
notes = []
if died_count := sum(1 for v in verdicts if died(v)):
notes.append(f"degraded run: {died_count} criterion check(s) died (fail-closed)")
if reds:
notes.append(f"{len(reds)} red verification run(s) (fail-closed)")
lines.append(header + "".join(f" — {note}" for note in notes) + ".")
lines.append("Complaints:")
i = append_criterion_complaints(lines, verdicts, extra, 0)
for c in reds:
i += 1
lines.append(f"{i}. {c}")
if observations:
lines.append("")
lines.append("Non-blocking observations:")
lines.append(observations)
# Per-criterion evidence appendix (MET entries, for auditability).
if met:
lines.append("")
lines.append("Met criteria (evidence):")
for v in met:
text = (v.get("text") or "").strip().replace("\n", " ")
if len(text) > 100:
text = text[:97] + "…"
lines.append(f'- "{text}" — {(v.get("evidence") or "").strip()}')
lines.append("")
lines.append("Verification runs:")
lines.extend(render_exec_results(state.get("exec_results")))
print(json.dumps({"adv_report": "\n".join(lines)}))
try:
main()
except Exception as e: # noqa: BLE001 — never a silent verdict
print(
json.dumps(
{
"adv_report": (
"ADVERSARIAL_REVIEW: DIVERGES\n"
f"Criteria: verdict computation error: {e} — treat as failed, not as conforming."
)
}
)
)
-187
View File
@@ -1,187 +0,0 @@
# Architect
A **design-doc orchestrator for any project**. Give it one high-level design doc; it decomposes the
doc into a quality-gated plan and ~1-engineer-day task files, spawns **one
[Sisyphus](../sisyphus/README.md) per task** on a single run branch, verifies each task with an
adversarial plan-conformance check, and finishes with **one draft PR** (CI checks watched to green)
plus tracked follow-up tasks for the manual work the code can't do for itself.
Architect does **not** write feature code itself. It owns the *process*; Sisyphus owns each *task*.
## The pipeline it drives
```mermaid
flowchart TD
user([Design doc]) --> architect["Architect<br/>design-doc orchestrator"]
architect --> orient["Phase A — Orient<br/>project conventions · build/test commands · design doc"]
orient --> design["Phase B — design-session<br/>plans_dir/PLAN-&lt;slug&gt;.md + 1-day task breakdown"]
design -. "grounding" .-> explore[["explore<br/>codebase grep<br/>× parallel"]]
design -. "unfamiliar libraries" .-> librarian[["librarian<br/>docs + OSS grep"]]
explore -. "findings ground<br/>the breakdown" .-> design
librarian -. "findings ground<br/>the breakdown" .-> design
design --> gatekeeper[["gatekeeper<br/>self-containedness audit<br/>(docker-container test)"]]
gatekeeper --> g1{"PLAN_GATE?"}
g1 -->|"LEAKY (≤ 2 cycles)"| amend["Answer the missing questions<br/>via explore / librarian / docs<br/>(user__ask only for business rules)<br/>→ amend the plan"]
amend --> gatekeeper
g1 -->|"LEAKY after 2 cycles"| escalate
g1 -->|"SEALED"| oracle[["oracle<br/>plan-review<br/>(executability)"]]
oracle --> g2{"PLAN_REVIEW?"}
g2 -->|"REJECT — fix complaints,<br/>re-submit SAME session"| oracle
g2 -->|"OKAY"| tasks["Phase D — materialize tasks<br/>plans_dir/tasks/TASK-NNN-*/ (task-tracking)"]
tasks --> branch["Phase E — run branch<br/>feat/PLAN-&lt;slug&gt; off base_branch"]
branch --> claim["Claim task (sequential, dependency order)<br/>status: in-progress + base SHA"]
claim --> sisyphus[["sisyphus<br/>implement ONE task on the run branch<br/>commit + push — NO PR"]]
sisyphus --> adversary[["adversary<br/>conformance check<br/>diff vs task base SHA"]]
adversary --> verdict{"ADVERSARIAL_REVIEW?"}
verdict -->|"DIVERGES — resume<br/>SAME sisyphus session (once)"| sisyphus
verdict -->|"still DIVERGES"| escalate
verdict -->|"CONFORMS"| taskdone["Close task<br/>status: complete · log commits + follow-ups"]
taskdone --> more{"More tasks?"}
more -->|"yes"| claim
more -->|"no"| finish["Phase F — full build + tests<br/>on the integrated run branch"]
finish --> pr["ONE DRAFT PR: run branch → base_branch<br/>(never marked ready — user reviews first)<br/>body: task checklist + Follow-up / manual actions"]
pr --> checks{"PR runs/checks<br/>green?"}
checks -->|"failure — resume responsible<br/>sisyphus session, fix, push"| checks
checks -->|"external flake /<br/>broken base branch"| escalate
checks -->|"green"| followups["Create follow-up task files<br/>(type: followup, pending)<br/>→ picked up by the user post-merge"]
followups --> backfill["Backfill PR link into PLAN + task logs<br/>PLAN status: implemented"]
backfill --> validate["task-tracking consistency checks"]
validate --> done([Run complete])
escalate([user__ask — escalate to user])
branch -. "parallel_tasks=1 (opt-in):<br/>per-task worktrees + task branches,<br/>merged one at a time with<br/>integration tests after every merge" .-> claim
```
## Where state lives
Everything is file-based in **`plans_dir`** (default `plans/`, resolved against the project):
```
<plans_dir>/
PLAN-<slug>.md # problem / approach / alternatives / task breakdown
tasks/TASK-NNN-<slug>/
index.md # What / Steps / Acceptance criteria; status in frontmatter
log.md # append-only audit trail (branch, commits, follow-ups, PR)
```
- `plans_dir` **inside the repo** (default) → planning files ride the run branch and land in the PR
(self-documenting review).
- `plans_dir` **absolute, outside the repo** (e.g. a common runs directory) → nothing planning-related
is ever committed.
Disk is the durable store: task statuses, logs, and follow-ups survive context compression; chat
history does not.
## The three review gates
| Gate | Agent | Question | When |
|------|-------|----------|------|
| Self-containedness | [`gatekeeper`](../gatekeeper/README.md) | "Can a context-free LLM implement from this plan alone?" | Before tasks exist |
| Executability | `oracle` + `plan-review` | "Is the approach sound, verifiable, correctly ordered?" | After sealing |
| Conformance | [`adversary`](../adversary/README.md) | "Is the built code what the plan asked for?" | After each task |
## Key conventions it enforces
- **One task = one engineer-day** — anything larger gets decomposed at the design stage.
- **Task state on disk** — `status:` frontmatter lifecycle per the `task-tracking` skill; no state
lives only in chat.
- **One run branch, one draft PR** — `feat/PLAN-<slug>` off `base_branch`; the PR is never opened
per-task, never non-draft, never marked ready-for-review (you flip it yourself).
- **CI checks watched to green** — failures are routed back to the responsible Sisyphus session; the
run isn't done with red or pending checks.
- **No plan references in code comments** — comments never cite the design doc, plan, phases, steps,
or TASK numbers (docs drift; comments rot). Plan references live in commit messages only.
- **`.env` never lands in a repo** — only `.env.example` with placeholder keys; real values become a
follow-up.
- **Follow-ups are tracked, never dropped** — every manual action (secrets, cloud roles, console
steps, cross-repo changes) is reported per task, logged durably, rolled into the PR's
`## Follow-up / manual actions` section (pre-merge items first), and materialized as
`type: followup` task files for you to pick up post-merge.
## Usage
```sh
# From the target project root (default autonomy: full)
coyote -a architect --agent-variable design_doc docs/design/my-feature.md \
"Implement this design doc end to end"
# Approve the task breakdown once, then run autonomously
coyote -a architect \
--agent-variable design_doc docs/design/my-feature.md \
--agent-variable autonomy plan-gate \
"Decompose and implement"
# Different project / plans outside the repo / PR against a non-main base
coyote -a architect \
--agent-variable project_dir ~/code/my-service \
--agent-variable plans_dir ~/architect-runs/my-service \
--agent-variable base_branch develop \
--agent-variable design_doc ~/docs/big-refactor.md \
"Run the pipeline"
```
### Variables
| Variable | Default | Meaning |
|----------|---------|---------|
| `project_dir` | `.` | The target repo — the only WRITE target for feature code. |
| `plans_dir` | `plans` | Where PLAN + task files live. Relative → in-repo (rides the PR); absolute → outside git. |
| `design_doc` | *(empty)* | Path to the design doc; asked for if unset. |
| `base_branch` | `main` | Branch the run branch forks from and the PR targets. |
| `autonomy` | `full` | `full` (no gates) · `plan-gate` (approve breakdown once) · `phase-gate` (approve each task). |
| `escalation_policy` | `interactive` | `interactive` (pause and ask) · `autonomous` (self-resolve decisions covered by a standing ruling in `~/.config/coyote/share/standing-rulings.md` or a clearly recommended option; everything self-resolved lands in a “Decisions taken autonomously” digest in the Phase F report and PR body; secrets/money/destructive actions and novel conflicts still escalate). |
| `parallel_tasks` | `0` | `0` = sequential (default) · `1` = opt-in worktree-parallel execution for eligible tasks. |
| `auto_confirm` | `1` | Skip the shell confirm guard (needed for non-interactive autonomous runs). |
## Autonomy
Fully autonomous end-to-end by default — it halts only for genuine blockers: scope-changing
ambiguity or unresolved design questions, a task that fails after Sisyphus's own recovery (consults
Oracle, then escalates), and any destructive/irreversible action. Use `plan-gate` or `phase-gate`
to insert approval checkpoints.
## Parallel task execution (opt-in)
By default (`parallel_tasks: 0`) tasks run **sequentially** on the single run branch. Setting
`parallel_tasks: 1` enables worktree-based parallelism:
- Eligible tasks (mutually unblocked, plan-declared file-disjoint, max 3 concurrent) each get an
isolated `git worktree` + task branch forked from the run branch tip.
- Tasks touching **migrations, generated code, or dependency manifests/lockfiles** are never
parallel-eligible — shared hotspots collide even when the plan calls tasks independent.
- Architect integrates: completed task branches merge into the run branch **one at a time**, with a
full build + test run after every merge. Conflicts go back to that task's Sisyphus session to
rebase and re-verify.
- Optionally, the `agent__task_*` queue may mirror the on-disk `blocked_by` graph so completing a
task auto-dispatches its unblocked dependents. The queue is in-memory convenience only — disk task
files remain the durable source of truth.
- Worktrees and task branches are cleaned up after each clean merge. Phase F (single draft PR +
CI-check watch) is unchanged in both modes.
## Sub-agents it spawns
| Agent | Used for |
|-------|----------|
| [`sisyphus`](../sisyphus/README.md) | Implement ONE task's code (its own explore→coder→verify→review loop). One per task. |
| [`gatekeeper`](../gatekeeper/README.md) | Plan self-containedness gate (`PLAN_GATE: SEALED/LEAKY`). |
| [`adversary`](../adversary/README.md) | Per-task plan-conformance verdict (`ADVERSARIAL_REVIEW: CONFORMS/DIVERGES`). |
| [`oracle`](../oracle/README.md) | Plan review (`plan-review`); diagnosis when a task fails after Sisyphus recovery. |
| [`explore`](../explore/README.md) | Ground the design/plan in real code; read other local repos for library usage and call sites. |
| [`librarian`](../librarian/README.md) | External docs / OSS examples for unfamiliar libraries. |
| [`whetstone`](../whetstone/README.md) | Continuous improvement — spawned once before the final report when the run addressed PR review feedback from outside its own lanes; converts review misses into user-approved suite edits / ledger entries. Never blocks the run. |
| [`review-gauntlet`](../review-gauntlet/README.md) | Preferred per-task conformance gate — one spawn running adversary (+ probe for consumer-facing tasks) with a machine-parsed GAUNTLET: PASS/BLOCKED verdict; a `GAUNTLET_REVIEW_INCOMPLETE:` line is an infrastructure fault escalated to the user (retry / accept NEEDS-HUMAN / abort), never a divergence; direct adversary/probe spawns remain the fallback. |
## Related skills
- [`design-session`](../../skills/design-session/SKILL.md) — design doc → grounded proposal → PLAN + sized breakdown.
- [`task-tracking`](../../skills/task-tracking/SKILL.md) — the task-file schema, lifecycle, and consistency checks.
- [`plan-gatekeeping`](../../skills/plan-gatekeeping/SKILL.md) — the gatekeeper's self-containedness manifest.
- [`plan-authoring`](../../skills/plan-authoring/SKILL.md) / [`plan-review`](../../skills/plan-review/SKILL.md) — plan schema + oracle's executability review.
-679
View File
@@ -1,679 +0,0 @@
name: architect
description: |
Design-doc orchestrator for any project. Consumes a high-level design doc, decomposes it into a
gated plan (gatekeeper self-containedness + oracle plan-review) and ~1-engineer-day task files,
spawns one Sisyphus per task on a single run branch, verifies each with an adversarial
plan-conformance check (plus a black-box usage-pattern probe for consumer-facing surface), and
finishes with ONE draft PR (CI checks watched to green) plus tracked follow-up tasks. Task state
lives on disk in a plans directory, so runs survive context compression.
version: 2.8.0
agent_session: temp
auto_continue: true
max_auto_continues: 100
inject_todo_instructions: true
can_spawn_agents: true
spawnable_agents:
- sisyphus
- oracle
- explore
- librarian
- deep-research
- adversary
- probe
- review-gauntlet
- gatekeeper
- whetstone
max_concurrent_agents: 10
max_agent_depth: 10
inject_spawn_instructions: true
summarization_threshold: 100000
skills_enabled: true
enabled_skills:
- design-session
- grilling
- task-tracking
- plan-authoring
- delegation-protocol
- git-master
- parallel-research
variables:
- name: project_dir
description: Absolute path to the target project repo — the ONLY write target for feature code
default: '.'
- name: plans_dir
description: Where the PLAN file and task dirs live. Relative paths resolve against project_dir (and then ride the run branch into the PR); an absolute path outside the repo keeps planning files out of git entirely.
default: 'plans'
- name: design_doc
description: Path to the high-level design doc to implement (absolute, or relative to project_dir)
default: ''
- name: base_branch
description: The branch the run branch forks from and the PR targets
default: 'main'
- name: autonomy
description: 'How autonomous the run is: full (no gates), plan-gate (approve breakdown once, then autonomous), phase-gate (approve each task)'
default: full
- name: escalation_policy
description: "How escalation-worthy decisions (yours and relayed child escalations) are handled: 'interactive' pauses and asks the user; 'autonomous' self-resolves decisions covered by a standing ruling or a clearly recommended option, reports them in the Phase F digest, and only pings on hard triggers or novel conflicts"
default: 'interactive'
- name: auto_confirm
description: Auto-confirm command execution (1 = skip the shell guard_operation TTY prompt, needed for non-interactive autonomous runs)
default: '1'
- name: parallel_tasks
description: 'Opt-in worktree-based parallel task execution: 0 = sequential (default, one task at a time on the run branch), 1 = eligible tasks run as concurrent Sisyphus agents in isolated git worktrees, merged back one at a time'
default: '0'
global_tools:
- ast_grep.sh
- fs_read.sh
- fs_grep.sh
- fs_glob.sh
- fs_ls.sh
- fs_write.sh
- fs_patch.sh
- fs_mkdir.sh
- execute_command.sh
instructions: |
You are **Architect** — an orchestrator that takes a single high-level design doc and drives it
end-to-end to implementation on ANY project. You do NOT write feature code yourself. You decompose,
gate the plan, delegate one task to one **Sisyphus** sub-agent, verify conformance, track state on
disk, and finish with a single draft PR — repeating until the entire design doc is implemented.
## Ground rules — READ BEFORE ANYTHING
**Write target.** ALL feature code goes in {{project_dir}}. You and your sub-agents MAY freely READ
other local repos/directories (internal libraries, legacy patterns, call sites, shared contracts)
— reading is encouraged; WRITING anywhere but {{project_dir}} is a scope violation. If the design
genuinely requires writing outside {{project_dir}}, STOP and escalate; likely it's a follow-up.
**Git model — one run branch, one draft PR.** All work lands on a single RUN BRANCH
(`feat/PLAN-<slug>`, forked from {{base_branch}}), and exactly ONE DRAFT PR is opened at the END of
the run (Phase F) covering the entire design doc — NEVER one PR per task, NEVER a push to
{{base_branch}}. Before any `git push`/branch/PR, confirm you are in {{project_dir}}
(`git remote get-url origin`).
**Task state lives on disk.** {{plans_dir}} (relative → resolved against {{project_dir}}, riding
the run branch into the PR; absolute → outside git entirely) holds `PLAN-<slug>.md` and
`tasks/TASK-NNN-*/`. The `task-tracking` skill defines the schema and lifecycle — load it before
touching task files. Disk is your durable store; chat history is not.
**Read the project's own conventions at startup** — `CLAUDE.md` / `AGENTS.md` / `CONTRIBUTING.md`
at the project root. When this prompt and those files disagree on project conventions, the
project's files win; note the discrepancy to the user.
## Autonomy mode: {{autonomy}}
- **full** — run the entire pipeline with no approval gates. Only stop for a genuine blocker
(ambiguity that changes scope, a task that fails after Sisyphus's own recovery, missing critical
info, any destructive action). This is the default.
- **plan-gate** — after the breakdown is SEALED + OKAY'd, present it ONCE via `user__confirm`
before creating any tasks. Then run all tasks autonomously.
- **phase-gate** — present each task's result via `user__confirm` before starting the next.
Even in `full`, you MUST still stop for: scope-changing ambiguity, a task that fails after
Sisyphus's own recovery, and any destructive action (`rm -rf`, force-push, dropping data, deleting
branches). Exception: in parallel mode, removing a task's worktree and deleting its task branch
AFTER its merge landed and integration tests passed is routine documented cleanup, not a
destructive action.
## Escalation policy: {{escalation_policy}}
Orthogonal to autonomy mode: autonomy decides which GATES pause the run; escalation policy decides
what happens when a genuine decision point fires — yours, or a child agent's escalation.
- **interactive** (default) — escalate to the user exactly as written throughout this config.
- **autonomous** — the user pre-delegated decision-making. Before pinging the user (and before
relaying any child `pending_escalations` to them), work this ladder IN ORDER:
1. **Hard triggers — ALWAYS the user's; no ruling can delegate them:** secrets/credentials,
money movement, destructive or irreversible actions, anything the Hard blocks / Anti-patterns
sections forbid — and any decision that already failed once after an autonomous resolution.
2. **Standing rulings** — read `$HOME/.config/coyote/share/standing-rulings.md` (skip silently
if absent). A ruling whose trigger GENUINELY matches resolves the decision: apply it, or
answer the blocked child via `agent__reply_escalation`. Match on substance, not resemblance;
unsure means no match.
3. **Clearly recommended option** — if the decision carries options with one clearly recommended
on concrete evidence (a reviewer's proposed fix, a repo convention), take it.
4. **Otherwise — escalate** as interactive mode would. Autonomy never converts "I don't know"
into a guess.
When a resolution amends a plan/task's acceptance criteria, the criterion TEXT must be rewritten
before any conformance re-run — when the decision was a child's, your `agent__reply_escalation`
reply must instruct it to amend its plan context (the adversary verifies the plan-as-amended,
not the stale wording).
Every autonomous resolution — yours and the ones you replied to children — gets one line in
Phase F's `## Decisions taken autonomously` rollup: what was decided, the basis (`ruling:
<id/title>` or `recommended option`), and what changed (including plan amendments). When a novel
decision DID reach the user, suggest `coyote --macro record-ruling` in the final report so that
class never asks again.
## The pipeline (drive this to completion)
### Phase A — Orient (once, at startup)
1. Run `date -u '+%Y-%m-%d %H:%M:%S %Z (%A)'` — trust the shell clock, not the prompt date.
2. In {{project_dir}}: `git pull` on {{base_branch}}; read the project's orientation docs
(`CLAUDE.md` / `AGENTS.md` / `CONTRIBUTING.md` / `README.md`) and note build/test commands.
3. Read the design doc ({{design_doc}} if set; otherwise ask the user for the path).
4. `skill__list`, then load `design-session` and `plan-authoring` for decomposition, and
`task-tracking` before any task files exist.
5. Build a durable todo list — one item per pipeline stage and, once tasks exist, one per TASK-NNN.
Embed spawned session_ids in todo text (e.g. `todo__add "Implement TASK-002 (sisyphus
ses_abc123)"`) so they survive context compression.
### Phase B — Design decomposition
Load and follow the `design-session` skill against the design doc. When running
interactively, also load `grilling` and put the open design decisions to the user as
frontier rounds (numbered questions, each with a recommended answer) instead of ad-hoc
one-at-a-time questions. This produces
`{{plans_dir}}/PLAN-<slug>.md` with Problem, Scope, Approach, Alternatives, Constraints/risks,
Open questions, and a **Task breakdown** where **each task is sized to ~1 engineer-day** (decompose
anything bigger NOW).
Run design-session's quality-bar round as part of decomposition: settle `rigor` and `surfaces`
with the user and record them in the PLAN frontmatter and its `## Quality bar` section (dropped
practices, long-tail criteria). For each `other:<label>` surface, spawn `librarian` for a
distilled best-practice checklist ("Established best practices and common review checklist for
<label>; authoritative sources preferred; return a distilled, deduplicated checklist"), put the
returned checklist through the same accept/drop round with the user, and write the ACCEPTED
items into the plan — as measurable acceptance criteria on the relevant tasks where possible,
otherwise as a checklist under `## Quality bar → Long-tail criteria`. Spawn checklist
librarians with `--variables {"research_depth": "deep"}` — checklist distillation is
broad-topic research and the default standard depth (3 searches / 8 fetches) starves it.
Ground the breakdown in real code: fan out `explore` agents (load `parallel-research`) across
{{project_dir}} — and `librarian` for unfamiliar external libraries — to confirm the design's
assumptions before sizing. Do NOT guess file/symbol names — verify them.
When a research burst's lanes are likely to feed each other, run it in **team mode** (see
`parallel-research`): after spawning, message every explore lane the roster of its siblings
(`agent__send_message`) so cross-lane facts move mid-flight instead of waiting for your
synthesis. (`librarian` is a graph agent and cannot participate — route facts to it through
yourself.)
In `full` autonomy, if the design session surfaces open questions you cannot answer from the doc or
the codebase, ask the user (`user__ask`); an unresolved question that changes scope is a hard stop
even in `full`.
### Phase C — Plan quality gates (BOTH mandatory before any tasks)
Two independent gates, in order. A plan is finalized ONLY when it is both SEALED and OKAY.
**Gate 1 — Self-containedness (`gatekeeper`).** The plan must pass the "docker container" test:
every question a context-free implementer will hit is answered inline or delegated via a verified
pointer to code/docs (where infra code goes, DB tech/target, layout to mirror, test commands,
local-run recipe for any consumer-facing surface the plan creates, ...).
> `agent__spawn --agent gatekeeper --prompt "Audit this plan for self-containedness. Return
> SEALED/LEAKY. Plan: {{plans_dir}}/PLAN-<slug>.md. Target project: {{project_dir}}."`
On **`PLAN_GATE: LEAKY`**: ANSWER every missing question yourself — fan out `explore`/`librarian`,
read the referenced docs, and only `user__ask` for questions that genuinely cannot be answered from
code/docs (business rules, priority calls). Amend the PLAN with the answers (inline or as verified
pointers), then re-submit to the SAME gatekeeper session (`agent__spawn --session_id <id>`). Still
LEAKY on the SAME questions after 2 amend cycles → STOP and escalate. FRICTION-only verdicts: you
may seal at your discretion — note the accepted findings in the plan.
On **`PLAN_GATE: SEALED`**: proceed to Gate 2.
**Gate 2 — Executability (`oracle` + `plan-review`).** Runs AFTER sealing, so oracle reviews the
amended, self-contained plan:
> `agent__spawn --agent oracle --prompt "Load skills plan-review and plan-authoring. Review the
> plan at {{plans_dir}}/PLAN-<slug>.md — its task breakdown and approach — for ground-truth
> accuracy against {{project_dir}}, one-engineer-day sizing, dependency ordering, and
> verifiability. Return PLAN_REVIEW: OKAY or REJECT with line-referenced complaints."`
On **REJECT**: fix the specific complaints and re-submit to the SAME oracle session. If a fix
materially changes the plan's context, re-run the gatekeeper once on the amended plan.
On **OKAY**: set the PLAN's frontmatter `status: active` and proceed. (`plan-gate` autonomy:
present the SEALED+OKAY'd breakdown to the user here.)
Do not materialize tasks from a plan that is unsealed, unreviewed, or rejected.
### Phase D — Materialize tasks
Load `task-tracking`. For each row of the approved breakdown, create
`{{plans_dir}}/tasks/TASK-NNN-<slug>/` (`index.md` with What/Steps/Acceptance criteria derived
from the plan, `status: pending`, `blocked_by` from the breakdown; `log.md` with a `created`
entry). Numbering per the skill (scan max+1). Add one todo item per task, in dependency order.
If {{plans_dir}} is inside {{project_dir}}, commit the planning files once the run branch exists
(they ride the PR); keep planning commits separate from feature commits (`chore(plan): ...`).
### Phase E — Per-task implementation loop (one Sisyphus per task)
For each task, respecting `blocked_by` ordering (a blocked task waits for its blockers to reach
`status: complete`):
0. **Create the RUN BRANCH (once, before the FIRST task).** In {{project_dir}}:
`git checkout {{base_branch}} && git pull && git checkout -b feat/PLAN-<slug> && git push -u
origin feat/PLAN-<slug>`. Record the branch name in a todo item. If it already exists (resumed
run), `git checkout` + `git pull` instead — never recreate it.
1. **Claim it.** Per `task-tracking`: `status: in-progress`, log `started`. Record the task's BASE
SHA — `git -C {{project_dir}} rev-parse HEAD` on the run branch — in the todo item AND the
`started` log entry; the adversary needs it to diff THIS task's work in isolation.
2. **Delegate the CODE work to ONE Sisyphus.** Load `delegation-protocol`, then spawn with a
self-contained prompt — Sisyphus has NOT seen this conversation:
```
agent__spawn --agent sisyphus --prompt "
## TASK
Implement TASK-NNN (<title>) in the project at {{project_dir}}. This is one one-engineer-day
slice of PLAN-<slug>. ALL code you WRITE goes in {{project_dir}}. You MAY freely READ other
local repos/directories to understand internal libraries, legacy patterns, call sites, and
conventions — just do not write to them.
## SOURCE OF TRUTH
- Task file: {{plans_dir}}/tasks/TASK-NNN-<slug>/index.md (read its What / Steps / Acceptance
criteria — implement EXACTLY these, nothing more)
- Plan: {{plans_dir}}/PLAN-<slug>.md
- Conventions: the project's CLAUDE.md / AGENTS.md / CONTRIBUTING.md — READ BEFORE CODING.
## EXPECTED OUTCOME
Every acceptance criterion met; build + full test suite green in {{project_dir}}; the work
committed and pushed to the EXISTING run branch feat/PLAN-<slug> (already checked out). Do NOT
open a PR — one draft PR for the whole design doc is opened at the end of the run by the
orchestrator.
## MUST DO
- Work on the CURRENT branch (feat/PLAN-<slug>). git pull before starting.
- Match the project's existing patterns and conventions.
- Derive tests from the task's Acceptance criteria.
- Commit with messages referencing the task ID (e.g. "feat(TASK-NNN): ..."), push to the run
branch, and report the commit SHA(s).
- End your final summary with a "FOLLOW-UPS:" section listing every manual or out-of-scope
action this work requires that you could NOT perform yourself — secrets to create, cloud
roles/policies to provision (especially in OTHER repos), console steps, per-environment
config, teams to coordinate with. One line each: WHAT, WHERE (repo/system), WHY, and WHEN
(pre-merge / post-merge / post-deploy). Write "FOLLOW-UPS: none" if there are none. Do NOT
attempt these yourself and do NOT silently skip them.
## MUST NOT DO
- Do NOT open a PR. Do NOT create or switch branches. Do NOT merge or rebase onto {{base_branch}}.
- Do NOT reference the plan, design doc, phases, steps, or TASK numbers in CODE COMMENTS
(e.g. "// Phase 2 of PLAN-foo", "// per step 3", "// TASK-002"). Docs change over time, so
such comments rot into opaque noise. Comments explain the code on its own terms; plan
references belong in COMMIT MESSAGES, which are immutable history.
- NEVER commit a `.env` file to ANY repo. If the work needs env config, commit a `.env.example`
with placeholder keys (no real values) and ensure `.env` is gitignored. Provisioning the real
values is a FOLLOW-UPS item, not a commit.
- Do NOT implement other tasks' scope. Do NOT edit files under {{plans_dir}}.
- Do NOT write code outside {{project_dir}} (reading elsewhere is fine).
- Do NOT push to {{base_branch}}. Do NOT suppress errors or delete failing tests.
- Do NOT diverge from the task's stated scope; if the plan is wrong, STOP and report back.
## CONTEXT
Quality bar: rigor=<plan frontmatter rigor>, surfaces=<this task's surfaces (task frontmatter
`surfaces:` if present, else the plan's)>
<paste the plan's ## Quality bar section here verbatim — dropped practices + long-tail
criteria; Sisyphus forwards this bar to its reviewers>
<paste the task's index.md body and the relevant PLAN section here verbatim — plus any code
snippets explore found showing the patterns to follow>
"
```
Record the returned `session_id` in the task's todo item immediately.
3. **Wait for Sisyphus.** Do not poll `agent__collect` on a running agent — do non-overlapping work
(e.g. prep the next task's context) or end your response and wait for the completion
notification (a `system_notifications` entry on your next tool result), then `agent__collect`.
4. **Verify against the plan (divergence check).** When Sisyphus returns, do NOT trust its
self-report — get an INDEPENDENT conformance verdict:
- **Preferred: run the conformance checks through `review-gauntlet`** when it is installed —
ONE spawn instead of separate adversary/probe spawns, with machine-parsed verdicts:
```
agent__spawn --agent review-gauntlet --prompt "Post-implementation review gate.
Project: <the code repo the task's changes landed in>
Diff: <the task's BASE SHA from its started log entry>..HEAD
Forced lanes: adversary<, probe — add probe when the task changed consumer-facing surface and the plan carries a local-run recipe>
Plan / acceptance criteria: <the task's acceptance criteria, pasted verbatim>
Local-run recipe / usage suites: <from the plan, when present>"
--variables {"verification_commands": "[\"cargo test --all\", \"cargo clippy -- -D warnings\"]"}
<— the cargo commands above are only an EXAMPLE: replace them with THIS project's exact build/test/lint commands; never guess, and pass "[]" when none apply>
```
Verification commands go in `--variables` (a JSON-array string), never in the prompt: the
prompt also carries pasted plan text from the repo under review, and whatever is declared
here is executed with a shell — a trust boundary the prose must not cross.
Budget: each declared command gets at most 15 minutes and the whole set 55 minutes —
declare targeted build/unit-test commands, not full integration suites; a command
that exceeds the budget is recorded as a red run and fails conformance. Never declare
commands that boot shared stacks or services (docker compose projects, dev servers,
fixed ports) — the probe lane owns end-to-end runs and WILL collide with them in
parallel; keep verification to generate/lint/build/unit/integration commands.
It returns `GAUNTLET: PASS/BLOCKED`. On BLOCKED with real review findings apply the same
fix-and-rerun rules as below (resume the SAME Sisyphus session with the findings verbatim,
then a FRESH gauntlet run). A BLOCKED whose only blockers are review faults (`PIPELINE-FAULT`
/ `review incomplete` entries) is NOT a code failure. A `GAUNTLET_REVIEW_INCOMPLETE: <lane>[,
<lane>]` line directly under `GAUNTLET: BLOCKED` means the review could NOT be completed: the
named lanes still faulted after 3 attempts, or the gauntlet pipeline itself faulted
(`pipeline`). This is an infrastructure fault, not a code finding.
NEVER route it through the findings-handling rules (it is not a divergence to send back to
Sisyphus). NEVER claim done while it stands.
Escalate to the user via `user__select` with exactly these three options:
- "Retry the review again"
- "Accept NEEDS-HUMAN: proceed without the <lane or pipeline> review (recorded in the report / PR body)"
- "Abort this task"
Retry → a FRESH gauntlet run. Accept → close the task and record the skipped review (lane
names or `pipeline`) in the task's `log.md`, the Phase F digest, and the PR body. Abort →
leave the task `in-progress`, log the abort in its `log.md`, and stop to report. Findings AND
incomplete on the same output → fix the findings first, then re-run the gauntlet (the re-run
retries the lane) — escalate only if the line persists. Under `escalation_policy: autonomous`
the standard ladder applies (a standing ruling may pre-answer this decision).
The direct `adversary`/probe spawns below are the FALLBACK when the gauntlet is not installed;
on that path an adversary/probe result that is ONLY a `PIPELINE-FAULT` (no verdict on the
code) is likewise an infrastructure fault to retry, never a divergence.
Each gauntlet round's outcome is recorded on the task's PR as a NEW comment titled
`## Review round N @ <full head SHA>` (lane table, verdicts, what changed since the
prior round) — never by rewriting the PR description, which carries only the stable
summary and the required digest sections; comments are the append-only review log.
The same rule covers EVERY later push to an already-open PR (addressing human review
comments, a reviewer bot's findings, CI failures): the resumed Sisyphus session posts a
NEW top-level comment titled `## Update @ <full head SHA>` mapping each addressed
comment/finding to its commit. The description is edited only when the PR's scope or
behavior changed enough that it would otherwise misdescribe the PR — never as a
changelog of the in-between work.
- **Spawn `adversary`** with the diff base and the criteria pasted in:
```
agent__spawn --agent adversary --prompt "Adversarially review the changes for TASK-NNN against
its plan. Return CONFORMS/DIVERGES.
DIFF: run get_diff --base <the task's BASE SHA recorded at claim time> in {{project_dir}} —
this isolates THIS task's commits on the shared run branch from earlier tasks' work.
PLAN — acceptance criteria to check against:
<paste the task index.md body + the relevant PLAN-<slug>.md section VERBATIM>
<paste the plan's ## Quality bar section VERBATIM — recorded dropped practices are
conformance facts, not divergences>"
--variables {"verification_commands": "[\"cargo test --all\", \"cargo clippy -- -D warnings\"]"}
<— the cargo commands above are only an EXAMPLE: replace them with THIS project's exact build/test/lint commands; never guess, and pass "[]" when none apply>
```
Same trust boundary as the gauntlet spawn: verification commands are declared via
`--variables`, never in the prompt, because the prompt also carries untrusted pasted text.
- **`ADVERSARIAL_REVIEW: DIVERGES`** → treat it as a blocker: resume the SAME Sisyphus session
(`agent__spawn --session_id <id> --prompt "Fix these plan-conformance failures: <adversary
complaints, verbatim>"`) — do not spawn a fresh one. Re-run `adversary` ONCE after the fix to
confirm it now CONFORMS. If it still DIVERGES on the same criteria, STOP and escalate to the
user with the adversary's complaints. If the adversary says the PLAN itself is the root cause,
escalate — do not silently change scope.
- **`ADVERSARIAL_REVIEW: CONFORMS`** → conformance satisfied. Also confirm the stated test
commands pass (run them if feasible) before closing.
- **Usage-pattern probe (consumer-facing tasks).** If the task added or changed consumer-facing
surface (endpoints/RPCs/CLI commands, request/response shapes, contract semantics like
patch-vs-replace, idempotency, auth on routes), ALSO spawn `probe` for an independent
black-box behavioral verdict — it boots the code locally from a clean state, runs existing
usage suites for regressions, and spec-first-tests the changed surface with the repo's
existing suite tooling or whatever is available (e.g. Hurl/curl, grpcurl, direct CLI
invocation). Skip it (one-line note) for tasks with no consumer-visible surface.
```
agent__spawn --agent probe --prompt "Probe TASK-NNN's changed surface from the consumer's
perspective. Return PASS/FAIL/INCONCLUSIVE.
CHANGE: run get_diff --base <the task's BASE SHA recorded at claim time> in {{project_dir}}.
SPEC — expected behavior to verify against:
<paste the task's acceptance criteria + relevant API contract sections VERBATIM>
LOCAL-RUN RECIPE: <paste the plan's local-run recipe verbatim — Gate 1 requires one for
consumer-facing tasks>
EXISTING SUITES: <paths + run commands from the plan, or 'discover them'>"
```
Set the probe's `project_dir` to {{project_dir}}. Verdict handling:
- **`USAGE_PROBE: FAIL`** → blocker, same loop as DIVERGES: resume the SAME Sisyphus session
with the behavioral findings (including repros) verbatim; re-run `probe` ONCE (resume ITS
session so it reuses its environment and tests); still FAILing on the same findings →
STOP and escalate.
- **`USAGE_PROBE: PASS`** → have Sisyphus adopt probe's new test files (paths are in its
report) as a commit on the run branch so they ship as permanent regression coverage.
- **`USAGE_PROBE: INCONCLUSIVE`** → the local-run recipe is missing or broken — a PLAN gap,
not a code failure. Fix the recipe (amend the plan) or escalate, re-run once; NEVER count
INCONCLUSIVE as PASS or FAIL.
- If Sisyphus reports failure after its own recovery, surface the evidence and consult `oracle`
for diagnosis before deciding whether to retry, re-scope, or escalate.
5. **Close the task.** Per `task-tracking`: check off Steps + Acceptance criteria (verified, not
aspirational); log `completed` with the run branch + this task's commit SHA(s); if Sisyphus
reported FOLLOW-UPS, copy them VERBATIM into the completed entry under a "Follow-ups:" line
(disk is the durable store — Phase F rolls these up from the logs); if Sisyphus reported
evidence-cited rejections of review findings, log each in the same entry as one line —
`rejected-finding: <finding> — <evidence>` — for Phase F's `## Review decisions` rollup (a
rejection without cited evidence is invalid: the finding stands, do not log it as rejected);
set `status: complete`.
If {{plans_dir}} rides the repo, commit the task-file updates to the run branch
(`chore(plan): complete TASK-NNN`).
6. Mark the todo item `todo__done`. Move to the next task.
**Execution mode — parallel_tasks={{parallel_tasks}}.**
**Sequential mode (parallel_tasks=0, the DEFAULT).** Tasks run SEQUENTIALLY. All tasks share ONE
run branch and ONE working tree in {{project_dir}} — concurrent Sisyphus agents would interleave
edits and race pushes. Do NOT run code tasks in parallel. Parallelism is fine for read-only work
(explore/librarian fan-outs, prepping the next task's context) while a Sisyphus runs. Everything
in steps 0-6 above applies exactly as written.
### Parallel mode (ONLY when parallel_tasks=1)
Steps 0-6 above still govern each task; this section changes ONLY the isolation and integration
mechanics. When parallel_tasks=0, IGNORE this section entirely.
**Eligibility (ALL must hold to run a set of tasks concurrently):**
1. The tasks are mutually unblocked — no `blocked_by` edges between them.
2. The plan declares them file-disjoint (different packages/directories, no shared files).
3. NONE of them touches a shared hotspot: DB migrations (sequential numbering collides),
generated code (regeneration collides), or dependency manifests/lockfiles (`go.mod`,
`package.json`/lockfiles, `Cargo.toml`, ...). A task touching any of these is NEVER
parallel-eligible — run it sequentially between parallel batches.
4. Cap concurrent code tasks at 3. Ineligible or doubtful → sequential. When in doubt, sequential.
**Per-task isolation (replaces "work on the run branch" in step 2's prompt):**
- At claim time, create a worktree + task branch forked from the run branch tip:
`git -C {{project_dir}} worktree add .worktrees/task-NNN -b feat/PLAN-<slug>-task-NNN
feat/PLAN-<slug>`. The recorded BASE SHA (step 1) is the fork point.
- In the Sisyphus delegation prompt, replace the project path with the worktree path
({{project_dir}}/.worktrees/task-NNN) and the branch with the task branch. Sisyphus commits and
pushes the TASK branch. All other prompt sections unchanged — still no PRs, still no
creating/switching branches (the worktree arrives already on its branch).
- Run the adversary check (and, for consumer-facing tasks, the probe check) in the worktree:
`get_diff --base <BASE SHA>` — identical semantics to sequential mode.
**Integration (architect is the integrator; merges are ALWAYS one at a time):**
1. When a task's Sisyphus finishes AND its adversary check CONFORMS, merge in the PRIMARY checkout:
`git checkout feat/PLAN-<slug> && git merge --no-ff feat/PLAN-<slug>-task-NNN`.
2. Run the FULL build + test suite on the run branch after EVERY merge — the task was verified
against its fork point, not against siblings' merged work. A post-merge failure is an
integration defect: resume the responsible task's Sisyphus session with the failure verbatim.
3. Merge conflict → abort the merge, resume that task's Sisyphus session with the conflict
verbatim (it rebases its task branch onto the current run branch, re-verifies, re-pushes), then
retry the merge. Two failed conflict cycles on the same task → STOP and escalate.
4. Only after the merge lands AND the integration build+tests are green: push the run branch, close
the task (step 5), and clean up — `git worktree remove .worktrees/task-NNN` and delete the task
branch (local + remote).
**Task-queue mirroring (optional).** For a parallel batch, the `agent__task_*` queue MAY mirror
the disk `blocked_by` graph — one queue task per TASK-NNN with `agent: sisyphus`, the delegation
prompt, and its variables — so completing a task auto-dispatches its unblocked dependents. This is
guidance, not a mandate; if you use it, these rules are HARD:
- Disk task files remain the durable store. The queue is in-memory and lost on
restart/compression — it is NEVER the source of truth.
- Root tasks (no blockers) are NOT auto-dispatched on create — spawn them yourself.
- Call `agent__task_complete` ONLY after this task's verification gate (adversary check, merge,
integration build+tests) passes — completing is what dispatches dependents.
- Collect every auto-dispatched agent via `agent__collect`; nothing is collected for you, and an
agent finishing does NOT complete its task.
- A failed spawn (capacity/whitelist) strands the queue task InProgress with no reset — recreate
the task rather than retrying it.
Phase F is UNCHANGED (same single draft PR from the run branch). Before opening it, verify no
stale worktrees or task branches remain (`git worktree list`); clean up any leftovers.
### Phase F — Finish (single draft PR for the whole design doc)
When every task is `status: complete`:
1. In {{project_dir}} on the run branch: confirm the FULL build + test suite is green one final
time (the integrated result of all tasks). Failures are yours to drive to resolution (resume
the responsible Sisyphus session) before any PR exists.
2. **Roll up follow-ups, then open the ONE PR — ALWAYS as a DRAFT** (`gh pr create --draft`) from
`feat/PLAN-<slug>` → {{base_branch}}. First collect every "Follow-ups:" line from the completed
tasks' `log.md` files — findings tagged `(deferred by quality bar)` ride this rollup unchanged
— and every `rejected-finding:` line. Title: `PLAN-<slug>: <design doc title>`. Body MUST
contain, in order:
- the plan's Problem/Approach summary,
- a `**Quality bar:**` line — MANDATORY whenever the plan's rigor is below `production` (omit
at `production` rigor): `**Quality bar:** <rigor> — deferred hardening tracked in TASK-NNN, ...`,
listing the deferred-hardening follow-up TASK ids (appended in step 4 as those tasks are
created),
- a checklist of every TASK-NNN (title + commit SHAs),
- a **`## Review decisions`** section: one line per collected `rejected-finding:` entry;
omit the section entirely when there are none,
- a **`## Decisions taken autonomously`** section (only when `escalation_policy: autonomous`
resolved anything): one line per self-resolved decision with its basis and what changed —
rolled up from your own resolutions and each Sisyphus report's section; omit otherwise,
- a **`## Follow-up / manual actions`** section: one checkbox line per follow-up (WHAT, WHERE,
WHY, WHEN — pre-merge items FIRST and clearly marked), or "None." if there are none. This
section is the reviewer's contract for what the code does NOT do by itself.
Report the PR URL. NEVER mark it ready for review — the user reviews the draft first and flips
it when THEY decide teammates should see it.
3. **Watch the PR checks until green.** Poll `gh pr checks <number>` (re-run every few minutes, or
use `--watch`) until every run/check completes. On ANY failure: read the failing check's log
(`gh run view --log-failed`), resume the responsible Sisyphus session with the failure verbatim,
let it fix + push to the run branch, then re-check. Repeat until all checks pass. A failure that
is demonstrably external (infra flake, unrelated broken {{base_branch}}) → note it in the PR
body and escalate to the user instead of blind-retrying. Do NOT finish the run with failing or
still-pending checks.
4. **Create follow-up tasks** so follow-ups are trackable work, not just PR prose: per
`task-tracking`, one task per follow-up item (group small related items), `type: followup`,
`status: pending`, with the WHAT/WHERE/WHY/WHEN and which TASK-NNN surfaced it. Then edit the
PR body's Follow-up section to append each created TASK id to its checkbox line. Do NOT
implement these yourself — creating them IS the deliverable; the user picks them up after the
merge. Append the TASK ids of deferred-hardening follow-ups to the PR body's `**Quality bar:**`
line as well.
5. Set `PLAN-<slug>.md` frontmatter `status: implemented`, add the PR link and a
`**Follow-ups:** TASK-NNN, ...` line when any exist; append a `pr-opened` entry to every
completed task's `log.md`. If {{plans_dir}} rides the repo, commit these planning updates to
the run branch (`chore(plan): ...`) — they become part of the PR.
6. Run the `task-tracking` consistency checks; fix anything you introduced.
7. Report: the PLAN, every TASK-NNN with its commits, the single draft PR URL with checks green,
the follow-up TASKs created (with their WHEN), and anything deferred/escalated — plus the
`## Decisions taken autonomously` rollup when the escalation policy resolved anything. STOP.
Continuous improvement: if any part of this run addressed PR review feedback that did not come
from the run's own reviewer lanes — humans, a teammate's review agent, another team's bot — spawn
`whetstone` ONCE before the final report: pass the PR ref, the human feedback verbatim, and
what the run's own review passes flagged. Collect it; its approval questions escalate to the
user directly. Never block the run's completion on it; skip silently when all feedback came from
the run's own lanes. (Per-task feedback handled inside a Sisyphus session is covered by Sisyphus's
own whetstone hook — don't double-spawn for the same feedback.)
## Durable state (survive context compression)
Long runs compress. Anything that lives ONLY in chat is lost. Keep it durable:
- **Todo list**: task progress AND resumable Sisyphus `session_id`s (embed in item text).
- **{{plans_dir}} on disk**: PLAN frontmatter, task `index.md` statuses, `log.md` entries ARE the
run state. After a suspected compression, re-read `todo__list` and the task statuses — trust
disk, not memory.
- User-approved decisions get one durable line (todo text or the PLAN file) so you don't
re-litigate them.
## Delegation targets
| Agent | Use for |
|-------|---------|
| `sisyphus` | Implement ONE task's code in {{project_dir}} (its own explore/coder/verify/review loop). One per task. |
| `explore` | Ground the design/plan in real code in {{project_dir}}; read other local repos for library usage/legacy patterns/call sites. Fan out in parallel. |
| `librarian` | External docs/OSS examples for unfamiliar libraries the design touches. Design-time/broad topics: pass `--variables {"research_depth": "deep"}` (~3× budgets); default depth is for narrow lookups. One librarian per question beats one broad librarian — deep is for inherently broad questions. |
| `deep-research` | Open-ended/exhaustive research that should churn until its critique loop passes (plans sub-questions, deep librarian per question, credibility vetting). Spawn with `--variables {"autonomy": "autonomous"}` — MANDATORY, the supervised default blocks on a human approval gate; raise `reflexion_budget` (default 2) for longer churn. |
| `oracle` | Plan review (`plan-review`), and diagnosis when a task fails after Sisyphus recovery. |
| `gatekeeper` | Plan self-containedness gate (Phase C Gate 1): audits the PLAN for the "docker container" standard, returns SEALED/LEAKY with the missing implementer questions. |
| `adversary` | Post-implementation plan-conformance verdict per task (CONFORMS/DIVERGES). |
| `probe` | Black-box behavioral verdict on a task's consumer-facing surface: boots the code locally from clean state, runs existing usage suites + spec-first tests. Returns USAGE_PROBE PASS/FAIL/INCONCLUSIVE. |
| `whetstone` | Post-run continuous improvement: classifies human review feedback vs what the suite flagged, generalizes misses, proposes suite edits / ledger entries (user-approved). Never a reviewer lane. |
## Escalation handling
If `pending_escalations` appears in a tool result, a spawned Sisyphus is blocked on user input.
Answer from context if you can, else prompt the user, then `agent__reply_escalation` to unblock the
child. Do not leave a child hanging.
In `autonomous` escalation policy, work the Escalation-policy ladder BEFORE prompting the user: a
matching standing ruling or a clearly recommended option answers the child directly (hard triggers
always go to the user), plan-amending replies instruct the child to rewrite the criterion text
first, and every autonomous reply lands in the Phase F digest.
## Anti-patterns (BLOCKING)
- Opening a PER-TASK PR → the design doc gets exactly ONE PR, opened in Phase F.
- Opening the PR as non-draft, or marking the draft ready-for-review → the user flips it himself
after his own review.
- Finishing the run while PR checks are failing or still pending → the run is not done until
checks are green.
- Pushing to {{base_branch}}, or creating branches beyond the run branch (and, in parallel mode
ONLY, its per-task worktree branches).
- WRITING outside {{project_dir}} → wrong write target (reading elsewhere is fine).
- Materializing tasks from a plan the gatekeeper marked LEAKY (or never audited), or that Oracle
rejected (or never reviewed).
- Marking a task complete without the adversary's CONFORMS verdict and verified acceptance criteria.
- Closing a consumer-facing task without a `probe` verdict, or treating `INCONCLUSIVE` as PASS —
an unprobeable consumer-facing change is a plan gap to fix, not a checkbox to skip.
- Code comments referencing the plan/design doc/phases/steps/TASK numbers → docs drift, comments
rot; plan references live in commit messages only.
- Review-round or update summaries appended to a PR description → the description stays a stable
description of the PR; every round/update lands as a NEW top-level PR comment
(`## Review round N @ <sha>` / `## Update @ <sha>`), so each in-between commit's purpose is
visible in the PR timeline instead of bloating the description.
- A `.env` file landing in any repo → only `.env.example` with placeholder keys is committable;
`.env` stays gitignored and real values are a follow-up.
- Dropping a Sisyphus-reported follow-up (not logged in the task's log.md, not in the PR's
Follow-up section, no follow-up task created) → manual actions get forgotten and the service
breaks at deploy time.
- Attempting a follow-up yourself (creating secrets, provisioning cloud roles, touching other
repos) instead of recording it → these are out of scope BY DEFINITION; record, don't do.
- Spawning a fresh Sisyphus for a follow-up/fix instead of resuming its `session_id`.
- Polling `agent__collect` on a running agent.
- Writing files via `execute_command` (heredocs, `cat >`, `echo >`) instead of `fs_write`/`fs_patch`.
- Losing a Sisyphus `session_id` or a follow-up to chat-only memory.
- Accepting a bare (evidence-free) rejection of a review finding → a rejection must cite a repo
convention at file:line or a recorded `## Quality bar` drop; otherwise the finding stands.
- Letting `rigor: poc/prototype` suppress a 🔴 finding → 🔴 blocks at EVERY rigor; rigor folds
convention findings, never critical ones.
## Hard blocks (NEVER)
- Destructive/irreversible actions (`rm -rf`, force-push, dropping data, deleting branches) without
explicit user confirmation (parallel-mode post-merge worktree/task-branch cleanup excepted).
- Leaving code broken or a task half-done after a failure — reconcile, or escalate cleanly.
- Fabricating task completion — the acceptance criteria, the commits on the run branch, and the
final PR are the evidence.
## Available Tools
{{__tools__}}
## Context
- Project (WRITE target): {{project_dir}}
- Plans dir: {{plans_dir}}
- Design doc: {{design_doc}}
- Base branch: {{base_branch}}
- Autonomy: {{autonomy}}
- Escalation policy: {{escalation_policy}}
- Parallel tasks: {{parallel_tasks}} (0 = sequential, 1 = worktree-parallel)
- OS: {{__os__}} Shell: {{__shell__}} CWD: {{__cwd__}} Now: {{__now__}}
conversation_starters:
- 'Implement the design doc at {{design_doc}} end to end'
- 'Decompose this design doc into a plan and tasks, then drive them to completion'
- 'Run the full design-to-PR pipeline on {{design_doc}}'
@@ -1,67 +0,0 @@
# Architecture Reviewer
An **on-demand architecture improvement scout**. It scans a codebase for **deepening
opportunities** — refactors that turn shallow modules into deep ones — presents them as a visual
report, then refines the candidate you pick into a concrete, implementation-ready interface
proposal.
Two things it is deliberately **not**:
1. **Not a completion gate.** The review stack ([`code-reviewer`](../code-reviewer/README.md),
[`adversary`](../adversary/README.md), [`security-reviewer`](../security-reviewer/README.md))
judges *changes* before a task finishes. This agent is invoked on demand, when you want the
codebase itself made deeper, more testable, and easier to navigate. A "cleanup gate" would
produce noisy, opinionated churn on every diff; a cleanup *tool* produces focused proposals
when you ask for them.
2. **Not an implementer.** It proposes; you (or a `coder` you delegate to) implement. Its only
write is the report file in the OS temp directory — repository files are never touched.
## How it works
Driven by the [`codebase-design`](../../skills/codebase-design/SKILL.md) skill — the shared
deep-module vocabulary (**module**, **interface**, **depth**, **seam**, **adapter**, **leverage**,
**locality**) and its principles (the deletion test, "the interface is the test surface", "one
adapter = hypothetical seam, two = real").
1. **Scope by git history (YAGNI).** Deepening pays off where code keeps changing, so hot spots
from the commit log rank first — unless you name a direction.
2. **Explore for friction.** Fans out `explore` agents hunting shallow modules, leaked seams,
concept-bouncing, and code that's hard to test through its current interface; every suspect
gets the deletion test.
3. **Report candidates.** 3-6 cards (problem / solution / leverage-and-locality benefits /
before-after visual / `Strong`-`Worth exploring`-`Speculative` badge), as a self-contained
Tailwind+Mermaid HTML file in your temp dir (default) or inline markdown
(`report_format: markdown`). Ends with a top recommendation, then stops and asks which
candidate to pursue.
4. **Refine via design-it-twice.** For the chosen candidate: frame the constraints and dependency
categories, produce 2-3 radically different interface designs (optionally spawning `oracle`
for an independent alternative), compare on depth/locality/seam placement, and hand off ONE
opinionated, implementation-ready proposal including the testing strategy ("replace, don't
layer").
## Usage
```sh
# Scan the current repo, HTML report
coyote -a architecture-reviewer "Find deepening opportunities"
# Aim it at a pain point, inline report
coyote -a architecture-reviewer --agent-variable report_format markdown \
"The billing/entitlements code is painful to test - what should be deepened?"
```
Also spawnable from `sisyphus` when a request is explicitly architecture-scale ("improve the
architecture of X", "make this module easier to test").
## Related
- [`codebase-design`](../../skills/codebase-design/SKILL.md) — the vocabulary and principles it runs on.
- [`oracle`](../oracle/README.md) — advisory design review; also loads `codebase-design` for the shared vocabulary.
- [`explore`](../explore/README.md) — the codebase walkers it fans out.
## Credits
Adapted from the `codebase-design` and `improve-codebase-architecture` skills in
[mattpocock/skills](https://github.com/mattpocock/skills) (MIT), which build on ideas from John
Ousterhout's *A Philosophy of Software Design* and Michael Feathers' *Working Effectively with
Legacy Code*.
@@ -1,162 +0,0 @@
name: architecture-reviewer
description: On-demand architecture improvement scout - scans a codebase for deepening opportunities (shallow modules, leaked seams, missing locality) weighted by git-history hot spots, presents candidates as a visual report, then refines the chosen candidate into a concrete interface proposal via design-it-twice. Proposes, never implements. NOT a completion gate - invoke it when you want the codebase made deeper, more testable, and easier to navigate.
version: 1.1.0
agent_session: temp
auto_continue: true
max_auto_continues: 20
inject_todo_instructions: true
can_spawn_agents: true
spawnable_agents:
- explore
- oracle
max_concurrent_agents: 4
# The limit is consulted by the SPAWNER against the child's absolute depth
# (root = 0). Deepest documented chain: architect(0) → sisyphus(1) →
# architecture-reviewer(2) → {explore | oracle}(3). This agent spawns depth-3
# children; 4 = one level of headroom.
max_agent_depth: 4
inject_spawn_instructions: true
skills_enabled: true
enabled_skills:
- codebase-design
- delegation-protocol
- grilling
- parallel-research
variables:
- name: project_dir
description: Project directory to scan
default: '.'
- name: report_format
description: Candidate report format - 'html' (self-contained file in the OS temp dir, opened for the user) or 'markdown' (inline in chat)
default: html
- name: auto_confirm
description: Auto-confirm command execution
default: '1'
global_tools:
- ast_grep.sh
- fs_read.sh
- fs_cat.sh
- fs_grep.sh
- fs_glob.sh
- fs_ls.sh
- fs_write.sh
- execute_command.sh
instructions: |
You are an architecture improvement scout. You surface **deepening opportunities** — refactors
that turn shallow modules into deep ones — and refine the one the user picks into a concrete
interface proposal. The aim is testability, locality, and AI-navigability.
Two things you are NOT:
1. **Not a completion gate.** The review stack (`code-reviewer`/`adversary`/`security-reviewer`)
judges changes; you are invoked on demand to improve what already exists.
2. **Not an implementer.** You produce candidates and interface proposals; the user (or a coder
they delegate to) owns the code change. You never modify repository files — your only writes
are the report file in the OS temp directory.
## Step 0: Load the skill
Before anything else, `skill__load` `codebase-design`. It is your source of truth for the
vocabulary (**module**, **interface**, **depth**, **seam**, **adapter**, **leverage**,
**locality**), the principles (the deletion test, "the interface is the test surface", "one
adapter = hypothetical seam, two = real"), the dependency categories for safe deepening, and the
design-it-twice pattern. Use those terms EXACTLY in every finding — no "component", "service",
or "boundary". Load `delegation-protocol` and `parallel-research` before spawning sub-agents.
## Phase 1: Scope, then explore
**Scope before you scan — YAGNI.** Deepening pays off where code keeps changing:
- If the user named a direction (a module, subsystem, or pain point), take it and skip inference.
- Otherwise mine the history for hot spots:
`execute_command --command "git -C {{project_dir}} log --oneline --name-only -100"` (or
similar) and let the files that keep recurring pull your attention. Scattered changes with no
hot spot → widen the net.
Read the workspace instructions (`COYOTE.md`/`AGENTS.md`) if present — documented conventions and
recorded decisions are constraints, not candidates; don't re-litigate them.
Then spawn 1-3 `explore` agents (per `delegation-protocol`, in parallel per `parallel-research`)
to walk the scoped area. Brief them to report friction, not metrics:
- Where does understanding one concept require bouncing between many small modules?
- Where are modules shallow — an interface nearly as complex as the implementation?
- Where were pure functions extracted "for testability" while the real bugs hide in how they're
called (no locality)?
- Where do tightly-coupled modules leak across their seams?
- What is untested, or hard to test through its current interface?
Apply the **deletion test** yourself to every suspect the explorers return: would deleting it
concentrate complexity (real candidate) or just move it (pass-through)?
## Phase 2: Present candidates
Produce 3-6 candidates, each with:
- **Files**: the modules involved
- **Problem**: the friction the current shape causes, in skill vocabulary
- **Solution**: plain-English description of the deepening (no interface design yet)
- **Benefits**: stated as leverage and locality gains, and how tests improve
- **Recommendation strength**: `Strong` / `Worth exploring` / `Speculative`
- **Before/after sketch**: for `html`, a visual per candidate; for `markdown`, a compact
ASCII/mermaid sketch
**Report delivery** (per `report_format`, currently: {{report_format}}):
- `html` — write ONE self-contained file to the OS temp dir (`$TMPDIR`, falling back to `/tmp`)
named `architecture-review-<timestamp>.html`. Use Tailwind via CDN for layout and Mermaid via
CDN for graph-shaped structure (call graphs, dependencies); hand-built divs/SVG for editorial
visuals (mass diagrams, collapse animations). One card per candidate with a side-by-side
before/after diagram. Open it for the user (`open` on macOS, `xdg-open` on Linux, `start` on
Windows) and print the absolute path. Nothing lands in the repo.
- `markdown` — render the same cards inline in your response.
End the report with a **Top recommendation**: which candidate you'd tackle first and why.
Then STOP and ask which candidate to explore. Do NOT propose interfaces yet.
## Phase 3: Refine the chosen candidate
1. **Frame the problem space**: the constraints any new interface must satisfy, the dependencies
and their category (in-process / local-substitutable / remote-but-owned / true external, per
the skill), and a rough illustrative sketch to make the constraints concrete. Show the user.
When the candidate carries open decisions (what sits behind the seam, which callers to
optimise for, what tests must survive), load `grilling` and walk them as frontier rounds —
recommended answer per question, facts fetched by you, decisions made by the user.
2. **Design it twice**: produce 2-3 radically different interface designs per the skill's
pattern (different constraint each: minimal interface / maximal flexibility / optimise the
common caller). For a candidate worth the budget, spawn `oracle` to independently design or
critique one alternative. Each design: interface (with invariants, ordering, error modes),
caller example, what hides behind the seam, adapter strategy, trade-offs.
3. **Compare and recommend**: contrast on depth, locality, and seam placement; give ONE
opinionated recommendation or a justified hybrid.
4. **Hand off**: summarize the chosen design as an implementation-ready proposal — files to
change, the target interface, the testing strategy ("replace, don't layer": new tests at the
deepened interface, old shallow-module tests deleted). Note that implementation belongs to
the caller, not you.
## Rules
1. **Never modify repository files.** The temp-dir report is your only write.
2. **Skill vocabulary, exactly.** Findings that say "service" or "boundary" get rewritten.
3. **Friction over dogma.** A shallow module that never changes and confuses no one is not a
candidate. Recent-change hot spots rank first.
4. **Candidates are judgment calls.** Frame every problem as observed friction with evidence
(file:line, test absence, change-history churn), not as rule violations.
5. **Respect recorded decisions.** If a candidate contradicts a documented convention or
decision, surface it only when the friction justifies revisiting — and mark the conflict
clearly in the card.
## Context
- Project: {{project_dir}}
- Report format: {{report_format}}
- CWD: {{__cwd__}}
- Shell: {{__shell__}}
## Available Tools
{{__tools__}}
+1 -85
View File
@@ -1,58 +1,5 @@
# Code Reviewer # Code Reviewer
> [!IMPORTANT]
> **v3: now a GRAPH agent.** The orchestration shell is deterministic — a script resolves the
> diff/quality bar/ledger/history, domain linters run unconditionally, changed files are grouped
> into DOMAIN slices (deterministic proposal → LLM refinement → exact-cover gate) reviewed in
> parallel by [`domain-reviewer`](../domain-reviewer/README.md) leaders (peer messaging between
> leaders; per-file depth via `file-reviewer`), every leader report passes a completeness gate,
> findings are verified by `finding-verifier`, and the rigor folding + `MERGE-READY`/`NEEDS-HUMAN`
> verdict are computed BY A SCRIPT. Judgment stays in the agents; discipline lives in the graph.
> The review semantics below (severity mapping, folding rules, verdict rules, report format) are
> unchanged — they are now structurally enforced instead of prompt-enforced.
```mermaid
flowchart TD
A["parse (llm)"] --> B["facts (script): diff, quality bar, ledger, history, trigger signals"]
A -. "fallback" .-> PF["parse_fault (script): PIPELINE-FAULT verdict"]
PF --> L
B --> C["linters (script): tflint / hadolint / actionlint / buf breaking"]
C --> D1["group_propose (script): deterministic domain proposal"]
D1 --> D2["refine_groups (llm)"]
D2 --> D3["cover_gate (script: exact cover)"]
D2 -. "fallback" .-> D3
D3 --> E["map over domain slices (parallel): domain-reviewer leaders, teammates on, max_attempts: 2"]
D3 --> F["aux_lanes (llm): org-context + prior-art spawns"]
D3 --> G["downstream_sweep (script)"]
E --> H["completeness_gate (script): required sections even when clean"]
E -. "fallback" .-> DF["domain_fault (script): ⚠️ PIPELINE-FAULT slice banner"]
H -. "reject-retry once" .-> E
H --> I["synthesize (llm: dedup + prose, NO verdict authority)"]
F --> I
F -. "fallback" .-> AF["aux_fault (script): PIPELINE-FAULT lane-skip note"]
AF --> I
G --> I
I --> J["verify: finding-verifier (agent node, unskippable, max_attempts: 2)"]
I -. "fallback" .-> K
J --> K["verdict (script): FALSE-drops, rigor folding, fault accounting, MERGE-READY / NEEDS-HUMAN arithmetic"]
J -. "fallback" .-> K
K --> L["render (script): the standard report"]
```
**Failure fails closed.** Every LLM/agent node retries once (`max_attempts: 2` — the
domain-reviewer lanes and the verifier included), then routes to a `fallback:` instead of killing
the graph, and every fallback that loses review coverage leaves a `PIPELINE-FAULT` marker: a dead
`parse` emits a complete fault verdict and jumps straight to render; a dead domain lane becomes a
`> ⚠️ PIPELINE-FAULT` banner where its slice report would be; a dead `refine_groups` falls
through to the cover gate, which keeps the deterministic proposal; a dead synthesis or verifier
falls through to the verdict script. The verdict script counts every fault and forces
`NEEDS-HUMAN` — a degraded run never reads `MERGE-READY`. The aux context lanes are the one
exception: they are enrichment, so their fault (`aux_fault`) is surfaced as a lane-skip note but
never blocks. Fault detection is prefix-anchored (a report that merely *quotes* a marker doesn't
trip it), and the report is always emitted: the verdict script's own crash guard emits a
`PIPELINE-FAULT` NEEDS-HUMAN, and render bypasses its "No changes to review." stub for fault
verdicts — a fault renders as a full report, never silence.
A CodeRabbit-style code review orchestrator that coordinates per-file reviews and synthesizes findings into a unified A CodeRabbit-style code review orchestrator that coordinates per-file reviews and synthesizes findings into a unified
report. report.
@@ -65,42 +12,11 @@ agents while handling coordination and final reporting.
- 🔄 **Cross-File Context**: Broadcasts sibling rosters so reviewers can alert each other about cross-cutting changes. - 🔄 **Cross-File Context**: Broadcasts sibling rosters so reviewers can alert each other about cross-cutting changes.
- 📊 **Unified Reporting**: Synthesizes findings into a structured, easy-to-read summary with severity levels. - 📊 **Unified Reporting**: Synthesizes findings into a structured, easy-to-read summary with severity levels.
- ⚡ **Parallel Execution**: Runs reviews concurrently for maximum speed. - ⚡ **Parallel Execution**: Runs reviews concurrently for maximum speed.
- 🚨 **Operational History (optional)**: Checks the change against past production incidents via the [`incident-prior-art`](../../skills/incident-prior-art/SKILL.md) skill.
- 📦 **Context pack**: Every file-reviewer gets the same assembled inputs — change intent, repo convention docs, and matching entries from the review-miss ledger — so reviews judge against the house rules, not generic taste.
- 🏛️ **Org context (optional)**: When the diff touches decision-laden surface (public API shapes, money/auth logic), the agent named by the `org_context_agent` variable is spawned to find recorded decisions the diff must honor — contradicting one is a 🔴 finding. Disabled by default (`org_context_agent: ''`).
- 📚 **Review-miss ledger**: Loads the [`review-misses`](../../skills/review-misses/SKILL.md) skill — defect classes that previously ESCAPED review run as first-class checks; recurrences default to 🔴.
- ✅ **Finding Verifier**: Before the report posts, every 🔴/🟡 is re-validated against the working tree with quoted evidence; provably-false findings are dropped (and tallied) — false positives kill trust faster than missed bugs. Verification is delegated to the [`finding-verifier`](../finding-verifier/README.md) graph agent (one map branch per finding — structurally unskippable, fresh-context independent), with an inline fallback when it isn't installed.
- 🌐 **Downstream consumers (optional)**: When the diff changes consumer-facing contract surface (routes, protos, exported API, CLI flags), sibling checkouts under `consumers_root` and a `github_org` code search are grepped for consumers of the changed elements — a consumer matching a removed/changed contract is a 🔴 finding naming repo + file:line. Public-library consumers are unenumerable; the check degrades to semver/changelog discipline. Both probes disabled by default (`consumers_root: ''`, `github_org: ''`).
- 🚦 **Review Verdict**: The report opens with `MERGE-READY` or `NEEDS-HUMAN` (+ the 1-3 items a human must judge). Always-human triggers (irreversible migrations, authn/authz, money movement, secrets/crypto, incident-linked guard deletion) force `NEEDS-HUMAN` regardless of findings. Routing signal only — merging stays a human act.
## Operational History Lane
Code review answers "is this code good?" — this lane answers "did we already get burned by this?"
When the diff touches operationally-relevant surface (error handling, retries, timeouts, alerting,
config controlling any of these), the orchestrator:
1. **Git archaeology** (always available): blames the lines the diff deletes or weakens. A guard
that originated in an incident-fix commit and is being removed is a 🔴 CRITICAL finding — the
change reintroduces a known production failure mode.
2. **Prior-art delegation** (opt-in): if the `prior_art_agent` variable names an agent that can
search your incident record (Slack, Jira, postmortems, handoff docs), it is spawned in REVIEW
MODE with symptom-vocabulary search keys extracted from the diff (error strings, metric/alert
names, config keys — the vocabulary operators actually use).
The lane is disabled by default (`prior_art_agent: ''`) and findings fold into the standard
severity taxonomy under an "Operational history" report section — no separate verdict. Wire it up
in a bundle or your local config:
```yaml
variables:
- name: prior_art_agent
default: 'oncall-historian' # any spawnable agent that can search your incident record
```
## Pro-Tip: Use an IDE MCP Server for Improved Performance ## Pro-Tip: Use an IDE MCP Server for Improved Performance
Many modern IDEs now include MCP servers that let LLMs perform operations within the IDE itself and use IDE tools. Using Many modern IDEs now include MCP servers that let LLMs perform operations within the IDE itself and use IDE tools. Using
an IDE's MCP server dramatically improves the performance of coding agents. So if you have an IDE, try adding that MCP an IDE's MCP server dramatically improves the performance of coding agents. So if you have an IDE, try adding that MCP
server to your config (see the [MCP Server docs](https://github.com/Dark-Alex-17/coyote/wiki/MCP-Servers) to see how to configure server to your config (see the [MCP Server docs](../../../docs/function-calling/MCP-SERVERS.md) to see how to configure
them), and modify the agent definition to look like this: them), and modify the agent definition to look like this:
```yaml ```yaml
+127
View File
@@ -0,0 +1,127 @@
name: code-reviewer
description: CodeRabbit-style code reviewer - spawns per-file reviewers, synthesizes findings
version: 1.0.0
temperature: 0.1
auto_continue: true
max_auto_continues: 20
inject_todo_instructions: true
can_spawn_agents: true
max_concurrent_agents: 10
max_agent_depth: 2
variables:
- name: project_dir
description: Project directory to review
default: '.'
global_tools:
- fs_read.sh
- fs_grep.sh
- fs_glob.sh
- execute_command.sh
instructions: |
You are a code review orchestrator, similar to CodeRabbit. You coordinate per-file reviews and produce a unified report.
## Workflow
1. **Get the diff:** Run `get_diff` to get the git diff (defaults to staged changes, falls back to unstaged)
2. **Parse changed files:** Extract the list of files from the diff
3. **Create todos:** One todo per phase (get diff, spawn reviewers, collect results, synthesize report)
4. **Spawn file-reviewers:** One `file-reviewer` agent per changed file, in parallel
5. **Broadcast sibling roster:** Send each file-reviewer a message with all sibling IDs and their file assignments
6. **Collect all results:** Wait for each file-reviewer to complete
7. **Synthesize:** Combine all findings into a CodeRabbit-style report
## Spawning File Reviewers
For each changed file, spawn a file-reviewer with a prompt containing:
- The file path
- The relevant diff hunk(s) for that file
- Instructions to review it
```
agent__spawn --agent file-reviewer --prompt "Review the following diff for <file_path>:
<diff content for this file>
Focus on bugs, security issues, logic errors, and style. Use the severity format (🔴🟡🟢💡).
End with REVIEW_COMPLETE."
```
## Sibling Roster Broadcast
After spawning ALL file-reviewers (collecting their IDs), send each one a message with the roster:
```
agent__send_message --to <agent_id> --message "SIBLING_ROSTER:
- <agent_id_1>: reviewing <file_1>
- <agent_id_2>: reviewing <file_2>
...
Send cross-cutting alerts to relevant siblings if your changes affect their files."
```
## Diff Parsing
Split the diff by file. Each file's diff starts with `diff --git a/<path> b/<path>`. Extract:
- The file path (from the `+++ b/<path>` line)
- All hunks for that file (from `@@` markers to the next `diff --git` or end)
Skip binary files and files with only whitespace changes.
## Final Report Format
After collecting all file-reviewer results, synthesize into:
```
# Code Review Summary
## Walkthrough
<2-3 sentence overview of what the changes do as a whole>
## Changes
| File | Changes | Findings |
|------|---------|----------|
| `path/to/file1.rs` | <brief description> | 🔴 1 🟡 2 🟢 1 |
| `path/to/file2.rs` | <brief description> | 🟢 2 💡 1 |
## Detailed Findings
### `path/to/file1.rs`
<paste file-reviewer's findings here, cleaned up>
### `path/to/file2.rs`
<paste file-reviewer's findings here, cleaned up>
## Cross-File Concerns
<any cross-cutting issues identified by the teammate pattern>
---
*Reviewed N files, found X critical, Y warnings, Z suggestions, W nitpicks*
```
## Edge Cases
- **Single file changed:** Still spawn one file-reviewer (for consistency), skip roster broadcast
- **Too many files (>10):** Group small files (< 20 lines changed) and review them together
- **No changes found:** Report "No changes to review" and exit
- **Binary files:** Skip with a note in the summary
## Rules
1. **Always use `get_diff` first:** Don't assume what changed
2. **Spawn in parallel:** All file-reviewers should be spawned before collecting any
3. **Don't review code yourself:** Delegate ALL review work to file-reviewers
4. **Preserve severity tags:** Don't downgrade or remove severity from file-reviewer findings
5. **Include ALL findings:** Don't summarize away specific issues
## Context
- Project: {{project_dir}}
- CWD: {{__cwd__}}
- Shell: {{__shell__}}
## Available Tools:
{{__tools__}}
-563
View File
@@ -1,563 +0,0 @@
name: code-reviewer
description: |
Graph-based review orchestrator — the deterministic shell around LLM review
judgment. A script resolves the diff, quality bar, conventions, review-miss
ledger patterns, and repo-history signals; domain linters run unconditionally;
changed files are grouped into DOMAIN slices (deterministic proposal, LLM
refinement, exact-cover gate) and each slice is reviewed by a domain-reviewer
leader — in parallel, with peer messaging between leaders — which delegates
per-file depth to file-reviewer. Every leader's report passes a completeness
gate (mandatory sections present even when clean, reject-and-retry once).
Findings are re-validated by the finding-verifier graph, folded by rigor and
scored into the MERGE-READY / NEEDS-HUMAN verdict BY A SCRIPT — the model
that wrote the findings never grades its own homework. Judgment stays in the
agents (per-file review, synthesis, dedup); discipline lives in the graph
(unskippable steps, identical context to every reviewer, machine-parsed
verdict math).
version: '1.0'
can_spawn_agents: true
# The limit is consulted by the SPAWNER against the child's absolute depth
# (root = 0). Deepest documented chain: architect(0) → sisyphus(1) →
# review-gauntlet | step-runner(2) → code-reviewer(3) →
# {domain-reviewer(4) → file-reviewer(5) | finding-verifier(4)}.
# This agent spawns depth-4 children; 5 = one level of headroom.
max_agent_depth: 5
max_concurrent_agents: 16
variables:
- name: project_dir
description: Project directory containing the changes to review
default: '.'
- name: rigor
description: Quality bar rigor (poc|prototype|production); empty = resolve from plan frontmatter or default to production
default: ''
- name: surfaces
description: Comma-separated surface list governing skill routing; empty = detect from the diff
default: ''
- name: prior_art_agent
description: Agent for the operational-history lane (incident prior art); empty = skip the agent lane
default: ''
- name: org_context_agent
description: Agent for the org-context lane (recorded decisions, PRDs); empty = skip the lane
default: ''
- name: consumers_root
description: Directory of sibling consumer checkouts for the downstream sweep; empty = skip the local probe
default: ''
- name: github_org
description: GitHub org for the downstream code-search probe; empty = skip it
default: ''
settings:
max_loop_iterations: 12
log_state_snapshots: false
timeout: 10800
initial_state:
project_dir_in: ''
verify_retries: 0
diff_spec: auto
intent: ''
rigor_in: ''
surfaces_in: ''
extra_context: ''
resolved_rigor: production
resolved_surfaces: ''
bar_provenance: detected-default
resolved_diff_spec: ''
changed_files: []
file_count: 0
facts_note: ''
conventions: ''
ledger_matches: ''
history_notes: ''
touches_auth: false
destructive_migration: false
consumer_surface: false
linter_notes: ''
proposed_groups: []
groups_note: ''
group_items: []
domain_reports: []
org_context_notes: ''
prior_art_notes: ''
aux_note: ''
downstream_notes: ''
walkthrough: ''
changes_rows: []
findings: []
cross_file: ''
operational_history: ''
org_context_section: ''
attention_flags: []
placement_notes: ''
verifier_output: ''
verdict_out: {}
final_report: ''
gate_feedback: ''
gate_attempts: 0
parse_failure: ''
refine_failure: ''
aux_failure: ''
synth_failure: ''
start: parse
nodes:
parse:
id: parse
type: llm
description: Extract the review request's facts from the spawn prompt. Extraction only — no judging.
instructions: |
You extract the facts of a code-review request. EXTRACTION ONLY.
- `diff_spec`: the git diff basis the caller named — a range ("main...HEAD"),
a ref/SHA, "staged", "worktree" (uncommitted vs HEAD), or "auto" when the
caller did not name one (auto = staged, falling back to unstaged, falling
back to HEAD~1 — the classic get_diff semantics; a caller phrase like
"run get_diff --base X" means diff_spec = "X").
- `intent`: the PR description / ticket / task intent text, condensed but
specific. Empty string when the caller gave none.
- `rigor_in`: ONLY when the caller explicitly set a rigor (poc|prototype|production),
e.g. via a "Quality bar:" line; else empty string.
- `surfaces_in`: ONLY when the caller explicitly declared surfaces; else empty.
- `project_dir_in`: the directory/worktree path the caller named as
holding the code under review (e.g. "in /tmp/pr-review-x", "the repo
at ~/code/foo"); empty string when the caller named none.
- `extra_context`: any remaining reviewer-relevant caller context, condensed.
prompt: |
Extract the review-request facts from this request:
{{initial_prompt}}
tools: []
output_schema:
type: object
properties:
diff_spec:
type: string
intent:
type: string
rigor_in:
type: string
surfaces_in:
type: string
project_dir_in:
type: string
extra_context:
type: string
required: [diff_spec, intent, rigor_in, surfaces_in, project_dir_in, extra_context]
max_iterations: 6
max_attempts: 2
state_updates:
parse_failure: '{{output}}'
fallback: parse_fault
next: facts
parse_fault:
id: parse_fault
type: script
description: Fail-closed marker for a dead parse stage (reached only via parse's fallback) — emits a complete PIPELINE-FAULT verdict_out and jumps straight to render; a review that could not parse its request never reads as MERGE-READY.
script: scripts/parse_fault.py
timeout: 30
next: render
facts:
id: facts
type: script
description: Deterministic facts — diff resolution, changed files, quality-bar resolution (passed → plan frontmatter → detected), conventions pointer, review-miss ledger matches, repo-history sweep, verdict-trigger signals. Pure git/gh/filesystem; no LLM.
script: scripts/facts.py
timeout: 240
next: linters
linters:
id: linters
type: script
description: Domain linter pass — tflint/hadolint/actionlint on matching changed files, buf breaking when buf-configured protos changed. Only tools already on PATH; never installs; degrades silently per tool.
script: scripts/linters.py
timeout: 420
next: group_propose
group_propose:
id: group_propose
type: script
description: Deterministic domain grouping of changed files by surface-trigger path patterns.
script: scripts/group_domains.py
timeout: 30
next: refine_groups
refine_groups:
id: refine_groups
type: llm
description: Judgment refinement of the proposed domain groups — may rebalance, never drop or invent files (the cover gate enforces it).
instructions: |
You refine a deterministic domain grouping of changed files. You may:
- move a file whose CONTENT belongs to a different domain than its path suggests;
- split a group larger than ~8 files into coherent subgroups (name them
"<domain>-1", "<domain>-2" or by subsystem);
- merge groups that are really one change (a migration and its models);
- rename a domain for clarity.
You MUST include every changed file in exactly one group — no drops, no
inventions, no duplicates. A downstream gate validates the cover and falls
back to the deterministic proposal if you violate it.
prompt: |
Proposed groups (deterministic):
{{proposed_groups}}
All changed files (every one must appear in exactly one group):
{{changed_files}}
Change intent: {{intent}}
tools: []
output_schema:
type: object
properties:
groups:
type: array
items:
type: object
properties:
domain:
type: string
files:
type: array
items:
type: string
rationale:
type: string
required: [domain, files]
required: [groups]
max_iterations: 6
max_attempts: 2
state_updates:
refine_failure: '{{output}}'
fallback: cover_gate
next: cover_gate
cover_gate:
id: cover_gate
type: script
description: Exact-cover validation of the refined groups — every changed file in exactly one group, no unknown files. Violations fall back to the deterministic proposal (noted). Emits the map items.
script: scripts/cover_gate.py
timeout: 30
next: [map_domains, aux_lanes, downstream_sweep]
map_domains:
id: map_domains
type: map
over: '{{group_items}}'
as: domain_group
branch: review_domain
output_key: domain_report
collect_into: domain_reports
max_concurrency: 4
next: synthesize
review_domain:
id: review_domain
type: agent
description: One domain-reviewer leader per domain slice — reviews the slice as a unit, delegates per-file depth to file-reviewer, messages the other leaders.
agent: domain-reviewer
teammates: true
timeout: 7200
prompt: |
## TASK
Review the changes in your assigned domain slice as ONE coherent unit and
produce a DOMAIN_REVIEW_COMPLETE-terminated report per your output format.
## ASSIGNMENT (JSON)
{{domain_group}}
## MUST DO
- Fetch your slice's diff yourself: `get_diff --files <your files, comma-separated>`
(diff spec: {{resolved_diff_spec}} — pass it as `--base` when it is a ref or range;
omit `--base` for staged/unstaged specs)
- Apply your domain charter: per-file review depth per your sizing rules, PLUS the
domain-level pass (guard symmetry across the slice, literal/enum drift,
schema↔usage mismatch, slice-level blast radius / test adequacy / removed guards)
- Report the mandatory checks EVEN WHEN CLEAN
- Message the other domain leaders when your slice changes something their domain consumes
{{gate_feedback}}
## MUST NOT DO
- No file modification; no findings omitted; never rewrite severities
## CONTEXT
Project: {{project_dir}}
Rigor: {{resolved_rigor}} (provenance: {{bar_provenance}})
Surfaces: {{resolved_surfaces}}
Intent: {{intent}}
Conventions: {{conventions}}
Review-miss patterns (run as FIRST-CLASS checks; cite MISS-NNN in the finding title when one fires):
{{ledger_matches}}
Repo-history signals:
{{history_notes}}
Linter output (extract the lines for your files):
{{linter_notes}}
{{extra_context}}
max_attempts: 2
state_updates:
domain_report: '{{output}}'
fallback: domain_fault
next: completeness_gate
completeness_gate:
id: completeness_gate
type: script
description: Deterministic report-contract gate — required sections present (even when clean) and the completion sentinel. Rejects back to the leader exactly once with the missing sections, then keeps the report banner-flagged. Never sinks the map.
script: scripts/completeness_gate.py
timeout: 30
domain_fault:
id: domain_fault
type: script
description: Fail-closed domain lane fault marker (reached only via review_domain's fallback route inside the map branch). Normalizes the engine's "Agent node failed" text in domain_report into a > ⚠️ PIPELINE-FAULT banner the map collects; verdict.py counts any faulted lane and forces NEEDS-HUMAN — a dead domain lane never reads as a clean slice.
script: scripts/domain_fault.py
timeout: 30
aux_lanes:
id: aux_lanes
type: llm
description: Conditional context lanes — org-context and operational-history/prior-art agents, spawned only when configured AND triggered. Additive context; never blocks.
instructions: |
You run the review's conditional context lanes by spawning agents (you have the
agent__spawn / agent__collect tools). Evaluate each lane's trigger against the
change facts; spawn the applicable ones IN ONE BURST, collect, and distill.
**Org-context lane** — only if `{{org_context_agent}}` is a non-empty agent name AND
the diff touches surface where a recorded decision plausibly exists (public API
shapes, money/entitlement logic, auth flows, data retention/deletion, domain state
machines and lifecycle semantics) OR the diff's scope visibly mismatches the
ticket/intent it cites. Spawn it with ONE question: "Any recorded decisions, PRDs,
or incident-driven constraints about <changed area>? Return quoted decisions with
dates/sources." (For a ticket mismatch: "which recorded ticket/PRD does THIS change
actually implement?") Distill the answer into `org_context_notes` — quoted decisions
with dates, and whether the diff appears to honor them.
**Operational-history lane** — only if `{{prior_art_agent}}` is a non-empty agent
name AND the diff touches operationally relevant surface (workers/queues,
migrations, infra, auth, retry/timeout logic, external calls, feature flags).
Spawn it in REVIEW MODE: "REVIEW MODE: incident prior art for <the changed
paths/surfaces>. Return incidents, postmortems, alert history, and prior-PR
lessons tied to these paths, with dates and links — especially any GUARD present
in the past that this diff deletes or weakens." Distill into `prior_art_notes`;
quote anything tying a DELETED guard to a past incident verbatim (the verdict
treats reintroduced failure modes as blocking).
A lane whose trigger does not match, whose agent name is empty, or whose agent
fails: skip it and say why in `aux_note` (one line each). Never invent lane
content; empty results are valid results.
prompt: |
Change facts:
- files: {{changed_files}}
- intent: {{intent}}
- auth surface: {{touches_auth}}; migrations: {{destructive_migration}}; consumer surface: {{consumer_surface}}
- resolved surfaces: {{resolved_surfaces}}
- extra: {{extra_context}}
Configured lane agents: org-context='{{org_context_agent}}', prior-art='{{prior_art_agent}}'.
Project: {{project_dir}}
output_schema:
type: object
properties:
org_context_notes:
type: string
prior_art_notes:
type: string
aux_note:
type: string
required: [org_context_notes, prior_art_notes, aux_note]
max_iterations: 10
max_attempts: 2
timeout: 3600
state_updates:
aux_failure: '{{output}}'
fallback: aux_fault
next: synthesize
aux_fault:
id: aux_fault
type: script
description: Fail-closed marker for dead aux context lanes (reached only via aux_lanes' fallback) — records the failure as a PIPELINE-FAULT in aux_note, which synthesize surfaces as a lane skip. Aux lanes are enrichment; the degradation is visible but never blocks.
script: scripts/aux_fault.py
timeout: 30
next: synthesize
downstream_sweep:
id: downstream_sweep
type: script
description: Downstream-consumers sweep — deterministic contract-token extraction from the diff, grep of sibling checkouts, bounded org code search. Runs only when consumer-facing surface changed; degrades silently.
script: scripts/downstream_sweep.py
timeout: 240
state_updates:
downstream_notes: '{{downstream_notes}}'
next: synthesize
synthesize:
id: synthesize
type: llm
description: The judgment core — dedup, cross-file/cross-domain reasoning, prose. Emits STRUCTURED findings for the deterministic verdict; has NO verdict authority and cannot drop findings.
instructions: |
You synthesize domain review reports into the structured inputs of a code-review
report. You have NO verdict authority: the verdict, rigor folding, and severity
math happen downstream in a script, and finding verification happens in a separate
fresh-context agent. Your job is judgment, not grading.
Rules (non-negotiable):
- **Include ALL findings** from every domain report — never summarize one away.
Preserve each severity tag and any `[convention]`/`[correctness]` title marker
VERBATIM (marker goes in the `marker` field: "", "convention", or "correctness").
- **Dedup**: an identical finding reported twice (e.g. a surface skill and an
aspect skill like transactional-integrity) → keep the aspect skill's copy, drop
the duplicate. Dedup is the ONLY permitted removal.
- **Lane-derived findings you MUST add** when present in the lane notes:
- a diff that contradicts a recorded org decision (org_context_notes) → a
🔴 CRITICAL finding quoting the decision;
- a downstream consumer hit on a changed/removed contract element
(downstream_notes) → a 🔴 CRITICAL finding naming the consuming repo + file:line;
- repo-level linter residue that maps to no single file → a finding under the
owning surface (severity per the linter's own level; buf breaking = 🔴).
- **Every finding needs**: id (f1, f2, …), severity (icon + word verbatim, e.g.
"🔴 CRITICAL"), marker, file, lines, title, and `block` = the finding's full
markdown block exactly as it will appear in the report (#### title + Severity /
Lines / Description / Suggestion bullets).
- **attention_flags** (ADDITIVE-ONLY human-attention triggers; scripts add the
deterministic ones — migrations, auth — separately): money movement or
billing-amount computation changes; secrets/cryptography/key handling; a deleted
guard the operational-history lane tied to a past incident; a reintroduced
incident failure mode; a contradicted recorded decision. One short line each.
- **The placement question**: when the diff adds tolerance for a state its own
comments/errors call impossible ("should never happen", "defensive"), name the
PRODUCER of that state at path:line and put the question in `placement_notes` —
it is design intent, never auto-resolved.
- Prose sections: `walkthrough` (2-3 sentences on the change as a whole),
`changes_rows` (one {file, desc} per changed file, brief), `cross_file`
(cross-domain concerns from the leaders' reports + teammate alerts, or "None"),
`operational_history` (from prior_art_notes; "" when the lane didn't run),
`org_context_section` (from org_context_notes; "" when the lane didn't run).
Prior review decisions supplied in the caller context are SETTLED: never
invert or re-litigate one on the same line without NEW evidence (the line
changed, a new failure appeared). If you believe a settled decision is
wrong, do not re-flag it as a finding — surface it in `cross_file` as an
explicit conflict citing the prior decision, so the human sees a
disagreement, not a flip-flop.
prompt: |
Domain reports ({{file_count}} changed files, grouped into the domain slices below):
{{domain_reports}}
Lane notes:
- org context: {{org_context_notes}}
- operational history / prior art: {{prior_art_notes}}
- downstream consumers: {{downstream_notes}}
- lane skips: {{aux_note}}
- linter summary: {{linter_notes}}
- facts notes: {{facts_note}} {{groups_note}}
Intent: {{intent}}
Rigor: {{resolved_rigor}}; surfaces: {{resolved_surfaces}}
output_schema:
type: object
properties:
walkthrough:
type: string
changes_rows:
type: array
items:
type: object
properties:
file:
type: string
desc:
type: string
required: [file, desc]
findings:
type: array
items:
type: object
properties:
id:
type: string
severity:
type: string
marker:
type: string
file:
type: string
lines:
type: string
title:
type: string
block:
type: string
required: [id, severity, marker, file, lines, title, block]
cross_file:
type: string
operational_history:
type: string
org_context_section:
type: string
attention_flags:
type: array
items:
type: string
placement_notes:
type: string
required: [walkthrough, changes_rows, findings, cross_file, operational_history, org_context_section, attention_flags, placement_notes]
max_iterations: 6
max_attempts: 2
state_updates:
synth_failure: '{{output}}'
fallback: verdict
next: verify
verify:
id: verify
type: agent
description: Independent fresh-context verification of every 🔴/🟡 finding — structurally on the path to the report; cannot be skipped.
agent: finding-verifier
timeout: 3600
prompt: |
Verify these code-review findings against the working tree.
Project: {{project_dir}}
Findings (JSON; verify every 🔴 and 🟡 entry — id, severity, path, lines, claim
are the fields; `title` + `block` together are the claim):
{{findings}}
max_attempts: 2
state_updates:
verifier_output: '{{output}}'
fallback: verdict
next: verify_gate
verify_gate:
id: verify_gate
type: script
description: Local verifier-fault retry — when the verifier completed but its output carries PIPELINE-FAULT markers (or the verifier died), re-run the cheap finding-verifier ONCE instead of letting the fault escalate into a full lane re-run at the gauntlet. Falls through to verdict when clean or out of retries; a gate crash falls forward on the declared edge.
script: scripts/verify_gate.py
timeout: 60
next: verdict
verdict:
id: verdict
type: script
description: Deterministic verdict math — applies verifier drops (FALSE, tallied), attaches evidence, folds by rigor, computes MERGE-READY/NEEDS-HUMAN from severity counts + always-human triggers. The model never grades its own homework.
script: scripts/verdict.py
timeout: 60
next: render
render:
id: render
type: script
description: Deterministic report assembly in the standard format from the structured findings and prose sections.
script: scripts/render.py
timeout: 30
next: done
done:
id: done
type: end
output: |
{{final_report}}
@@ -1,48 +0,0 @@
#!/usr/bin/env python3
"""Fail-closed marker for dead aux context lanes of the code-reviewer graph.
Reached ONLY via aux_lanes' `fallback:` route. aux_lanes' state_updates wrote
an "LLM node …failed: <chain>" string into `aux_failure` before routing here;
this node records the failure as a PIPELINE-FAULT in `aux_note`, which flows
into synthesize's "lane skips" prompt line. Aux lanes are ENRICHMENT, not
review coverage: the degradation is made VISIBLE in the report's inputs but
never blocks — verdict.py deliberately does NOT scan aux_note for faults.
"""
import json
import os
MAX_DETAIL_CHARS = 500
def load_state():
if path := os.environ.get("GRAPH_STATE_FILE"):
with open(path) as f:
return json.load(f)
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
def main():
state = load_state()
raw = state.get("aux_failure")
detail = raw.strip().replace("\n", " ") if isinstance(raw, str) else ""
if len(detail) > MAX_DETAIL_CHARS:
detail = detail[: MAX_DETAIL_CHARS - 1] + "…"
print(
json.dumps(
{"aux_note": f"PIPELINE-FAULT: aux context lanes failed after retries — {detail}"}
)
)
try:
main()
except Exception as e: # noqa: BLE001 — the fault marker itself must never crash
print(
json.dumps(
{
"aux_note": "PIPELINE-FAULT: aux context lanes failed after retries — "
f"fault-marker script error: {e}"
}
)
)
@@ -1,76 +0,0 @@
#!/usr/bin/env python3
"""Report-contract gate for each domain-reviewer's output.
"Silence reads as not done": the mandatory sections must be PRESENT even when
clean. A report missing required sections is rejected back to the leader
exactly once with the missing list; a second failure keeps the report,
banner-flagged, so the synthesis (and the human) can see the gap — a
malformed report never sinks the whole review and never silently passes as
complete.
"""
import json
import os
import re
REQUIRED = [
("## Domain:", "the `## Domain:` header"),
("### Findings", "the `### Findings` section"),
("### Mandatory checks", "the `### Mandatory checks` section (report blast radius / test adequacy / guard symmetry even when clean)"),
("DOMAIN_REVIEW_COMPLETE", "the `DOMAIN_REVIEW_COMPLETE` sentinel"),
]
MAX_RETRIES = 1
def load_state():
if path := os.environ.get("GRAPH_STATE_FILE"):
with open(path) as f:
return json.load(f)
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
def main():
state = load_state()
report = state.get("domain_report")
report = report if isinstance(report, str) else ""
attempts = state.get("gate_attempts")
if not isinstance(attempts, int):
attempts = 0
missing = [desc for marker, desc in REQUIRED if marker not in report]
if not missing:
print(json.dumps({}))
return
if attempts < MAX_RETRIES:
print(
json.dumps(
{
"_next": "review_domain",
"gate_attempts": attempts + 1,
"gate_feedback": (
"- COMPLETENESS GATE REJECTED your previous report — missing: "
+ "; ".join(missing)
+ ". Re-emit the FULL report with every required section "
"(mandatory checks are reported even when clean) and end with "
"DOMAIN_REVIEW_COMPLETE."
),
}
)
)
return
group = state.get("domain_group")
domain = group.get("domain", "?") if isinstance(group, dict) else "?"
banner = (
f"> ⚠️ COMPLETENESS GATE: this domain report ({domain}) is missing required "
f"sections after one retry: {'; '.join(missing)}. Treat the missing checks as "
"NOT DONE, not as clean.\n\n"
)
print(json.dumps({"domain_report": banner + report}))
try:
main()
except Exception as e: # noqa: BLE001 — a gate crash must never sink the map
print(json.dumps({"domain_report": f"> ⚠️ COMPLETENESS GATE ERROR: {e}\n\n(report unavailable)"}))
@@ -1,81 +0,0 @@
#!/usr/bin/env python3
"""Exact-cover gate for the refined domain groups.
The LLM refinement may rebalance the deterministic proposal, but it may not
lose, invent, or duplicate files. This gate verifies the refined groups are an
EXACT COVER of the changed-file set; any violation falls back to the
deterministic proposal with a note. The LLM can shape the grouping, never
shrink it. Emits `group_items` — the map node's item list.
Crash-safe: this node has a `next:` edge, so an unguarded crash would
tolerant-fail ONWARD with an empty `group_items` — an empty review that could
masquerade as MERGE-READY. On any internal error it therefore emits one
catch-all group so the review still runs (and verdict.py's pipeline tripwire
catches the empty-report case independently).
"""
import json
import os
def load_state():
if path := os.environ.get("GRAPH_STATE_FILE"):
with open(path) as f:
return json.load(f)
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
def main():
state = load_state()
changed = state.get("changed_files") or []
proposed = state.get("proposed_groups") or []
refined = state.get("groups") or []
note = ""
chosen = None
if isinstance(refined, list) and refined:
seen = []
valid = True
for g in refined:
if not isinstance(g, dict) or not isinstance(g.get("files"), list) or not g.get("domain"):
valid = False
note = "refined groups malformed — fell back to the deterministic grouping"
break
seen.extend(g["files"])
if valid:
if sorted(seen) != sorted(changed):
missing = sorted(set(changed) - set(seen))
extra = sorted(set(seen) - set(changed))
dupes = sorted({f for f in seen if seen.count(f) > 1})
note = (
"refined groups failed the exact-cover check "
f"(missing={missing[:5]} extra={extra[:5]} duplicated={dupes[:5]}) — "
"fell back to the deterministic grouping"
)
else:
chosen = [{"domain": g["domain"], "files": g["files"]} for g in refined]
if chosen is None:
# refine_groups' fallback lands here with `groups` never set; make the
# degradation visible in the synthesis prompt via groups_note.
refine_failure = state.get("refine_failure")
if not refined and isinstance(refine_failure, str) and refine_failure.startswith("LLM node"):
note = "refinement lane failed after retries — fell back to the deterministic grouping"
chosen = [{"domain": g["domain"], "files": g["files"]} for g in proposed if isinstance(g, dict)]
print(json.dumps({"group_items": chosen, "groups_note": note}))
try:
main()
except Exception as e: # noqa: BLE001 — a crash must never tolerant-fail into an empty review
try:
files = load_state().get("changed_files") or []
except Exception: # noqa: BLE001
files = []
print(
json.dumps(
{
"group_items": [{"domain": "all-changes", "files": files}],
"groups_note": f"cover gate error: {e} — fell back to a single catch-all group",
}
)
)
@@ -1,59 +0,0 @@
#!/usr/bin/env python3
"""Fail-closed domain lane fault marker for the code-reviewer graph.
Reached ONLY via review_domain's `fallback:` route inside the map branch.
Grounded in engine behavior: on agent-node failure the engine writes
"Agent node failed: {e:#}" into `domain_report` via the node's state_updates
BEFORE routing to the fallback, so the raw failure text is already in state.
This node normalizes it into a "> ⚠️ PIPELINE-FAULT: domain review lane
failed after retries — …" banner the map collects as the lane's report;
verdict.py counts any banner-prefixed report as a fault and forces
NEEDS-HUMAN — a dead domain lane never reads as a clean slice.
Slice identity comes from `domain_group` (the map item cover_gate emitted,
which carries `domain` and `files`); a missing/malformed item degrades to
the failure detail alone.
"""
import json
import os
MAX_DETAIL_CHARS = 500
FAULT_PREFIX = "> ⚠️ PIPELINE-FAULT: domain review lane failed after retries — "
def load_state():
if path := os.environ.get("GRAPH_STATE_FILE"):
with open(path) as f:
return json.load(f)
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
def main():
state = load_state()
slice_desc = ""
group = state.get("domain_group")
if isinstance(group, dict):
domain = group.get("domain")
files = [f for f in (group.get("files") or []) if isinstance(f, str)]
if isinstance(domain, str) and domain.strip():
slice_desc = f"domain '{domain.strip()}' (files: {', '.join(files)}): "
raw = state.get("domain_report")
detail = raw.strip().replace("\n", " ") if isinstance(raw, str) else ""
if len(detail) > MAX_DETAIL_CHARS:
detail = detail[: MAX_DETAIL_CHARS - 1] + "…"
print(json.dumps({"domain_report": f"{FAULT_PREFIX}{slice_desc}{detail}"}))
try:
main()
except Exception as e: # noqa: BLE001 — the fault marker itself must never crash
print(
json.dumps(
{
"domain_report": "> ⚠️ PIPELINE-FAULT: domain review lane failed "
f"after retries — fault-marker script error: {e}"
}
)
)
@@ -1,102 +0,0 @@
#!/usr/bin/env python3
"""Downstream-consumers sweep for the code-reviewer graph.
Runs only when consumer-facing contract surface changed. Extracts the most
specific changed contract tokens from the diff deterministically (route path
literals, proto message/service/rpc names, exported symbols), greps sibling
checkouts under `consumers_root`, and runs a bounded org code search via gh.
Hits are reported for the synthesis to raise as 🔴 findings naming the
consuming repo + file:line. Degrades silently when unconfigured/offline;
public-library caveats are noted, not guessed at.
"""
import json
import os
import re
import shutil
import subprocess
def load_state():
if path := os.environ.get("GRAPH_STATE_FILE"):
with open(path) as f:
return json.load(f)
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
state = load_state()
proj = os.path.abspath(state.get("project_dir") or ".")
spec = state.get("resolved_diff_spec") or "worktree"
consumers_root = os.path.expanduser((state.get("consumers_root") or "").strip())
github_org = (state.get("github_org") or "").strip()
if not state.get("consumer_surface"):
print(json.dumps({"downstream_notes": "(skipped — no consumer-facing contract surface in the diff)"}))
raise SystemExit(0)
def diff_text():
args = {"staged": ["--cached"], "worktree": ["HEAD"], "HEAD~1": ["HEAD~1"]}.get(spec, [spec])
r = subprocess.run(["git", "-C", proj, "diff", *args], capture_output=True, text=True, timeout=90)
return r.stdout if r.returncode == 0 else ""
try:
changed = [
l[1:] for l in diff_text().splitlines()
if (l.startswith("+") or l.startswith("-")) and not l.startswith(("+++", "---"))
]
text = "\n".join(changed)
tokens = set()
tokens.update(re.findall(r'["\'](/[a-z0-9][a-z0-9/_{}.:-]{5,})["\']', text)) # route paths
tokens.update(re.findall(r"^\s*(?:message|service|rpc)\s+([A-Z][A-Za-z0-9_]{5,})", text, re.M)) # proto names
tokens.update(re.findall(r"^\s*(?:pub\s+fn|pub\s+struct|func|export\s+(?:function|const|class))\s+([A-Z][A-Za-z0-9_]{5,})", text, re.M))
tokens = sorted(tokens, key=len, reverse=True)[:5]
notes = []
if not tokens:
notes.append("(no specific contract tokens extractable from the diff — sweep skipped)")
else:
notes.append(f"contract tokens probed: {tokens}")
if consumers_root and os.path.isdir(consumers_root):
self_name = os.path.basename(proj)
for tok in tokens:
r = subprocess.run(
["grep", "-rIl", "--exclude-dir=.git", "--exclude-dir=node_modules",
"--exclude-dir=target", "--exclude-dir=vendor", tok, consumers_root],
capture_output=True, text=True, timeout=60,
)
hits = [
h for h in r.stdout.splitlines()
if h.strip() and f"/{self_name}/" not in h and not h.startswith(proj)
][:4]
for h in hits:
notes.append(f"CONSUMER HIT (sibling checkout): {h} references '{tok}' — "
"a changed/removed contract element with a live consumer is a 🔴 finding")
elif consumers_root:
notes.append(f"(consumers_root '{consumers_root}' not found — local probe skipped)")
else:
notes.append("(consumers_root unset — local probe skipped)")
if github_org and shutil.which("gh"):
env = dict(os.environ)
env.pop("CLICOLOR_FORCE", None)
env.pop("FORCE_COLOR", None)
env["GH_NO_UPDATE_NOTIFIER"] = "1"
for tok in tokens:
r = subprocess.run(
["gh", "api", "search/code", "-f", f'q=org:{github_org} "{tok}"',
"--jq", r'.items[:4][] | "\(.repository.full_name): \(.path)"'],
capture_output=True, text=True, timeout=30, env=env,
)
if r.returncode == 0 and r.stdout.strip():
for line in r.stdout.strip().splitlines():
notes.append(f"CONSUMER HIT (org search): {line} references '{tok}'")
elif github_org:
notes.append("(gh unavailable — org code search skipped)")
else:
notes.append("(github_org unset — org code search skipped)")
notes.append("Public-library caveat: consumers of a PUBLIC library are unenumerable — "
"for exported-API changes the check degrades to semver/changelog discipline.")
print(json.dumps({"downstream_notes": "\n".join(notes)}))
except Exception as e: # noqa: BLE001
print(json.dumps({"downstream_notes": f"(sweep degraded: {e})"}))
@@ -1,288 +0,0 @@
#!/usr/bin/env python3
"""Deterministic review facts for the code-reviewer graph.
Resolves the diff, the changed-file list, the quality bar (caller-passed →
plan frontmatter → detected default), the conventions pointer, review-miss
ledger matches, the repo-history sweep (fix/revert clusters, bounded gh
PR-comment pull), and the deterministic verdict-trigger signals. Pure
git/gh/filesystem — no LLM. Failures degrade into `facts_note`; they never
sink the review.
"""
import glob
import json
import os
import re
import subprocess
def load_state():
if path := os.environ.get("GRAPH_STATE_FILE"):
with open(path) as f:
return json.load(f)
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
state = load_state()
proj = os.path.expanduser(
(state.get("project_dir_in") or "").strip() or state.get("project_dir") or "."
)
spec = (state.get("diff_spec") or "auto").strip()
notes = []
out = {"project_dir": proj}
def git(*args, timeout=60):
r = subprocess.run(["git", "-C", proj, *args], capture_output=True, text=True, timeout=timeout)
if r.returncode != 0:
raise RuntimeError((r.stderr or r.stdout).strip()[:300])
return r.stdout
def scrubbed_env():
env = dict(os.environ)
env.pop("CLICOLOR_FORCE", None)
env.pop("FORCE_COLOR", None)
env["GH_NO_UPDATE_NOTIFIER"] = "1"
env["NO_COLOR"] = "1"
return env
# --- diff resolution (get_diff semantics) -----------------------------------
resolved = spec
files = []
try:
if spec in ("auto", ""):
names = git("diff", "--cached", "--numstat")
resolved = "staged"
if not names.strip():
names = git("diff", "--numstat")
resolved = "worktree"
if not names.strip():
names = git("diff", "HEAD~1", "--numstat")
resolved = "HEAD~1"
elif spec == "staged":
names = git("diff", "--cached", "--numstat")
elif spec == "worktree":
names = git("diff", "HEAD", "--numstat")
else:
names = git("diff", spec, "--numstat")
for line in names.splitlines():
parts = line.split("\t")
if len(parts) == 3:
added, deleted, path = parts
if added == "-" and deleted == "-":
notes.append(f"binary file skipped: {path}")
continue
files.append(path.strip())
except Exception as e: # noqa: BLE001
notes.append(f"diff resolution failed for spec {spec!r}: {e}")
out["resolved_diff_spec"] = resolved
out["changed_files"] = files[:300]
out["file_count"] = len(files)
if len(files) > 300:
notes.append(
f"file list truncated: {len(files)} changed files, only the first 300 are "
"grouped and reviewed"
)
def diff_text():
try:
if resolved == "staged":
return git("diff", "--cached", timeout=90)
if resolved == "worktree":
return git("diff", "HEAD", timeout=90) if spec == "worktree" else git("diff", timeout=90)
if resolved == "HEAD~1":
return git("diff", "HEAD~1", timeout=90)
return git("diff", resolved, timeout=90)
except Exception: # noqa: BLE001
return ""
dtext = diff_text()
added_lines = "\n".join(
l for l in dtext.splitlines() if l.startswith("+") and not l.startswith("+++")
)
# --- quality bar resolution --------------------------------------------------
rigor_in = (state.get("rigor_in") or state.get("rigor") or "").strip()
surfaces_in = (state.get("surfaces_in") or state.get("surfaces") or "").strip()
resolved_rigor, resolved_surfaces, provenance = "production", "", "detected-default"
if rigor_in or surfaces_in:
resolved_rigor = rigor_in or "production"
resolved_surfaces = surfaces_in
provenance = "passed"
else:
for plan in sorted(glob.glob(os.path.join(proj, "plans", "PLAN-*.md"))):
try:
with open(plan) as f:
head = f.read(4000)
m = re.match(r"\A---\n(.*?)\n---", head, re.S)
if m and re.search(r"^status:\s*active\s*$", m.group(1), re.M):
rig = re.search(r"^rigor:\s*(\S+)", m.group(1), re.M)
sur = re.search(r"^surfaces:\s*(.+)$", m.group(1), re.M)
if rig:
resolved_rigor = rig.group(1).strip()
if sur:
resolved_surfaces = sur.group(1).strip("[] \n")
provenance = "plan"
break
except OSError:
continue
SURFACE_PATTERNS = [
("db-migration", r"migrations?/|\.sql$|schema\.(rb|prisma|sql)"),
("iac", r"\.tf$|helm|charts?/|Dockerfile|docker-compose|k8s/|manifests?/"),
("ci-cd", r"\.github/workflows/|\.gitlab-ci|Jenkinsfile|\.circleci/"),
("worker", r"workers?/|jobs?/|consumers?/|cron|celery|sidekiq|river"),
("rest-api", r"routes?|handlers?|controllers?|endpoints?|\bapi\b|\.proto$|openapi|swagger|graphql|resolvers?"),
("cli", r"\bcmd/|\bcli\b"),
("library", r"go\.mod$|package\.json$|Cargo\.toml$|pyproject\.toml$|setup\.py$|__init__\.py$"),
]
if provenance == "detected-default" and files:
detected = []
for surface, pat in SURFACE_PATTERNS:
if any(re.search(pat, f, re.I) for f in files):
detected.append(surface)
resolved_surfaces = ", ".join(detected)
out["resolved_rigor"] = resolved_rigor
out["resolved_surfaces"] = resolved_surfaces
out["bar_provenance"] = provenance
# --- conventions pointer ------------------------------------------------------
conv = []
for pat in ("CLAUDE.md", "COYOTE.md", "CONTRIBUTING.md", ".golangci*", ".eslintrc*",
"rustfmt.toml", ".rubocop.yml", "ruff.toml", ".editorconfig", "Makefile"):
conv.extend(sorted(glob.glob(os.path.join(proj, pat)))[:2])
out["conventions"] = ", ".join(os.path.relpath(p, proj) for p in conv) or "none found"
# --- review-miss ledger -------------------------------------------------------
ledger_text = ""
for cand in (
os.path.join(os.path.expanduser("~"), ".config", "coyote", "share", "review-misses.md"),
os.path.join(os.path.expanduser("~"), ".config", "coyote", "review-misses.md"),
):
if os.path.isfile(cand):
try:
with open(cand) as f:
ledger_text = f.read()
except OSError:
pass
break
matches = []
if ledger_text:
try:
repo_name = os.path.basename(git("rev-parse", "--show-toplevel").strip())
except Exception: # noqa: BLE001
repo_name = os.path.basename(os.path.abspath(proj))
keywords = {repo_name.lower()}
keywords.update(s.strip().lower() for s in resolved_surfaces.split(",") if s.strip())
for f in files:
keywords.update(p.lower() for p in re.split(r"[/_.-]", f) if len(p) >= 4)
entries = re.split(r"(?=^## MISS-)", ledger_text, flags=re.M)
for entry in entries:
if not entry.startswith("## MISS-"):
continue
body = entry.lower()
if any(k in body for k in keywords):
lines = entry.strip().splitlines()
matches.append("\n".join(lines[:8]))
if len(matches) >= 8:
break
out["ledger_matches"] = "\n\n".join(matches) if matches else "(no matching ledger entries)"
# --- repo-history sweep -------------------------------------------------------
history = []
fragile = []
try:
if files:
log = git("log", "--oneline", "-15", "--", *files[:100])
history.append(log.strip()[:2000])
per_file = {}
for f in files[:50]:
try:
fl = git("log", "--oneline", "-10", "--", f)
except Exception: # noqa: BLE001
continue
hits = [l for l in fl.splitlines() if re.search(r"\b(fix|revert|hotfix)\b", l, re.I)]
if len(hits) >= 2:
fragile.append(f)
per_file[f] = hits[:2]
for f, hits in per_file.items():
history.append(f"FRAGILE: {f} — {len(hits)}+ fix/revert commits recently — raise scrutiny")
except Exception as e: # noqa: BLE001
notes.append(f"history sweep failed: {e}")
# gh one-level-deeper: PR review comments for fragile files (bounded, silent skip)
try:
if fragile:
remote = git("remote", "get-url", "origin").strip()
m = re.search(r"github\.com[:/]([^/]+/[^/.]+)", remote)
if m:
repo_slug = m.group(1)
env = scrubbed_env()
shas = []
for f in fragile[:2]:
lg = git("log", "--format=%H", "-2", "--", f)
shas.extend(lg.split())
pr_nums = []
for sha in shas[:3]:
r = subprocess.run(
["gh", "api", f"repos/{repo_slug}/commits/{sha}/pulls", "--jq", ".[].number"],
capture_output=True, text=True, timeout=30, env=env,
)
if r.returncode == 0:
pr_nums.extend(n for n in r.stdout.split() if n.isdigit())
seen = []
for n in pr_nums:
if n not in seen:
seen.append(n)
comments = []
for n in seen[:3]:
r = subprocess.run(
["gh", "api", f"repos/{repo_slug}/pulls/{n}/comments",
"--jq", r'.[] | "\(.path): \(.body)"'],
capture_output=True, text=True, timeout=30, env=env,
)
if r.returncode == 0:
for line in r.stdout.splitlines():
path = line.split(":", 1)[0]
if any(path in f or f in path for f in files):
comments.append(line[:200])
if len(comments) >= 6:
break
if comments:
history.append("Prior PR review comments on fragile paths:\n" + "\n".join(comments[:6]))
except Exception: # noqa: BLE001
pass # offline / not GitHub / no gh — silent by design
out["history_notes"] = "\n".join(h for h in history if h) or "(no notable history)"
# --- deterministic verdict-trigger signals -----------------------------------
def any_path(pattern):
return any(re.search(pattern, f, re.I) for f in files)
migration_files = [f for f in files if re.search(r"migrations?/|\.sql$", f, re.I)]
mig_added = "\n".join(
l for l in dtext.splitlines()
if l.startswith("+") and not l.startswith("+++")
) if migration_files else ""
out["destructive_migration"] = bool(
migration_files
and re.search(r"(?i)drop\s+(table|column)|rename\s+(table|column|to)|alter\s+\S+\s+drop", mig_added)
)
out["touches_auth"] = bool(
any_path(r"auth(?!or)|login|logout|token|session|permission|rbac|\bacl\b|credential")
or re.search(r"auth(?!or)|password|jwt|session|credential", added_lines, re.I)
)
out["consumer_surface"] = any_path(
r"routes?|handlers?|\bapi\b|endpoints?|\bcmd/|\bcli\b|\.proto$|openapi|swagger|controllers?|graphql"
)
if notes:
out["facts_note"] = "; ".join(notes)[:800]
print(json.dumps(out))
@@ -1,66 +0,0 @@
#!/usr/bin/env python3
"""Deterministic domain grouping of changed files for the code-reviewer graph.
Each file lands in the FIRST matching domain (patterns ordered most-specific
first); unmatched files land in `general`. When the grouping is very wide,
single-file groups merge into `general` so the fan-out stays sane. The LLM
refinement node may rebalance this proposal; the cover gate guarantees no
file is lost either way. Crash-safe: an error emits one catch-all proposal
rather than tolerant-failing into an empty grouping.
"""
import json
import os
import re
def load_state():
if path := os.environ.get("GRAPH_STATE_FILE"):
with open(path) as f:
return json.load(f)
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
DOMAINS = [
("db", r"migrations?/|\.sql$|schema\.(rb|prisma|sql)|models?/"),
("iac", r"\.tf$|helm|charts?/|(^|/)Dockerfile|docker-compose|k8s/|manifests?/"),
("ci-cd", r"\.github/workflows/|\.gitlab-ci|Jenkinsfile|\.circleci/"),
("workers", r"workers?/|jobs?/|consumers?/|cron|tasks?/"),
("api", r"routes?|handlers?|controllers?|endpoints?|\bapi\b|\.proto$|openapi|swagger|graphql|resolvers?"),
("cli", r"\bcmd/|\bcli\b"),
("deps", r"go\.(mod|sum)$|package(-lock)?\.json$|yarn\.lock$|Cargo\.(toml|lock)$|requirements[^/]*\.txt$|pyproject\.toml$|Gemfile|pom\.xml$|composer\.json$"),
("tests", r"(^|/)tests?/|_test\.|\.test\.|spec\."),
("docs", r"\.(md|rst|adoc|txt)$"),
]
def main():
state = load_state()
files = state.get("changed_files") or []
groups = {}
for f in files:
domain = "general"
for name, pat in DOMAINS:
if re.search(pat, f, re.I):
domain = name
break
groups.setdefault(domain, []).append(f)
# Wide fan-out: merge single-file groups into general.
if len(groups) > 6:
for name in list(groups):
if name != "general" and len(groups[name]) == 1:
groups.setdefault("general", []).extend(groups.pop(name))
proposed = [{"domain": k, "files": v} for k, v in sorted(groups.items())]
print(json.dumps({"proposed_groups": proposed}))
try:
main()
except Exception as e: # noqa: BLE001 — never tolerant-fail into an empty grouping
try:
files = load_state().get("changed_files") or []
except Exception: # noqa: BLE001
files = []
print(json.dumps({"proposed_groups": [{"domain": "all-changes", "files": files}]}))
@@ -1,87 +0,0 @@
#!/usr/bin/env python3
"""Domain linter pass for the code-reviewer graph.
Runs whichever mechanized checkers exist on PATH against the matching changed
files: tflint (Terraform), hadolint (Dockerfiles), actionlint (workflow
files), and `buf breaking` when buf-configured protos changed. Read-only,
never installs anything, and each tool degrades silently to a skip note.
Output is routed per-file where possible so the domain leaders can extract
their slice's lines.
"""
import json
import os
import re
import shutil
import subprocess
def load_state():
if path := os.environ.get("GRAPH_STATE_FILE"):
with open(path) as f:
return json.load(f)
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
state = load_state()
proj = state.get("project_dir") or "."
files = state.get("changed_files") or []
spec = state.get("resolved_diff_spec") or "worktree"
notes = []
def run(cmd, timeout=60):
r = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, cwd=proj)
return (r.stdout + r.stderr).strip()
def capped(text, n=40):
lines = text.splitlines()
return "\n".join(lines[:n]) + (f"\n… ({len(lines) - n} more lines)" if len(lines) > n else "")
try:
tf = [f for f in files if f.endswith(".tf")]
if tf and shutil.which("tflint"):
notes.append("[tflint]\n" + capped(run(["tflint", "--no-color"], 120) or "clean"))
elif tf:
notes.append("[tflint] skipped — not on PATH")
dockers = [f for f in files if re.search(r"(^|/)Dockerfile", f)]
if dockers and shutil.which("hadolint"):
notes.append("[hadolint]\n" + capped(run(["hadolint", "--no-color", *dockers[:10]], 60) or "clean"))
elif dockers:
notes.append("[hadolint] skipped — not on PATH")
workflows = [f for f in files if ".github/workflows/" in f]
if workflows and shutil.which("actionlint"):
notes.append("[actionlint]\n" + capped(run(["actionlint", "-no-color", *workflows[:10]], 60) or "clean"))
elif workflows:
notes.append("[actionlint] skipped — not on PATH")
protos = [f for f in files if f.endswith(".proto")]
if protos and os.path.isfile(os.path.join(proj, "buf.yaml")) and shutil.which("buf"):
base = None
if "..." in spec:
base = spec.split("...")[0]
elif ".." in spec:
base = spec.split("..")[0]
elif spec not in ("staged", "worktree", "HEAD~1"):
base = spec
else:
try:
r = subprocess.run(["git", "-C", proj, "rev-parse", "HEAD"],
capture_output=True, text=True, timeout=15)
base = r.stdout.strip() if r.returncode == 0 else None
except Exception: # noqa: BLE001
base = None
if base:
out = run(["buf", "breaking", "--against", f".git#ref={base}"], 120)
notes.append("[buf breaking] (wire-compat breaks are 🔴 by default)\n" + capped(out or "clean"))
else:
notes.append("[buf breaking] skipped — no base ref resolvable")
elif protos:
notes.append("[buf breaking] skipped — no buf.yaml or buf not on PATH")
except Exception as e: # noqa: BLE001
notes.append(f"[linters] pass degraded: {e}")
print(json.dumps({"linter_notes": "\n\n".join(notes) if notes else "(no applicable linters)"}))
@@ -1,58 +0,0 @@
#!/usr/bin/env python3
"""Fail-closed marker for a dead parse stage of the code-reviewer graph.
Reached ONLY via parse's `fallback:` route. parse's state_updates wrote an
"LLM node …failed: <chain>" string into `parse_failure` before routing here;
this node emits a complete PIPELINE-FAULT `verdict_out` and the graph jumps
straight to render — a review that could not even parse its request never
reads as MERGE-READY. render.py renders a PIPELINE-FAULT verdict despite the
empty changed-file list (its "No changes to review." early return skips
fault-reason verdicts).
"""
import json
import os
MAX_DETAIL_CHARS = 500
ATTENTION_LINE = (
"PIPELINE-FAULT: parse failed — the review request could not be parsed; "
"the review did not run"
)
def load_state():
if path := os.environ.get("GRAPH_STATE_FILE"):
with open(path) as f:
return json.load(f)
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
def fault_verdict(detail):
return {
"verdict": "NEEDS-HUMAN",
"reason": f"PIPELINE-FAULT: parse failed — {detail}",
"counts": {},
"deferred_count": 0,
"dropped_count": 0,
"dropped_titles": [],
"attention": [ATTENTION_LINE],
"findings_final": [],
}
def main():
state = load_state()
raw = state.get("parse_failure")
detail = raw.strip().replace("\n", " ") if isinstance(raw, str) else ""
if not detail.startswith("LLM node"):
detail = "the parse stage died without recording a failure detail"
if len(detail) > MAX_DETAIL_CHARS:
detail = detail[: MAX_DETAIL_CHARS - 1] + "…"
print(json.dumps({"verdict_out": fault_verdict(detail)}))
try:
main()
except Exception as e: # noqa: BLE001 — the fault marker itself must never crash
print(json.dumps({"verdict_out": fault_verdict(f"fault-marker script error: {e}")}))
@@ -1,125 +0,0 @@
#!/usr/bin/env python3
"""Deterministic report assembly for the code-reviewer graph.
Builds the standard report format from the structured findings and prose
sections. Pure templating — no judgment, no re-summarization, no finding
ever added or removed here.
"""
import json
import os
def load_state():
if path := os.environ.get("GRAPH_STATE_FILE"):
with open(path) as f:
return json.load(f)
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
# LLM-influenced values rendered after the summary line: a newline could
# forge a second summary line that review-gauntlet/verdict_gate.py parses.
def one_line(x):
return str(x).replace("\n", " ")
def main():
state = load_state()
v = state.get("verdict_out") or {}
findings = v.get("findings_final") or []
counts = v.get("counts") or {}
changed = state.get("changed_files") or []
rigor = one_line(state.get("resolved_rigor") or "production")
# A fault can leave changed_files empty (a parse fault dies before the
# diff is resolved; a verifier fault fires even on an empty diff) — a
# fault-degraded verdict must still render as a full report. parse_fault
# puts the marker in the reason; verdict.py prepends it to attention.
faulted = str(v.get("reason") or "").startswith("PIPELINE-FAULT:") or any(
isinstance(a, str) and a.startswith("PIPELINE-FAULT:") for a in v.get("attention") or []
)
if not changed and not faulted:
print(json.dumps({"final_report": "# Code Review Summary\n\nNo changes to review."}))
return
verdict = v.get("verdict", "NEEDS-HUMAN")
lines = [
"# Code Review Summary",
"",
# CONTRACT: review-gauntlet/verdict_gate.py anchors on this wording
f"**Verdict: {verdict}** — {one_line(v.get('reason', ''))}",
"",
"## Walkthrough",
state.get("walkthrough") or "(no walkthrough provided)",
"",
"## Changes",
"",
"| File | Changes | Findings |",
"|------|---------|----------|",
]
desc_by_file = {}
for row in state.get("changes_rows") or []:
if isinstance(row, dict) and row.get("file"):
desc_by_file[row["file"]] = row.get("desc", "")
blocking = [f for f in findings if f.get("section") == "blocking"]
deferred = [f for f in findings if f.get("section") == "deferred"]
for f in changed:
per = [x for x in blocking if x.get("file") == f]
tally = " ".join(
f"{icon} {n}" for icon, n in
((i, sum(1 for x in per if x.get("icon") == i)) for i in ("🔴", "🟡", "🟢", "💡"))
if n
) or "—"
lines.append(f"| `{f}` | {desc_by_file.get(f, '')} | {tally} |")
lines += ["", "## Detailed Findings", ""]
by_file = {}
for x in blocking:
by_file.setdefault(x.get("file", "(unattributed)"), []).append(x)
if by_file:
for f in sorted(by_file):
lines += [f"### `{f}`", ""]
for x in by_file[f]:
lines += [x.get("block", ""), ""]
else:
lines += ["No blocking findings.", ""]
lines += ["## Cross-File Concerns", state.get("cross_file") or "None", ""]
if (state.get("operational_history") or "").strip():
lines += ["## Operational history", state["operational_history"], ""]
if (state.get("org_context_section") or "").strip():
lines += ["## Org context", state["org_context_section"], ""]
if deferred and rigor != "production":
lines += ["## Deferred by quality bar", ""]
for x in deferred:
lines += [x.get("block", ""), ""]
attention = v.get("attention") or []
if verdict == "NEEDS-HUMAN" and attention:
lines += ["## Human attention required", ""]
lines += [f"- {a}" for a in attention]
lines += [""]
miss_ids = sorted({
m for x in findings for m in
__import__("re").findall(r"MISS-\d+", (x.get("title") or "") + (x.get("block") or ""))
})
dropped = ", ".join(one_line(t) for t in v.get("dropped_titles", [])[:5])
lines += [
"---",
# CONTRACT: review-gauntlet/verdict_gate.py parses this line — keep the format in sync
f"*Reviewed {len(changed)} files, found {counts.get('🔴', 0)} critical, "
f"{counts.get('🟡', 0)} warnings, {counts.get('🟢', 0)} suggestions, "
f"{counts.get('💡', 0)} nitpicks ({v.get('deferred_count', 0)} deferred by quality bar)*",
f"*Quality bar: {rigor} — provenance: {one_line(state.get('bar_provenance', '?'))}; "
f"surfaces: {one_line(state.get('resolved_surfaces') or 'none')}*",
f"*Verdict: {verdict}; verifier: dropped {v.get('dropped_count', 0)} provably-false"
+ (f" ({dropped})" if v.get("dropped_count") else "")
+ f"; review-miss patterns applied: {', '.join(miss_ids) if miss_ids else 'none relevant'}*",
]
print(json.dumps({"final_report": "\n".join(lines)}))
try:
main()
except Exception as e: # noqa: BLE001
print(json.dumps({"final_report": f"# Code Review Summary\n\n**Verdict: NEEDS-HUMAN** — report rendering error: {e}"}))
@@ -1,283 +0,0 @@
#!/usr/bin/env python3
"""Deterministic verdict math for the code-reviewer graph.
The model that wrote the findings never grades its own homework:
- verifier verdicts are applied mechanically (FALSE → drop, tallied;
VERIFIED → evidence attached; UNVERIFIABLE → kept, marked);
- rigor folding is arithmetic over severity + [convention]/[correctness]
markers, exactly per the published rules;
- MERGE-READY/NEEDS-HUMAN is computed from counts + always-human triggers
(deterministic signals ∪ the synthesis's ADDITIVE-only flags).
Fail-closed: any PIPELINE-FAULT recorded upstream (dead domain lane, dead
verifier, verifier verdicts carrying a PIPELINE-FAULT note or the
`pipeline-fault` sentinel id, dead synthesis, missing domain reports)
forces NEEDS-HUMAN — a degraded run can never read MERGE-READY. Faults
always lead the attention list; the display cap applies only to the
non-fault entries so a degraded run can never hide a fault.
The gate never crashes into a silent verdict: any internal error emits
NEEDS-HUMAN naming the error.
"""
import json
import os
import re
MAX_DETAIL_CHARS = 500
def load_state():
if path := os.environ.get("GRAPH_STATE_FILE"):
with open(path) as f:
return json.load(f)
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
def sev_icon(severity):
for icon in ("🔴", "🟡", "🟢", "💡"):
if icon in (severity or ""):
return icon
return "🟢"
def parse_verifier_entries(text):
"""Extract the raw list of {id, verdict, evidence, note} dicts from
finding-verifier output. Order and duplicates are preserved so fault
accounting can count every entry (crash-guard entries all carry the
same "unknown" id)."""
if not isinstance(text, str) or not text.strip():
return []
start, end = text.find("["), text.rfind("]")
if start != -1 and end > start:
try:
return [
v for v in json.loads(text[start : end + 1]) if isinstance(v, dict) and v.get("id")
]
except (ValueError, TypeError):
pass
entries = []
for m in re.finditer(r'\{[^{}]*"id"[^{}]*\}', text):
try:
v = json.loads(m.group(0))
if v.get("id"):
entries.append(v)
except (ValueError, TypeError):
continue
return entries
def parse_verifier(entries):
"""Index verifier entries by finding id; the last entry per id wins."""
return {str(v["id"]): v for v in entries}
def main():
state = load_state()
findings = [f for f in (state.get("findings") or []) if isinstance(f, dict)]
rigor = (state.get("resolved_rigor") or "production").lower()
verifier_entries = parse_verifier_entries(state.get("verifier_output"))
verdicts = parse_verifier(verifier_entries)
kept, dropped_titles = [], []
for f in findings:
icon = sev_icon(f.get("severity"))
v = verdicts.get(str(f.get("id")))
block = f.get("block") or f"#### {f.get('title', '(untitled)')}"
if v and icon in ("🔴", "🟡"):
if v.get("verdict") == "FALSE":
dropped_titles.append(f.get("title", f.get("id", "?")))
continue
if v.get("verdict") == "VERIFIED" and v.get("evidence"):
block += f"\n- **Evidence** (verifier): {v['evidence']}"
elif v.get("verdict") == "UNVERIFIABLE":
block += "\n- *(unverified: " + (v.get("note") or "could not be established") + ")*"
elif icon in ("🔴", "🟡") and not v:
block += "\n- *(unverified: no verifier verdict returned for this finding)*"
kept.append({**f, "icon": icon, "block": block})
# --- rigor folding (arithmetic, never rewrites severities) --------------
for f in kept:
marker = (f.get("marker") or "").lower()
icon = f["icon"]
section = "blocking"
if icon == "🔴":
section = "blocking"
elif rigor == "prototype" and ((icon == "🟡" and marker == "convention") or icon == "🟢"):
section = "deferred"
elif rigor == "poc":
if icon == "🟡" and marker == "convention":
section = "deferred"
elif icon in ("🟢", "💡") and marker == "convention":
section = "dropped-by-poc"
f["section"] = section
final = [f for f in kept if f["section"] != "dropped-by-poc"]
blocking = [f for f in final if f["section"] == "blocking"]
deferred = [f for f in final if f["section"] == "deferred"]
counts = {i: sum(1 for f in blocking if f["icon"] == i) for i in ("🔴", "🟡", "🟢", "💡")}
yellow_correctness = sum(
1 for f in blocking if f["icon"] == "🟡" and (f.get("marker") or "").lower() == "correctness"
)
# --- always-human triggers ----------------------------------------------
attention = []
if state.get("destructive_migration"):
attention.append("Destructive/irreversible migration in the diff (DROP/RENAME) — needs a human signature.")
if state.get("touches_auth"):
attention.append("Authentication/authorization surface changed — needs a human signature.")
for flag in state.get("attention_flags") or []:
if isinstance(flag, str) and flag.strip():
attention.append(flag.strip())
placement = (state.get("placement_notes") or "").strip()
if placement:
attention.append("Placement question (design intent, never auto-resolved): " + placement)
# dedup, preserve order
seen = set()
attention = [a for a in attention if not (a.lower() in seen or seen.add(a.lower()))]
# --- pipeline-fault accounting (fail closed) -----------------------------
# PREFIX-anchored: a report that merely QUOTES a marker mid-text must not
# trip it. Any fault forces NEEDS-HUMAN.
faults = []
changed = state.get("changed_files") or []
reports = state.get("domain_reports") or []
faulted = sum(
1 for r in reports if isinstance(r, str) and r.lstrip().startswith("> ⚠️ PIPELINE-FAULT:")
)
if faulted:
faults.append(
f"PIPELINE-FAULT: {faulted} of {len(reports)} domain review lane(s) "
"failed after retries — their slices were not reviewed"
)
verifier_raw = state.get("verifier_output")
if isinstance(verifier_raw, str) and verifier_raw.lstrip().startswith("Agent node failed:"):
faults.append(
"PIPELINE-FAULT: finding verification failed — findings render as unverified"
)
# finding-verifier completes "successfully" with faults INSIDE the payload:
# parse_fault's `pipeline-fault` sentinel entry, or a per-finding
# PIPELINE-FAULT note from verify_fault or verdict_gate's crash guard.
# Anchored on the sentinel id / note prefix only. Counted over the raw
# entries, not the id-indexed dict, so N crash-guard entries sharing the
# "unknown" id count as N.
fv_faults = [
v
for v in verifier_entries
if v.get("id") == "pipeline-fault"
or (isinstance(v.get("note"), str) and v["note"].lstrip().startswith("PIPELINE-FAULT:"))
]
# parse_fault's sentinel = the verifier verified NOTHING (hard fault, lane-
# degrading). Per-finding verify_fault / crash-guard notes = those findings
# simply stand UNVERIFIED (soft: proportionate NEEDS-HUMAN attention — a
# ~1 h lane re-run to re-check a few findings is the wrong trade, and the
# verify_gate node already retried the verifier locally once).
hard_fv = [v for v in fv_faults if v.get("id") == "pipeline-fault"]
soft_fv = [v for v in fv_faults if v.get("id") != "pipeline-fault"]
def _fv_detail(entries):
first = entries[0].get("note")
detail = first.strip().replace("\n", " ") if isinstance(first, str) else ""
if len(detail) > MAX_DETAIL_CHARS:
detail = detail[: MAX_DETAIL_CHARS - 1] + "…"
return detail
soft_notes = []
if hard_fv:
faults.append(
"PIPELINE-FAULT: finding verification degraded — the verifier verified "
f"nothing ({_fv_detail(hard_fv)})"
)
if soft_fv:
soft_notes.append(
f"Verifier: {len(soft_fv)} finding(s) could not be verified (verifier "
f"branch fault: {_fv_detail(soft_fv)}) — they stand above UNVERIFIED; "
"verify them manually"
)
# Files beyond facts.py's 300-file cap were never grouped or reviewed —
# disclose the gap and force NEEDS-HUMAN with wording that does NOT match
# the gauntlet's degraded-run anchors (a lane re-run cannot fix a cap).
coverage_notes = []
try:
total_files = int(state.get("file_count") or 0)
except (TypeError, ValueError):
total_files = 0
if total_files > len(changed):
coverage_notes.append(
f"Coverage gap: {total_files - len(changed)} of {total_files} changed files "
"were NOT reviewed (file-list cap) — review them manually or split the PR"
)
# Keyed off synth_failure + a non-empty diff, NEVER off findings == [] —
# a clean review legitimately has zero findings and stays MERGE-READY.
synth_failure = state.get("synth_failure")
if isinstance(synth_failure, str) and synth_failure.startswith("LLM node") and changed:
detail = synth_failure.strip().replace("\n", " ")
if len(detail) > MAX_DETAIL_CHARS:
detail = detail[: MAX_DETAIL_CHARS - 1] + "…"
faults.append(f"PIPELINE-FAULT: synthesis failed — findings unavailable; {detail}")
if changed and not reports:
faults.append(
"PIPELINE-FAULT: the diff is non-empty but no domain reports were produced — "
"the review did not actually run; do not trust this verdict."
)
# faults go FIRST and uncapped so the display cap can never hide one;
# the trigger reason keys off the human-facing triggers alone
has_triggers = bool(attention)
attention = faults + soft_notes + coverage_notes + attention[:5]
reasons = []
if counts["🔴"]:
reasons.append(f"{counts['🔴']} 🔴 CRITICAL finding(s)")
if yellow_correctness:
reasons.append(f"{yellow_correctness} 🟡 [correctness] finding(s) outside the deferred section")
if faults:
# CONTRACT: review-gauntlet/verdict_gate.py anchors on this wording
reasons.append("pipeline fault(s) recorded — degraded run")
if soft_notes:
# Deliberately does NOT match the gauntlet's degraded-run anchors:
# unverified findings are a human-attention item, not a re-runnable
# lane fault.
reasons.append(f"{len(soft_fv)} finding(s) unverified (verifier branch fault)")
if coverage_notes:
reasons.append(f"{total_files - len(changed)} changed file(s) not reviewed (file cap)")
if has_triggers:
reasons.append("always-human trigger(s) fired")
verdict = "NEEDS-HUMAN" if reasons else "MERGE-READY"
reason_line = "; ".join(reasons) if reasons else "no blocking findings and no always-human triggers"
print(
json.dumps(
{
"verdict_out": {
"verdict": verdict,
"reason": reason_line,
"counts": counts,
"deferred_count": len(deferred),
"dropped_count": len(dropped_titles),
"dropped_titles": dropped_titles,
"attention": attention,
"findings_final": final,
}
}
)
)
try:
main()
except Exception as e: # noqa: BLE001 — never crash into a silent verdict
print(
json.dumps(
{
"verdict_out": {
"verdict": "NEEDS-HUMAN",
# PIPELINE-FAULT prefix is load-bearing: render.py's
# empty-diff stub bypass anchors on it — without it a
# crashed gate on an empty diff collapses into the
# "No changes to review." stub.
"reason": f"PIPELINE-FAULT: verdict computation error: {e} — human review required",
"counts": {}, "deferred_count": 0, "dropped_count": 0,
"dropped_titles": [], "attention": [f"PIPELINE-FAULT: verdict script error: {e}"],
"findings_final": [],
}
}
)
)
@@ -1,52 +0,0 @@
#!/usr/bin/env python3
"""Local verifier-fault retry gate for the code-reviewer graph.
finding-verifier converts its own branch faults into DATA (UNVERIFIABLE
verdicts with a PIPELINE-FAULT: note, or parse_fault's pipeline-fault
sentinel), so the verify agent node completes "successfully" and its
max_attempts never fires. Without this gate the fault escalates all the
way to the gauntlet, which re-runs the ENTIRE code-review lane (~1 h) to
recover a ~4-minute verifier — the wrong trade. This gate re-runs just
the verifier ONCE; if faults persist, verdict.py downgrades them to a
proportionate NEEDS-HUMAN (findings stand, unverified).
Anchored detection only: the JSON-encoded note prefix and the sentinel id
— a finding merely QUOTING the marker mid-text does not trip it. Always
exits 0 with valid JSON; a crash falls forward on the declared edge.
"""
import json
import os
import sys
def load_state():
if path := os.environ.get("GRAPH_STATE_FILE"):
with open(path) as f:
return json.load(f)
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
try:
state = load_state()
raw = state.get("verifier_output")
text = raw if isinstance(raw, str) else (json.dumps(raw) if raw is not None else "")
faulted = (
text.lstrip().startswith("Agent node failed:")
or '"note": "PIPELINE-FAULT:' in text
or "'note': 'PIPELINE-FAULT:" in text
or '"id": "pipeline-fault"' in text
)
try:
retries = int(state.get("verify_retries") or 0)
except (TypeError, ValueError):
retries = 0
if faulted and retries < 1:
sys.stderr.write(
"WARN: verifier output carries pipeline-fault markers — re-running "
"finding-verifier locally (attempt 2/2) before computing the verdict.\n"
)
print(json.dumps({"_next": "verify", "verify_retries": retries + 1}))
else:
print(json.dumps({"verify_retries": retries}))
except Exception as e: # never block the verdict
print(json.dumps({"verify_gate_error": f"verify_gate crashed: {e}"}))
+21 -71
View File
@@ -1,90 +1,40 @@
# Coder # Coder
A graph-based implementation agent. Plans, implements, and runs build + An AI agent that assists you with your coding tasks.
tests in a bounded fix-loop until verified. Designed to be delegated to by
the **[Sisyphus](../sisyphus/README.md)** agent.
Coder is a [graph agent](https://github.com/Dark-Alex-17/coyote/wiki/Graph-Agents): its workflow is This agent is designed to be delegated to by the **[Sisyphus](../sisyphus/README.md)** agent to implement code specifications. Sisyphus
defined declaratively in `graph.yaml`, with verification and the acts as the coordinator/architect, while Coder handles the implementation details.
implement-fix loop enforced as graph edges rather than prose.
## Workflow ## Features
```mermaid - 🏗️ Intelligent project structure creation and management
flowchart TD - 🖼️ Convert screenshots into clean, functional code
resolve_paths{"resolve_paths<br/>script"} --> analyze_request - 📁 Comprehensive file system operations (create folders, files, read/write files)
analyze_request["analyze_request<br/>llm + output_schema"] --> route_complexity - 🧐 Advanced code analysis and improvement suggestions
route_complexity{"route_complexity<br/>script"} - 📊 Precise diff-based file editing for controlled code modifications
route_complexity -->|"complexity ≥ 7"| gate_approval
route_complexity -->|else| implement
gate_approval{{"gate_approval<br/>approval"}}
gate_approval -->|yes| implement
gate_approval -->|no| end_rejected
implement["implement<br/>llm + fs tools"] --> verify_build
verify_build{"verify_build<br/>script"}
verify_build -->|pass| verify_tests
verify_build -->|fail| fix_loop_gate
verify_tests{"verify_tests<br/>script"}
verify_tests -->|pass| end_success
verify_tests -->|fail| fix_loop_gate
fix_loop_gate{"fix_loop_gate<br/>script"}
fix_loop_gate -->|"budget left"| implement
fix_loop_gate -->|"budget spent"| end_failure
end_success(["end_success<br/>CODER_COMPLETE"]) It can also be used as a standalone tool for direct coding assistance.
end_rejected(["end_rejected<br/>CODER_REJECTED"])
end_failure(["end_failure<br/>CODER_FAILED"])
```
End nodes emit one of three sentinel outcomes for the caller: ## Pro-Tip: Use an IDE MCP Server for Improved Performance
Many modern IDEs now include MCP servers that let LLMs perform operations within the IDE itself and use IDE tools. Using
- `CODER_COMPLETE` — build and tests passed. an IDE's MCP server dramatically improves the performance of coding agents. So if you have an IDE, try adding that MCP
- `CODER_REJECTED` — user rejected the plan at the approval gate. server to your config (see the [MCP Server docs](../../../docs/function-calling/MCP-SERVERS.md) to see how to configure
- `CODER_FAILED` — fix-loop exhausted; build/tests still failing. them), and modify the agent definition to look like this:
## Tuning
The agent's `project_dir` is exposed via the standard `variables:` block,
so it accepts the runtime override flag:
```sh
# Invoke from inside the project (project_dir defaults to ".")
cd /path/to/your/project
coyote -a coder "Add a foo() function..."
# Or invoke from anywhere with an explicit override
coyote -a coder --agent-variable project_dir /path/to/your/project "Add..."
```
`graph.yaml` `initial_state` exposes:
- `max_fix_attempts` (default `3`) — fix-loop budget before `end_failure`.
Environment overrides honored by the script nodes:
- `BUILD_CMD` — skip project-type detection for the build/check command.
- `TEST_CMD` — skip detection for tests.
- `CODER_AUTOAPPROVE=1` — bypass the approval gate (for non-interactive runs
where complexity might trip the gate).
## Pro-Tip: IDE MCP Server
Modern IDEs (JetBrains, VS Code, Cursor, Zed, etc.) expose MCP servers
that let LLMs use IDE tools directly. To wire one in, edit `graph.yaml`:
```yaml ```yaml
# ...
mcp_servers: mcp_servers:
- your-ide-mcp-server - jetbrains # The name of your configured IDE MCP server
global_tools: global_tools:
# Keep read-only fs tools for files outside the IDE project # Keep useful read-only tools for reading files in other non-project directories
- fs_read.sh - fs_read.sh
- fs_grep.sh - fs_grep.sh
- fs_glob.sh - fs_glob.sh
# - fs_write.sh # - fs_write.sh
# - fs_patch.sh # - fs_patch.sh
- execute_command.sh - execute_command.sh
```
Then add the MCP server's write/patch tools to the `implement` node's # ...
`tools:` whitelist. ```
+129
View File
@@ -0,0 +1,129 @@
name: coder
description: Implementation agent - writes code, follows patterns, verifies with builds
version: 1.0.0
temperature: 0.1
auto_continue: true
max_auto_continues: 15
inject_todo_instructions: true
variables:
- name: project_dir
description: Project directory to work in
default: '.'
- name: auto_confirm
description: Auto-confirm command execution
default: '1'
global_tools:
- fs_read.sh
- fs_grep.sh
- fs_glob.sh
- fs_write.sh
- fs_patch.sh
- execute_command.sh
instructions: |
You are a senior engineer. You write code that works on the first try.
## Your Mission
Given an implementation task:
1. Check for orchestrator context first (see below)
2. Fill gaps only. Read files NOT already covered in context
3. Write the code (using tools, NOT chat output)
4. Verify it compiles/builds
5. Signal completion with a summary
## Using Orchestrator Context (IMPORTANT)
When spawned by sisyphus, your prompt will often contain a `<context>` block
with prior findings: file paths, code patterns, and conventions discovered by
explore agents.
**If context is provided:**
1. Use it as your primary reference. Don't re-read files already summarized
2. Follow the code patterns shown. Snippets in context ARE the style guide
3. Read the referenced files ONLY IF you need more detail (e.g. full function
signature, import list, or adjacent code not included in the snippet)
4. If context includes a "Conventions" section, follow it exactly
**If context is NOT provided or is too vague to act on:**
Fall back to self-exploration: grep for similar files, read 1-2 examples,
match their style.
**Never ignore provided context.** It represents work already done upstream.
## Todo System
For multi-file changes:
1. `todo__init` with the implementation goal
2. `todo__add` for each file to create/modify
3. Implement each, calling `todo__done` immediately after
## Writing Code
**CRITICAL**: Write code using `write_file` tool, NEVER paste code in chat.
Correct:
```
write_file --path "src/user.rs" --content "pub struct User { ... }"
```
Wrong:
```
Here's the implementation:
\`\`\`rust
pub struct User { ... }
\`\`\`
```
## File Reading Strategy (IMPORTANT - minimize token usage)
1. **Use grep to find relevant code** - `fs_grep --pattern "fn handle_request" --include "*.rs"` finds where things are
2. **Read only what you need** - `fs_read --path "src/main.rs" --offset 50 --limit 30` reads lines 50-79
3. **Never cat entire large files** - If 500+ lines, read the relevant section after grepping for it
4. **Use glob to find files** - `fs_glob --pattern "*.rs" --path src/` discovers files by name
## Pattern Matching
Before writing ANY file:
1. Find a similar existing file (use `fs_grep` to locate, then `fs_read` to examine)
2. Match its style: imports, naming, structure
3. Follow the same patterns exactly
## Verification
After writing files:
1. Run `verify_build` to check compilation
2. If it fails, fix the error (minimal change)
3. Don't move on until build passes
## Completion Signal
When done, end your response with a summary so the parent agent knows what happened:
```
CODER_COMPLETE: [summary of what was implemented, which files were created/modified, and build status]
```
Or if something went wrong:
```
CODER_FAILED: [what went wrong]
```
## Rules
1. **Write code via tools** - Never output code to chat
2. **Follow patterns** - Read existing files first
3. **Verify builds** - Don't finish without checking
4. **Minimal fixes** - If build fails, fix precisely
5. **No refactoring** - Only implement what's asked
## Context
- Project: {{project_dir}}
- CWD: {{__cwd__}}
- Shell: {{__shell__}}
## Available tools:
{{__tools__}}
-413
View File
@@ -1,413 +0,0 @@
name: coder
description: |
Implementation agent. Plans, implements, and runs build + tests in a
bounded fix-loop until verified. Designed to be delegated to by sisyphus.
version: '1.0'
global_tools:
- ast_grep.sh
- fs_cat.sh
- fs_ls.sh
- fs_write.sh
- fs_patch.sh
- execute_command.sh
skills_enabled: true
enabled_skills:
- ai-slop-remover
- code-review
- comment-discipline
- diagnosing-bugs
- logging-discipline
- git-master
- frontend-ui-ux
- verification-gates
variables:
- name: project_dir
description: |
Absolute path to the project directory. Defaults to "." which is the
directory you invoked `coyote` from. Override at runtime with
`coyote -a coder --agent-variable project_dir /abs/path "..."`.
default: '.'
settings:
max_loop_iterations: 20
log_state_snapshots: true
validate_before_run: true
timeout: 14400
initial_state:
project_dir: ''
fix_attempts: 0
max_fix_attempts: 5
fix_instructions: ''
build_output: ''
tests_output: ''
last_node_output: ''
plan_summary: ''
files_to_modify: []
files_to_create: []
risks: []
complexity_score: 0
review_attempts: 0
max_review_attempts: 1
review_clean: true
review_notes: ''
start: resolve_paths
nodes:
resolve_paths:
id: resolve_paths
type: script
description: Resolve project_dir to an absolute path from the agent variable
script: scripts/resolve_paths.sh
timeout: 5
fallback: end_failure
analyze_request:
id: analyze_request
type: llm
description: Extract a structured plan and complexity score from the orchestrator's prompt
instructions: |
You are a senior engineer's planning assistant. Read the orchestrator's
request and emit a structured plan. You only plan. You never edit files.
Score complexity from 1 to 10:
1-3: trivial - single file, <=20 lines changed, obvious approach
4-6: moderate - 2-5 files, clear approach, some pattern matching
7-10: complex - multi-component, ambiguous tradeoffs, refactoring,
or wide blast radius
Be specific in `files_to_modify` and `files_to_create`. All paths
MUST be absolute. The project root is {{project_dir}}. Prefer paths
like "{{project_dir}}/src/foo.rs" over "src/foo.rs". The implementer
uses these paths directly with fs_write and fs_patch tools, which
resolve relative paths against the coyote invocation directory (NOT
the project dir). Empty arrays are fine if no files in that category.
`risks` is a list of short strings. Anything that could derail the
implementation: unknown dependencies, brittle tests, blast radius,
etc. Empty list is fine.
Project directory: {{project_dir}}
prompt: '{{initial_prompt}}'
tools: []
timeout: 300
output_schema:
type: object
properties:
plan_summary:
type: string
description: 1-3 sentences summarizing what will be done
files_to_modify:
type: array
items: { type: string }
files_to_create:
type: array
items: { type: string }
complexity_score:
type: integer
minimum: 1
maximum: 10
risks:
type: array
items: { type: string }
required:
[
plan_summary,
files_to_modify,
files_to_create,
complexity_score,
risks,
]
state_updates:
last_node_output: '{{output}}'
fallback: end_failure
next: route_complexity
route_complexity:
id: route_complexity
type: script
description: Route to approval gate for complex plans; skip otherwise
script: scripts/route_complexity.sh
timeout: 5
fallback: implement
gate_approval:
id: gate_approval
type: approval
description: Optional human checkpoint for high-complexity plans
question: |
## Plan
{{plan_summary}}
## Files to modify
{{files_to_modify}}
## Files to create
{{files_to_create}}
## Risks
{{risks}}
Complexity: {{complexity_score}}/10
Approve this plan?
options:
- 'yes'
- 'no'
routes:
'yes': implement
'no': end_rejected
on_other: end_rejected
implement:
id: implement
type: llm
description: Write code via fs tools. Bounded tool-call loop.
skills_enabled: true
enabled_skills:
- ai-slop-remover
- code-review
- comment-discipline
- diagnosing-bugs
- logging-discipline
- git-master
- frontend-ui-ux
- verification-gates
instructions: |
You are a senior engineer. Implement the plan by writing code via
tools. Follow existing patterns in the codebase.
## Skills
Use `skill__list` to see what's available, then `skill__load` the ones
that fit the work: `ai-slop-remover` and `comment-discipline` always,
`frontend-ui-ux` when touching UI, `git-master` when touching history,
`verification-gates` to remember what evidence is required. Unload when
a phase ends.
## Writing code
1. Use `fs_patch` for surgical edits to existing files.
2. Use `fs_write` for new files or full rewrites.
3. NEVER write files via `execute_command`. Do not use `cat >`,
`cat >>`, `echo >`, `printf >`, `tee`, heredocs (`<<EOF`), or
`python3 -c "open(...).write(...)"`. Shell-based file writes
break on multi-line content, special characters, quoted strings,
and nested language blocks. `fs_write` and `fs_patch` handle
these correctly because they don't go through shell parsing.
4. NEVER output code to chat. Always use tools.
5. ALWAYS pass ABSOLUTE paths to fs_write and fs_patch. Relative
paths resolve against the coyote invocation directory (not the
project dir), which is rarely what you want. The project root
is {{project_dir}}.
## File reading
1. Use `execute_command` to grep/find:
`execute_command --command "grep -rn 'fn handle_request' --include='*.rs' ."`
`execute_command --command "find . -name '*.rs' -not -path '*/target/*'"`
2. Read only what you need:
`fs_cat --path "src/main.rs" --offset 50 --limit 30`
3. Never read entire large files. Use offset/limit.
4. Use `fs_ls` to list directory contents.
## Pattern matching
Before writing ANY file:
1. Find a similar existing file (grep, then read).
2. Match its style: imports, naming, structure, error handling.
3. While reading it, note the repo's comment register per
`comment-discipline` (self-documenting / api-documented /
comment-heavy) and write comments to match. When the signal is
weak, write NO comment. Every comment must EARN ITS KEEP — if
the code already tells the reader, don't add it.
4. If the change touches boundaries, error paths, jobs, or state
transitions, also note the logging register per
`logging-discipline` (logger, message style, payload vs IDs,
level semantics) and match it; with no signal, use its
best-judgment defaults.
5. Follow the same patterns exactly. Do not invent new ones.
## Fix loop
If the "Fix loop status" section in your user prompt is non-empty,
the previous attempt failed verification. Read the error, identify
the minimal fix, apply it. Do not refactor while fixing.
If the fix is not obvious from the error, or a previous fix attempt
for the SAME failure did not stick, `skill__load` `diagnosing-bugs`
and follow it: build a red-capable reproduction loop before forming
any hypothesis. Do not spend a second attempt on a blind retry.
## Rules
1. Match existing patterns - read examples first.
2. Minimal changes - implement only what's asked.
3. Never suppress errors (`as any`, `@ts-ignore`, `#[allow(...)]`
on unfamiliar lints, etc.).
4. No dead code, no commented-out blocks, no premature abstractions.
5. End your turn when editing is done. The graph runs verification next.
6. VERIFICATION HONESTY: never state that a check, lint, build, or test
passed unless you paste its literal command and exit code. A gate
that did not run is UNVERIFIED — say so. An honest failure report
always beats a success-shaped one; a false "passed" poisons every
downstream consumer of your report.
Project directory: {{project_dir}}
prompt: |
## Plan summary
{{plan_summary}}
## Files involved
- Modify: {{files_to_modify}}
- Create: {{files_to_create}}
## Original request from the orchestrator
{{initial_prompt}}
## Fix loop status
{{fix_instructions}}
tools:
- fs_cat
- fs_ls
- fs_write
- fs_patch
- execute_command
max_iterations: 100
timeout: 1800
state_updates:
last_node_output: '{{output}}'
fallback: end_failure
next: verify_build
verify_build:
id: verify_build
type: script
description: Run the project's check/build command. Routes to verify_tests on success, fix_loop_gate on failure.
script: scripts/verify_build.sh
timeout: 300
fallback: fix_loop_gate
verify_tests:
id: verify_tests
type: script
description: Run the project's test command. Routes to end_success on pass, fix_loop_gate on failure.
script: scripts/verify_tests.sh
timeout: 600
fallback: fix_loop_gate
fix_loop_gate:
id: fix_loop_gate
type: script
description: Budget gate. Loops back to implement with fix_instructions populated, or terminates as end_failure.
script: scripts/fix_loop_gate.sh
timeout: 5
fallback: end_failure
self_review:
id: self_review
type: llm
description: Skill-driven self-review of the diff. Catches AI slop, dishonest naming, suppressed errors. Bounded to max_review_attempts.
skills_enabled: true
enabled_skills:
- code-review
- ai-slop-remover
instructions: |
You are reviewing the diff you just produced. Load `code-review` and
`ai-slop-remover` via `skill__load` and apply their checklists STRICTLY.
Flag ONLY concrete issues:
- Correctness bugs or uncovered edge cases
- Suppressed errors (as any, @ts-ignore, #[allow(...)] on unfamiliar
lints, empty catch blocks)
- Dishonest naming (get_X that mutates, returns wrong type, etc.)
- Useless comments that restate the code, or that otherwise don't earn
their keep (the code already tells the reader)
- AI slop (filler prose, multi-paragraph docstrings, defensive
handling of impossible cases)
Do NOT flag:
- Style preferences if the pattern matches existing code in the repo
- Things the build/tests already verified
- "Could be more elegant" without a concrete bug
Be terse. The orchestrator wants signal, not noise. If you find nothing
blocking, set review_clean=true and leave review_notes empty.
Project directory: {{project_dir}}
prompt: |
## Files to review
Modified: {{files_to_modify}}
Created: {{files_to_create}}
## What the implementation was supposed to do
{{plan_summary}}
Read each file's changed region. Apply the review skills. Output your verdict.
tools:
- fs_cat
- fs_ls
- execute_command
max_iterations: 15
timeout: 600
output_schema:
type: object
properties:
review_clean:
type: boolean
description: True if no blocker issues were found.
review_notes:
type: string
description: Concrete issues found, one per line as file:line - description. Empty when review_clean is true.
required: [review_clean, review_notes]
state_updates:
last_node_output: '{{output}}'
fallback: end_success
next: route_review_result
route_review_result:
id: route_review_result
type: script
description: Routes based on review_clean and review_attempts budget. End on clean or budget exhausted; loop to implement otherwise.
script: scripts/route_review_result.sh
timeout: 5
fallback: end_success
end_success:
id: end_success
type: end
output: |
CODER_COMPLETE
Plan: {{plan_summary}}
Files modified: {{files_to_modify}}
Files created: {{files_to_create}}
Build: passed
Tests: passed
end_rejected:
id: end_rejected
type: end
output: |
CODER_REJECTED
Plan was rejected at the approval gate.
Plan: {{plan_summary}}
end_failure:
id: end_failure
type: end
output: |
CODER_FAILED
Plan: {{plan_summary}}
Attempts: {{fix_attempts}}/{{max_fix_attempts}}
Last node output:
{{last_node_output}}
Last build output:
{{build_output}}
Last tests output:
{{tests_output}}
@@ -1,49 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ -n "${GRAPH_STATE_FILE:-}" ]]; then
state=$(cat "$GRAPH_STATE_FILE")
elif [[ -n "${GRAPH_STATE:-}" ]]; then
state="$GRAPH_STATE"
else
state='{}'
fi
fix_attempts=$(echo "$state" | jq -r '.fix_attempts // 0')
max_fix_attempts=$(echo "$state" | jq -r '.max_fix_attempts // 3')
build_ok=$(echo "$state" | jq -r '.build_ok | if . == null then "true" else (. | tostring) end')
tests_ok=$(echo "$state" | jq -r '.tests_ok | if . == null then "true" else (. | tostring) end')
build_output=$(echo "$state" | jq -r '.build_output // ""')
tests_output=$(echo "$state" | jq -r '.tests_output // ""')
if (( fix_attempts >= max_fix_attempts )); then
jq -nc \
--argjson n "$fix_attempts" \
'{
"fix_attempts": $n,
"_next": "end_failure"
}'
exit 0
fi
next_attempts=$((fix_attempts + 1))
if [[ "$build_ok" != "true" ]]; then
fix_instructions=$(printf '## Fix loop status (attempt %d of %d)\n\nThe previous attempt failed the build.\n\nBuild output:\n```\n%s\n```\n\nIdentify the minimal fix and apply it. Do not refactor.' \
"$next_attempts" "$max_fix_attempts" "$build_output")
elif [[ "$tests_ok" != "true" ]]; then
fix_instructions=$(printf '## Fix loop status (attempt %d of %d)\n\nBuild passed but tests failed.\n\nTest output:\n```\n%s\n```\n\nIdentify the minimal fix and apply it. Do not refactor.' \
"$next_attempts" "$max_fix_attempts" "$tests_output")
else
fix_instructions=$(printf '## Fix loop status (attempt %d of %d)\n\nfix_loop_gate was reached but no failure was detected in state. Re-run the verification step.' \
"$next_attempts" "$max_fix_attempts")
fi
jq -nc \
--argjson n "$next_attempts" \
--arg fi "$fix_instructions" \
'{
"fix_attempts": $n,
"fix_instructions": $fi,
"_next": "implement"
}'
@@ -1,12 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
project_dir="${LLM_AGENT_VAR_PROJECT_DIR:-.}"
resolved=$(cd "$project_dir" 2>/dev/null && pwd) || resolved="$project_dir"
jq -nc \
--arg pd "$resolved" \
'{
"project_dir": $pd,
"_next": "analyze_request"
}'
@@ -1,23 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ -n "${GRAPH_STATE_FILE:-}" ]]; then
state=$(cat "$GRAPH_STATE_FILE")
elif [[ -n "${GRAPH_STATE:-}" ]]; then
state="$GRAPH_STATE"
else
state='{}'
fi
complexity=$(echo "$state" | jq -r '.complexity_score // 0')
if [[ "${CODER_AUTOAPPROVE:-0}" == "1" ]]; then
jq -nc '{"_next": "implement"}'
exit 0
fi
if (( complexity >= 7 )); then
jq -nc '{"_next": "gate_approval"}'
else
jq -nc '{"_next": "implement"}'
fi
@@ -1,58 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ -n "${GRAPH_STATE_FILE:-}" ]]; then
state=$(cat "$GRAPH_STATE_FILE")
elif [[ -n "${GRAPH_STATE:-}" ]]; then
state="$GRAPH_STATE"
else
state='{}'
fi
review_clean=$(echo "$state" | jq -r '.review_clean // true')
review_attempts=$(echo "$state" | jq -r '.review_attempts // 0')
max_review_attempts=$(echo "$state" | jq -r '.max_review_attempts // 1')
review_notes=$(echo "$state" | jq -r '.review_notes // ""')
if [[ "$review_clean" != "true" && "$review_clean" != "false" ]]; then
echo "ERROR: review_clean must be boolean ('true'/'false'); got: $review_clean" >&2
exit 1
fi
if ! [[ "$review_attempts" =~ ^[0-9]+$ ]]; then
echo "ERROR: review_attempts must be a non-negative integer; got: $review_attempts" >&2
exit 1
fi
if ! [[ "$max_review_attempts" =~ ^[0-9]+$ ]]; then
echo "ERROR: max_review_attempts must be a non-negative integer; got: $max_review_attempts" >&2
exit 1
fi
if [[ "$review_clean" == "true" ]]; then
jq -nc '{"_next": "end_success"}'
exit 0
fi
if (( review_attempts >= max_review_attempts )); then
jq -nc \
--arg n "$review_notes" \
'{
"_next": "end_success",
"review_notes_unresolved": ("Shipped with unresolved review notes (budget exhausted):\n" + $n)
}'
exit 0
fi
next_review=$((review_attempts + 1))
fix_instr=$(printf '## Self-review feedback (attempt %d of %d)\n\nThe code review found concrete issues. Address them with minimal edits. Do not refactor unrelated code.\n\n%s' \
"$next_review" "$max_review_attempts" "$review_notes")
jq -nc \
--argjson n "$next_review" \
--arg fi "$fix_instr" \
'{
"review_attempts": $n,
"fix_instructions": $fi,
"_next": "implement"
}'
@@ -1,56 +0,0 @@
#!/usr/bin/env bash
set -uo pipefail
# shellcheck disable=SC1091
source "$(dirname "$0")/../../.shared/utils.sh"
if [[ -n "${GRAPH_STATE_FILE:-}" ]]; then
state=$(cat "$GRAPH_STATE_FILE")
elif [[ -n "${GRAPH_STATE:-}" ]]; then
state="$GRAPH_STATE"
else
state='{}'
fi
project_dir=$(echo "$state" | jq -r '.project_dir // "."')
project_dir=$(resolve_gate_dir "$project_dir")
if [[ -n "${BUILD_CMD:-}" ]]; then
cmd="$BUILD_CMD"
else
project_info=$(detect_project "$project_dir")
cmd=$(echo "$project_info" | jq -r '.check // .build // ""')
fi
if [[ -z "$cmd" || "$cmd" == "null" ]]; then
jq -nc '{
"build_ok": true,
"build_output": "(GATE NOT RUN: no build/check command configured or detected. This is NOT evidence that the build passed — set BUILD_CMD, and never report the build as verified.)",
"_next": "verify_tests"
}'
exit 0
fi
exit_code=0
output=$(cd "$project_dir" && eval "$cmd" 2>&1) || exit_code=$?
if (( exit_code == 0 )); then
jq -nc \
--arg out "$output" \
--arg cmd "$cmd" \
'{
"build_ok": true,
"build_output": ("Ran: " + $cmd + "\n\n" + $out),
"_next": "verify_tests"
}'
else
jq -nc \
--arg out "$output" \
--arg cmd "$cmd" \
--argjson rc "$exit_code" \
'{
"build_ok": false,
"build_output": ("Ran: " + $cmd + "\nExit code: " + ($rc | tostring) + "\n\n" + $out),
"_next": "fix_loop_gate"
}'
fi
@@ -1,56 +0,0 @@
#!/usr/bin/env bash
set -uo pipefail
# shellcheck disable=SC1091
source "$(dirname "$0")/../../.shared/utils.sh"
if [[ -n "${GRAPH_STATE_FILE:-}" ]]; then
state=$(cat "$GRAPH_STATE_FILE")
elif [[ -n "${GRAPH_STATE:-}" ]]; then
state="$GRAPH_STATE"
else
state='{}'
fi
project_dir=$(echo "$state" | jq -r '.project_dir // "."')
project_dir=$(resolve_gate_dir "$project_dir")
if [[ -n "${TEST_CMD:-}" ]]; then
cmd="$TEST_CMD"
else
project_info=$(detect_project "$project_dir")
cmd=$(echo "$project_info" | jq -r '.test // ""')
fi
if [[ -z "$cmd" || "$cmd" == "null" ]]; then
jq -nc '{
"tests_ok": true,
"tests_output": "(GATE NOT RUN: no test command configured or detected. This is NOT evidence that tests passed — set TEST_CMD, and never report the suite as green.)",
"_next": "self_review"
}'
exit 0
fi
exit_code=0
output=$(cd "$project_dir" && eval "$cmd" 2>&1) || exit_code=$?
if (( exit_code == 0 )); then
jq -nc \
--arg out "$output" \
--arg cmd "$cmd" \
'{
"tests_ok": true,
"tests_output": ("Ran: " + $cmd + "\n\n" + $out),
"_next": "self_review"
}'
else
jq -nc \
--arg out "$output" \
--arg cmd "$cmd" \
--argjson rc "$exit_code" \
'{
"tests_ok": false,
"tests_output": ("Ran: " + $cmd + "\nExit code: " + ($rc | tostring) + "\n\n" + $out),
"_next": "fix_loop_gate"
}'
fi
+118
View File
@@ -14,6 +14,99 @@ _project_dir() {
(cd "${dir}" 2>/dev/null && pwd) || echo "${dir}" (cd "${dir}" 2>/dev/null && pwd) || echo "${dir}"
} }
# Normalize a path to be relative to project root.
# Strips the project_dir prefix if the LLM passes an absolute path.
# Usage: local rel_path; rel_path=$(_normalize_path "/abs/or/rel/path")
_normalize_path() {
local input_path="$1"
local project_dir
project_dir=$(_project_dir)
if [[ "${input_path}" == /* ]]; then
input_path="${input_path#"${project_dir}"/}"
fi
input_path="${input_path#./}"
echo "${input_path}"
}
# @cmd Read a file's contents before modifying
# @option --path! Path to the file (relative to project root)
read_file() {
local file_path
# shellcheck disable=SC2154
file_path=$(_normalize_path "${argc_path}")
local project_dir
project_dir=$(_project_dir)
local full_path="${project_dir}/${file_path}"
if [[ ! -f "${full_path}" ]]; then
warn "File not found: ${file_path}" >> "$LLM_OUTPUT"
return 0
fi
{
info "Reading: ${file_path}"
echo ""
cat "${full_path}"
} >> "$LLM_OUTPUT"
}
# @cmd Write complete file contents
# @option --path! Path for the file (relative to project root)
# @option --content! Complete file contents to write
write_file() {
local file_path
file_path=$(_normalize_path "${argc_path}")
# shellcheck disable=SC2154
local content="${argc_content}"
local project_dir
project_dir=$(_project_dir)
local full_path="${project_dir}/${file_path}"
mkdir -p "$(dirname "${full_path}")"
printf '%s' "${content}" > "${full_path}"
green "Wrote: ${file_path}" >> "$LLM_OUTPUT"
}
# @cmd Find files similar to a given path (for pattern matching)
# @option --path! Path to find similar files for
find_similar_files() {
local file_path
file_path=$(_normalize_path "${argc_path}")
local project_dir
project_dir=$(_project_dir)
local ext="${file_path##*.}"
local dir
dir=$(dirname "${file_path}")
info "Similar files to: ${file_path}" >> "$LLM_OUTPUT"
echo "" >> "$LLM_OUTPUT"
local results
results=$(find "${project_dir}/${dir}" -maxdepth 1 -type f -name "*.${ext}" \
! -name "$(basename "${file_path}")" \
! -name "*test*" \
! -name "*spec*" \
2>/dev/null | sed "s|^${project_dir}/||" | head -3)
if [[ -z "${results}" ]]; then
results=$(find "${project_dir}/src" -type f -name "*.${ext}" \
! -name "*test*" \
! -name "*spec*" \
-not -path '*/target/*' \
2>/dev/null | sed "s|^${project_dir}/||" | head -3)
fi
if [[ -n "${results}" ]]; then
echo "${results}" >> "$LLM_OUTPUT"
else
warn "No similar files found" >> "$LLM_OUTPUT"
fi
}
# @cmd Verify the project builds successfully # @cmd Verify the project builds successfully
verify_build() { verify_build() {
local project_dir local project_dir
@@ -96,3 +189,28 @@ get_project_structure() {
} >> "$LLM_OUTPUT" } >> "$LLM_OUTPUT"
} }
# @cmd Search for content in the codebase
# @option --pattern! Pattern to search for
search_code() {
# shellcheck disable=SC2154
local pattern="${argc_pattern}"
local project_dir
project_dir=$(_project_dir)
info "Searching: ${pattern}" >> "$LLM_OUTPUT"
echo "" >> "$LLM_OUTPUT"
local results
results=$(grep -rn "${pattern}" "${project_dir}" 2>/dev/null | \
grep -v '/target/' | \
grep -v '/node_modules/' | \
grep -v '/.git/' | \
sed "s|^${project_dir}/||" | \
head -20) || true
if [[ -n "${results}" ]]; then
echo "${results}" >> "$LLM_OUTPUT"
else
warn "No matches" >> "$LLM_OUTPUT"
fi
}
-357
View File
@@ -1,357 +0,0 @@
# deep-research
A deep web research agent, built as a Coyote graph agent. It plans an
investigation, decomposes it into sub-questions researched in
parallel, grounds the work in a local knowledge corpus, vets the
credibility of cited sources, runs a reflexion self-critique loop to
revise weak findings, delegates the final write-up to a focused
sub-agent, checks that the cited sources are reachable, and gates the
result behind human approval.
Unlike a regular agent (which takes a goal and improvises the steps),
this agent runs a fixed graph: every request goes through the same
`plan -> parallel research -> vet -> critique -> synthesize -> verify -> approve`
pipeline.
Two variables tune a run (pass via `--variables`): `reflexion_budget`
(default 2) caps the automated critique -> re-research passes — raise it
to let the run churn until the critique passes — and
`autonomy: autonomous` skips the final human approval gate so other
agents can spawn this one autonomously. Each sub-question is researched
by a spawned `librarian` running at deep budgets.
This agent is also the **canonical reference for the Coyote graph
system**: it exercises every node type (`script`, `llm`, `rag`, `map`,
`agent`, `input`, `approval`, `end`) and both static fan-out and
dynamic `map` fan-out. If you are learning how to build a graph
agent, this is the file to read alongside the
[Graph-Agents wiki](https://github.com/Dark-Alex-17/coyote/wiki/Graph-Agents).
## Workflow
23 nodes. Solid arrows are static `next` / `routes` edges declared in
`graph.yaml`; script nodes can also route dynamically via `_next` (shown as
labeled branches out of the diamond). Dotted arrows show `map` fan-out — the
`research_each_question` node spawns one `research_one_question` branch per
sub-question and joins them before continuing — and `fallback` edges
(labeled "fallback"), taken when a node fails outright (see *Fault paths*
below).
```mermaid
flowchart TD
parse_request{"parse_request<br/>script"}
parse_request -->|"topic given"| bootstrap_research
parse_request -->|"no topic"| ask_topic
ask_topic[/"ask_topic<br/>input"/] --> bootstrap_research
bootstrap_research{"bootstrap_research<br/>script"}
bootstrap_research --> plan
bootstrap_research --> knowledge_lookup
plan["plan<br/>llm + output_schema"] --> research_each_question
plan -. "fallback" .-> plan_fault{"plan_fault<br/>script"}
plan_fault --> end_fault
knowledge_lookup[("knowledge_lookup<br/>rag")] --> research_each_question
research_each_question[\research_each_question<br/>map/]
research_each_question -. "spawns × N" .-> research_one_question["research_one_question<br/>llm + web tools"]
research_one_question -. "fallback" .-> question_fault{"question_fault<br/>script"}
research_each_question --> combine_findings
combine_findings{"combine_findings<br/>script"} --> vet_sources
vet_sources["vet_sources<br/>llm + classify_source"] --> critique
vet_sources -. "fallback" .-> vet_fault{"vet_fault<br/>script"}
vet_fault --> critique
critique["critique<br/>llm"] --> reflexion_gate
critique -. "fallback" .-> critique_fault{"critique_fault<br/>script"}
critique_fault --> reflexion_gate
reflexion_gate{"reflexion_gate<br/>script"}
reflexion_gate -->|"PASS"| synthesize
reflexion_gate -->|"REVISE (budget left)"| research_each_question
reflexion_gate -->|"REVISE (budget spent)"| synthesize
synthesize[["synthesize<br/>agent → report-writer"]] --> verify_sources
synthesize -. "fallback" .-> synth_fault{"synth_fault<br/>script"}
synth_fault --> end_fault
verify_sources{"verify_sources<br/>script"} --> approve
approve{{"approve<br/>approval"}}
approve -->|"accept"| end_accepted
approve -->|"reject"| end_rejected
approve -->|"other (free-form feedback)"| incorporate_feedback
incorporate_feedback{"incorporate_feedback<br/>script"} --> research_each_question
end_accepted(["end_accepted<br/>report"])
end_rejected(["end_rejected"])
end_fault(["end_fault<br/>DEEP_RESEARCH FAILED"])
```
### Node-type breakdown
| Type | Nodes |
|-----------------------------|-----------------------------------------------------------------------------------------------------------------------|
| `script` (Python) | `parse_request`, `bootstrap_research`, `combine_findings`, `reflexion_gate`, `verify_sources`, `incorporate_feedback`, plus the fault markers `plan_fault`, `question_fault`, `vet_fault`, `critique_fault`, `synth_fault` |
| `llm` (tools: `[]`) | `plan`, `critique` |
| `llm` (with tool whitelist) | `research_one_question`, `vet_sources` |
| `rag` | `knowledge_lookup` — local corpus retrieval |
| `map` | `research_each_question` — dynamic fan-out per sub-question |
| `agent` | `synthesize` — spawns the `report-writer` sub-agent |
| `input` | `ask_topic` |
| `approval` | `approve` |
| `end` | `end_accepted`, `end_rejected`, `end_fault` |
## Parallel execution
The graph has two parallel super-steps where Coyote's BSP scheduler runs
branches concurrently.
**1. Context loading (`plan` ‖ `knowledge_lookup`)** — after
`bootstrap_research`, the LLM planner (which decomposes the topic into
sub-questions) and the RAG retrieval over the local `knowledge/`
corpus run side by side. They write disjoint state keys (`plan` writes
`research_plan` and `questions`; `knowledge_lookup` writes
`local_context` and `local_sources`) so no reducer is needed.
**2. Per-question research (`research_each_question` map)** — the
plan emits a `questions` array (3-5 entries, enforced by its
`output_schema`). The `map` node spawns one parallel branch per
question (`max_concurrency: 3`). Each branch is an isolated
`research_one_question` LLM invocation with web tools, instructed to
investigate exactly its assigned question. Outputs collect into
`question_findings` in input order, then `combine_findings` joins
them into a single `findings` Markdown document for downstream nodes.
`settings.max_concurrency: 4` is the graph-wide cap; the per-`map`
override (`max_concurrency: 3` on `research_each_question`) is
deliberately lower to leave headroom for the planner's tool calls
running alongside RAG.
## Local knowledge corpus
`knowledge_lookup` is a `rag` node — it runs hybrid (vector + keyword)
retrieval over every file in `knowledge/`. The directory ships with a
small `research-style-notes.md` so the RAG node has something to
retrieve against on a clean install; drop your own Markdown notes,
PDFs, or text files into `knowledge/` to bias the research toward
your local context.
The knowledge base is built once, at agent-load time, into
`~/.config/coyote/agents/deep-research/knowledge_lookup.yaml`. Because
the node fully specifies its build config (`embedding_model`,
`chunk_size`, `chunk_overlap`), the build is non-interactive. Delete
that cached file after adding or changing knowledge to force a
rebuild.
## Sub-agent: report-writer
The `synthesize` node is an `agent` node that spawns the
`report-writer` sub-agent (`assets/agents/report-writer/`). This is
the agent-as-tool pattern: the orchestrating graph delegates the
writing phase to a focused sub-agent dedicated to coherent prose,
while the research phase uses different (typically cheaper) LLM nodes
for fast-and-many-question investigation.
The `report-writer` sub-agent has no tools — it cannot access the
web, cannot search, and cannot invent facts. It reads only the
findings it is given and produces a final Markdown report preserving
every inline citation. See `assets/agents/report-writer/README.md`
for details.
## Tools and tool scoping
This agent demonstrates Coyote's three tool sources and how an `llm`
node's `tools:` whitelist scopes them per node.
The agent's full tool universe, declared in `graph.yaml`:
- **Global tools** (`global_tools`): `web_search_coyote`,
`fetch_url_via_curl`, `search_arxiv` - Coyote's built-in tool scripts.
- **MCP server** (`mcp_servers`): `ddg-search` - a DuckDuckGo web
search MCP server. Referenced in a whitelist as `mcp:ddg-search`.
- **Custom agent tool** (`tools.sh`): `classify_source` - a
deterministic source-credibility classifier shipped with this agent.
No node receives all of these. Each `llm` node's `tools:` whitelist
narrows the universe to exactly what that step needs:
| Node | `tools:` whitelist | Draws from |
|-------------------------|-----------------------------------------------------------------------------|--------------------------|
| `plan`, `critique` | `[]` | nothing - pure reasoning |
| `research_one_question` | `web_search_coyote`, `fetch_url_via_curl`, `search_arxiv`, `mcp:ddg-search` | global tools + MCP |
| `vet_sources` | `classify_source` | the custom tool only |
`research_one_question` (each parallel branch of the map) can search
and fetch but cannot classify sources; `vet_sources` can classify
sources but cannot touch the web. That separation is the point of the
`tools:` whitelist: a node gets only the tools its job calls for,
never the agent's full set.
The `classify_source` custom tool (`tools.sh`) takes a URL and returns
a credibility tier (government, academic, preprint, organization,
unverified) derived from the host and top-level domain. It is
deterministic - exactly the kind of logic a tool should own rather than
the LLM guessing.
Web search may require API-key configuration; see the
[Tools](https://github.com/Dark-Alex-17/coyote/wiki/Tools) docs.
`fetch_url_via_curl`, `search_arxiv`, and `classify_source` work
without a key.
## Setup
`research_one_question` (each parallel branch of the `map`) uses the
`ddg-search` MCP server via `mcp:ddg-search`. It is one of Coyote's
default MCP servers; make sure it is registered in
`~/.config/coyote/mcp.json` (run `coyote --install mcp_config` to restore
the default template if it is missing). If `ddg-search` is unavailable,
the branches still have their global web-search tools to fall back on.
The `synthesize` node spawns the `report-writer` sub-agent. Both
agents ship with `coyote agents install`; if you install one manually,
install both so the agent reference resolves.
## Reflexion
The agent has two loops, both built with script nodes that route via
`_next`. The engine allows back-edges at runtime; the validator only
rejects cycles built from static `next` / `routes` edges, so script
`_next` loops are always allowed.
**Automated reflexion loop.** After the parallel research map and
`vet_sources`, the `critique` node reviews the merged findings
against the research plan and the source credibility assessment, and
emits `VERDICT: PASS` or `VERDICT: REVISE` with specific feedback.
`reflexion_gate.py` then:
- `PASS` -> continue to `synthesize`.
- `REVISE`, budget remaining -> loop back to `research_each_question`,
with the critique injected as `research_feedback` so every parallel
branch sees it on the retry.
- `REVISE`, budget spent -> continue to `synthesize` anyway (the human
approval step is the final backstop).
The budget is `MAX_REFLEXION_REVISIONS` in `reflexion_gate.py`
(default 2, so the research map runs at most 3 times per pass).
**Human-feedback loop.** At `approve` the user answers `accept`,
`reject`, or types their own feedback. A free-form answer routes via
the approval node's `on_other` to `incorporate_feedback.py`, which
folds that text into `research_feedback` and loops back to
`research_each_question` for another parallel pass.
`settings.max_loop_iterations` (40) is the engine's infinite-loop
backstop: it caps the total visits to any single node.
## Fault paths
Every stage that can die outright has a `fallback:` edge to a small
fault-marker script — the dashed edges in the diagram. A fallback fires
after a node's retries are exhausted (`research_one_question` and
`synthesize` each get two attempts via `max_attempts: 2`); the engine
writes its failure text into the node's state key before routing, and
the marker normalizes that text into a `PIPELINE-FAULT: ...` entry in
`pipeline_faults`. The five routes:
- `plan -> plan_fault -> end_fault` — fail-closed: no plan means
nothing to research.
- `research_one_question -> question_fault` — branch-local, inside the
map branch: one dead research lane becomes a fault finding instead of
sinking the whole map.
- `vet_sources -> vet_fault -> critique` — degrade-visibly: vetting is
a quality lane, not the product; `source_assessment` becomes a
neutral "unvetted" note.
- `critique -> critique_fault -> reflexion_gate` — degrade-visibly: the
rewritten critique deliberately has no `VERDICT:` line, so the gate's
malformed-critique PASS default proceeds to synthesis.
- `synthesize -> synth_fault -> end_fault` — fail-closed: no report
means no product.
On the fail-closed routes, `end_fault` renders the
`DEEP_RESEARCH FAILED — PIPELINE-FAULT: ...` sentinel. On the
degrade-visibly routes, `verify_sources` folds the accumulated faults
into a `## Pipeline notes` section shown at the approval step and
appended to the accepted report (`{{report}}{{pipeline_notes}}`;
empty when there were no faults). Degraded output is never a silent
pass — every fault is visible to the reviewer or the caller — and some
end node always renders its sentinel, so the graph degrades instead of
dying.
All 11 scripts also carry a top-level crash guard: a script that
throws records a fault and fails forward where the flow allows it (a
crashed `reflexion_gate` proceeds to `synthesize`; a crashed
`verify_sources` reports that sources were NOT checked) instead of
killing the run.
## Running
```sh
coyote agents install # ships deep-research
coyote -a deep-research "How does HTTP/3 differ from HTTP/2?"
coyote -a deep-research "Recent advances in solid-state batteries"
coyote -a deep-research # no prompt -> triggers ask_topic
```
## Anti-hallucination
- `research_one_question` (each map branch) is instructed to back
every claim with a real retrieved source and never to fabricate
URLs, titles, or DOIs.
- `vet_sources` classifies every cited source so weak sources are
visible to the critique step.
- `critique` independently reviews the merged findings and sends weak
or uncited work back for another parallel research pass.
- `synthesize` (the `report-writer` sub-agent) is grounded: it may use
only the gathered findings and must keep each claim's inline source.
It has no tools and cannot browse the web.
- `verify_sources` probes every cited URL / DOI with an HTTP HEAD
request and reports which are unreachable, so the human reviewer
sees broken citations before approving.
- Fault paths degrade visibly, never silently: a `PIPELINE-FAULT`
can only ever surface as a failure sentinel or a `## Pipeline
notes` section, never as a clean report.
## Customizing
- **Loop budget.** `MAX_REFLEXION_REVISIONS` in `reflexion_gate.py`.
- **Map concurrency.** The `research_each_question` node's
`max_concurrency: 3` caps simultaneous web-research branches.
Raise to investigate more questions in parallel; lower to be gentle
on rate-limited providers.
- **Per-node model.** Add `model: anthropic:...` to any `llm` node.
Cheap models work well for `plan` / `critique` / `vet_sources`; the
heavy intelligence is needed in `research_one_question` and the
`report-writer` sub-agent.
- **Tool scope.** Narrow the `research_one_question` node's `tools:`
list to constrain where each branch looks (for example, drop
`web_search_coyote` and `mcp:ddg-search` to force arXiv-only
research).
- **Local knowledge.** Drop files into `knowledge/` to bias every
research branch toward your local context (see the *Local
knowledge corpus* section above).
- **Different writer.** Replace `agent: report-writer` on the
`synthesize` node with the name of any other agent. The
orchestrator does not care what kind of agent the writer is.
- **Skip approval.** Point both `approve` routes at `end_accepted`,
or wire `verify_sources` straight to an `end` node.
## Files
```
assets/agents/deep-research/
graph.yaml - agent config + 23-node workflow
tools.sh - classify_source custom tool
README.md - this file
knowledge/
README.md - corpus-format notes
research-style-notes.md - starter knowledge file (replace with your notes)
scripts/
parse_request.py - _next: bootstrap_research, or ask_topic if no topic
bootstrap_research.py - fan-out source: next [plan, knowledge_lookup]
plan_fault.py - fail-closed fault marker: dead plan -> end_fault
question_fault.py - branch-local fault marker for a dead research lane
combine_findings.py - joins map output (question_findings) into findings
vet_fault.py - degrade-visibly fault marker: unvetted sources
critique_fault.py - degrade-visibly fault marker: skipped critique
reflexion_gate.py - _next: research_each_question (revise) or synthesize
synth_fault.py - fail-closed fault marker: no report -> end_fault
verify_sources.py - HTTP HEAD on cited URLs / DOIs; folds pipeline_faults
into the report's "## Pipeline notes" section
incorporate_feedback.py - _next: research_each_question, with user feedback
```
See also `assets/agents/report-writer/` — the sub-agent the
`synthesize` node spawns.
-373
View File
@@ -1,373 +0,0 @@
name: deep-research
description: |
Deep web research workflow. Plans an investigation, decomposes it
into sub-questions researched in parallel, grounds the work in a
local knowledge corpus, vets the credibility of cited sources, runs
a reflexion self-critique loop to revise weak or incomplete findings,
delegates the final write-up to a focused sub-agent, checks that the
cited sources are reachable, and gates the result behind human
approval. A reviewer's free-form feedback at the approval step feeds
back into another research pass. Each sub-question is researched by a
spawned librarian running at deep budgets. Tunable via --variables:
reflexion_budget (default 2) caps the automated critique->re-research
passes - raise it to let the run churn until the critique passes;
autonomy=autonomous skips the human approval gate (REQUIRED when
another agent spawns this one).
This is the canonical Coyote graph-agent reference: it exercises every
node type (script, llm, rag, map, agent, input, approval, end) and
both static fan-out and dynamic map fan-out.
version: '1.0'
global_tools:
- web_search_coyote.sh
- fetch_url_via_curl.sh
- search_arxiv.sh
mcp_servers:
- ddg-search
conversation_starters:
- 'How does HTTP/3 differ from HTTP/2?'
- 'Summarize recent advances in solid-state battery chemistry'
variables:
- name: reflexion_budget
description: "Max automated critique->re-research passes before synthesis proceeds anyway. Default 2; raise it (4-8) for exhaustive runs that should churn until the critique passes."
default: '2'
- name: autonomy
description: "supervised (default): the finished report stops at the human approval gate. autonomous: skip the gate and return the report directly - REQUIRED when another agent spawns this one (a human gate would block the run)."
default: 'supervised'
settings:
max_loop_iterations: 120
log_state_snapshots: false
validate_before_run: true
max_concurrency: 4
initial_state:
research_feedback: ''
research_attempts: 0
local_context: ''
local_sources: ''
# Fault-path defaults. `questions: []` also keeps the
# knowledge_lookup lane alive for one super-step when `plan` dies inside the
# static fan-out: research_each_question maps over zero items while the
# plan_fault → end_fault route renders the failure sentinel.
pipeline_faults: []
pipeline_notes: ''
fault_text: ''
questions: []
start: parse_request
nodes:
parse_request:
id: parse_request
type: script
script: scripts/parse_request.py
next: bootstrap_research
ask_topic:
id: ask_topic
type: input
question: 'What would you like me to research?'
validation: 'len(input) > 0'
state_updates:
topic: '{{input}}'
next: bootstrap_research
bootstrap_research:
id: bootstrap_research
type: script
script: scripts/bootstrap_research.py
next: [plan, knowledge_lookup]
plan:
id: plan
type: llm
instructions: |
You are a research planner. Given a topic, produce a focused
research plan and decompose it into 3-5 specific sub-questions
that can each be researched independently in parallel.
The plan is a short narrative naming the key questions and the
kinds of sources that would be authoritative. The sub-questions
are precise, self-contained queries (each one is sent on its own
to a separate research worker, so they must be answerable
without each other's context).
prompt: 'Research topic: {{topic}}'
tools: []
output_schema:
type: object
properties:
research_plan:
type: string
description: A short plan narrative.
questions:
type: array
items: { type: string }
minItems: 1
maxItems: 6
description: 3-5 specific, self-contained sub-questions.
required: [research_plan, questions]
# On success plan_failure holds the node's structured JSON output; on
# failure the engine writes "LLM node ...failed: <chain>" into it BEFORE
# routing to the fallback (schema properties are only merged on success).
state_updates:
plan_failure: '{{output}}'
fallback: plan_fault
next: research_each_question
plan_fault:
id: plan_fault
type: script
description: Fail-closed marker for a dead planning stage (reached only via plan's fallback) — normalizes the failure text captured in plan_failure into a PIPELINE-FAULT and continues to end_fault; no plan means nothing to research, so the DEEP_RESEARCH FAILED sentinel always renders. The sibling knowledge_lookup lane survives one more super-step by mapping over the empty initial_state `questions` list.
script: scripts/plan_fault.py
fallback: end_fault
next: end_fault
knowledge_lookup:
id: knowledge_lookup
type: rag
documents:
- ./knowledge/
query: '{{topic}}'
top_k: 6
chunk_size: 1000
chunk_overlap: 100
state_updates:
local_context: '{{output.context}}'
local_sources: '{{output.sources}}'
next: research_each_question
research_each_question:
id: research_each_question
type: map
over: '{{questions}}'
as: question
branch: research_one_question
collect_into: question_findings
output_key: finding
max_concurrency: 3
next: combine_findings
research_one_question:
id: research_one_question
type: agent
agent: librarian
# The leaf research primitive: triage -> doc+OSS search -> cited
# synthesis. `inputs:` overlays the librarian graph's initial state,
# pinning research_depth to deep for every sub-question lane.
prompt: |
Research question: {{question}}
Local context that may help:
{{local_context}}
{{research_feedback}}
inputs:
research_depth: deep
timeout: 1900
max_attempts: 2
state_updates:
finding: '{{output}}'
# On failure the engine writes "Agent node failed: <chain>" into `finding`
# via the state_updates above BEFORE routing to the fallback. The marker
# is branch-local: a map branch's fallback must stay inside the branch.
fallback: question_fault
question_fault:
id: question_fault
type: script
description: Fail-closed marker for a dead research lane (reached only via research_one_question's fallback route inside the map branch). Normalizes the engine's failure text in `finding` into a PIPELINE-FAULT finding the map collects — one dead lane never sinks the whole map. combine_findings later lifts PIPELINE-FAULT findings into pipeline_faults for the report's Pipeline notes.
script: scripts/question_fault.py
combine_findings:
id: combine_findings
type: script
script: scripts/combine_findings.py
next: vet_sources
vet_sources:
id: vet_sources
type: llm
instructions: |
You assess the credibility of the sources cited in a set of
research findings. For every distinct source URL in the findings,
call the `classify_source` tool to get its credibility tier. Then
summarize: which claims rest on HIGH-credibility sources, and
which rest on PREPRINT or UNVERIFIED sources and so need
corroboration. Do NOT do any new research -- assess only what is
already cited.
prompt: |
Findings to assess:
{{findings}}
tools:
- classify_source
max_iterations: 15
state_updates:
source_assessment: '{{output}}'
fallback: vet_fault
next: critique
vet_fault:
id: vet_fault
type: script
description: Degrade-visibly marker for a dead vetting stage (reached only via vet_sources' fallback) — vetting is a quality lane, not the product. Records a PIPELINE-FAULT, rewrites source_assessment to a neutral "unvetted" note, and continues to critique, the stage vet_sources' own next pointed to.
script: scripts/vet_fault.py
fallback: critique
next: critique
critique:
id: critique
type: llm
instructions: |
You are a meticulous research reviewer. Judge whether the
findings below are good enough to synthesize a complete,
well-supported report that answers the research plan.
Mark the findings REVISE if ANY of these hold:
- A research-plan question is unanswered or only weakly
addressed.
- A factual claim has no source, or cites a source that looks
fabricated.
- The findings lean on a single source where corroboration is
needed.
- A key claim rests only on a PREPRINT or UNVERIFIED source,
per the source credibility assessment below.
- An obvious counter-perspective or recent development is
missing.
Otherwise mark them PASS.
Respond in EXACTLY this format, nothing else:
VERDICT: <PASS or REVISE>
FEEDBACK: <if REVISE, be specific and actionable -- name the gaps
and what kind of source would close them; if PASS, write "none">
prompt: |
Research plan:
{{research_plan}}
Findings under review:
{{findings}}
Source credibility assessment:
{{source_assessment}}
tools: []
state_updates:
critique: '{{output}}'
fallback: critique_fault
next: reflexion_gate
critique_fault:
id: critique_fault
type: script
description: Degrade-visibly marker for a dead critique stage (reached only via critique's fallback) — critique is a quality lane, not the product. Records a PIPELINE-FAULT and rewrites `critique` to a neutral note with deliberately NO "VERDICT:" line, so reflexion_gate's malformed-critique PASS default proceeds to synthesis; then continues to reflexion_gate, the stage critique's own next pointed to.
script: scripts/critique_fault.py
fallback: reflexion_gate
next: reflexion_gate
reflexion_gate:
id: reflexion_gate
type: script
script: scripts/reflexion_gate.py
next: synthesize
synthesize:
id: synthesize
type: agent
agent: report-writer
prompt: |
Research topic: {{topic}}
Findings (organized by sub-question, with inline citations):
{{findings}}
Source credibility assessment:
{{source_assessment}}
Produce the final report following your instructions.
timeout: 300
max_attempts: 2
state_updates:
report: '{{output}}'
# On failure the engine writes "Agent node failed: <chain>" into `report`
# via the state_updates above BEFORE routing to the fallback.
fallback: synth_fault
next: verify_sources
synth_fault:
id: synth_fault
type: script
description: Fail-closed marker for a dead synthesis stage (reached only via synthesize's fallback) — no report means no product. Normalizes the "Agent node failed:" text captured in `report` into a PIPELINE-FAULT and continues to end_fault, which renders the DEEP_RESEARCH FAILED sentinel.
script: scripts/synth_fault.py
fallback: end_fault
next: end_fault
verify_sources:
id: verify_sources
type: script
script: scripts/verify_sources.py
next: approval_gate
approval_gate:
id: approval_gate
type: script
description: Autonomy router - autonomous runs (spawned by another agent) skip the human approval gate and end accepted with the source check + pipeline notes attached; supervised (default) proceeds to the approval node. A crash fails forward via `next` to approve, the safe human-gated direction.
script: scripts/approval_gate.py
next: approve
approve:
id: approve
type: approval
question: |
Research report on: {{topic}}
{{report}}
----
{{source_check}}{{pipeline_notes}}
----
Accept this report? Pick "accept" or "reject", or type specific
feedback to send the research back for another pass.
options:
- 'accept'
- 'reject'
routes:
'accept': end_accepted
'reject': end_rejected
on_other: incorporate_feedback
state_updates:
decision: '{{choice}}'
incorporate_feedback:
id: incorporate_feedback
type: script
script: scripts/incorporate_feedback.py
end_accepted:
id: end_accepted
type: end
output: '{{report}}{{pipeline_notes}}'
end_autonomous:
id: end_autonomous
type: end
output: |
{{report}}
----
{{source_check}}{{pipeline_notes}}
end_rejected:
id: end_rejected
type: end
output: "Research on '{{topic}}' was rejected and discarded."
end_fault:
id: end_fault
type: end
output: 'DEEP_RESEARCH FAILED — PIPELINE-FAULT: {{fault_text}}'
@@ -1,23 +0,0 @@
# Local knowledge corpus for deep-research
The `knowledge_lookup` node in `graph.yaml` is a `rag` node that runs
hybrid (vector + keyword) retrieval over every file in this directory.
Drop your own notes, papers (PDFs), Markdown docs, or text files here
and they will be indexed into a per-agent knowledge base on first run.
Coyote supports common file types out of the box: `.md`, `.txt`, `.pdf`,
`.html`, and others. Subdirectories are walked recursively.
A small starter file (`research-style-notes.md`) ships so the RAG
node has something non-empty to retrieve against on a clean install.
Replace or extend it with your own materials to bias the research
phase toward your local context.
To force the knowledge base to rebuild after you add or change files,
delete the cached index:
```sh
rm ~/.config/coyote/agents/deep-research/knowledge_lookup.yaml
```
The next run will rebuild from the current contents of this directory.
@@ -1,49 +0,0 @@
# Research style notes
These are general principles the `deep-research` agent should keep in
mind regardless of topic. Replace this file with your own notes if you
want to bias retrieval toward your local context.
## What "good research" means here
- **Every factual claim cites a source you actually retrieved.** Never
fabricate URLs, page titles, authors, or DOIs.
- **Primary sources beat aggregators.** Prefer the original paper, the
RFC, the standards body, or the manufacturer over a blog summarizing
them.
- **Corroboration matters where stakes are high.** If a single source
makes a strong claim, look for a second independent source before
taking it as established.
- **Disagreement is information, not noise.** If two credible sources
disagree, report the disagreement and the reasoning on each side.
- **Old does not mean wrong.** A 2014 RFC is still authoritative if no
newer one has obsoleted it; check before assuming a source is stale.
## Source-tier heuristics
The `vet_sources` node uses these rough tiers to weigh credibility.
The custom tool `classify_source` (see `tools.sh`) implements this
deterministically by hostname / TLD.
- **HIGH:** government domains (`.gov`, `.mil`), academic institutions
(`.edu`, university subdomains), peer-reviewed journals, standards
bodies (IETF/RFCs, W3C, ISO, IEEE, NIST), and primary documents from
the entities being researched (e.g. a vendor's official spec page).
- **PREPRINT:** arXiv, bioRxiv, medRxiv, SSRN. Useful but not yet
peer-reviewed; treat numeric claims with extra caution.
- **ORGANIZATION:** established nonprofits, standards-adjacent groups,
industry consortia. Reliable for their stated mission but may have a
perspective.
- **UNVERIFIED:** general web pages, blogs, news aggregators, social
media. Useful for leads but should not be the only source for a
factual claim.
## Common pitfalls to flag in critique
- A claim cited only to a PREPRINT or UNVERIFIED source on a numeric
or contested point.
- A research-plan question that the findings address only obliquely.
- "Findings" that paraphrase a single source three times rather than
triangulating.
- Citation collisions where two sources are listed but turn out to
be the same study reported via different aggregators.
@@ -1,38 +0,0 @@
#!/usr/bin/env python3
"""Autonomy router for deep-research's approval gate.
Runs after verify_sources. When the `autonomy` variable is
'autonomous' (the run was spawned by another agent — architect or
sisyphus research tiers — with no human at the terminal), the report
skips the human approval node and ends accepted: the reflexion loop
already served as the quality gate, and end_autonomous carries the
source check + pipeline notes with the report so the caller sees them.
Any other value (default 'supervised') proceeds to the human approval
gate unchanged.
Routing (`_next`):
- autonomy == 'autonomous' -> end_autonomous
- anything else -> approve
Fail-safe: a crash here fails FORWARD via the node's `next` to
`approve` — the safe, human-gated direction.
"""
import json
import os
def load_state():
if path := os.environ.get("GRAPH_STATE_FILE"):
with open(path) as f:
return json.load(f)
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
def main():
state = load_state()
autonomy = str(state.get("autonomy") or "supervised").strip().lower()
target = "end_autonomous" if autonomy == "autonomous" else "approve"
print(json.dumps({"_next": target}))
main()
@@ -1,26 +0,0 @@
#!/usr/bin/env python3
"""Fan-out source for context loading.
Has no logic of its own. Exists so the static `next: [plan, knowledge_lookup]`
list on this node fans out into two parallel branches (the LLM planner and
the RAG knowledge lookup) as a single super-step. The validator requires
declared parallel-branch script outputs, so we emit an empty JSON object
explicitly here.
Fail-safe: the node reads no state and computes nothing, so its
happy-path output IS the sane degraded output — the guard emits the same
empty object on any crash. No PIPELINE-FAULT is recorded: appending one
would require reading state (the very thing that crashed) or clobbering
faults recorded upstream by parse_request, and a crash here loses nothing.
"""
import json
def main():
print(json.dumps({}))
try:
main()
except Exception: # noqa: BLE001 — the fan-out source must never crash the graph
print(json.dumps({}))
@@ -1,70 +0,0 @@
#!/usr/bin/env python3
"""Join the per-question map outputs into a single `findings` string.
The `research_each_question` map writes `question_findings` (an array,
one entry per sub-question, in input order). Downstream nodes
(`vet_sources`, `critique`, `synthesize`) read `{{findings}}` as a
single block, so this script renders the array as a Markdown document
with one section per question.
Fault visibility: a research lane that died lands here as a finding
starting with "PIPELINE-FAULT:" (written by question_fault inside the map
branch, where `pipeline_faults` itself is out of reach — map branches only
surface their collected output_key). This script lifts such findings into
`pipeline_faults` (deduplicated, since a reflexion or feedback loop can
re-run the map) so dead lanes surface in the report's Pipeline notes.
Fail-safe: on a crash (e.g. malformed GRAPH_STATE), `findings`
becomes the fault text itself and a PIPELINE-FAULT entry is recorded —
the pipeline proceeds visibly degraded, and the human approval gate is
the backstop. Existing faults are preserved best-effort.
"""
import json
import os
def load_state():
path = os.environ.get("GRAPH_STATE_FILE")
if path:
with open(path) as f:
return json.load(f)
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
def safe_faults():
try:
state = load_state()
return [f for f in (state.get("pipeline_faults") or []) if isinstance(f, str)]
except Exception: # noqa: BLE001 — best-effort preservation only
return []
def main():
state = load_state()
questions = state.get("questions") or []
per_question = state.get("question_findings") or []
faults = [f for f in (state.get("pipeline_faults") or []) if isinstance(f, str)]
sections = []
for idx, q in enumerate(questions):
body = per_question[idx] if idx < len(per_question) else ""
if isinstance(body, dict) or isinstance(body, list):
body = json.dumps(body, indent=2)
if isinstance(body, str) and body.startswith("PIPELINE-FAULT:") and body not in faults:
faults.append(body)
sections.append(f"## {q}\n\n{body}")
findings = "\n\n".join(sections) if sections else "No findings gathered."
print(json.dumps({"findings": findings, "pipeline_faults": faults}))
try:
main()
except Exception as e: # noqa: BLE001 — a crashed join must degrade, not die
faults = safe_faults()
fault = (
f"PIPELINE-FAULT: combining findings crashed — {e}; per-question "
"findings may exist in state but could not be joined"
)
faults.append(fault)
print(json.dumps({"findings": fault, "pipeline_faults": faults}))
@@ -1,71 +0,0 @@
#!/usr/bin/env python3
"""Degrade-visibly marker for a dead critique stage of deep-research.
Reached ONLY via critique's `fallback:` route. Grounded in engine behavior:
on llm-node failure the engine writes "LLM node …failed: <chain>" into
`critique` via the node's state_updates BEFORE routing to the fallback.
Critique is a quality lane, not the product, so this path degrades instead
of failing closed: it records a PIPELINE-FAULT (surfaced later in the
report's Pipeline notes) and rewrites `critique` to a neutral note, then
continues to `reflexion_gate` — the stage critique's own `next` pointed to.
The rewritten note deliberately contains NO "VERDICT:" line:
reflexion_gate's parse_verdict defaults to PASS when no verdict line is
found (its documented malformed-critique behavior), so a dead critique
proceeds to synthesis instead of burning the revision budget on a fault.
"""
import json
import os
MAX_DETAIL_CHARS = 500
CRITIQUE_NOTE = (
"Critique unavailable — the critique stage failed (pipeline fault); "
"proceeding to synthesis without an automated review pass."
)
def load_state():
if path := os.environ.get("GRAPH_STATE_FILE"):
with open(path) as f:
return json.load(f)
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
def safe_faults():
try:
state = load_state()
return [f for f in (state.get("pipeline_faults") or []) if isinstance(f, str)]
except Exception: # noqa: BLE001 — best-effort preservation only
return []
def main():
state = load_state()
faults = [f for f in (state.get("pipeline_faults") or []) if isinstance(f, str)]
raw = state.get("critique")
detail = raw.strip().replace("\n", " ") if isinstance(raw, str) else ""
if not detail.startswith("LLM node"):
detail = "the critique stage died without recording a failure detail"
if len(detail) > MAX_DETAIL_CHARS:
detail = detail[: MAX_DETAIL_CHARS - 1] + "…"
fault = f"PIPELINE-FAULT: critique failed — {detail}"
if fault not in faults:
faults.append(fault)
print(json.dumps({"pipeline_faults": faults, "critique": CRITIQUE_NOTE}))
try:
main()
except Exception as e: # noqa: BLE001 — the fault marker itself must never crash
faults = safe_faults()
faults.append(f"PIPELINE-FAULT: critique failed — fault-marker script error: {e}")
print(
json.dumps(
{
"pipeline_faults": faults,
"critique": CRITIQUE_NOTE,
}
)
)
@@ -1,77 +0,0 @@
#!/usr/bin/env python3
"""Fold a reviewer's free-form feedback back into the research loop.
Runs when the user answers the approval step with their own text
instead of "accept" or "reject". That text (saved by the approval node
as `decision`) becomes `research_feedback`, and the graph loops back to
`research_each_question` for another informed pass (each sub-question is
re-researched in parallel with the new feedback in context). The
reflexion counter is reset so the user-driven pass gets a fresh revision
budget.
Routing (`_next`): always research_each_question.
Fail-safe: a crash here means the user's feedback text cannot be
recovered, but their INTENT — another research pass — is unambiguous
(they chose neither accept nor reject). The degraded route therefore
still loops to `research_each_question`, with a generic feedback note
naming the loss and a PIPELINE-FAULT entry (best-effort preserving
existing faults) that surfaces in the next approval's Pipeline notes.
"""
import json
import os
def load_state():
path = os.environ.get("GRAPH_STATE_FILE")
if path:
with open(path) as f:
return json.load(f)
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
def safe_faults():
try:
state = load_state()
return [f for f in (state.get("pipeline_faults") or []) if isinstance(f, str)]
except Exception: # noqa: BLE001 — best-effort preservation only
return []
def main():
state = load_state()
feedback = (state.get("decision") or "").strip()
output = {
"_next": "research_each_question",
"research_attempts": 0,
"research_feedback": (
"The user reviewed the report and asked for changes. Treat "
"this as the top priority for the next pass:\n\n" + feedback
),
}
print(json.dumps(output))
try:
main()
except Exception as e: # noqa: BLE001 — a crashed feedback fold must degrade, not die
faults = safe_faults()
faults.append(
f"PIPELINE-FAULT: feedback incorporation crashed — {e}; the reviewer's "
"feedback text could not be recovered"
)
print(
json.dumps(
{
"_next": "research_each_question",
"research_attempts": 0,
"research_feedback": (
"The user reviewed the report and asked for changes, but "
"the feedback text could not be recovered (pipeline "
"fault). Re-run the research pass with extra care and "
"note this loss."
),
"pipeline_faults": faults,
}
)
)
@@ -1,54 +0,0 @@
#!/usr/bin/env python3
"""Entry router for deep-research.
Reads the caller's prompt from state. If it contains a usable research
topic, stores it as `topic` and falls through to the static `next`
(plan). If the prompt is empty, routes to `ask_topic` so the user can
supply one interactively.
Routing (`_next`):
- prompt present -> (no _next; static next: plan)
- prompt empty -> ask_topic
Fail-safe: a crash here (e.g. malformed GRAPH_STATE) means the
caller's prompt cannot be read at all, so proceeding silently is
meaningless — the degraded route is `ask_topic`, which asks the user for
the topic directly, plus a PIPELINE-FAULT entry so the loss is visible in
the final report's Pipeline notes. Clobbering `pipeline_faults` is safe
here: this is the start node, so the list can only hold its initial [].
"""
import json
import os
def load_state():
path = os.environ.get("GRAPH_STATE_FILE")
if path:
with open(path) as f:
return json.load(f)
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
def main():
state = load_state()
prompt = (state.get("initial_prompt") or "").strip()
if prompt:
print(json.dumps({"topic": prompt}))
else:
print(json.dumps({"_next": "ask_topic"}))
try:
main()
except Exception as e: # noqa: BLE001 — a crashed entry router must degrade, not die
print(
json.dumps(
{
"_next": "ask_topic",
"pipeline_faults": [
f"PIPELINE-FAULT: request parsing crashed — {e}; asked the "
"user for the research topic instead"
],
}
)
)
@@ -1,58 +0,0 @@
#!/usr/bin/env python3
"""Fail-closed marker for a dead planning stage of the deep-research graph.
Reached ONLY via plan's `fallback:` route. Grounded in engine behavior: on
llm-node failure the engine writes "LLM node …failed: <chain>" into
`plan_failure` via the node's state_updates BEFORE routing to the fallback
(on success the same key holds the node's structured JSON output, which
never starts with the "LLM node" prefix). No plan means no sub-questions —
there is nothing to research — so this path is fail-closed: record the
PIPELINE-FAULT and continue to `end_fault`, which renders the
DEEP_RESEARCH FAILED sentinel.
The planner runs inside the static fan-out [plan, knowledge_lookup]. While
this marker routes to end_fault, the sibling branch proceeds one more
super-step into `research_each_question`, whose `{{questions}}` resolves to
the initial_state [] — the doomed lane maps over zero items and the graph
returns end_fault's output at the next join.
"""
import json
import os
MAX_DETAIL_CHARS = 500
def load_state():
if path := os.environ.get("GRAPH_STATE_FILE"):
with open(path) as f:
return json.load(f)
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
def main():
state = load_state()
faults = [f for f in (state.get("pipeline_faults") or []) if isinstance(f, str)]
raw = state.get("plan_failure")
detail = raw.strip().replace("\n", " ") if isinstance(raw, str) else ""
if not detail.startswith("LLM node"):
detail = "the planning stage died without recording a failure detail"
if len(detail) > MAX_DETAIL_CHARS:
detail = detail[: MAX_DETAIL_CHARS - 1] + "…"
fault_text = f"planning failed — nothing to research: {detail}"
faults.append(f"PIPELINE-FAULT: {fault_text}")
print(json.dumps({"fault_text": fault_text, "pipeline_faults": faults}))
try:
main()
except Exception as e: # noqa: BLE001 — the fault marker itself must never crash
fault_text = f"planning failed — fault-marker script error: {e}"
print(
json.dumps(
{
"fault_text": fault_text,
"pipeline_faults": [f"PIPELINE-FAULT: {fault_text}"],
}
)
)
@@ -1,71 +0,0 @@
#!/usr/bin/env python3
"""Fail-closed marker for a dead research lane of the deep-research graph.
Reached ONLY via research_one_question's `fallback:` route inside the
`research_each_question` map branch (branch-local — a map branch's fallback
must stay inside the branch subgraph). Grounded in engine behavior: on
branch failure the engine writes its failure text into `finding` via the
node's state_updates BEFORE routing to the fallback — "Agent node failed:
<chain>" now that the branch spawns a librarian agent, "LLM node …failed:
<chain>" for an llm branch (on success the same key holds the researched
findings, which never start with either prefix). This node normalizes the failure text into a
PIPELINE-FAULT finding and the chain ends here — the map collects the fault
as the lane's `finding`, so one dead research lane never sinks the whole
map. combine_findings later lifts PIPELINE-FAULT findings into
`pipeline_faults` so the fault also surfaces in the report's Pipeline notes.
The question text is part of the fault so distinct dead lanes with identical
engine failure text do not collapse into one entry there.
"""
import json
import os
MAX_DETAIL_CHARS = 500
MAX_QUESTION_CHARS = 200
def load_state():
if path := os.environ.get("GRAPH_STATE_FILE"):
with open(path) as f:
return json.load(f)
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
def fault_finding(question, detail):
return {
"finding": (
"PIPELINE-FAULT: question research failed — finding unavailable — "
f'{detail} (question: "{question}")'
)
}
def question_label(state):
raw = state.get("question")
question = raw.strip().replace("\n", " ") if isinstance(raw, str) else ""
if not question:
return "(unknown question)"
if len(question) > MAX_QUESTION_CHARS:
question = question[: MAX_QUESTION_CHARS - 1] + "…"
return question
def main():
state = load_state()
raw = state.get("finding")
detail = raw.strip().replace("\n", " ") if isinstance(raw, str) else ""
if not detail.startswith(("LLM node", "Agent node")):
detail = "the research lane died without recording a failure detail"
if len(detail) > MAX_DETAIL_CHARS:
detail = detail[: MAX_DETAIL_CHARS - 1] + "…"
print(json.dumps(fault_finding(question_label(state), detail)))
try:
main()
except Exception as e: # noqa: BLE001 — the fault marker itself must never crash
print(
json.dumps(
fault_finding("(unknown question)", f"fault-marker script error: {e}")
)
)
@@ -1,101 +0,0 @@
#!/usr/bin/env python3
"""Reflexion gate for deep-research.
Runs after `critique` has reviewed the current research findings. If the
critique's verdict is REVISE and the reflexion budget (the graph's
`reflexion_budget` variable, default 2) is not spent,
loops back to `research` with the critique attached as
`research_feedback`, so the retry is informed rather than a blind
re-run. Otherwise it proceeds to `synthesize`.
Routing (`_next`):
- verdict PASS -> synthesize
- verdict REVISE, budget remaining -> research_each_question (+ research_feedback)
- verdict REVISE, budget spent -> synthesize
Reflexion is a best-effort quality booster, not a hard gate: once the
budget is spent the workflow proceeds anyway, and the human approval
step is the final backstop.
Fail-safe: a crash here fails FORWARD to `synthesize` — the same
direction as the existing malformed-critique PASS default — with a
PIPELINE-FAULT entry recorded (best-effort preserving existing faults),
so a broken gate costs the run its automated critique routing, never the
run itself.
"""
import json
import os
import re
# Default automated revision passes when the `reflexion_budget` variable
# is absent or malformed. `research` runs at most budget + 1 times per
# user pass. Callers raise it via --variables {"reflexion_budget": "6"}
# to let the run churn until the critique passes.
DEFAULT_REFLEXION_REVISIONS = 2
def load_state():
path = os.environ.get("GRAPH_STATE_FILE")
if path:
with open(path) as f:
return json.load(f)
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
def safe_faults():
try:
state = load_state()
return [f for f in (state.get("pipeline_faults") or []) if isinstance(f, str)]
except Exception: # noqa: BLE001 — best-effort preservation only
return []
def as_int(value, default=0):
try:
return int(value)
except (TypeError, ValueError):
return default
def parse_verdict(critique):
"""Pull PASS/REVISE from the critique's `VERDICT:` line. Defaults to
PASS when no verdict line is found, so a malformed critique lets the
workflow proceed instead of burning the whole revision budget."""
match = re.search(r"VERDICT:\s*([A-Za-z]+)", critique, re.IGNORECASE)
if not match:
return "PASS"
return match.group(1).upper()
def main():
state = load_state()
critique = state.get("critique") or ""
verdict = parse_verdict(critique)
attempts = as_int(state.get("research_attempts"))
budget = as_int(state.get("reflexion_budget"), DEFAULT_REFLEXION_REVISIONS)
if verdict == "REVISE" and attempts < budget:
feedback = (
"A reviewer judged the previous research pass incomplete. "
"Address every point in the critique below:\n\n" + critique
)
output = {
"_next": "research_each_question",
"research_attempts": attempts + 1,
"research_feedback": feedback,
}
else:
output = {"_next": "synthesize"}
print(json.dumps(output))
try:
main()
except Exception as e: # noqa: BLE001 — a crashed gate fails forward, never dies
faults = safe_faults()
faults.append(
f"PIPELINE-FAULT: reflexion gate crashed — {e}; proceeded to synthesis "
"without automated critique routing"
)
print(json.dumps({"_next": "synthesize", "pipeline_faults": faults}))
@@ -1,51 +0,0 @@
#!/usr/bin/env python3
"""Fail-closed marker for a dead synthesis stage of the deep-research graph.
Reached ONLY via synthesize's `fallback:` route. Grounded in engine
behavior: on agent-node failure the engine writes "Agent node failed:
{e:#}" into `report` via the node's state_updates BEFORE routing to the
fallback (on success the same key holds the report-writer's report, which
never starts with that prefix). No report means no product, so this path is
fail-closed: normalize the failure text, record the PIPELINE-FAULT, and
continue to `end_fault`, which renders the DEEP_RESEARCH FAILED sentinel.
"""
import json
import os
MAX_DETAIL_CHARS = 500
def load_state():
if path := os.environ.get("GRAPH_STATE_FILE"):
with open(path) as f:
return json.load(f)
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
def main():
state = load_state()
faults = [f for f in (state.get("pipeline_faults") or []) if isinstance(f, str)]
raw = state.get("report")
detail = raw.strip().replace("\n", " ") if isinstance(raw, str) else ""
if not detail.startswith("Agent node failed:"):
detail = "the synthesis stage died without recording a failure detail"
if len(detail) > MAX_DETAIL_CHARS:
detail = detail[: MAX_DETAIL_CHARS - 1] + "…"
fault_text = f"report synthesis failed — no report was produced: {detail}"
faults.append(f"PIPELINE-FAULT: {fault_text}")
print(json.dumps({"fault_text": fault_text, "pipeline_faults": faults}))
try:
main()
except Exception as e: # noqa: BLE001 — the fault marker itself must never crash
fault_text = f"report synthesis failed — fault-marker script error: {e}"
print(
json.dumps(
{
"fault_text": fault_text,
"pipeline_faults": [f"PIPELINE-FAULT: {fault_text}"],
}
)
)
@@ -1,116 +0,0 @@
#!/usr/bin/env python3
"""Check that the sources cited in the research report are reachable.
Scans the final report for URLs and DOIs, probes each with a HEAD
request, and writes a `source_check` summary into state so the human
reviewer sees broken citations at the approval step.
Times out per request so a slow source cannot stall the graph.
Pipeline-notes fold: end-node templates render unconditionally, so the
conditional "## Pipeline notes" section of the accepted report is built
HERE — the last stop before the approval gate on every accepted path,
including reflexion and feedback re-loops. `pipeline_notes` is the
rendered section when `pipeline_faults` is non-empty and the empty
string otherwise, keeping the happy-path `{{report}}{{pipeline_notes}}`
output byte-identical to the pre-hardening `{{report}}`.
Fail-safe: per-URL probes were already guarded; the top level now
is too. On a crash the summary says sources were NOT checked, a
PIPELINE-FAULT is recorded (best-effort preserving existing faults), and
pipeline_notes still renders — the approval gate remains the backstop.
"""
import json
import os
import re
import urllib.error
import urllib.request
DOI_RE = re.compile(r"\b(10\.\d{4,9}/[-._;()/:A-Z0-9]+)", re.IGNORECASE)
URL_RE = re.compile(r"https?://[^\s)\]\}\"'>]+")
def load_state():
path = os.environ.get("GRAPH_STATE_FILE")
if path:
with open(path) as f:
return json.load(f)
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
def safe_faults():
try:
state = load_state()
return [f for f in (state.get("pipeline_faults") or []) if isinstance(f, str)]
except Exception: # noqa: BLE001 — best-effort preservation only
return []
def render_notes(faults):
entries = [f for f in (faults or []) if isinstance(f, str) and f.strip()]
if not entries:
return ""
return "\n\n## Pipeline notes\n\n" + "\n".join(f"- {e}" for e in entries)
def reachable(url, timeout=5.0):
req = urllib.request.Request(url, method="HEAD")
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
return 200 <= resp.status < 400
except urllib.error.HTTPError as e:
return 200 <= e.code < 400
except Exception:
return False
def main():
state = load_state()
report = state.get("report") or ""
urls = sorted({u.rstrip(".,;)") for u in URL_RE.findall(report)})
dois = sorted(set(DOI_RE.findall(report)))
results = []
for url in urls:
ok = reachable(url)
results.append(f" {'OK' if ok else 'UNREACHABLE'} {url}")
for doi in dois:
url = f"https://doi.org/{doi}"
if url in urls:
continue
ok = reachable(url)
results.append(f" {'OK' if ok else 'UNREACHABLE'} DOI {doi} ({url})")
if not results:
summary = "No web sources were cited in the report."
else:
summary = (
f"Source reachability ({len(results)} checked):\n"
+ "\n".join(results)
)
notes = render_notes(state.get("pipeline_faults"))
print(json.dumps({"source_check": summary, "pipeline_notes": notes}))
try:
main()
except Exception as e: # noqa: BLE001 — a crashed source check must degrade, not die
faults = safe_faults()
faults.append(
f"PIPELINE-FAULT: source verification crashed — {e}; cited sources "
"were not checked for reachability"
)
print(
json.dumps(
{
"source_check": (
"Source verification crashed (pipeline fault) — cited "
"sources were NOT checked for reachability."
),
"pipeline_notes": render_notes(faults),
"pipeline_faults": faults,
}
)
)
@@ -1,71 +0,0 @@
#!/usr/bin/env python3
"""Degrade-visibly marker for a dead source-vetting stage of deep-research.
Reached ONLY via vet_sources' `fallback:` route. Grounded in engine
behavior: on llm-node failure the engine writes "LLM node …failed: <chain>"
into `source_assessment` via the node's state_updates BEFORE routing to the
fallback. Vetting is a quality lane, not the product, so this path degrades
instead of failing closed: it records a PIPELINE-FAULT (surfaced later in
the report's Pipeline notes) and rewrites `source_assessment` to a neutral
"unvetted" note so downstream prompts (critique, synthesize) never carry
raw engine error text, then continues to `critique` — the stage
vet_sources' own `next` pointed to.
"""
import json
import os
MAX_DETAIL_CHARS = 500
UNVETTED_NOTE = (
"Source credibility assessment unavailable — the vetting stage failed "
"(pipeline fault); treat every cited source as unvetted and needing "
"corroboration. Do not request revision solely because this assessment "
"is unavailable — re-research cannot restore it; judge the findings on "
"their own evidence."
)
def load_state():
if path := os.environ.get("GRAPH_STATE_FILE"):
with open(path) as f:
return json.load(f)
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
def safe_faults():
try:
state = load_state()
return [f for f in (state.get("pipeline_faults") or []) if isinstance(f, str)]
except Exception: # noqa: BLE001 — best-effort preservation only
return []
def main():
state = load_state()
faults = [f for f in (state.get("pipeline_faults") or []) if isinstance(f, str)]
raw = state.get("source_assessment")
detail = raw.strip().replace("\n", " ") if isinstance(raw, str) else ""
if not detail.startswith("LLM node"):
detail = "the vetting stage died without recording a failure detail"
if len(detail) > MAX_DETAIL_CHARS:
detail = detail[: MAX_DETAIL_CHARS - 1] + "…"
fault = f"PIPELINE-FAULT: source vetting failed — {detail}"
if fault not in faults:
faults.append(fault)
print(json.dumps({"pipeline_faults": faults, "source_assessment": UNVETTED_NOTE}))
try:
main()
except Exception as e: # noqa: BLE001 — the fault marker itself must never crash
faults = safe_faults()
faults.append(f"PIPELINE-FAULT: source vetting failed — fault-marker script error: {e}")
print(
json.dumps(
{
"pipeline_faults": faults,
"source_assessment": UNVETTED_NOTE,
}
)
)
-39
View File
@@ -1,39 +0,0 @@
#!/usr/bin/env bash
set -e
# @env LLM_OUTPUT=/dev/stdout The output path
# @cmd Classify the credibility tier of a web source from its URL.
# A deterministic check based on the host and top-level domain. Use it
# to weigh how much trust to place in a source before relying on it.
# @option --url! The full source URL to classify
classify_source() {
# shellcheck disable=SC2154
local url="$argc_url"
local host="${url#*://}"
host="${host%%/*}"
host="${host##*@}"
host="${host%%:*}"
host="$(printf '%s' "$host" | tr '[:upper:]' '[:lower:]')"
local tier
case "$host" in
'')
tier="UNKNOWN - no host could be parsed from the URL" ;;
*.gov | *.gov.* | *.mil)
tier="HIGH - government source" ;;
*.edu | *.edu.* | *.ac.*)
tier="HIGH - academic institution" ;;
arxiv.org | *.arxiv.org | biorxiv.org | *.biorxiv.org | medrxiv.org | *.medrxiv.org | ssrn.com | *.ssrn.com)
tier="PREPRINT - not yet peer reviewed, corroborate before citing" ;;
wikipedia.org | *.wikipedia.org)
tier="TERTIARY - encyclopedia, good for orientation not citation" ;;
*.org | *.org.*)
tier="MEDIUM - organization site, check for institutional bias" ;;
*)
tier="UNVERIFIED - general web source, corroborate before citing" ;;
esac
printf '%s: %s\n' "${host:-<none>}" "$tier" >> "$LLM_OUTPUT"
}
+1 -1
View File
@@ -2,6 +2,6 @@
This agent serves as a demo to guide agent development and showcase various agent capabilities. This agent serves as a demo to guide agent development and showcase various agent capabilities.
To enable tools, Coyote will look for the first `tools.py` or `tools.sh` file it finds in this directory. To enable tools, Loki will look for the first `tools.py` or `tools.sh` file it finds in this directory.
The base configuration using `tools.py`. To switch to using `tools.sh`, rename or remove `tools.py`. The base configuration using `tools.py`. To switch to using `tools.sh`, rename or remove `tools.py`.
-43
View File
@@ -1,43 +0,0 @@
# Domain Reviewer
A **domain-slice review leader**. Where `file-reviewer` reviews one file's diff
in isolation, domain-reviewer reviews a group of *related* changed files — the
DB migration plus the queries that use it, the routes plus their handlers — as
ONE coherent unit. It runs as a map branch of the `code-reviewer` graph: one
leader per domain, in parallel, with peer messaging between leaders.
## Why it exists
The strongest recurring review misses are **cross-file-within-domain** defects:
seed data drifting from canonical enums, a guard fixed in one of N twin paths,
a migration renaming a column the query layer still uses. Per-file reviewers
each hold one fragment of those bugs and only connect them if sibling messaging
happens to fire. A domain leader holds the whole slice in one context — the
connection is the default, not a lucky message.
## How it works
1. Fetches its slice's diff itself (`get_diff --files <its files>`).
2. **Sizing**: a small slice (≤3 files, <400 changed lines) is reviewed
directly with the full `code-review` skill stack; a larger one fans out one
`file-reviewer` per file (classic delegation-protocol prompts + sibling
roster) and the leader synthesizes.
3. **The domain pass** (always, its unique value): guard symmetry across the
slice, literal/constant drift between files, schema↔usage mismatch, and the
mandatory blast-radius / test-adequacy / removed-guards checks at slice
level — reported even when clean.
4. **Leader-to-leader messaging**: the code-reviewer graph runs leaders with
`teammates: true`, so the DB leader can tell the API leader "the migration
renames `status` → `state`, check your serializers" mid-review.
5. Emits a machine-checkable report (`## Domain:` / `### Findings` with a
`File` field per finding / `### Mandatory checks` / `### Cross-Domain
Concerns` / `DOMAIN_REVIEW_COMPLETE`) — the graph's completeness gate
rejects a report missing required sections exactly once, then flags it.
## Notes
- Read-only; severities per the standard 🔴/🟡/🟢/💡 mapping; never rewrites a
file-reviewer's severity; dedups identical findings (aspect skill's copy wins).
- Normally spawned by the `code-reviewer` graph, not directly — but a direct
spawn works: give it a domain name, a file list, a diff spec, and the quality
bar in the prompt.
-166
View File
@@ -1,166 +0,0 @@
name: domain-reviewer
description: Domain-slice review leader - reviews a group of related changed files as ONE coherent unit (cross-file-within-domain checks are first-class), delegating per-file depth to file-reviewer when the slice is large. Runs as a map branch of the code-reviewer graph with teammate messaging to the other domain leaders.
version: 1.0.0
auto_continue: true
max_auto_continues: 20
inject_todo_instructions: false
skills_enabled: true
enabled_skills:
- code-review
- ai-slop-remover
- transactional-integrity
- logging-discipline
- rest-api-review
- cli-review
- library-review
- worker-review
- iac-review
- migration-review
- cicd-review
spawnable_agents:
- file-reviewer
max_concurrent_agents: 8
# The limit is consulted by the SPAWNER against the child's absolute depth
# (root = 0). Deepest documented chain: architect(0) → sisyphus(1) →
# review-gauntlet | step-runner(2) → code-reviewer(3) → domain-reviewer(4) →
# file-reviewer(5). This agent spawns depth-5 children; 6 = one level of headroom.
# NOTE: can_spawn_agents is not yet set, so the file-reviewer hop is documented
# but inert; 6 keeps the limit forward-compatible for when it is wired.
max_agent_depth: 6
variables:
- name: project_dir
description: Project directory containing the changes under review
default: '.'
- name: auto_confirm
description: Auto-confirm command execution
default: '1'
global_tools:
- fs_read.sh
- fs_grep.sh
- fs_glob.sh
- fs_cat.sh
- fs_ls.sh
instructions: |
You are a DOMAIN REVIEW LEADER. You review one domain's slice of a code change — a group of
related files (the DB migration plus the queries that use it; the routes plus their handlers) —
as ONE coherent unit. Your unique value over per-file review is the space BETWEEN the files:
cross-file-within-domain defects that no single-file reviewer can see. You either review the
slice directly or lead a team of `file-reviewer` agents through it, and you always add the
domain-level pass on top.
Your spawn prompt IS your entire context: an ASSIGNMENT (JSON with `domain` and `files`), a diff
spec, the quality bar (rigor/surfaces + provenance), and the shared context pack (intent,
conventions, review-miss patterns, repo-history signals, linter output).
## Step 0: Load review skills
Before reading any code, `skill__load` `code-review` and `ai-slop-remover`. Additionally load
`transactional-integrity` when the slice touches state-changing code (DB writes, transactions,
queue/webhook/job handlers, retries, external side effects) and `logging-discipline` when it
touches boundaries, error paths, background jobs, or state transitions. Load the surface skill(s)
for your domain per this table (load a row's skill when your files match that surface's trigger
AND the surface is in the resolved surfaces list; when provenance is `detected-default`, a
trigger match alone suffices):
| declared surface | skill loaded |
|---|---|
| `rest-api` (and `grpc`/`graphql` aliases) | `rest-api-review` |
| `cli` | `cli-review` |
| `library` | `library-review` |
| `worker` | `worker-review` |
| `iac` | `iac-review` |
| `db-migration` | `migration-review` |
| `ci-cd` | `cicd-review` |
## Workflow
1. **Fetch your slice's diff**: `get_diff --files <your files, comma-separated>` (add
`--base <ref-or-range>` when the diff spec names one; omit it for staged/unstaged review).
2. **Choose depth**:
- **Direct** (slice is ≤3 files AND <400 changed lines): review each file's diff yourself,
applying the loaded skill checklists exactly as a file-reviewer would.
- **Delegate** (bigger): spawn one `file-reviewer` per file IN PARALLEL, each with the full
TASK / EXPECTED OUTCOME / MUST DO / MUST NOT DO / CONTEXT structure — pass through the
context pack fields from YOUR prompt verbatim (intent, conventions, review-miss patterns,
rigor/surfaces + provenance, this file's linter output) and paste each file's diff hunks
inline (the reviewer cannot see your context). Group tiny files (<20 changed lines) into a
shared reviewer when the slice exceeds ~8 files. After spawning ALL of them, send each the
sibling roster (`SIBLING_ROSTER:` with ids + file assignments) so they can exchange
cross-cutting alerts. Then collect results — do not poll.
3. **The domain pass** (ALWAYS yours, direct or delegated — this is why you exist). Check the
SLICE AS A WHOLE for:
- **Guard symmetry**: a guard/validation/limit changed in one file — grep the slice (and the
repo) for its siblings (same interface, same job kind, same naming family); a fix applied
to one of N twins is N-1 latent bugs.
- **Literal/constant drift**: values that must agree across the slice's files (enum literals
vs seed data, config keys vs readers, error codes vs handlers) — quote both sides when they
disagree.
- **Schema ↔ usage mismatch**: migrations vs the queries/models that use the schema; proto
definitions vs marshaling code; API shapes vs client calls within the slice.
- **Blast radius, test adequacy, and removed guards at slice level** (the mandatory checks
from `code-review`) — REPORT them under `### Mandatory checks` even when clean; silence
reads as not done.
4. **Talk to the other domain leaders** (you run alongside them with peer messaging armed):
check your inbox before finalizing, and send a leader a 1-2 sentence alert when your slice
changes something their domain consumes (a renamed column, a changed route shape, a moved
constant). Messages move facts sideways; they never replace your report.
5. **Synthesize the domain report** in the exact output format below.
## Output Format
Structure your final response EXACTLY as:
```
## Domain: <domain name>
### Summary
<2-3 sentences: what this slice changes as a unit>
### Findings
#### <finding title (optional [convention]/[correctness] marker per the loaded skills)>
- **Severity**: 🔴 CRITICAL | 🟡 WARNING | 🟢 SUGGESTION | 💡 NITPICK
- **File**: <path>
- **Lines**: <start>-<end>
- **Description**: <clear explanation of the issue>
- **Suggestion**: <how to fix it>
#### <next finding>
...
### Mandatory checks
- Blast radius: <result, or "clean — callers of the changed symbols checked: <list>">
- Test adequacy: <result, or "clean — behavior changes covered by: <tests>">
- Guard symmetry: <result, or "clean — no guard changes in this slice" / "twins checked: <list>">
### Cross-Domain Concerns
<alerts you sent or received and concerns affecting other domains, or "None">
DOMAIN_REVIEW_COMPLETE
```
## Rules
1. **You are read-only.** Never modify files.
2. **Every finding carries a File field** — your report covers multiple files and the synthesis
is machine-assisted; a finding without a file is unroutable.
3. **Severity tags per the standard mapping** (🔴 correctness/security/data-loss; 🟡 logic
errors, races, missing error handling; 🟢 clarity/coupling/missing tests; 💡 style/nitpicks).
Preserve your file-reviewers' severities verbatim; dedup identical findings (keep the aspect
skill's copy).
4. **Include ALL findings** — yours and your file-reviewers' — never summarize any away.
5. **Mandatory checks report even when clean.**
6. **End with DOMAIN_REVIEW_COMPLETE** on its own line.
## Context
- Project: {{project_dir}}
- CWD: {{__cwd__}}
## Available Tools
{{__tools__}}
-63
View File
@@ -1,63 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
# @env LLM_OUTPUT=/dev/stdout
# @env LLM_AGENT_VAR_PROJECT_DIR=.
# @describe Domain-slice reviewer tools
_project_dir() {
local dir="${LLM_AGENT_VAR_PROJECT_DIR:-.}"
(cd "${dir}" 2>/dev/null && pwd) || echo "${dir}"
}
# @cmd Get the git diff for this domain's file slice. Defaults to staged changes, falls back to unstaged, then HEAD~1. Pass --base for a ref or range (e.g. "main...HEAD").
# @option --base Optional base ref or range to diff against (e.g. "main...HEAD", "HEAD~3", a SHA)
# @option --files Comma-separated list of file paths to limit the diff to (your assignment's files)
get_diff() {
local project_dir
project_dir=$(_project_dir)
# shellcheck disable=SC2154
local base="${argc_base:-}"
# shellcheck disable=SC2154
local files_csv="${argc_files:-}"
local -a path_args=()
if [[ -n "${files_csv}" ]]; then
local IFS=','
local f
for f in ${files_csv}; do
f="$(echo "${f}" | sed -e 's/^ *//' -e 's/ *$//')"
[[ -n "${f}" ]] && path_args+=("${f}")
done
fi
_diff() {
if ((${#path_args[@]})); then
(cd "${project_dir}" && git diff "$@" -- "${path_args[@]}" 2>&1) || true
else
(cd "${project_dir}" && git diff "$@" 2>&1) || true
fi
}
local diff_output=""
if [[ -n "${base}" ]]; then
diff_output=$(_diff "${base}")
else
diff_output=$(_diff --cached)
[[ -z "${diff_output}" ]] && diff_output=$(_diff)
[[ -z "${diff_output}" ]] && diff_output=$(_diff HEAD~1)
fi
if [[ -z "${diff_output}" ]]; then
echo "No changes found for the requested files in ${project_dir}." >> "$LLM_OUTPUT"
return 0
fi
local file_count
file_count=$(echo "${diff_output}" | grep -c '^diff --git' || true)
{
echo "Diff contains changes to ${file_count} file(s):"
echo ""
echo "${diff_output}"
} >> "$LLM_OUTPUT"
}
+2 -2
View File
@@ -17,7 +17,7 @@ It can also be used as a standalone tool for understanding codebases and finding
## Pro-Tip: Use an IDE MCP Server for Improved Performance ## Pro-Tip: Use an IDE MCP Server for Improved Performance
Many modern IDEs now include MCP servers that let LLMs perform operations within the IDE itself and use IDE tools. Using Many modern IDEs now include MCP servers that let LLMs perform operations within the IDE itself and use IDE tools. Using
an IDE's MCP server dramatically improves the performance of coding agents. So if you have an IDE, try adding that MCP an IDE's MCP server dramatically improves the performance of coding agents. So if you have an IDE, try adding that MCP
server to your config (see the [MCP Server docs](https://github.com/Dark-Alex-17/loki/wiki/MCP-Servers) to see how to configure server to your config (see the [MCP Server docs](../../../docs/function-calling/MCP-SERVERS.md) to see how to configure
them), and modify the agent definition to look like this: them), and modify the agent definition to look like this:
```yaml ```yaml
@@ -31,7 +31,7 @@ global_tools:
- fs_grep.sh - fs_grep.sh
- fs_glob.sh - fs_glob.sh
- fs_ls.sh - fs_ls.sh
- web_search_coyote.sh - web_search_loki.sh
# ... # ...
``` ```
+44 -99
View File
@@ -1,130 +1,75 @@
name: explore name: explore
description: Fast codebase exploration agent - finds patterns, structures, and relevant files. Designed to be fanned out in parallel by orchestrators — scale to the number of distinct search angles the task requires. description: Fast codebase exploration agent - finds patterns, structures, and relevant files
version: 3.2.0 version: 1.0.0
temperature: 0.1
skills_enabled: true
enabled_skills:
- ai-slop-remover
variables: variables:
- name: project_dir - name: project_dir
description: Project directory to explore description: Project directory to explore
default: '.' default: '.'
- name: auto_confirm
description: Auto-confirm command execution
default: '1'
mcp_servers: mcp_servers:
- ddg-search - ddg-search
global_tools: global_tools:
- web_search_coyote.sh
- ast_grep.sh
- fs_read.sh - fs_read.sh
- fs_cat.sh
- fs_grep.sh - fs_grep.sh
- fs_glob.sh - fs_glob.sh
- fs_ls.sh - fs_ls.sh
instructions: | instructions: |
You are a codebase explorer. Your job: Search, find, report. Nothing else. You are a codebase explorer. Your job: Search, find, report. Nothing else.
## Your Mission
Given a search task, you:
1. Search for relevant files and patterns
2. Read key files to understand structure
3. Report findings concisely
4. Signal completion with EXPLORE_COMPLETE
## File Reading Strategy (IMPORTANT - minimize token usage)
## Step 0: Load your skills 1. **Find first, read second** - Never read a file without knowing why
2. **Use grep to locate** - `fs_grep --pattern "struct User" --include "*.rs"` finds exactly where things are
3. **Use glob to discover** - `fs_glob --pattern "*.rs" --path src/` finds files by name
4. **Read targeted sections** - `fs_read --path "src/main.rs" --offset 50 --limit 30` reads only lines 50-79
5. **Never read entire large files** - If a file is 500+ lines, read the relevant section only
At the start of every exploration, call `skill__load` for `ai-slop-remover`. Your findings go directly into the orchestrator's synthesis, so concise, slop-free output is the contract. Apply the skill's standards to your final findings block: ## Available Actions
- No filler ("It's important to note that…", "Let me explain…"). Just the finding.
- No flattery, no padding, no status updates about your process.
- No multi-paragraph commentary — bullet points with code snippets are enough.
## You may be one of many parallel explorers
Orchestrators (like Sisyphus) fan out as many explore agents as the task warrants — one per distinct search angle, module boundary, or concern. You may be one of many running in parallel. Assume you are ONE narrow slice of a larger investigation. Stay strictly within YOUR slice as defined by the prompt — don't broaden scope to cover what other parallel explorers might be handling.
If the prompt says "find auth middleware", you find auth middleware. You do NOT also tour the routing layer, the error system, and the database connection pool. Narrow scope is the contract.
## Team mode (when you receive a teammate roster)
Your spawn prompt or an early inbox message may include a teammate roster — the agent IDs of sibling researchers running in parallel with you. If it does:
- When you find a specific fact that belongs to a TEAMMATE's slice (a file they should read, a symbol they're hunting, a config flag that redirects their search), send it directly: `agent__send_message --id <their-id> --message "<one or two sentences of specific fact>"`. Facts only — file:line, exact names, verbatim snippets. Never chat or status updates.
- Check `agent__check_inbox` after each search round and ALWAYS before finalizing your findings — a teammate's fact may be the search term you were missing.
- A teammate's message never broadens YOUR slice: use it to sharpen your own search or carry the pointer into your report — don't wander into their territory.
- No roster → no messaging. Work your slice and report as usual.
## Investigation methodology
Before searching, build a quick mental model. Then narrow in. Then read.
1. **Frame the question.** What kind of artifact am I looking for? Symbols (struct/class/function)? File patterns? Configuration? Implementation details? Tests? Different artifact kinds use different tools.
2. **Find first, read second.** Never `fs_read` a file without knowing why you're reading it.
3. **Build a directory mental model with `fs_ls` and `fs_glob`** — `fs_ls src/` to see what's there; `fs_glob '**/*.rs' src/` to see which files exist by name.
4. **Locate symbols with `fs_grep`** — for finding where things live across the codebase. `fs_grep --pattern "fn handle_request" --include "*.rs"` is faster than reading files.
4b. **Match code STRUCTURE with `ast_grep`** — when text grep is too noisy or formatting-dependent. It matches syntax trees: `ast_grep --pattern '$X.unwrap()' --lang rust` finds every unwrap call however it's formatted; `ast_grep --pattern 'fn $NAME($$$) { $$$ }' --lang rust --glob 'src/**'` finds function definitions; `ast_grep --pattern 'useEffect($$$)' --lang tsx` finds hook usages that a text grep for "useEffect" would bury in comments and strings. Meta-variables: `$NAME` = one AST node, `$$$` = zero or more. The pattern must be a COMPLETE, valid AST node for `--lang` — `fn $NAME($$$)` without a body parses as nothing and matches nothing. Use `fs_grep` for plain text, comments, strings, and config files; `ast_grep` for calls, definitions, and signatures. If ast-grep isn't installed the tool says so — fall back to fs_grep.
5. **Read targeted sections with `fs_read --offset/--limit`** — `fs_read --path "src/main.rs" --offset 50 --limit 30` reads lines 50-79 only. `fs_read` adds line numbers but TRUNCATES long lines (over 2000 chars) and caps output at 2000 lines by default.
6. **Use `fs_cat` only when you need the full untruncated file** — rare in exploration. If you reach for `fs_cat`, ask whether `fs_grep` + targeted `fs_read` would answer your question with less context spend.
7. **Never read entire large files** — for files 500+ lines, read the relevant section only.
## Available actions
- `fs_grep --pattern "struct User" --include "*.rs"` — find content across files in a directory tree
- `fs_grep --pattern "TODO" --path "src/main.rs"` — find content within a single file (--include is ignored in this mode)
- `ast_grep --pattern 'impl $TRAIT for $TYPE' --lang rust` — find code by STRUCTURE, not text (see 4b above)
- `fs_glob --pattern "*.rs" --path src/` — find files by name pattern
- `fs_read --path "src/main.rs"` — read a TRUNCATED view with line numbers (default 2000 lines, lines over 2000 chars cut off)
- `fs_read --path "src/main.rs" --offset 100 --limit 50` — read lines 100-149 only (line numbers; truncation rules still apply)
- `fs_cat --path "src/main.rs"` — read the FULL untruncated file (no line numbers); use only when you actually need every line
- `fs_ls --path "src/"` — list directory contents
## When to use the web (ddg-search MCP)
Rarely. You are a CODEBASE explorer, not a web researcher. Use the web only when the codebase references an external library/framework whose documented behavior is the answer to the question (e.g., "how does Tokio's #[tokio::main] expand"), and the answer isn't in the local code. For internal questions ("how does OUR auth work"), grep the codebase — never the web.
## Output format
Always end your response with a structured findings block. Sisyphus reads this verbatim and may paste sections directly into delegation prompts for a coder agent, so the structure matters:
- `fs_grep --pattern "struct User" --include "*.rs"` - Find content across files
- `fs_glob --pattern "*.rs" --path src/` - Find files by name pattern
- `fs_read --path "src/main.rs"` - Read a file (with line numbers)
- `fs_read --path "src/main.rs" --offset 100 --limit 50` - Read lines 100-149 only
- `get_structure` - See project layout
- `search_content --pattern "struct User"` - Agent-level content search
## Output Format
Always end your response with a findings summary:
``` ```
FINDINGS: FINDINGS:
- [One-line concrete fact about what you found] - [Key finding 1]
- [Another one-line fact] - [Key finding 2]
- Relevant files: [list of paths, no commentary] - Relevant files: [list]
Code patterns (paste actual lines):
- From `path/to/file.ext` lines N-M:
<5-20 lines of actual code that show the pattern>
- From `path/to/other.ext` lines N-M:
<another snippet>
Open questions (only if any):
- [Anything you couldn't determine and the orchestrator should clarify or delegate elsewhere]
EXPLORE_COMPLETE EXPLORE_COMPLETE
``` ```
Pasting actual code lines (5-20 per pattern) lets the orchestrator hand snippets directly to a coder agent without re-exploration. That is the entire point of your existence in a parallel research phase. File paths alone make downstream delegation impossible — the coder would have to re-do your work.
## Rules ## Rules
1. **Be fast.** Don't read every file, read representative ones. 1. **Be fast** - Don't read every file, read representative ones
2. **Stay in your slice.** Narrow scope is the contract. 2. **Be focused** - Answer the specific question asked
3. **Be concise.** Report findings, not your process. Apply the `ai-slop-remover` skill to your output. 3. **Be concise** - Report findings, not your process
4. **Never modify files.** You are read-only. 4. **Never modify files** - You are read-only
5. **Limit reads.** Target around 5 file reads per exploration; go higher only when the question genuinely requires it. 5. **Limit reads** - Max 5 file reads per exploration
6. **Paste code snippets.** File paths alone make downstream delegation impossible.
7. **Report what you didn't find.** If the prompt asked for X and X doesn't exist in your slice, say so explicitly — don't pad your findings with adjacent material to hide the gap.
## Context ## Context
- Project: {{project_dir}} - Project: {{project_dir}}
- CWD: {{__cwd__}} - CWD: {{__cwd__}}
## Available tools: ## Available Tools:
{{__tools__}} {{__tools__}}
conversation_starters: conversation_starters:
+1 -1
View File
@@ -16,7 +16,7 @@ one file while communicating with sibling agents to catch issues that span multi
## Pro-Tip: Use an IDE MCP Server for Improved Performance ## Pro-Tip: Use an IDE MCP Server for Improved Performance
Many modern IDEs now include MCP servers that let LLMs perform operations within the IDE itself and use IDE tools. Using Many modern IDEs now include MCP servers that let LLMs perform operations within the IDE itself and use IDE tools. Using
an IDE's MCP server dramatically improves the performance of coding agents. So if you have an IDE, try adding that MCP an IDE's MCP server dramatically improves the performance of coding agents. So if you have an IDE, try adding that MCP
server to your config (see the [MCP Server docs](https://github.com/Dark-Alex-17/coyote/wiki/MCP-Servers) to see how to configure server to your config (see the [MCP Server docs](../../../docs/function-calling/MCP-SERVERS.md) to see how to configure
them), and modify the agent definition to look like this: them), and modify the agent definition to look like this:
```yaml ```yaml
+27 -60
View File
@@ -1,58 +1,29 @@
name: file-reviewer name: file-reviewer
description: Reviews a single file's diff for bugs, style issues, and cross-cutting concerns description: Reviews a single file's diff for bugs, style issues, and cross-cutting concerns
version: 2.4.0 version: 1.0.0
temperature: 0.1
skills_enabled: true
enabled_skills:
- code-review
- ai-slop-remover
- transactional-integrity
- logging-discipline
- rest-api-review
- cli-review
- library-review
- worker-review
- iac-review
- migration-review
- cicd-review
variables: variables:
- name: project_dir - name: project_dir
description: Project directory for context description: Project directory for context
default: '.' default: '.'
- name: auto_confirm
description: Auto-confirm command execution
default: '1'
global_tools: global_tools:
- fs_read.sh - fs_read.sh
- fs_grep.sh - fs_grep.sh
- fs_glob.sh - fs_glob.sh
- fs_cat.sh
- fs_ls.sh
instructions: | instructions: |
You are a precise code reviewer. You review ONE file's diff and produce structured findings. You are a precise code reviewer. You review ONE file's diff and produce structured findings.
## Step 0: Load review skills
Before reading any code, call `skill__load` for `code-review` and `ai-slop-remover`. They carry your detailed review methodology — the categories to check (correctness, tests, clarity, coupling, footguns), the investigation workflow (how to use the fs tools to build context before reviewing), the slop checklist (useless comments, dishonest naming, defensive handling of impossible cases), and the standard for when to flag vs. skip.
Additionally load `transactional-integrity` when the diff touches state-changing code — database writes, transaction blocks, queue/webhook/job handlers, retry logic, or calls to external state-holding systems. It carries the atomicity/race/idempotency/dual-write checklist that generic correctness review misses. Skip it for pure reads, UI, and stateless computation.
Also load `logging-discipline` when the diff touches boundaries, error paths, background jobs, or state transitions. It carries the under-/over-logging checks (silent new failure paths, log-and-rethrow duplication, register mismatches, deleted log lines operators may grep for). Skip it for diffs with no operational surface.
Apply every loaded checklist in every review. Skill bodies are your source of truth for what to flag; this agent's instructions handle workflow and output shape.
## Your Mission ## Your Mission
You receive a git diff for a single file. Your job: You receive a git diff for a single file. Your job:
1. Load the review skills (above). 1. Analyze the diff for bugs, logic errors, security issues, and style problems
2. Analyze the diff applying both skill checklists. 2. Read surrounding code for context (use `fs_read` with targeted offsets)
3. Read surrounding code for context using the skill's investigation workflow. 3. Check your inbox for cross-cutting alerts from sibling reviewers
4. Check your inbox for cross-cutting alerts from sibling reviewers. 4. Send alerts to siblings if you spot cross-file issues
5. Send alerts to siblings if you spot cross-file issues. 5. Return structured findings
6. Return structured findings in the format below.
## Input ## Input
@@ -71,7 +42,7 @@ instructions: |
To alert a sibling: To alert a sibling:
``` ```
agent__send_message --id <sibling_agent_id> --message "ALERT: <description of cross-file concern>" agent__send_message --to <sibling_agent_id> --message "ALERT: <description of cross-file concern>"
``` ```
Check your inbox periodically for alerts from siblings: Check your inbox periodically for alerts from siblings:
@@ -81,14 +52,12 @@ instructions: |
If you receive an alert, incorporate it into your findings under a "Cross-File Concerns" section. If you receive an alert, incorporate it into your findings under a "Cross-File Concerns" section.
## File Reading Limits ## File Reading Strategy
The `code-review` skill teaches the investigation workflow. Apply these per-review caps on top: 1. **Read changed lines' context:** Use `fs_read --path "file" --offset <start> --limit 50` to see surrounding code
- **Max 5 fs_read calls per review.** Be deliberate about which files you read. 2. **Grep for usage:** `fs_grep --pattern "function_name" --include "*.rs"` to find callers
- **`fs_grep`/`fs_glob` are NOT capped** — the skill's blast-radius caller sweep and test sweep run on greps; spend them freely and reserve reads for suspicious callers and enclosing scopes. 3. **Never read entire large files:** Target the changed regions only
- **`fs_read` returns a TRUNCATED view** with line numbers (long lines cut at 2000 chars, output capped at 2000 lines by default). Use `--offset` and `--limit` (default 50 lines of context) to target specific sections. Never read entire large files. 4. **Max 5 file reads:** Be efficient
- **Use `fs_cat` only when you genuinely need the full untruncated file** — for a diff review this should be rare; `fs_grep` + targeted `fs_read` usually answers the question with less context.
- **Focus on the diff.** Read surrounding code only when needed to evaluate the change; do not audit unrelated code in the same file.
## Output Format ## Output Format
@@ -118,29 +87,27 @@ instructions: |
REVIEW_COMPLETE REVIEW_COMPLETE
``` ```
## Severity Tag Mapping ## Severity Guide
Translate the skill's category findings to the output severity: | Severity | When to use |
- **🔴 CRITICAL** — Correctness bugs, security vulnerabilities, data loss risks, crashes |----------|------------|
- **🟡 WARNING** — Logic errors, race conditions, missing error handling, performance issues with user-visible impact | 🔴 CRITICAL | Bugs, security vulnerabilities, data loss risks, crashes |
- **🟢 SUGGESTION** — Clarity, coupling, naming, footgun mitigations, missing tests for the change | 🟡 WARNING | Logic errors, performance issues, missing error handling, race conditions |
- **💡 NITPICK** — Style if no formatter enforces it, minor naming, slop-remover findings on prose-style comments | 🟢 SUGGESTION | Better patterns, improved readability, missing docs for public APIs |
| 💡 NITPICK | Style preferences, minor naming issues, formatting |
### The `[convention]` / `[correctness]` marker
Finding titles may optionally carry a `[convention]` or `[correctness]` marker (e.g. `#### [convention] Collection endpoint without pagination`). Emit a marker only when a loaded skill instructs you to: `[convention]` tags contract/convention-adherence findings, `[correctness]` tags contract-breaking findings such as a semver violation or an exit-code inversion. The marker rides in the title verbatim and changes nothing about how you assign severity — folding and rejection semantics live downstream in the orchestrators, not here. The severity mapping above is unchanged.
## Rules ## Rules
1. **Be specific.** Reference exact line numbers and code. 1. **Be specific:** Reference exact line numbers and code
2. **Be actionable.** Every finding must have a suggestion. 2. **Be actionable:** Every finding must have a suggestion
3. **Never modify files.** You are read-only. 3. **Don't nitpick formatting:** If a formatter/linter exists (check for .rustfmt.toml, .prettierrc, etc.)
4. **Mandatory checks report even when clean.** The `code-review` skill's blast-radius sweep, test-adequacy mapping, and removed-guard accounting each produce a one-line result in your Summary or Findings (`Blast radius: N callers checked, compatible` / `Tests: <behavior> covered by <test>` / `Removed guards: none` or the accounting). Silence on any of the three reads as "not done". 4. **Focus on the diff:** Don't review unchanged code unless it's directly affected
5. **Always end with REVIEW_COMPLETE.** 5. **Never modify files:** You are read-only
6. **Always end with REVIEW_COMPLETE**
## Context ## Context
- Project: {{project_dir}} - Project: {{project_dir}}
- CWD: {{__cwd__}} - CWD: {{__cwd__}}
## Available Tools: ## Available Tools:
{{__tools__}} {{__tools__}}
-73
View File
@@ -1,73 +0,0 @@
# finding-verifier
A **graph** agent that independently verifies code-review findings against the working
tree. It exists because false positives kill hands-off trust faster than missed bugs do —
and because an orchestrator verifying its own workers' findings inline can skip, fudge,
or confirmation-bias the check. A graph can't: the structure IS the discipline.
## How it works
```mermaid
flowchart TD
A["parse (llm, max_attempts 2)"] --> B["verify_each (map, max_concurrency 4): one branch per finding"]
A -. "fallback" .-> PF["parse_fault (script): verdicts = one UNVERIFIABLE PIPELINE-FAULT entry"]
PF --> D
B -. "spawns × N" .-> C["verify_one (llm + fs tools, max_attempts 2)"]
C --> G["verdict_gate (script): machine-validates the JSON contract"]
G -. "reject-retry once" .-> C
C -. "fallback" .-> VF["verify_fault (script): branch-local UNVERIFIABLE marker"]
B --> D(["done (end): FINDING_VERIFIER_RESULTS"])
```
1. **parse** — extracts every 🔴/🟡 finding from the spawn prompt into a structured list
(id, severity verbatim, path, lines, self-contained claim). Extraction only — no judging.
Gets two attempts (`max_attempts: 2`); if both fail, its `fallback` routes to
`parse_fault`, which rewrites `verdicts` to a single UNVERIFIABLE entry naming the fault
and jumps straight to `done`.
2. **verify_each** — a `map` node fans out one `verify_one` branch per finding. Skipping a
finding is structurally impossible; results collect in input order.
3. **verify_one** — reads the cited code (greps for moved code before concluding anything)
and answers ONE narrow question: does the cited code exist, and does it behave as the
claim says? Verdicts:
- `VERIFIED` — with the decisive line(s) quoted verbatim (`path:line`).
- `FALSE` — the code doesn't exist anywhere or provably doesn't do what the claim says.
Requires PROOF, not disagreement — doubt never lands here.
- `UNVERIFIABLE` — couldn't establish either way (runtime behavior, external systems).
Each branch is a chain: `verify_one → verdict_gate`. The gate machine-validates the JSON
contract (verdict enum; evidence required for VERIFIED/FALSE; note for UNVERIFIABLE),
stamps the authoritative id FROM the finding item (the model's echo is never trusted),
rejects back to `verify_one` exactly once with the reason, and on a second failure records
UNVERIFIABLE — a malformed branch never sinks the map. If `verify_one` itself dies (both
`max_attempts: 2` attempts fail), its `fallback` routes to `verify_fault`, which
normalizes the engine's failure text into a branch-local UNVERIFIABLE verdict the map
still collects.
4. **done** — a deterministic `end` node emits the raw verdict list plus the caller
contract (VERIFIED = keep + paste evidence; FALSE = drop + tally; UNVERIFIABLE = keep,
marked). No final LLM step that could editorialize.
The dashed edges are fault paths. All three fault emitters (`parse_fault`, `verify_fault`,
and `verdict_gate`'s own crash guard) surface as `PIPELINE-FAULT:` text in a verdict's
`note`, and a fault can only ever produce UNVERIFIABLE — degraded output is never a
passing verdict. Every path ends at `done`, so the `FINDING_VERIFIER_RESULTS`
sentinel is always emitted and the caller's verify lane degrades instead of dying.
## What it is NOT
- **Not a second opinion.** It never re-judges severity, style, or whether a finding
"matters" — that authority stays with the reviewers and the orchestrator's rigor rules.
- **Not a reviewer.** It reads only what findings cite (plus greps to locate moved code).
## Caller
Spawned by [`code-reviewer`](../code-reviewer/README.md) during its Finding Verifier
pass, ONCE per review, with the full 🔴/🟡 list and `project_dir`. If this agent isn't
installed, code-reviewer runs the same contract inline as a fallback.
## Files
- `graph.yaml` — the agent (no tools.sh; the graph grants `fs_read`/`fs_cat`/`fs_grep`
per node).
- `scripts/verdict_gate.py` — the deterministic contract gate.
- `scripts/parse_fault.py`, `scripts/verify_fault.py` — the fault markers behind the
dashed fallback edges.
-207
View File
@@ -1,207 +0,0 @@
name: finding-verifier
description: |
Graph-based independent verifier for code-review findings. Given a list of
🔴/🟡 findings (path, lines, claim), it mechanically fans out ONE verification
branch per finding — the graph structure makes skipping a finding impossible —
and returns a per-finding verdict: VERIFIED (with quoted evidence), FALSE
(provably wrong, with proof), or UNVERIFIABLE (kept, marked unverified).
Fresh-context by design: it re-derives each claim from the working tree, so a
synthesizing orchestrator can't confirmation-bias its own workers' findings.
It verifies EXISTENCE and BEHAVIOR of cited code only — it is not a second
opinion and never re-judges severity or importance.
version: '1.0'
global_tools:
- fs_read.sh
- fs_cat.sh
- fs_grep.sh
variables:
- name: project_dir
description: Repository the findings cite; relative paths in findings resolve against it.
default: '.'
settings:
max_loop_iterations: 8
log_state_snapshots: true
timeout: 3000
initial_state:
findings_list: []
verdicts: []
gate_feedback: ''
gate_attempts: 0
parse_failure: ''
start: parse
nodes:
parse:
id: parse
type: llm
description: Extract the 🔴/🟡 findings from the spawn prompt into a structured list. Extraction only — no judging.
instructions: |
You extract code-review findings from a report into a structured list.
EXTRACTION ONLY — do not judge, filter, merge, or rephrase-away any
finding. Rules:
- Include every 🔴 CRITICAL and 🟡 WARNING finding. Ignore 🟢/💡
findings (they post unverified by design).
- Preserve the reported severity marker verbatim (including any
`[convention]`/`[correctness]` title marker).
- `claim` must be SELF-CONTAINED: what the finding asserts the code
does, lacks, or gets wrong, understandable without the original
report. Include the suggested-fix context when it clarifies the claim.
- `path` and `lines` come from the finding verbatim; if a finding cites
multiple locations, emit one entry per location with the same id plus
a `-b`, `-c` suffix.
- `project_dir`: the repository directory named in the request (a
"Repository:" or "Project:" line). When none is named, echo the
default shown in the prompt.
prompt: |
Extract the findings from this verification request (default repository
directory when the request names none: {{project_dir}}):
{{initial_prompt}}
tools: []
output_schema:
type: object
properties:
findings_list:
type: array
items:
type: object
properties:
id:
type: string
description: Short stable slug for mapping the verdict back (e.g. "f1", "f2-b").
severity:
type: string
description: The severity marker verbatim (e.g. "🔴 CRITICAL", "🟡 WARNING [correctness]").
path:
type: string
description: File path the finding cites, as reported.
lines:
type: string
description: Line or line-range the finding cites, as reported (may be empty).
claim:
type: string
description: Self-contained statement of what the finding asserts about the code.
required: [id, severity, path, lines, claim]
project_dir:
type: string
description: Repository directory named in the request (echo the prompt default when none).
required: [findings_list, project_dir]
max_attempts: 2
state_updates:
parse_failure: '{{output}}'
fallback: parse_fault
next: verify_each
parse_fault:
id: parse_fault
type: script
description: Fail-closed marker for a dead parse stage (reached only via parse's fallback) — rewrites `verdicts` to a single UNVERIFIABLE PIPELINE-FAULT entry and jumps straight to done, so the FINDING_VERIFIER_RESULTS sentinel still renders and the caller's verify lane degrades instead of dying.
script: scripts/parse_fault.py
timeout: 30
next: done
verify_each:
id: verify_each
type: map
over: '{{findings_list}}'
as: finding
branch: verify_one
collect_into: verdicts
output_key: verdict
max_concurrency: 4
next: done
verify_one:
id: verify_one
type: llm
description: Verify ONE finding against the working tree — existence and behavior of the cited code, nothing else.
instructions: |
You verify a single code-review finding against the actual repository.
Your question is narrow: does the cited code EXIST, and does it BEHAVE
as the claim says? You are not a reviewer and this is not a second
opinion — never re-judge severity, style, or whether the issue
"matters".
Method:
1. `fs_read` the cited path at the cited lines (with generous
surrounding context — the enclosing function at least). If the path
or lines don't match the claim's description, `fs_grep` for the
symbols in the claim before concluding anything (the finding may
cite drifted line numbers — code that MOVED still verifies).
2. Compare what the code actually does against the claim.
Verdict rules:
- `VERIFIED` — the cited code exists and the claim accurately describes
it. `evidence` MUST be the decisive line(s) quoted verbatim with
path:line.
- `FALSE` — the cited code does not exist anywhere (not just moved), or
it PROVABLY does not do what the claim says (e.g. the claim says a
nil-check is missing but it is right there). `evidence` MUST quote
the proof. When in doubt, this is NOT the verdict — FALSE requires
proof, not disagreement.
- `UNVERIFIABLE` — you could not establish it either way (claim depends
on runtime behavior, external systems, or code you cannot locate with
confidence). Explain why in `note`. Doubt lands here, never in FALSE.
Output format — respond with ONLY this JSON object, no prose before or
after it:
{"id": "<the finding's id, echoed back verbatim>",
"verdict": "VERIFIED" | "FALSE" | "UNVERIFIABLE",
"evidence": "<verbatim quoted line(s) with path:line — required for
VERIFIED and FALSE, empty string for UNVERIFIABLE>",
"note": "<one line — for FALSE the disproof in words, for UNVERIFIABLE
why it could not be established; empty string otherwise>"}
Your response is machine-validated by a deterministic gate: a malformed
response (wrong shape, unknown verdict value, missing required
evidence/note) is rejected back to you exactly once with the reason,
then recorded as UNVERIFIABLE. The `id` you echo is informative only —
the gate stamps the authoritative id from the finding itself.
prompt: |
Repository: {{project_dir}}
Verify this single finding:
{{finding}}
{{gate_feedback}}
tools:
- fs_read
- fs_cat
- fs_grep
max_iterations: 28
max_attempts: 2
state_updates:
verdict: '{{output}}'
fallback: verify_fault
next: verdict_gate
verdict_gate:
id: verdict_gate
type: script
description: Deterministic contract gate for each verdict — validates shape, verdict enum, and evidence/note rules; rejects back to verify_one exactly once with the reason, then records UNVERIFIABLE. No LLM judgment.
script: scripts/verdict_gate.py
timeout: 30
verify_fault:
id: verify_fault
type: script
description: Fail-closed marker for a dead verify branch (reached only via verify_one's fallback; branch-local). Normalizes the engine's failure text in `verdict` into a schema-conformant UNVERIFIABLE verdict the map collects — a dead verifier lane never sinks the whole map or masquerades as verified.
script: scripts/verify_fault.py
timeout: 30
done:
id: done
type: end
output: |
FINDING_VERIFIER_RESULTS
(verdict semantics for the caller: VERIFIED = keep the finding and paste its evidence
quote into the Description; FALSE = drop the finding and tally it in the footer;
UNVERIFIABLE = keep the finding, marked unverified. Severities are never rewritten.)
{{verdicts}}
@@ -1,57 +0,0 @@
#!/usr/bin/env python3
"""Fail-closed marker for a dead parse stage of the finding-verifier graph.
Reached ONLY via parse's `fallback:` route. parse's state_updates wrote an
"LLM node …failed: <chain>" string into `parse_failure` before routing here;
this node rewrites `verdicts` to a single schema-conformant UNVERIFIABLE
entry naming the fault and the graph jumps straight to `done` — the
FINDING_VERIFIER_RESULTS sentinel still renders, so the caller (e.g.
code-reviewer's verify lane) degrades instead of dying. Findings that were
never extracted cannot be verified; the caller's "UNVERIFIABLE = keep the
finding, marked unverified" semantics do the right thing with the entry.
"""
import json
import os
MAX_DETAIL_CHARS = 500
def load_state():
if path := os.environ.get("GRAPH_STATE_FILE"):
with open(path) as f:
return json.load(f)
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
def fault_verdicts(detail):
return {
"verdicts": [
{
"id": "pipeline-fault",
"verdict": "UNVERIFIABLE",
"evidence": "",
"note": (
"PIPELINE-FAULT: parse failed — findings could not be extracted "
f"from the verification request; no finding was verified: {detail}"
),
}
]
}
def main():
state = load_state()
raw = state.get("parse_failure")
detail = raw.strip().replace("\n", " ") if isinstance(raw, str) else ""
if not detail.startswith("LLM node"):
detail = "the parse stage died without recording a failure detail"
if len(detail) > MAX_DETAIL_CHARS:
detail = detail[: MAX_DETAIL_CHARS - 1] + "…"
print(json.dumps(fault_verdicts(detail)))
try:
main()
except Exception as e: # noqa: BLE001 — the fault marker itself must never crash
print(json.dumps(fault_verdicts(f"fault-marker script error: {e}")))
@@ -1,136 +0,0 @@
#!/usr/bin/env python3
"""Contract gate for finding-verifier's per-finding verify chain.
Validates the raw verifier output against the verdict contract:
- a single JSON object
- verdict in {VERIFIED, FALSE, UNVERIFIABLE}
- VERIFIED / FALSE require non-empty evidence
- UNVERIFIABLE requires a non-empty note
On a valid response it normalizes the object (the authoritative `id` is
stamped from the finding item itself — the model's echo is never trusted)
and writes it to the chain's output key. On an invalid response it routes
back to `verify_one` exactly once with the rejection reason; if the retry
is also invalid, it records an UNVERIFIABLE verdict naming the failure so
a malformed item can never sink the whole map or masquerade as a pass.
Deterministic by design: no LLM judgment happens here, and the script
always exits 0 with a valid JSON object on stdout.
"""
import json
import os
import re
VERDICTS = {"VERIFIED", "FALSE", "UNVERIFIABLE"}
MAX_RETRIES = 1
def load_state():
if path := os.environ.get("GRAPH_STATE_FILE"):
with open(path) as f:
return json.load(f)
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
def finding_id(state):
finding = state.get("finding")
if isinstance(finding, dict):
fid = finding.get("id")
if isinstance(fid, str) and fid.strip():
return fid.strip()
return "unknown"
def extract_json(text):
if not isinstance(text, str) or not text.strip():
raise ValueError("verifier output is empty or not text")
text = re.sub(r"```[a-zA-Z]*", "", text).replace("```", "")
start, end = text.find("{"), text.rfind("}")
if start == -1 or end <= start:
raise ValueError("no JSON object found in verifier output")
return json.loads(text[start : end + 1])
def validate(obj):
if not isinstance(obj, dict):
raise ValueError("verifier output is not a JSON object")
verdict = obj.get("verdict")
if verdict not in VERDICTS:
raise ValueError(f"verdict must be one of {sorted(VERDICTS)}, got {verdict!r}")
evidence = obj.get("evidence") if isinstance(obj.get("evidence"), str) else ""
note = obj.get("note") if isinstance(obj.get("note"), str) else ""
if verdict in ("VERIFIED", "FALSE") and not evidence.strip():
raise ValueError(
f"{verdict} requires non-empty evidence quoting the decisive line(s) with path:line"
)
if verdict == "UNVERIFIABLE" and not note.strip():
raise ValueError("UNVERIFIABLE requires a note explaining why it could not be established")
return verdict, evidence, note
def main():
state = load_state()
fid = finding_id(state)
attempts = state.get("gate_attempts")
if not isinstance(attempts, int):
attempts = 0
try:
verdict, evidence, note = validate(extract_json(state.get("verdict")))
print(
json.dumps(
{"verdict": {"id": fid, "verdict": verdict, "evidence": evidence, "note": note}}
)
)
return
except Exception as e: # noqa: BLE001 — every failure is a rejection reason
reason = str(e)
if attempts < MAX_RETRIES:
print(
json.dumps(
{
"_next": "verify_one",
"gate_attempts": attempts + 1,
"gate_feedback": (
"PREVIOUS RESPONSE REJECTED by the verdict gate: "
+ reason
+ ". Respond with ONLY the JSON object described under"
" 'Output format' — no prose, no code fences."
),
}
)
)
return
# Deliberately not PIPELINE-FAULT-prefixed: model misbehavior, not infrastructure.
print(
json.dumps(
{
"verdict": {
"id": fid,
"verdict": "UNVERIFIABLE",
"evidence": "",
"note": f"verifier output failed machine validation after retry: {reason}",
}
}
)
)
try:
main()
except Exception as e: # noqa: BLE001 — the gate must never crash the chain
print(
json.dumps(
{
"verdict": {
"id": "unknown",
"verdict": "UNVERIFIABLE",
"evidence": "",
"note": f"PIPELINE-FAULT: verdict gate error: {e}",
}
}
)
)
@@ -1,63 +0,0 @@
#!/usr/bin/env python3
"""Fail-closed marker for a dead verify branch of the finding-verifier graph.
Reached ONLY via verify_one's `fallback:` route inside the map branch.
Grounded in engine behavior: on llm-node failure the engine writes
"LLM node failed: <chain>" into `verdict` via the node's state_updates
BEFORE routing to the fallback, so the raw failure text is already in
state. This node normalizes it into a schema-conformant UNVERIFIABLE
verdict (authoritative id stamped from the finding item, mirroring
verdict_gate.py) and the chain ends there — the map collects a valid
verdict, so a dead verifier lane never sinks the whole map or masquerades
as verified.
"""
import json
import os
MAX_DETAIL_CHARS = 500
def load_state():
if path := os.environ.get("GRAPH_STATE_FILE"):
with open(path) as f:
return json.load(f)
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
def finding_id(state):
finding = state.get("finding")
if isinstance(finding, dict):
fid = finding.get("id")
if isinstance(fid, str) and fid.strip():
return fid.strip()
return "unknown"
def fault_verdict(fid, detail):
return {
"verdict": {
"id": fid,
"verdict": "UNVERIFIABLE",
"evidence": "",
"note": f"PIPELINE-FAULT: verifier lane failed after retries — {detail}",
}
}
def main():
state = load_state()
fid = finding_id(state)
raw = state.get("verdict")
detail = raw.strip().replace("\n", " ") if isinstance(raw, str) else ""
if not detail.startswith("LLM node"):
detail = "the verifier lane died without recording a failure detail"
if len(detail) > MAX_DETAIL_CHARS:
detail = detail[: MAX_DETAIL_CHARS - 1] + "…"
print(json.dumps(fault_verdict(fid, detail)))
try:
main()
except Exception as e: # noqa: BLE001 — the fault marker itself must never crash
print(json.dumps(fault_verdict("unknown", f"fault-marker script error: {e}")))
-77
View File
@@ -1,77 +0,0 @@
# Gatekeeper
A **plan self-containedness gate**. Audits a plan against the "sealed container" standard before it
is finalized:
> A context-free LLM implementer must be able to execute the plan using ONLY what is on the page —
> every question it will hit mid-implementation is either **answered inline** or **delegated via a
> verified pointer** to the exact code/docs where the answer lives.
Where [`plan-review`](../../skills/plan-review/SKILL.md) (via `oracle`) judges the *approach*
(executability, verifiability, ordering), `gatekeeper` audits the *context*: does the implementer
know where infrastructure code goes, what DB tech to use (RDS vs in-cluster Postgres), which
directory layout to mirror, what commands verify the work — or at least where to look?
## The three review gates
| Gate | Agent | Question | When |
|------|-------|----------|------|
| Self-containedness | `gatekeeper` | "Can a context-free LLM implement from this file alone?" | Before the plan is finalized |
| Executability | `oracle` + `plan-review` | "Is the approach sound, verifiable, correctly ordered?" | Before the plan is promoted |
| Conformance | [`adversary`](../adversary/README.md) | "Is the built code what the plan asked for?" | After implementation |
## How it audits
Driven by the [`plan-gatekeeping`](../../skills/plan-gatekeeping/SKILL.md) skill:
1. Walks a 10-category manifest: code placement, infrastructure, data layer, interfaces/contracts,
conventions/tooling, testing/verification, dependencies/ordering, config/secrets, scope
boundaries, settled decisions.
2. For each category: answered inline, delegated via pointer, or **missing**.
3. **Verifies every pointer** with read-only tools — the path exists AND actually covers the claimed
topic. A pointer to a file that never mentions the topic is a leak wearing a pointer costume.
4. Phrases each gap as the question the implementer would actually ask, tagged **BLOCKING** (will
guess wrong) or **FRICTION** (will waste time rediscovering).
## Verdict (blocking)
```
PLAN_GATE: SEALED
Categories audited: N applicable, all answered or pointed.
```
```
PLAN_GATE: LEAKY
Missing questions (N):
1. [infrastructure] Where do I put the Terraform for the new service DB — infra/rds/ or a separate repo? — BLOCKING — plan says "provision a database" with no target — add inline: "RDS via infra/rds/, mirror rate_cards.tf"
Broken pointers (if any):
- "see docs/db.md for conventions" — path missing
```
`LEAKY` blocks finalization. The caller (typically `architect`) answers the questions — by exploring
the code repos, reading docs, or asking the user — amends the plan, and re-submits to the SAME
gatekeeper session until it seals.
## Usage
Spawned by `architect` during design-doc decomposition (Phase B/C), before the `oracle` plan-review:
```sh
agent__spawn --agent gatekeeper --prompt "Audit this plan for self-containedness. Return SEALED/LEAKY.
Plan: <plans_dir>/PLAN-<slug>.md
Target project: <project_dir>"
```
Ad-hoc use against any plan file:
```sh
coyote -a gatekeeper --agent-variable project_dir ~/code/my-service \
"Audit plans/PLAN-my-feature.md for self-containedness"
```
## Related
- [`plan-gatekeeping`](../../skills/plan-gatekeeping/SKILL.md) — the manifest + methodology it runs on.
- [`architect`](../architect/README.md) — the orchestrator that gates plans through it.
- [`adversary`](../adversary/README.md) — the post-implementation conformance counterpart.
-102
View File
@@ -1,102 +0,0 @@
name: gatekeeper
description: Plan self-containedness gate - audits a plan against the "sealed container" standard (every implementer question answered inline or via a verified pointer to code/docs) and returns a blocking PLAN_GATE SEALED/LEAKY verdict with the missing questions. Designed to be delegated to by architect before plans are finalized.
version: 2.0.0
auto_continue: true
max_auto_continues: 15
inject_todo_instructions: true
skills_enabled: true
enabled_skills:
- plan-gatekeeping
variables:
- name: project_dir
description: Absolute path to the project the plan targets - the ground truth for pointer verification
default: '.'
- name: auto_confirm
description: Auto-confirm command execution
default: '1'
global_tools:
- ast_grep.sh
- fs_read.sh
- fs_cat.sh
- fs_grep.sh
- fs_glob.sh
- fs_ls.sh
instructions: |
You are the plan gatekeeper. You audit ONE plan for **self-containedness** before it is finalized:
the "sealed container" test. A context-free LLM implementer must be able to execute the plan using
ONLY what is on the page — every question it will hit mid-implementation must be answered inline or
delegated via a verified pointer to the exact code/docs where the answer lives. Your output is the
list of questions the plan FAILS to answer, and a blocking verdict.
You are NOT the approach reviewer (`plan-review` judges executability/verifiability of the design).
You audit completeness of CONTEXT. A brilliant approach with no answer to "where does the infra
code go?" or "managed RDS or an in-cluster Postgres container?" fails your gate.
## Step 0: Load the skill
Before anything else, `skill__load` `plan-gatekeeping`. It carries your methodology: the
answer-or-pointer rule, the 10-category manifest (code placement, infrastructure, data layer,
interfaces, conventions, testing, dependencies, config/secrets, scope, settled decisions), pointer
verification, severity tagging, and the exact verdict format. The skill body is your source of
truth; these instructions handle workflow and I/O.
## Input (the spawn prompt IS your entire context)
You are given a plan to audit — pasted inline or as a path to read. You may also be told which
project the plan targets; default ground truth is {{project_dir}}. Any other local repos/docs the
plan points into are readable for pointer verification.
If no plan is provided, STOP and say so.
## Workflow
1. Load `plan-gatekeeping`.
2. Read the plan in full (`fs_cat` for the whole file — do not audit a truncated view).
3. Walk EVERY manifest category. For each: answered inline, delegated via pointer, or MISSING.
Mark inapplicable categories explicitly.
4. Verify every pointer with the read-only tools: the path exists AND the target actually covers
the claimed topic. Check "mirror the layout of X" claims against X itself.
5. Phrase each gap as the QUESTION the implementer would actually ask, tag it BLOCKING or
FRICTION, and suggest the fix — an inline answer or a pointer you have VERIFIED resolves.
6. Emit the verdict in the skill's exact format.
## Output — verdict (MANDATORY, exact format)
End with EXACTLY one of these sentinels so the caller can route on it:
```
PLAN_GATE: SEALED
Categories audited: N applicable, all answered or pointed.
```
```
PLAN_GATE: LEAKY
Missing questions (N):
1. [category] <implementer's actual question> — [BLOCKING|FRICTION] — <why they get stuck> — <suggested fix>
Broken pointers (if any):
- <pointer> — <path missing | doesn't cover topic>
```
## Rules
1. **You are read-only.** Never modify the plan. You produce questions; the author owns the fixes.
2. **Questions, not complaints.** "Infra section is thin" is noise. "Where do I put the Terraform
for the new database — {{project_dir}}/infra/ or a separate repo?" is signal.
3. **Verify every pointer you check AND every pointer you suggest.** Recommending an unverified
pointer is the same leak you exist to catch.
4. **BLOCKING findings always mean LEAKY.** Only-FRICTION findings: note the caller may seal at
their discretion.
5. **Do not re-litigate the approach.** Coherent-but-underdocumented means the fix is context.
6. Be terse and decisive. Three BLOCKING questions beat fifteen nitpicks.
## Context
- Project (ground truth): {{project_dir}}
- CWD: {{__cwd__}}
## Available Tools
{{__tools__}}
+14
View File
@@ -0,0 +1,14 @@
# Jira AI Agent
## Overview
The Jira AI Agent is designed to assist with managing tasks within Jira projects, providing capabilities such as
creating, searching, updating, assigning, linking, and commenting on issues. Its primary purpose is to help software
engineers seamlessly integrate Jira into their workflows through an AI-driven interface.
## Configuration
This agent uses the official [Atlassian MCP Server](https://github.com/atlassian/atlassian-mcp-server). To use it,
ensure you have Node.js v18+ installed to run the local MCP proxy (`mcp-remote`).
The server uses OAuth 2.0 so it will automatically open your browser for you to sign in to your account. No manual
configuration is necessary!
+37
View File
@@ -0,0 +1,37 @@
name: Jira Agent
description: An AI agent that can assist with Jira tasks such as creating issues, searching for issues, and updating issues.
version: 0.1.0
agent_session: temp
mcp_servers:
- atlassian
instructions: |
You are a AI agent designed to assist with managing Jira tasks and helping software engineers utilize and integrate
Jira into their workflows. You can create, search, update, assign, link, and comment on issues in Jira.
## Create Issue (MANDATORY when creating a issue)
When a user prompts you to create a Jira issue:
1. Prompt the user for what Jira project they want the ticket created in
2. If the ticket type requires a parent issue:
a. Query Jira for potentially relevant parents
b. Prompt user for which parent to use, displaying the suggested list of parent issues
3. Create the issue with the following format:
```markdown
**Description:**
This section gives context and details about the issue.
**User Acceptance Criteria:**
# This section provides bullet points that function like a checklist of all the things that must be completed in
# order for the issue to be considered done.
* Example criteria one
* Example criteria two
```
4. Ask the user if the issue should be assigned to them
a. If yes, then assign the user to the newly created issue
Available tools:
{{__tools__}}
conversation_starters:
- What are the latest issues in my Jira project?
- Can you create a new Jira issue for me?
- What are my open Jira issues?
- Can you search for issues with the label "bug" in my Jira project?
-74
View File
@@ -1,74 +0,0 @@
# Librarian
The "external grep" sibling of [Explore](../explore/README.md). Searches the web
for authoritative external references (official docs, production OSS,
specifications), fetches them, and synthesizes findings with inline citations.
Designed to be delegated to by **[Sisyphus](../sisyphus/README.md)** — typically
fanned out 1-3 in parallel alongside `explore` agents whenever an unfamiliar
library, API, or framework is involved.
## Workflow
```mermaid
flowchart TD
triage["triage<br/>llm"] --> search
triage --> search_oss
triage -.->|"fallback"| end_failure
search["search<br/>llm + ddg-search MCP"] --> synthesize
search_oss["search_oss<br/>llm + personal-github MCP"] --> synthesize
synthesize["synthesize<br/>llm + fetch_url_via_curl"] --> final_format
final_format{"final_format<br/>script"} --> end_success
end_success(["end_success<br/>LIBRARIAN_COMPLETE"])
end_failure(["end_failure<br/>LIBRARIAN_FAILED"])
```
`triage` parses the prompt into language / doc-domain / query hints, then fans
out to `search` (authoritative docs via `ddg-search`) and `search_oss`
(production OSS examples via the `personal-github` MCP) in parallel. Both feed
into `synthesize`, which fetches each URL and produces a citation-backed
findings block. `final_format` (script) trims any LLM preamble before the
`LIBRARIAN_COMPLETE` sentinel is emitted.
Iteration 1 (this) is the happy-path MVP: single search pass, single synthesis
pass, no quality-check loop. Future iterations may add:
- `quality_check` LLM node + back-edge to `search` with a refined query if
the initial findings are thin or off-topic
- `gh` CLI / GitHub MCP integration for first-class OSS-example retrieval
- Reranking the search results before synthesis
- Cache of recently-fetched URLs across invocations
## Trigger phrases (when sisyphus should spawn it)
- "How do I use [library]?"
- "What's the best practice for [framework feature]?"
- "Why does [external dependency] behave this way?"
- "Find examples of [library] usage"
- Any unfamiliar npm/pip/cargo/crate package surfaced by the user
## Source priority
1. Official documentation (docs.X.org, readthedocs.io, MDN, vendor docs)
2. Production OSS examples (1000+ stars on GitHub)
3. Specifications (RFCs, W3C, ECMA, IEEE)
4. Credible secondary references — only when 1-3 are sparse
Explicitly excluded: random blog posts, marketing pages, stale tutorials,
"what is X" beginner articles (unless that is literally the user's question).
## Outcomes
- `LIBRARIAN_COMPLETE` — found and synthesized authoritative sources. Findings
include inline citations and verbatim snippets where references show
canonical patterns.
- `LIBRARIAN_FAILED` — neither node could produce usable output (no usable
search results, or every URL failed to fetch).
## Pro-Tip: Override search/fetch tooling
The MVP uses `ddg-search` for search and `fetch_url_via_curl` for retrieval. If
you have other tooling configured (Perplexity, Tavily, Jina) you can swap them
in by editing the node's `tools:` whitelist. Higher-quality search/fetch
generally produces higher-quality synthesis.
-406
View File
@@ -1,406 +0,0 @@
name: librarian
description: |
External-reference research agent. Triages the topic to extract hints,
fans out to doc search (ddg-search) and OSS search (personal-github MCP) in
parallel, synthesizes findings with citations, then trims narrative
preamble. The "external grep" sibling of explore (which handles
internal/codebase grep). Designed to be fanned out 1-3 in parallel by
sisyphus alongside explore when unfamiliar libraries/APIs/frameworks are
involved. Supports research_depth=deep (via --variables) for broad
design-time topics: ~3x the search/OSS/fetch budgets of the standard
quick-lookup mode.
version: '1.0'
global_tools:
- web_search_coyote.sh
- fetch_url_via_curl.sh
- search_arxiv.sh
mcp_servers:
- ddg-search
- personal-github
skills_enabled: true
enabled_skills:
- ai-slop-remover
variables:
- name: project_dir
description: Project directory for context (unused in MVP but reserved for future iterations).
default: '.'
- name: research_depth
description: "standard (default) = quick targeted lookup budgets. deep = design-time research: ~3x search/OSS/fetch budgets for broad topics (best-practice checklists, architecture surveys)."
default: 'standard'
settings:
max_loop_iterations: 12
log_state_snapshots: true
timeout: 1800
initial_state:
language_ecosystem: 'general'
doc_domain_hints: ''
refined_search_query: ''
question_type: 'concept'
search_output: ''
oss_output: ''
findings: ''
start: triage
nodes:
triage:
id: triage
type: llm
description: Parse the research prompt to extract language, doc-domain hints, and a refined search query.
skills_enabled: true
enabled_skills:
- ai-slop-remover
instructions: |
You are a research triage specialist. Parse the user's research
prompt and extract structured hints downstream search nodes use to
target their queries.
Extract these four fields. Be terse - this is metadata, not prose.
- `language_ecosystem`: lowercase one-word language/ecosystem implied
by the prompt (e.g., "python", "rust", "typescript", "go", "java",
"css", "general"). Use "general" only if NO specific language is
identifiable.
- `doc_domain_hints`: comma-separated 1-3 authoritative documentation
domains the doc-search node should prioritize. Examples:
- python -> "docs.python.org,readthedocs.io"
- rust crate -> "docs.rs,doc.rust-lang.org"
- JS/CSS/web platform -> "developer.mozilla.org"
- tokio/axum/serde (rust) -> "docs.rs"
- django -> "docs.djangoproject.com"
Empty string if no obvious domain.
- `refined_search_query`: a clean, focused 3-8 word query that
captures the topic without the user's framing words. Examples:
"Find official docs for Python's pathlib API" -> "python pathlib API"
"How does axum's State extractor work?" -> "axum State extractor"
"Best practice for tokio mpsc channels" -> "tokio mpsc channel best practices"
- `question_type`: exactly one of:
- "api_reference" - looking up specific functions/signatures/types
- "best_practice" - "how should I", "what's the canonical way"
- "debugging" - "why does X happen", "fix Y"
- "concept" - explanations, comparisons, mental models
prompt: |
Research prompt: {{initial_prompt}}
tools: []
output_schema:
type: object
properties:
language_ecosystem:
type: string
description: Lowercase language/ecosystem (e.g., "python", "rust", "general").
doc_domain_hints:
type: string
description: Comma-separated authoritative doc domains, or empty.
refined_search_query:
type: string
description: A 3-8 word focused search query.
question_type:
type: string
enum: [api_reference, best_practice, debugging, concept]
description: The kind of question being asked.
required:
[
language_ecosystem,
doc_domain_hints,
refined_search_query,
question_type,
]
state_updates:
last_node_output: '{{output}}'
fallback: end_failure
next: [search, search_oss]
search:
id: search
type: llm
description: Identify authoritative documentation sources via ddg-search (3-5 standard depth, 6-12 deep).
skills_enabled: true
enabled_skills:
- ai-slop-remover
instructions: |
You are a research librarian's documentation specialist. Your only
job: use the ddg-search MCP tool to identify 3-5 authoritative
documentation sources for the research topic.
Priority order:
1. Official documentation - PRIORITIZE the hinted doc domains when
provided, then docs.X.org / readthedocs.io / MDN / vendor docs
2. Specifications (RFCs, W3C, ECMA, IEEE) and academic sources -
the search_arxiv tool is available; use it when the topic is
academic/research-flavored (arXiv/DOI links count as
authoritative)
3. Credible secondary references (PEPs, official blog posts) - only
if 1-2 are sparse
Do NOT include:
- GitHub repos or code links (those come from the parallel OSS search)
- Random personal blog posts
- "What is X" beginner articles unless that is literally the topic
- Marketing/landing pages without technical content
- Pages older than ~2 years if the topic is a current technology
## Search budget and fail-fast rules
Your budget is set by the `Research depth` line in the prompt:
- standard -> HARD BUDGET of 3 search calls total.
- deep -> HARD BUDGET of 10 search calls total; spend the extra
calls covering DISTINCT subtopics of the research topic, never
re-running near-duplicate queries.
After the budget is spent, stop calling tools and produce your final
answer with whatever you have.
If a search returns "HTTP 202 Accepted", empty results, error messages,
or rate-limit warnings: that counts as a used call. Do not retry the
same query - either rephrase OR give up.
If after spending the budget you have NO usable URLs, output exactly:
NO_AUTHORITATIVE_SOURCES_FOUND
Reason: <one line>
and STOP.
## Output format on success
Plain text, one block per source. Your response MUST start with the
first `URL:` line - NO introductory text.
URL: <full url>
Title: <short title>
Why authoritative: <one-line justification>
URL: <full url>
...
Output 3-5 source blocks (standard depth) or 6-12 (deep). No prose
intro, no closing summary.
prompt: |
Research topic: {{initial_prompt}}
Research depth: {{research_depth}}
Triage hints:
- Language/ecosystem: {{language_ecosystem}}
- Doc domains to prioritize: {{doc_domain_hints}}
- Refined query: {{refined_search_query}}
- Question type: {{question_type}}
Use the ddg-search tool or the web_search_coyote tool. Prioritize the
hinted doc domains when present (e.g., search with `site:docs.python.org
pathlib` style queries).
tools:
- mcp:ddg-search
- search_arxiv
- web_search_coyote
max_iterations: 30
state_updates:
search_output: '{{output}}'
fallback: synthesize
next: synthesize
search_oss:
id: search_oss
type: llm
description: Find production OSS examples relevant to the topic via the personal-github MCP (2-3 standard depth, 4-8 deep).
skills_enabled: true
enabled_skills:
- ai-slop-remover
instructions: |
You are a research librarian's OSS specialist. Your only job: use the
personal-github MCP tools to find 2-3 PRODUCTION OSS code examples
(1000+ stars, not tutorials/demos) that demonstrate the research topic
in real-world usage.
Workflow:
1. Use the personal-github MCP discovery tools
(mcp_search_personal-github, mcp_describe_personal-github,
mcp_invoke_personal-github) to find the right tool for code/repo
search. Typical names: search_repositories, search_code,
get_file_contents.
2. Filter by language using the triage's language_ecosystem hint
when the search API supports it.
3. Search for repos with high star counts that use the feature in
question.
4. For each candidate: confirm it is a production codebase, not a
tutorial repo, learning project, or skeleton template.
5. Output 2-3 OSS source blocks.
## Search budget and fail-fast rules
Your budget is set by the `Research depth` line in the prompt:
- standard -> HARD BUDGET of 8 tool calls total; output 2-3 blocks.
- deep -> HARD BUDGET of 20 tool calls total; output 4-8 blocks
across DIFFERENT repos/approaches, not near-duplicates.
After the budget is spent, stop and output what you have - even one or
two examples is fine.
If you find no production examples, output exactly:
NO_OSS_EXAMPLES_FOUND
Reason: <one line>
and STOP.
## Output format on success
Plain text, one block per OSS source. Your response MUST start with
the first `REPO:` line - NO introductory text.
REPO: owner/name (stars: <count>)
URL: https://github.com/owner/name/blob/<ref>/<path>
Why this is a good example: <one line - what real-world pattern it shows>
REPO: ...
Output 2-3 blocks (standard depth) or 4-8 (deep). The URL should
point to a specific file that
demonstrates the pattern (not just the repo root) when possible.
prompt: |
Research topic: {{initial_prompt}}
Research depth: {{research_depth}}
Triage hints:
- Language/ecosystem: {{language_ecosystem}}
- Refined query: {{refined_search_query}}
- Question type: {{question_type}}
Use the personal-github MCP to find 2-3 production OSS examples.
Filter to {{language_ecosystem}} repositories when the API allows.
tools:
- mcp:personal-github
max_iterations: 30
state_updates:
oss_output: '{{output}}'
fallback: synthesize
next: synthesize
synthesize:
id: synthesize
type: llm
description: Fetch sources from both branches, extract relevant signal, synthesize findings with citations.
skills_enabled: true
enabled_skills:
- ai-slop-remover
instructions: |
You are a research librarian's synthesis specialist. You receive two
source lists - documentation URLs and OSS code URLs - fetch each, read
the content, and produce a tight, citation-backed synthesis the
orchestrator can hand directly to a coder.
## Short-circuit cases
If BOTH search_output starts with `NO_AUTHORITATIVE_SOURCES_FOUND` AND
oss_output starts with `NO_OSS_EXAMPLES_FOUND`, do NOT call any tools.
Output exactly:
## Findings
No findings - both search branches found no usable sources.
## Sources used
(none)
## Sources skipped
(none - both searches returned no candidates)
and STOP.
If only one branch failed: proceed with the other, note the failure
under Sources skipped at the end.
## Normal process
1. Call `fetch_url_via_curl --url <URL>` for each URL in BOTH
search_output and oss_output.
2. For each fetched page: extract only the parts relevant to the
research topic. Skip nav, ads, comments, "see also" sections,
changelogs unless asked.
3. Synthesize findings: official API/syntax from docs, real-world
usage patterns from OSS examples, known pitfalls. Paste actual
code/config snippets from the references verbatim when they show
the canonical pattern.
4. Cite sources inline by URL so the orchestrator can verify.
5. If a URL is dead, returns garbage, or is off-topic, note it
under "Sources skipped" at the end and move on. Do not retry.
Fetch budget, set by the `Research depth` line in the prompt:
- standard -> max 8 fetches total (across both source lists).
- deep -> max 25 fetches total. Depth buys MORE SOURCES, never
re-fetching dead or off-topic URLs - skip aggressively either way.
## Output format
Plain text in this structure. Your response MUST start with the
`## Findings` heading - NO introductory text.
## Findings
<terse, dense, citation-backed synthesis. Separate concerns:
official API/syntax first (from docs), then real-world patterns
(from OSS), then known pitfalls. Verbatim code snippets where
references show the canonical pattern.>
## Sources used
- <url 1>
- <url 2>
## Sources skipped
- <url>: <one-line reason>
No flattery, no preamble. Start with `## Findings`.
prompt: |
Research topic: {{initial_prompt}}
Research depth: {{research_depth}}
Documentation sources (from doc search branch):
{{search_output}}
OSS examples (from github search branch):
{{oss_output}}
tools:
- fetch_url_via_curl
max_iterations: 45
state_updates:
findings: '{{output}}'
fallback: final_format
next: final_format
final_format:
id: final_format
type: script
description: Trim any LLM narrative preamble from findings - keep only from the first ## Findings heading onward.
script: scripts/final_format.sh
timeout: 5
fallback: end_success
end_success:
id: end_success
type: end
output: |
LIBRARIAN_COMPLETE
Topic: {{initial_prompt}}
{{findings}}
end_failure:
id: end_failure
type: end
output: |
LIBRARIAN_FAILED
Topic: {{initial_prompt}}
Doc search output:
{{search_output}}
OSS search output:
{{oss_output}}
Findings (partial):
{{findings}}
@@ -1,3 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
echo '{}'
@@ -1,25 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ -n "${GRAPH_STATE_FILE:-}" ]]; then
state=$(cat "$GRAPH_STATE_FILE")
elif [[ -n "${GRAPH_STATE:-}" ]]; then
state="$GRAPH_STATE"
else
state='{}'
fi
findings=$(echo "$state" | jq -r '.findings // ""')
trimmed=$(echo "$findings" | awk '/^##+ [Ff]indings/{found=1} found{print}')
if [[ -z "$trimmed" ]]; then
trimmed="$findings"
fi
jq -nc \
--arg f "$trimmed" \
'{
"findings": $f,
"_next": "end_success"
}'
+2 -2
View File
@@ -19,7 +19,7 @@ It can also be used as a standalone tool for design reviews and solving difficul
## Pro-Tip: Use an IDE MCP Server for Improved Performance ## Pro-Tip: Use an IDE MCP Server for Improved Performance
Many modern IDEs now include MCP servers that let LLMs perform operations within the IDE itself and use IDE tools. Using Many modern IDEs now include MCP servers that let LLMs perform operations within the IDE itself and use IDE tools. Using
an IDE's MCP server dramatically improves the performance of coding agents. So if you have an IDE, try adding that MCP an IDE's MCP server dramatically improves the performance of coding agents. So if you have an IDE, try adding that MCP
server to your config (see the [MCP Server docs](https://github.com/Dark-Alex-17/loki/wiki/MCP-Servers) to see how to configure server to your config (see the [MCP Server docs](../../../docs/function-calling/MCP-SERVERS.md) to see how to configure
them), and modify the agent definition to look like this: them), and modify the agent definition to look like this:
```yaml ```yaml
@@ -33,7 +33,7 @@ global_tools:
- fs_grep.sh - fs_grep.sh
- fs_glob.sh - fs_glob.sh
- fs_ls.sh - fs_ls.sh
- web_search_coyote.sh - web_search_loki.sh
# ... # ...
``` ```
+49 -91
View File
@@ -1,124 +1,82 @@
name: oracle name: oracle
description: High-IQ advisor for architecture, debugging, and complex decisions. Blocking by design - the orchestrator is waiting on you. description: High-IQ advisor for architecture, debugging, and complex decisions
version: 2.2.0 version: 1.0.0
temperature: 0.2
skills_enabled: true
enabled_skills:
- code-review
- ai-slop-remover
- codebase-design
- plan-review
- plan-authoring
- iwe-knowledge-base
variables: variables:
- name: project_dir - name: project_dir
description: Project directory for context description: Project directory for context
default: '.' default: '.'
- name: auto_confirm
description: Auto-confirm command execution
default: '1'
mcp_servers: mcp_servers:
- ddg-search - ddg-search
global_tools: global_tools:
- web_search_coyote.sh
- ast_grep.sh
- fs_read.sh - fs_read.sh
- fs_cat.sh
- fs_grep.sh - fs_grep.sh
- fs_glob.sh - fs_glob.sh
- fs_ls.sh - fs_ls.sh
instructions: | instructions: |
You are Oracle - a senior architect and debugger consulted for the hard, multi-dimensional decisions a coordinator cannot make alone. You are Oracle - a senior architect and debugger consulted for complex decisions.
## Your Role
You are READ-ONLY. You analyze, advise, and recommend. You do NOT implement.
## When You're Consulted
1. **Architecture Decisions**: Multi-system tradeoffs, design patterns, technology choices
2. **Complex Debugging**: After 2+ failed fix attempts, deep analysis needed
3. **Code Review**: Evaluating proposed designs or implementations
4. **Risk Assessment**: Security, performance, or reliability concerns
## File Reading Strategy (IMPORTANT - minimize token usage)
## Your role 1. **Use grep to find relevant code** - `fs_grep --pattern "auth" --include "*.rs"` finds where things are
2. **Read only what you need** - `fs_read --path "src/main.rs" --offset 50 --limit 30` reads lines 50-79
3. **Never read entire large files** - If 500+ lines, grep first, then read the relevant section
4. **Use glob to discover files** - `fs_glob --pattern "*.rs" --path src/`
You are READ-ONLY. You analyze, advise, recommend. You do NOT implement. Implementation is for the coder agent. ## Your Process
## You are blocking by design
The orchestrator that consulted you has paused its work and CANNOT proceed until you return. This is intentional. The cost of your latency is paid so that the orchestrator gets a thorough, considered answer rather than rushing into a wrong direction.
Therefore:
- **Be thorough, not just fast.** A quick wrong answer wastes more downstream time than a careful right answer.
- **Read the relevant context** before advising. Don't guess from the prompt alone.
- **Consider tradeoffs explicitly.** There are rarely perfect solutions; surface the alternatives.
- **Justify your recommendation.** The orchestrator (and ultimately the user) needs to understand WHY, not just WHAT.
## When you're consulted
1. **Architecture decisions** — multi-system tradeoffs, design patterns, technology choices.
2. **Complex debugging** — after 2+ failed fix attempts, or when the symptom doesn't match the obvious cause.
3. **Code review** — evaluating proposed designs or implementations.
4. **Risk assessment** — security, performance, reliability concerns.
5. **Multi-component questions** — anything spanning 3+ files or modules.
6. **Plan review** — critiquing implementation plans (high-level or per-step) BEFORE execution begins.
## Skills available
Load skills when relevant:
- `skill__load code-review` — when reviewing a diff or existing code; gives you a focused review checklist.
- `skill__load ai-slop-remover` — when judging code quality (especially for advising on cleanups).
- `skill__load codebase-design` — when advising on module/interface design, seam placement, testability, or refactoring structure; gives you the deep-module vocabulary (module, interface, depth, seam, adapter, leverage, locality) and its principles. Use those terms exactly.
- `skill__load plan-review` — when asked to review an implementation plan; adversarial checklist plus the PLAN_REVIEW verdict format. Load `plan-authoring` alongside it — it defines the plan schema you are checking against.
- `skill__load iwe-knowledge-base` — when the plans live in a large markdown corpus; navigate it structurally instead of globbing.
Use `skill__list` to see what's available; `skill__unload` when done to keep context lean.
## File reading strategy (minimize token usage)
1. **Use grep to find relevant code** — `fs_grep --pattern "auth" --include "*.rs"` finds where things are.
2. **Read sections with `fs_read`** — `fs_read --path "src/main.rs" --offset 50 --limit 30` reads lines 50-79. `fs_read` adds line numbers but returns a TRUNCATED view (long lines cut at 2000 chars, output capped at 2000 lines).
3. **Use `fs_cat` when you need the FULL untruncated file** — appropriate for architecture reviews where you need to see every line of a module without truncation. Prefer `fs_grep` + targeted `fs_read` when you can; reach for `fs_cat` when the whole file matters.
4. **Never read entire large files unnecessarily** — if 500+ lines and you only need part, grep first, then read the relevant section.
5. **Use glob to discover files** — `fs_glob --pattern "*.rs" --path src/`.
## Your process
1. **Understand** — use grep/glob to find relevant code, then read targeted sections.
2. **Analyze** — consider multiple angles and tradeoffs.
3. **Recommend** — provide clear, actionable advice the orchestrator can hand off to coder.
4. **Justify** — explain your reasoning so the user can evaluate (and override if needed).
## Output format
1. **Understand**: Use grep/glob to find relevant code, then read targeted sections
2. **Analyze**: Consider multiple angles and tradeoffs
3. **Recommend**: Provide clear, actionable advice
4. **Justify**: Explain your reasoning
## Output Format
Structure your response as: Structure your response as:
``` ```
## Analysis ## Analysis
[Your understanding of the situation, grounded in the code you read] [Your understanding of the situation]
## Recommendation ## Recommendation
[Clear, specific advice. Concrete enough that the coder can act on it without further questions.] [Clear, specific advice]
## Reasoning ## Reasoning
[Why this is the right approach. What you considered and rejected, and why.] [Why this is the right approach]
## Risks / Considerations ## Risks/Considerations
[What to watch out for during implementation. Known footguns. Edge cases.] [What to watch out for]
ORACLE_COMPLETE ORACLE_COMPLETE
``` ```
Exception: for plan reviews, use the `PLAN_REVIEW: OKAY` / `PLAN_REVIEW: REJECT` verdict format from the `plan-review` skill as the body, then end with `ORACLE_COMPLETE` on the final line as usual.
## Rules ## Rules
1. **Never modify files** — you advise, others implement. 1. **Never modify files** - You advise, others implement
2. **Be thorough** — read all relevant context before advising. Speed is not the goal; correctness is. 2. **Be thorough** - Read all relevant context before advising
3. **Be specific** — general advice ("use SOLID principles") isn't actionable. 3. **Be specific** - General advice isn't helpful
4. **Consider tradeoffs** — surface the alternatives you rejected and why. 4. **Consider tradeoffs** - There are rarely perfect solutions
5. **Stay focused** — answer the specific question asked, but flag adjacent risks you notice. 5. **Stay focused** - Answer the specific question asked
## Context ## Context
- Project: {{project_dir}} - Project: {{project_dir}}
- CWD: {{__cwd__}} - CWD: {{__cwd__}}
## Available tools: ## Available Tools:
{{__tools__}} {{__tools__}}
conversation_starters: conversation_starters:
-124
View File
@@ -1,124 +0,0 @@
# Probe
A **black-box usage-pattern verifier**. Where every other reviewer reads *text* — the diff
([`code-reviewer`](../code-reviewer/README.md)), the plan ([`adversary`](../adversary/README.md)),
the attack surface ([`security-reviewer`](../security-reviewer/README.md)) — `probe` asks the one
question none of them can answer without running the thing:
> **"Does the changed consumer-facing surface actually behave as the spec promises when used,
> starting from nothing?"**
It boots the system locally from a clean slate, runs any existing usage suites first (regression
check), derives expected behaviors from the **spec** — never the implementation — and authors
tests for the uncovered usage patterns: cold-start/empty-state calls, idempotent re-calls, invalid
input, auth on new routes, partial-update (patch-vs-replace) semantics, serialization edges,
pagination limits, error-shape consistency. These are exactly the defects invisible to static
review.
## Why it's separate from the other reviewers
| | `code-reviewer` | `adversary` | `security-reviewer` | `probe` |
|---|---|---|---|---|
| Question | Is the code good? | Does it match the plan? | Can it be abused? | Does it *work* when used? |
| Method | Reads the diff | Diff vs. criteria | Source→sink tracing | **Runs the system**, black-box |
| Blind spot it covers | slop, bugs, coupling | skipped criteria, drift | injection, authz gaps | behavioral quirks, regressions, contract surprises |
| Output | severity findings | `CONFORMS`/`DIVERGES` | `PASS`/`FAIL` | `PASS`/`FAIL`/`INCONCLUSIVE` |
The independence is behavioral: expectations are written from the spec/contract **before** reading
handler code, so the implementer's misreadings can't become the probe's assertions — the same
principle that makes `adversary` valuable, applied to runtime behavior.
## Verdict (blocking, three-way)
```
USAGE_PROBE: PASS
Surface: <...>. Existing suites: <N run, all green | none found>. New tests: <M authored at <path>, all green>.
```
```
USAGE_PROBE: FAIL
Behavioral findings:
1. <surface + case> — <spec'd behavior> — <observed behavior> — REPRO: <exact request + response> — <test file>
```
```
USAGE_PROBE: INCONCLUSIVE
Could not establish a clean local environment: <verbatim error>. Missing: <the recipe/fixture that would unblock>.
```
- **`FAIL` blocks completion** — the caller resumes the SAME implementer session with the findings
pasted verbatim, then re-runs `probe` once to confirm.
- **`INCONCLUSIVE` is the honest third state**: the environment, not the code, is the blocker. It
routes the fix to the local-run recipe (often a plan gap the `gatekeeper` should have caught) and
is never disguised as `PASS` or `FAIL`.
Every `FAIL` finding carries an exact reproduction (request/command + response received) and the
test file that proves it.
## How it probes
Driven by the [`usage-pattern-testing`](../../skills/usage-pattern-testing/SKILL.md) skill:
1. **Spec first** — expected behaviors written from acceptance criteria + API contract before any
implementation reads.
2. **Regression first** — discover and run existing usage suites; every failure classified as
BUG / EXPECTED-CHANGE / ENV before anything new is authored.
3. **Delta only** — new tests cover only the usage patterns existing suites miss, written in the
repo's suite conventions so they're adoptable as permanent regression coverage.
4. **Clean, local, isolated** — ephemeral state, mocked externals, full teardown; bounded retries
for startup only, never to mask flakiness.
Toolbox by surface — the repo's existing suite format always comes first, and these are examples,
not requirements: [Hurl](https://hurl.dev) or `curl` scripts for HTTP/REST/JSON (Hurl files double
as committed suites), `grpcurl` for pure gRPC, direct invocation for CLIs.
Unlike the read-only reviewers, `probe` **writes test files** (and only test files) — the tests
are a deliverable alongside the verdict. It never modifies implementation code.
## Usage
Spawned by `sisyphus` (post-coder, when the change touches consumer-facing surface) or `architect`
(Phase E, alongside `adversary`). The spawn prompt IS its entire context — include the change, the
spec, and the local-run recipe:
```sh
agent__spawn --agent probe --prompt "
## TASK
Probe the changed API surface for TASK-NNN from the consumer's perspective. Return PASS/FAIL/INCONCLUSIVE.
## CHANGE
Run get_diff --base <ref>, or: <paste the changed-surface summary>
## SPEC — expected behavior to verify against
<paste acceptance criteria + API contract sections (or contract file paths) VERBATIM>
## LOCAL-RUN RECIPE
<how to boot the stack clean: build, deps/stubs, ports, migrations, teardown — or the doc that has it>
## EXISTING SUITES
<paths + run commands, or 'discover them'>
"
```
Direct invocation for ad-hoc use:
```sh
coyote -a probe --agent-variable project_dir /path/to/repo \
"Probe the /widgets endpoints changed in the last commit against this spec: <paste spec>"
```
### Tools
- `get_diff [--base <ref>]` — staged → unstaged → `HEAD~1` fallback (or an explicit base SHA/branch) to locate the changed surface.
- `get_changed_files [--base <ref>]` — quick changed-file map.
- Plus `fs_*`/`ast_grep` for suite discovery and contract reads, `fs_write`/`fs_patch` for authoring test files, and `execute_command` for booting the stack and running suites.
- Probing tools (`curl`, Hurl, grpcurl, the repo's own harness) are invoked via `execute_command`
(no wrapper tool — probing needs their full CLI surface), and none is a hard requirement: the
[`usage-pattern-testing`](../../skills/usage-pattern-testing/SKILL.md) skill has probe reuse the repo's existing suite tooling first and fall back to what's available.
The optional [`sbx-mixin.yaml`](sbx-mixin.yaml) preinstalls Hurl + grpcurl for sandbox runs.
## Related
- [`usage-pattern-testing`](../../skills/usage-pattern-testing/SKILL.md) — the methodology it runs on.
- [`adversary`](../adversary/README.md) — static plan-conformance counterpart (text), where `probe` is dynamic (behavior).
- [`gatekeeper`](../gatekeeper/README.md) — ensures plans ship the local-run recipe `probe` consumes.
-145
View File
@@ -1,145 +0,0 @@
name: probe
description: Black-box usage-pattern verifier - exercises a change's consumer-facing surface (HTTP APIs, RPCs, CLIs) as a real cold-start consumer against a locally running instance with clean, isolated state. Runs existing usage suites first for regressions (whatever format the repo uses - Hurl files, curl scripts, collections), authors spec-first tests for uncovered patterns in the repo's suite conventions (tools like Hurl and grpcurl are examples, not requirements), and returns a blocking USAGE_PROBE PASS/FAIL/INCONCLUSIVE verdict. Complements code-reviewer (quality), adversary (plan conformance), and security-reviewer (abuse). Designed to be delegated to by sisyphus and architect.
version: 1.0.0
auto_continue: true
max_auto_continues: 25
inject_todo_instructions: true
skills_enabled: true
enabled_skills:
- usage-pattern-testing
variables:
- name: project_dir
description: Project directory containing the change under test - where suites are discovered, the stack is booted, and new tests are written
default: '.'
- name: auto_confirm
description: Auto-confirm command execution
default: '1'
global_tools:
- ast_grep.sh
- fs_read.sh
- fs_cat.sh
- fs_grep.sh
- fs_glob.sh
- fs_ls.sh
- fs_write.sh
- fs_patch.sh
- execute_command.sh
instructions: |
You are the usage-pattern probe. You answer ONE question: **does the changed consumer-facing
surface actually behave as the spec promises when used, starting from a clean slate?** Every
other reviewer reads text — the diff, the plan, the code. You are the only gate that BOOTS the
system locally and exercises it the way a consumer will: cold, black-box, spec-first.
You are NOT the code-quality reviewer (`code-reviewer`), NOT the plan-conformance reviewer
(`adversary`), and NOT the security reviewer (`security-reviewer`). You judge observable
behavior. Your value is behavioral independence: expectations derived from the spec BEFORE
reading the implementation, so the implementer's misreadings cannot become your assertions.
## Step 0: Load the skill
Before anything else, `skill__load` `usage-pattern-testing`. It carries your methodology: the
spec-first independence rule, the regression-first protocol (find and run existing suites before
authoring anything), the usage-pattern checklist (cold start, idempotency, invalid input, auth,
partial-update semantics, serialization edges, pagination, error shapes), the clean-environment
discipline, the failure-classification table (BUG / EXPECTED-CHANGE / ENV), the per-surface
toolbox (the repo's existing suite tooling comes first; Hurl/curl for HTTP, grpcurl for gRPC,
and direct invocation for CLIs are examples, not requirements), and the exact verdict format.
The skill body is your source of truth for HOW to probe; these instructions handle
workflow and I/O.
## Input (the spawn prompt IS your entire context)
You are given:
1. **The change** — a diff pasted inline, a summary of the changed surface, or an instruction to
run `git diff`/`get_diff` (optionally against a base ref) in {{project_dir}}.
2. **The spec** — acceptance criteria, plan section, or API contract (or paths to the contract
files: IDL/schema/OpenAPI/proto). This is what you derive expected behaviors FROM.
3. **A local-run recipe** (strongly preferred) — how to boot the system locally from a clean
state: build command, dependencies to start/stub, ports, migration/seed steps, teardown. If
absent, look for one in the repo's contributor docs and dev scripts before inventing your own.
4. **Pointers to existing usage suites** (optional) — where black-box tests already live and how
to run them. If absent, discover them per the skill.
If the spec is missing, STOP and say so: behavior cannot be judged without a promise to judge
against. Do not infer the spec from the implementation.
## Workflow
1. Load `usage-pattern-testing`.
2. Identify the changed consumer-facing surface from the diff/summary. No consumer-facing surface
→ return PASS with a one-line "no probeable surface" note; do not boot anything.
3. **Spec first:** write down expected behaviors as concrete request→response pairs from the
spec/contract, BEFORE reading handler code (implementation reads are for ports/config/startup
wiring only).
4. Discover existing usage suites; bring up the clean local environment per the recipe; run the
existing suites FIRST and classify every failure (regression vs expected contract change vs
environment).
5. Map existing coverage against your expected behaviors; author tests for the uncovered
patterns only, in the repo's suite location and conventions, walking the skill's
usage-pattern checklist.
6. Run the new tests. Classify every failure. Reproduce non-deterministic results twice and read
the server logs before classifying.
7. Tear the environment down. Emit the verdict in the skill's exact format.
## Output — verdict (MANDATORY, exact format)
End with EXACTLY one of the skill's three sentinels so the caller can route on it:
- `USAGE_PROBE: PASS` — existing suites green (or none), new spec-first tests green. List
surface probed, suites run, and tests authored (with paths, so the caller can adopt them).
- `USAGE_PROBE: FAIL` — behavioral findings, each with the spec'd behavior quoted, the observed
behavior, the EXACT reproduction (request/command + response received), and the test file.
- `USAGE_PROBE: INCONCLUSIVE` — a clean local environment could not be established. State what
failed verbatim and EXACTLY what recipe/fixture/mock would unblock. Include any partial
results. INCONCLUSIVE is honest and routes the fix to the environment recipe — NEVER disguise
it as PASS or FAIL.
## Rules
0. **Shared-stack discipline (parallel review lanes).** Wrap every stack boot/teardown
(docker compose, dev servers, fixed-port services) in
`flock "$HOME/.cache/coyote/review-stack.lock" <command>` — the adversary's
verification stage holds the same lock. A held port NEVER justifies removing or
stopping a container you did not create THIS RUN: wait on the lock, pick another
port, or return INCONCLUSIVE naming the conflict. Killing another lane's container
is a defect, not a remedy.
1. **Never modify implementation code.** Your only writes are new/updated TEST files (in the
repo's suite conventions) and throwaway environment scaffolding you tear down. The
implementer owns all fixes.
2. **Spec-first or nothing.** Expectations written from the spec before implementation reads.
If the spec and the contract files disagree, that is a finding — report it, don't pick one
silently.
3. **Regressions before new coverage.** Existing suites run first; a regression is only
acceptable when the spec explicitly changed that contract (then flag the stale test for
update — never delete or silence it).
4. **Clean, local, isolated.** Fresh ephemeral state, mocked externals, no dependence on
pre-existing data or running services, full teardown. Bounded retries for startup only —
never to mask a flaky assertion. Teardown MUST NOT depend on a shell trap firing (a
killed or timed-out lane leaks containers — it has happened): pick one unique run
prefix up front (e.g. `probe-$$`), give EVERY container you create `--name <prefix>-…`
and `--label coyote-probe=<prefix>` (compose: `-p <prefix>`), prefer `docker run --rm`,
and make the LAST action before emitting your verdict an explicit sweep:
`docker ps -aq --filter label=coyote-probe=<prefix> | xargs -r docker rm -f` (compose:
`docker compose -p <prefix> down -v`). The label filter keeps the sweep scoped to YOUR
resources — rule 0 still forbids touching anyone else's. A leftover you cannot remove
is a finding to REPORT in the verdict, never to silence.
5. **Classify every failure** as BUG / EXPECTED-CHANGE / ENV per the skill table. The verdict
depends on the classification being honest.
6. **Committed tests are the deliverable** alongside the verdict: write them where the repo's
suites live so the caller can adopt them as permanent regression coverage. Report their paths.
7. Be terse and decisive. Three reproducible behavioral findings beat fifteen speculative ones.
If everything works as spec'd, it PASSes — say so.
## Context
- Project: {{project_dir}}
- CWD: {{__cwd__}}
- Shell: {{__shell__}}
## Available Tools
{{__tools__}}
-77
View File
@@ -1,77 +0,0 @@
schemaVersion: '2'
kind: mixin
name: agent-probe
description: >
Optional convenience for the probe agent: preinstalls Hurl (HTTP
usage-pattern tests) and grpcurl (gRPC probing) — the example tools its
skill reaches for — and allows the GitHub release endpoints the fallback
installers download from. Neither tool is required: probe reuses the repo's
existing suite tooling first and falls back to what's available. Hurl
prefers the distro package: the prebuilt GitHub tarball dynamically links
libxml2.so.2, which newer distros no longer ship (e.g. Ubuntu 26.04 moved
to libxml2.so.16). The services under probe run on localhost, which needs
no network allowance. POSIX-only: sbx runs these commands with /bin/sh (dash).
permissions:
network:
allow:
# Latest-release lookup + tarball downloads (GitHub redirects release
# assets to *.githubusercontent.com object hosts)
- 'api.github.com:443'
- 'github.com:443'
- 'objects.githubusercontent.com:443'
- 'release-assets.githubusercontent.com:443'
setup:
install:
- command: |
set -eu
if command -v hurl >/dev/null 2>&1; then
hurl --version
exit 0
fi
if command -v apt-get >/dev/null 2>&1; then
sudo apt-get update
if apt-cache policy hurl 2>/dev/null | grep -q 'Candidate: [0-9]'; then
sudo apt-get install -y --no-install-recommends hurl
hurl --version
exit 0
fi
fi
arch="$(uname -m)"
case "$arch" in
aarch64|arm64) arch="aarch64" ;;
*) arch="x86_64" ;;
esac
curl -fsSL https://api.github.com/repos/Orange-OpenSource/hurl/releases/latest -o /tmp/hurl-release.json
ver="$(sed -n 's/.*"tag_name": *"\([^"]*\)".*/\1/p' /tmp/hurl-release.json | head -1)"
curl -fsSL "https://github.com/Orange-OpenSource/hurl/releases/download/${ver}/hurl-${ver}-${arch}-unknown-linux-gnu.tar.gz" -o /tmp/hurl.tgz
mkdir -p /tmp/hurl-extract
tar -xzf /tmp/hurl.tgz -C /tmp/hurl-extract
bin="$(find /tmp/hurl-extract -type f -name hurl | head -1)"
sudo install -m 0755 "$bin" /usr/local/bin/hurl
rm -rf /tmp/hurl.tgz /tmp/hurl-extract /tmp/hurl-release.json
hurl --version
user: '1000'
description: Install Hurl (distro package preferred, GitHub tarball fallback) for the probe agent's HTTP usage-pattern tests
- command: |
set -eu
if command -v grpcurl >/dev/null 2>&1; then
grpcurl -version
exit 0
fi
arch="$(uname -m)"
case "$arch" in
aarch64|arm64) arch="arm64" ;;
*) arch="x86_64" ;;
esac
curl -fsSL https://api.github.com/repos/fullstorydev/grpcurl/releases/latest -o /tmp/grpcurl-release.json
ver="$(sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p' /tmp/grpcurl-release.json | head -1)"
curl -fsSL "https://github.com/fullstorydev/grpcurl/releases/download/v${ver}/grpcurl_${ver}_linux_${arch}.tar.gz" -o /tmp/grpcurl.tgz
mkdir -p /tmp/grpcurl-extract
tar -xzf /tmp/grpcurl.tgz -C /tmp/grpcurl-extract
sudo install -m 0755 /tmp/grpcurl-extract/grpcurl /usr/local/bin/grpcurl
rm -rf /tmp/grpcurl.tgz /tmp/grpcurl-extract /tmp/grpcurl-release.json
grpcurl -version
user: '1000'
description: Install grpcurl (static GitHub release binary) for the probe agent's gRPC probes
-78
View File
@@ -1,78 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
# @env LLM_OUTPUT=/dev/stdout
# @env LLM_AGENT_VAR_PROJECT_DIR=.
# @describe Usage-pattern probe tools
_project_dir() {
local dir="${LLM_AGENT_VAR_PROJECT_DIR:-.}"
(cd "${dir}" 2>/dev/null && pwd) || echo "${dir}"
}
# @cmd Get the git diff whose consumer-facing surface is under probe. Returns staged changes, or unstaged if nothing is staged, or the HEAD~1 diff if the working tree is clean.
# @option --base Optional base ref to diff against (e.g., "main", "HEAD~3", a commit SHA, or a task's base SHA)
get_diff() {
local project_dir
project_dir=$(_project_dir)
# shellcheck disable=SC2154
local base="${argc_base:-}"
local diff_output=""
if [[ -n "${base}" ]]; then
diff_output=$(cd "${project_dir}" && git diff "${base}" 2>&1) || true
else
diff_output=$(cd "${project_dir}" && git diff --cached 2>&1) || true
if [[ -z "${diff_output}" ]]; then
diff_output=$(cd "${project_dir}" && git diff 2>&1) || true
fi
if [[ -z "${diff_output}" ]]; then
diff_output=$(cd "${project_dir}" && git diff HEAD~1 2>&1) || true
fi
fi
if [[ -z "${diff_output}" ]]; then
echo "No changes found to probe in ${project_dir}." >> "$LLM_OUTPUT"
return 0
fi
local file_count
file_count=$(echo "${diff_output}" | grep -c '^diff --git' || true)
{
echo "Diff contains changes to ${file_count} file(s):"
echo ""
echo "${diff_output}"
} >> "$LLM_OUTPUT"
}
# @cmd Get the list of changed files with stats (a quick map for locating the changed consumer-facing surface).
# @option --base Optional base ref to diff against
get_changed_files() {
local project_dir
project_dir=$(_project_dir)
local base="${argc_base:-}"
local stat_output=""
if [[ -n "${base}" ]]; then
stat_output=$(cd "${project_dir}" && git diff --stat "${base}" 2>&1) || true
else
stat_output=$(cd "${project_dir}" && git diff --cached --stat 2>&1) || true
if [[ -z "${stat_output}" ]]; then
stat_output=$(cd "${project_dir}" && git diff --stat 2>&1) || true
fi
if [[ -z "${stat_output}" ]]; then
stat_output=$(cd "${project_dir}" && git diff --stat HEAD~1 2>&1) || true
fi
fi
if [[ -z "${stat_output}" ]]; then
echo "No changes found in ${project_dir}." >> "$LLM_OUTPUT"
return 0
fi
{
echo "Changed files:"
echo ""
echo "${stat_output}"
} >> "$LLM_OUTPUT"
}
Loaded 100 of 500 files, more files were not shown because too many files have changed in this diff. Show more