Mechanically enforced task gates for Claude Code. Quests define acceptance criteria as tree-sitter AST queries — hooks block git commit, git push, and stop until all gates pass.
Gates operate on the parsed AST, not text. Comments and strings are invisible. Can't be cheated.
cargo install tree-sitter-clitree-sitter init-config
mkdir -p ~/.config/tree-sitter/grammars && cd ~/.config/tree-sitter/grammars
git clone --depth 1 https://github.com/tree-sitter/tree-sitter-javascript.git
git clone --depth 1 https://github.com/tree-sitter/tree-sitter-python.git
git clone --depth 1 https://github.com/tree-sitter/tree-sitter-typescript.git
git clone --depth 1 https://github.com/tree-sitter/tree-sitter-rust.gitAdd the grammars directory to ~/.config/tree-sitter/config.json:
{
"parser-directories": [
"/Users/YOU/.config/tree-sitter/grammars"
]
}Verify: tree-sitter parse <any-source-file> should output an S-expression AST.
/plugin marketplace add alecmarcus/claude-plugins
/plugin install odyssey@alecmarcus
- Plan → ExitPlanMode hook fires, sets
.plan-pendingmarker - Define gates → Claude creates quest with tree-sitter gates, marker clears, edits unblocked
- Work → Claude implements the task
- Commit → hook runs all gates. Fails → blocked. Passes → committed, quest auto-archived
- Stop → hook blocks if any gates still failing
| Event | Hook | Behavior |
|---|---|---|
PreToolUse Bash |
enforce-quests.sh |
Blocks git commit/push if gates fail |
PreToolUse Edit/Write |
require-quest.sh |
Blocks code edits if plan approved but no quest defined |
PostToolUse ExitPlanMode |
plan-to-quest.sh |
Sets marker, injects gate-creation instruction |
| Stop | stop-guard.sh |
Blocks stop if gates fail |
/quest create
Or quests are created automatically when exiting plan mode.
/quest check
/quest abandon <name>
Gates use check-ast — a tree-sitter query wrapper bundled with the plugin.
check-ast <file> '<tree-sitter-query>' [--min N] [--max N] [--exact N] [--zero]Function has parameter:
check-ast app.js '(function_declaration
name: (identifier) @fn
parameters: (formal_parameters
(assignment_pattern left: (identifier) @p))
(#eq? @fn "greet")
(#eq? @p "language"))'All calls have 2+ arguments:
check-ast app.js '(call_expression
function: (identifier) @fn
arguments: (arguments (_) (_))
(#eq? @fn "greet"))' --min 2Old name fully removed:
check-ast app.js '((identifier) @id (#eq? @id "oldName"))' --zeroTo discover the right query structure for any file, run tree-sitter parse <file> and inspect the AST node types.
MIT