This document describes the process for adding new git commands to zagi.
- Zig 0.15+
- Bun (for integration tests)
- zagi is always lowercase
- No emojis in code or output
- Concise output optimized for agents
When committing code, always include the complete user prompt that created the change:
git commit -m "<commit-message>" --prompt "<complete-user-prompt-for-this-commit>"The --prompt should contain the full user request that led to this commit, not a summary. This preserves context for code review.
Example:
git commit -m "Add logout button" --prompt "Add a logout button to the header. When clicked it should clear the session and redirect to /login"When --prompt is used, zagi automatically stores metadata in git notes:
refs/notes/agent- detected AI agent (claude, opencode, cursor, windsurf, vscode, terminal)refs/notes/prompt- the user prompt textrefs/notes/session- full session transcript (for Claude Code, OpenCode)
View metadata with:
git log --prompts # show prompts (truncated to 200 chars)
git log --agent # show which AI agent made each commit
git log --session # show session transcript (paginated, first 20k bytes)
git log --session --session-offset=20000 # continue from byte 20000Agent mode is automatically enabled when running inside AI tools:
- Claude Code (sets
CLAUDECODE=1) - OpenCode (sets
OPENCODE=1) - VS Code, Cursor, Windsurf (detected from terminal environment)
You can also enable it manually:
export ZAGI_AGENT=my-agentWhen agent mode is active:
git commitwill fail without--prompt, ensuring all AI-generated commits have their prompts recorded- Destructive commands are blocked to prevent data loss
zagi provides two agent subcommands for autonomous task execution using the RALPH pattern (Recursive Agent Loop Pattern for Humans).
Starts an interactive planning session where an AI agent collaborates with you to design and create tasks.
# Start an interactive session (agent will ask what you want to build)
zagi agent plan
# Start with initial context
zagi agent plan "Add user authentication with JWT"
# Preview the prompt without executing
zagi agent plan --dry-runThe planning agent follows an interactive protocol:
- Explore codebase: Reads AGENTS.md and relevant code to understand architecture
- Ask clarifying questions: Asks about scope, constraints, and preferences before drafting any plan
- Propose plan: Presents a numbered implementation plan for your review
- Create tasks: Only creates tasks after you explicitly approve the plan
This collaborative approach ensures the agent gathers all necessary context before committing to a task breakdown. The agent will ask 2-4 focused questions at a time about:
- Scope: What's included/excluded, edge cases, MVP vs nice-to-haves
- Constraints: Performance requirements, dependencies, compatibility
- Preferences: Approach/patterns, integration with existing code, testing expectations
- Acceptance criteria: How we know it's done, what success looks like
Executes the RALPH loop to automatically complete pending tasks.
# Run until all tasks complete (or fail 3x)
zagi agent run
# Run only one task then exit
zagi agent run --once
# Preview what would run without executing
zagi agent run --dry-run
# Set delay between tasks (default: 2 seconds)
zagi agent run --delay 5
# Safety limit - stop after N tasks
zagi agent run --max-tasks 10The run loop will:
- Pick the next pending task
- Execute it with the configured agent
- Mark it done on success (agent calls
zagi tasks done <task-id>) - Skip tasks that fail 3 consecutive times
- Continue until all tasks complete
Control which AI agent executes tasks using environment variables:
# Use Claude Code (default)
ZAGI_AGENT=claude zagi agent run
# Use opencode
ZAGI_AGENT=opencode zagi agent run
# Use custom command with auto mode flags
ZAGI_AGENT=claude ZAGI_AGENT_CMD="myclaude --flag" zagi agent run
# Use completely custom tool (no auto flags)
ZAGI_AGENT_CMD="aider --yes" zagi agent runSee docs/setup.md for full configuration details.
These commands cause unrecoverable data loss and are blocked when ZAGI_AGENT is set:
| Command | Reason |
|---|---|
reset --hard |
Discards all uncommitted changes |
checkout . |
Discards all working tree changes |
clean -f/-fd/-fx |
Permanently deletes untracked files |
restore . |
Discards all working tree changes |
restore --worktree |
Discards working tree changes |
push --force/-f |
Overwrites remote history |
push --delete |
Deletes remote branch |
push <remote> :<branch> |
Deletes remote branch (refspec syntax) |
stash drop |
Permanently deletes stashed changes |
stash clear |
Permanently deletes all stashes |
branch -D |
Force deletes branch (even if not merged) |
Safe alternatives:
reset --soft- keeps changes stagedreset(no flags) - keeps changes in working treeclean -n- dry run, shows what would be deleted and ask user to deletebranch -d- only deletes if merged
Before implementing, understand the existing git command:
# Run the command and observe output
git <command>
# Test different scenarios
git <command> <args>
# Check exit codes
git <command>; echo "exit: $?"
# Test error cases
git <command> nonexistentDocument:
- What does it output on success?
- What does it output on failure?
- What are common flags/options?
- What exit codes does it use?
Identify what would be better for agents:
| Problem | Solution |
|---|---|
| Silent success (no confirmation) | Show what was done |
| Verbose errors | Concise error messages |
| Multi-line output with decoration | Compact, parseable format |
| Unclear state | Show current state after action |
Key questions:
- What information does an agent need to continue?
- What feedback confirms the action worked?
- Can we reduce output while preserving meaning?
Before implementing, confirm with the user:
- Proposed output format
- Default flags/behavior differences from git
- Error message format
Example:
Proposed `zagi add` output:
Success:
staged: 2 files
A new-file.txt
M changed-file.txt
Error:
error: file.txt not found
Confirm? (y/n)
Create src/cmds/<command>.zig:
const std = @import("std");
const c = @cImport(@cInclude("git2.h"));
const git = @import("git.zig");
pub const Error = error{
NotARepository,
// command-specific errors
};
pub fn run(allocator: std.mem.Allocator, args: [][:0]u8) Error!void {
// Implementation using libgit2
// Return errors instead of calling std.process.exit()
}Important: Never call std.process.exit() in command modules. Always return errors to main.zig for centralized handling. This enables unit testing and keeps exit codes consistent.
Add routing in src/main.zig:
const cmd_name = @import("cmds/<command>.zig");
// ...
} else if (std.mem.eql(u8, cmd, "<command>")) {
cmd_name.run(allocator, args) catch |err| {
try handleError(err);
};
}After implementing similar code twice, ask before abstracting:
"I've now implemented marker functions in both status.zig and add.zig. Should I extract these to a shared git.zig module?"
Only abstract when:
- Same code appears in 2+ places
- The abstraction is obvious and stable
- User confirms it's worth the indirection
Add tests for pure functions in the module:
const testing = std.testing;
test "functionName - description" {
try testing.expectEqualStrings("expected", functionName(input));
}For functions that use std.process.exit(), test via integration tests instead.
Update build.zig to include new test files:
const cmd_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("src/cmds/<command>.zig"),
.target = target,
.optimize = optimize,
}),
});
cmd_tests.root_module.linkLibrary(libgit2_dep.artifact("git2"));zig build # build
zig build test # run zig unit tests
./zig-out/bin/zagi <command>Fix any compiler errors. Test manually with various inputs.
Create test/src/<command>.test.ts:
import { describe, test, expect } from "vitest";
import { execFileSync } from "child_process";
describe("zagi <command>", () => {
test("produces smaller output than git", () => {
// Compare output size
});
test("functional correctness", () => {
// Verify behavior matches git
});
});
describe("performance", () => {
test("zagi is reasonably fast", () => {
// Benchmark timing
});
});Run with:
cd test && bun i && bun run testTo run all tests (Zig + TypeScript):
zig build test && cd test && bun run testIf benchmarks show issues:
- Profile to find bottlenecks
- Consider caching libgit2 state
- Reduce allocations
- Batch operations where possible
src/
main.zig # Entry point, command routing
passthrough.zig # Pass-through to git CLI
cmds/
git.zig # Shared utilities (markers, errors)
log.zig # zagi log
status.zig # zagi status
diff.zig # zagi diff
add.zig # zagi add
commit.zig # zagi commit
<command>.zig # New commands
test/
src/
log.test.ts # log integration tests
status.test.ts # status integration tests
diff.test.ts # diff integration tests
add.test.ts # add integration tests
commit.test.ts # commit integration tests
fixtures/
setup.ts # Test fixture repo creation
Zagi commands only output concise, agent-optimized formats. We don't provide flags like --full or --verbose to get git's standard output.
Reasoning: If a user wants the full git output, they can use the passthrough flag:
zagi -g log # runs: git log
zagi -g diff # runs: git diffThis avoids duplicating git's output formatting in zagi. Every zagi command should do one thing well: provide a concise format optimized for agents.
- Investigate git command behavior
- Design agent-friendly output format
- Confirm API with user
- Implement in
src/cmds/<command>.zig - Add routing in
main.zig - Extract shared code (if 2+ usages, ask first)
- Add Zig unit tests
- Build and test manually
- Add TypeScript integration tests
- Run benchmarks
- Optimize if needed