Saci 0.66.0
dotnet tool install --global Saci --version 0.66.0
dotnet new tool-manifest
dotnet tool install --local Saci --version 0.66.0
#tool dotnet:?package=Saci&version=0.66.0
nuke :add-package Saci --version 0.66.0
saci
Software Agent for Continuous Improvement: a command line tool (and a small web server) that runs autonomous agents on your GitHub repositories. saci picks the work, starts Claude Code headless to do the thinking, and applies the result on GitHub.
| Profile | What it does |
|---|---|
| triager | Looks at open bugs and features. Bugs: searches for duplicates, checks the problem still exists on the current code and tries to reproduce it. Features: checks the request is ready to build and writes acceptance criteria and implementation notes, or the open questions a person must answer, or the slices it should be split into. Posts its conclusion and labels the issue or moves it in a project. Looks again when the reporter answers a request for information. |
| developer | First takes care of the pull requests it already opened: review comments (fix or explain, then resolve the thread), comments people leave, failing checks, conflicts with the base branch, and work it left unfinished. Then takes issues the triager approved (bugs by default), implements each in its own git worktree and opens a draft pull request linked to the issue. |
| reviewer | Checks a pull request: build, tests, does it really solve the issue, breaking changes, and does it work when the application runs (in a real browser with Playwright for a web application). Posts a short review with a confidence (0–100%) and findings by severity (critical, required, suggestion, question); labels an approved pull request and marks it ready for review; otherwise leaves review comments, with code suggestions, for the developer. |
| tester | Nobody gives it an issue: it builds the application, runs its tests, starts it and uses it like a careful tester would (in a real browser with Playwright for web applications), trying the main flows and their edges. Every problem it reproduces a second time from a clean start becomes a GitHub issue with steps, expected and actual behaviour and evidence, after checking it is not already reported; what it saw only once is not filed. The triager then checks those issues like any other bug. |
| learner | Once a saci pull request is merged or closed, studies what people had to correct and proposes the lessons as a pull request on the personas, so the same mistake is not made twice. |
| custom | Any profile you define in the configuration: which issues it takes, what Claude must do, what happens for each outcome. |
saci works with any number of repositories. Each profile has a persona per repository: extra
instructions that tell the agent how that repository is built, tested and run. Personas are files
of the repository (.saci/personas/<profile>.md), read from its default branch like the shared
configuration, so every clone and every colleague's saci follow the same instructions. A persona
describes the repository, never one machine: no paths, ports, databases or tools of a laptop.
Requirements
- .NET SDK 10
git, the GitHub CLIgh(logged in:gh auth login) andclaude(logged in) on thePATH- To move issues in a GitHub Project:
gh auth refresh -s project - For the reviewer's browser test: a Playwright MCP server available to Claude, for example
claude mcp add playwright -- npx @playwright/mcp@latest
Try it without installing
saci.ps1 builds and runs saci straight from this clone. saci works on the repository of the
folder you are in, so call the script by its path from inside any clone:
cd C:\git\my-repo
C:\git\t6\saci\saci.ps1 doctor
C:\git\t6\saci\saci.ps1 triage --dry-run
Install
saci is a .NET tool published on NuGet.org, so anyone with the .NET 10 SDK installs it with:
dotnet tool install --global Saci
If that fails with Settings file 'DotnetToolSettings.xml' was not found in the package, the
machine has an older .NET SDK: the message is the SDK's way of saying it cannot read a tool built
for .NET 10. Check with dotnet --list-sdks, install the .NET 10 SDK and run the command again.
Run it from a folder without a NuGet.Config of its own (your home folder, not inside a repository
with private feeds), so only NuGet.org is asked.
saci changelog lists what changed since the version you run: every release is a merge to
main, so each one names the pull requests it merged and the issues they resolved (--from/--to
for a range, --last 3 for the newest three); the release notes on GitHub carry the same list.
Afterwards saci update installs the newest version (saci update --check only tells whether
there is one; dotnet tool update --global Saci does the same). saci called without arguments
prints the help and, once a day, looks for a newer release and installs it in the background the
same way (SACI_NO_AUTO_UPDATE=1 turns that off; a build run from source never updates anything). A running saci locks its own
files, so the update runs in the background right after the command exits, without opening a
window; ~/.saci/logs/update.log has what it did. Every merge to main publishes
a version (0.<build>.0) through .github/workflows/release.yml: the package goes to NuGet.org
by Trusted Publishing (a trusted publishing policy on nuget.org for this repository and
workflow, plus the repository variable NUGET_USER = the nuget.org profile that owns it; the
secret NUGET_API_KEY is the fallback), to the organization's GitHub Packages feed, and is attached
to a GitHub release; scripts/install-release.ps1 installs from that release with gh when
NuGet.org is not reachable.
From a clone
.\scripts\install.ps1 # builds this clone and installs the `saci` command for your user
.\scripts\install.ps1 -ToolPath C:\tools # or install into a folder of your choice
.\scripts\install.ps1 -Uninstall # remove it; ~/.saci is kept
Run the script again after pulling changes to replace the installed version.
Getting started
saci init --no-repo # creates ~/.saci/config.json (this installation's settings)
saci doctor # checks git, gh, claude, the GitHub identity and the configuration
cd C:\git\my-repo
saci init # registers the repository, lets Claude write .saci/config.json
# (kinds, trust, gates, tester areas...) from the code and the issues and
# opens a pull request with it, then writes a persona per profile
saci persona edit reviewer # tell the reviewer how to start the app for the browser test
saci persona edit tester # ... and the tester how to build, start, sign in and what matters most
saci triage --dry-run # see what the triager would pick up
saci triage # triage up to 5 issues
saci develop # pull requests first, then one triaged issue -> pull request
saci review # verify one saci pull request
saci test --area checkout # explore the running app looking for problems; file them as issues
saci solve --issue 42 # one issue end to end: triage, develop, review (your approval included)
saci run # a full round: every profile, then learning and cleanup
saci run --watch --interval 10m # ... forever, until Ctrl+C
Every command that changes something supports --dry-run. saci <command> --help lists the options.
--max n bounds the items of one pass (one when not given, --max 0 for no limit); --times n repeats the pass (each one takes the next
items, with claims, budget and the usage guard checked again) and stops early when a pass finds
nothing to do — saci triage --max 5 --times 4 triages up to twenty issues in four passes.
--fill-window is for when you are done with Claude until the 5-hour window resets: saci may
take that window up to usage.fillWindowPercent (95%) and keeps running passes until it is
nearly used up, resets, or nothing is left — saci run --fill-window before leaving for the
evening. The 7-day limit still applies. --fill-week is the same for the week: both limits go
to their fill percents (usage.fillWeekPercent, 95%), a used-up 5-hour window is slept through
until it resets, and the run ends when the 7-day window is nearly used up or resets; with nothing
to do it waits usage.fillIdleWait (30m) and looks again — saci run --fill-week on Friday evening.
When you simply want a run to go ahead whatever the windows say, --ignore-limits skips the two
window checks for that run (the pause, the active hours and Claude's own refusals still apply).
Any of these long runs ends cleanly with Ctrl+C in its terminal or, from anywhere on the machine,
with saci stop: every running saci finishes the item it is on and stops (sleeps are
interrupted within a minute); runs started afterwards are not affected, and saci stop --clear
withdraws the request. saci status shows a pending stop.
Interactive mode
saci shell opens a shell where you type commands without the saci prefix (saci alone prints the help):
saci t6-enterprise/saci> triage --dry-run
saci t6-enterprise/saci> cd C:\git\other-repo
saci acme/other-repo> usage pause 2h
saci acme/other-repo> exit
The prompt shows the repository of the current folder; cd <folder> changes it; Ctrl+C cancels the
running command and keeps the shell; help lists the commands. saci shell does the same from a script.
--interactive on any command runs it and then stays in the shell (saci triage --dry-run --interactive), so the first command needs no second step.
saci remembers the folders it was used in (saci folders, or a bare cd in the shell). cd 2
jumps to the second most recent one, and spawn 2 (or spawn C:\git\other-repo) opens another
saci in a new terminal window in that folder, so you can run several repositories or several
clones side by side. spawn <folder> run --watch opens it straight into a watch loop.
What you see while it runs
While an agent works you see what Claude says and which tools it calls, a "still working" line every minute during long sessions, and after every item a progress line:
progress: 2/5 items (2 ok) · 03:12 · 2 sessions · 47 tool calls · 160k in / 3.9k out · $0.61 · 5h 46% · 7d 70%
items done, elapsed time, Claude sessions started, tool calls, tokens in/out, cost so far and the
share of your subscription windows used. The same line closes each profile (triager done: …).
Every session has a GUID, and everything about it is local
Each item saci works on runs in a Claude session with a GUID. The GUID is in every comment and
pull request saci writes (Claude session \e94eb179-…``), and everything about that session
lives on the machine that ran it: under ~/.saci/runs/<owner>/<repo>/, one folder per run with
prompt.md, system-prompt.md, output.json, transcript.jsonl, run.json (session id,
profile, item, branch, worktree, outcome, cost) and saci.log (what saci reported while it
ran). saci session show <id> — or saci session show #9352, --pr 9444 — prints the
record, the worktree, every run folder with its files and the end of each log, and the command to
resume the session. The dashboard's timeline links the transcripts.
When a session was not good enough — a wrong verdict, weak reasoning, a bad patch, a comment that
misses the point, too much spent — flag it: saci session flag <id|#issue|pr N> -m "what is wrong".
saci files an issue in its own repository with your words, the session record, every run's prompt,
system prompt, answer, transcript and log (secrets redacted, in a secret gist linked from the issue),
so the protocol, the personas or the code can be improved from a concrete case. The session record
keeps the issue's URL.
The shared .saci/config.json is read from the default branch (origin/main or origin/master,
as last fetched), whatever branch a clone or a worktree has checked out: release branches never need
a copy of it. Until the file is committed there, the working tree of the clone (or of another
registered clone) serves and saci says so.
Clickable issues and pull requests
In a terminal that shows hyperlinks (Windows Terminal, VS Code, iTerm2, WezTerm, kitty, GNOME
Terminal...) every #123 and owner/name#123 in saci's output is a link: click it to open the
issue or pull request on GitHub. The text stays exactly as it is, so logs and pipes are unaffected.
SACI_LINKS=1 forces the links on, SACI_LINKS=0 turns them off. The dashboard's results table
links its targets too.
Colours
In a terminal the output is coloured so the kind of line is clear at a glance: timestamps are dim,
warnings yellow, errors red, --verbose detail dim; in agent lines the [dry-run] tag is cyan, the
[owner/name] repository tag magenta and the profile that speaks (triager:, developer:...) bold;
the summary marks results [ ok ] in green and [FAIL] in red. Colours are off when output goes to
a pipe or a file, in --json mode, when NO_COLOR is set or TERM=dumb; SACI_COLOR=1 forces
them on and SACI_COLOR=0 off. saci --help (and a bare saci) opens with this build's version
(saci 0.12.0 - ...), then lists the commands alphabetically,
with the ones that run the agents (triage, develop, review, test, solve, run) in green.
Logs
Everything saci does is written to ~/.saci/logs/saci-<date>.log (14 days are kept): every
command, what the agents decided, and every git, gh and claude process with its duration and
exit code (never their input or secrets). saci logs shows the end of today's file, saci logs --follow keeps printing while agents or the web server run, and --verbose on any command (or
SACI_VERBOSE=1) shows the same detail on the console.
Commands
| Command | Purpose |
|---|---|
saci init, saci doctor, saci config path\|show |
Set up and check the installation |
saci changelog [--from v] [--to v] [--last n] |
What each release merged and the issues it resolved, from the installed version to the newest by default |
saci update [--check] [--version v] |
Install the latest release (or show whether there is one) |
saci repo add [path], list, show, remove |
Register repositories. Adding another folder of a registered repository joins the same entry |
saci persona init\|generate\|show\|edit\|path [profile] |
Manage personas per repository and profile |
saci triage [--issue n[,m\|a-b]] [--kind Bug] [--max n] [--times n] [--parallel n] [--force] |
Run the triager |
saci develop [--issue n[,m\|a-b] \| --pr n[,m\|a-b]] [--max n] [--times n] [--parallel n] |
Run the developer |
saci review [--pr n[,m\|a-b] [--force]] [--max n] [--times n] [--parallel n] |
Run the reviewer (--force: a draft too, from scratch) |
saci review --pr n --interactive |
Review in the terminal: numbered items, ask n ..., why n ..., fix n, check, then post or quit |
saci test [--area a] [--max n] [--dry-run] |
Run the tester now: explore the application and file what it finds |
saci solve --issue n[,m\|a-b] [--retriage] [--dry-run] |
Take issues end to end: triage → develop → review; naming the issue counts as approval |
saci agent <name> [--issue n[,m\|a-b]] [--max n] |
Run a profile defined in the configuration |
saci learn [--pr n] [--max n] |
Learn from finished pull requests |
saci run [--repo r] [--profiles a,b] [--times n] [--watch] [--interval 5m] |
Run a full round on the registered repositories |
saci clean [--all] [--dry-run] |
Remove the worktrees of finished work |
saci stats [--repo r] [--days n] |
What the agents cost, per repository, profile and day |
saci status |
What every saci process on this machine is working on right now |
saci folders, saci spawn <folder\|n> [command…] |
Folders saci was used in; open another saci in one of them, in a new window |
saci remote [--name n] |
A Claude Code session with Remote Control that operates saci for you from claude.ai, on any device |
saci usage, saci usage pause [2h], saci usage resume |
How much of your Claude subscription saci used; stop the agents while you need it yourself |
saci reset --issue n[,m\|a-b] [--dry-run] |
Start an issue over: forget saci's session and worktree for it, put the ready label back |
saci stop [--clear] |
Tell every running saci on this machine to finish its current item and stop its run (--watch, --times, --fill-window, --fill-week) |
saci session list, saci session show <id\|#n> [--issue n] [--pr n], saci session resume <id> |
The Claude sessions saci started; everything kept locally about one (runs, files, logs); open one interactively |
saci bug list, saci bug submit |
Bug reports about saci itself that are waiting to be filed |
saci logs [--lines n] [--follow] [--date d] |
The saci log; --verbose on any command prints the same detail |
saci shell |
Interactive mode (saci alone prints the help) |
saci session flag <id\|#n\|pr n> -m "…" |
Flag a session as not good enough: an issue in the saci repository with its files and your words |
saci web start [--foreground] [--dashboard-only] [--port n], saci web stop, saci web status |
The web dashboard as a background process of this machine |
--interactive (any command) |
Run the command, then stay in the interactive shell |
saci docs [topic], saci schema <name> |
The documentation and the JSON Schemas of the installed version |
saci mcp |
Serve saci to MCP clients (Claude Code, Claude Desktop) as typed tools |
--issue / --pr (where they apply) |
A number, a comma-separated list (5241,5244) or a range (5244-5248), repeatable; several items run one after the other |
--json (any command) |
Machine-readable output, one JSON object per line |
--ignore-limits (any command) |
Skip the 5-hour / 7-day window limits for this run (pause, active hours and Claude's refusals still apply) |
Release lines
Repositories that support several versions label their issues with the version and fix them on
that line's branch. branches maps labels to branches (explicit rules, or a labelPattern
with a branchTemplate): the triager checks and reproduces the issue on that branch (one
checkout per branch, and its comment says Checked on release branch …), the developer creates
its branch from it and opens the pull request against it. With several version labels the oldest
line wins — the fix goes there first. A branch that does not exist on origin falls back to the
default branch with a warning. Issues without a version label use the default branch.
One language per repository
With language set in the repository's .saci/config.json ("pt-BR", "English"…), every
agent writes what people read — triage comments, rewritten descriptions, pull request titles and
descriptions, reviews — in that language, whatever language the input is in. An issue written in
another language is put into it by the triager (title and body; the original text stays under a
folded "Original text" heading, done once), and the reviewer does the same for a pull request's
title and description. saci init infers the language from the repository's issues.
Issues somebody is already fixing
An issue that an open pull request closes (Fixes #n, or linked in the Development panel) is
not triaged, typed or labelled by the triager: somebody is on it, and saci's only business there
is the review of that pull request. The dry run lists them. saci triage --issue n still triages
it when you ask explicitly.
Every issue gets a type
Root issues should carry a GitHub issue type. With triager.assignType on (the default), open
issues without one become triage candidates: Claude picks the type among the repository's own
issue types, saci sets it, and the triage goes on as a bug or a request according to that type.
A type the triager does not take (a Task, a chore) only gets its type and a short comment.
Sub-issues are typed only with "subIssues": true, because many teams leave them untyped on
purpose. Closed issues are never changed; repositories without issue types are left alone.
Poor descriptions get rewritten
A confirmed bug whose report would leave a developer guessing gets a better description
(triager.rewrite, on by default): the exact steps the triager followed to reproduce it,
expected and actual behaviour, environment and data, in the repository's issue template for
that kind when it has one (.github/ISSUE_TEMPLATE, matched by the template's type, labels
or name, or named in rewrite.templates), every field filled. The original report stays under a
collapsed "Original report" heading, nothing is invented, and an issue is rewritten once. Issues
that were not confirmed are never rewritten. Turn it off with "rewrite": { "enabled": false }.
Features, not only bugs
The triager handles Bug and Feature issues by default (triager.kinds). A feature gets one of
ready (with acceptance criteria and implementation notes), needsDesign (open questions for a
person), split (proposed slices; triager.createSlices: true creates them as linked issues) or
outOfScope. A ready feature is labelled saci:needs-approval: the developer only takes bugs
by default (developer.kinds), and a feature waits until a person either swaps the label for
saci:ready and adds Feature to developer.kinds, or comments @saci take on it.
Big work does not have to fit one sitting: the developer can answer partial, saci opens the pull
request as a draft with the remaining work listed, continues it on the next run in the same Claude
session, and only hands it to the reviewer once it is complete. A draft is never reviewed, nor is a
pull request with an open review thread, whoever opened it: both are the author's turn, and the
reviewer waits until the pull request is marked ready and the threads are resolved
(saci review --pr n --force reviews one anyway, and always from scratch), except where developer.draftPullRequest makes every pull request a draft — there
saci's own drafts are what awaits its review, and approval marks them ready.
In issue comments, trusted people can type @saci take (develop this now, whatever its labels)
and @saci stop (leave it alone until the next @saci take). saci puts 👀 on a @saci comment
when it picks it up (a control command, or the take the developer starts on), so the person
knows it was seen before the answer or the pull request arrives.
How it works
selects work structured verdict
GitHub ───────────────────▶ saci ───────────────────────────────▶ GitHub
(issues, pull requests) │ ▲ (comments, labels, project
▼ │ status, pull requests, reviews)
claude -p --session-id <guid> / --resume <guid>
(in an isolated git worktree, with the repository persona)
- Claude decides, saci acts. Claude investigates and writes code, then answers with JSON that follows a schema. saci turns that answer into GitHub changes, so the workflow is the same on every run and can be tested.
- Sessions are kept. saci records the GUID of every Claude session per issue and pull request
and resumes it on the next turn: the developer that answers a review comment is the same
conversation that wrote the code.
saci session resume <id>opens it for you. While Claude works you see what it says and which tools it calls; the whole stream is kept as a transcript. - GitHub is the state. What was triaged, reviewed or answered is recorded in hidden markers inside saci's comments, so runs are idempotent and work from any machine.
- Your checkout is never touched. All work happens in git worktrees under
~/.saci/worktrees. Pull requests are opened as drafts. Finished worktrees are removed bysaci cleanand at the end of everysaci run. On Windows, setworktreeRootto a short folder such asC:\wif the repository has deep paths.
The developer and the reviewer hand a pull request back and forth: review findings become review
threads, the developer answers and resolves them, the reviewer looks again. The developer also
reacts to failing checks, conflicts with the base branch and comments people leave. A problem it
could not fix is not retried until the branch changes, and after developer.maxFeedbackRounds
(5) or reviewer.maxRounds (3) rounds saci stops and leaves the pull request to a human.
Configuration
Configuration has two homes:
- Per installation, in your user folder (
~/.saci/config.json): the registered repositories and their folders, the worktree root, parallelism, budgets, the subscription guard, the web server, notifications, custom profiles, your Claude defaults. - Per repository, committed in the repository as
.saci/config.json: how saci behaves on it (which issues the triager takes and how, the developer's branch names and labels, the reviewer's gates and browser test, the tester's areas and checks, who is trusted, the project, learning, Claude settings). Everyone who runs saci on a clone gets the same behaviour. The file is read from the default branch (origin/main, as last fetched), so release branches and feature branches need no copy; its behaviour sections replace the ones of your local entry, whileslug,paths,worktreeRoot,maxParallelanddailyBudgetUsdalways stay local. Only until the file is committed does the working tree's copy serve (right aftersaci init).
saci init inside a clone writes .saci/config.json for you: Claude studies the repository
(README, contributing rules, hooks, CI, templates) and the issues on GitHub (types, labels, who
reports them) and chooses kinds, labels, trust, gates, branch naming and tester areas, explaining
each choice and what the team should decide. The file is committed on a saci/init branch and
proposed as a pull request with those explanations, so the team reviews it on GitHub like any
change; the clone is not touched (--no-pr writes it into the clone for you to commit instead).
saci init also writes the persona of every profile that has none into the same pull request
(--no-personas to skip). saci persona generate later rewrites personas the same way, starting
from the ones the repository has (or a file an older saci left on this machine, which it moves into
the repository); saci persona edit edits the file in the clone for you to commit; the learner
proposes its lessons on a saci/lessons branch, one pull request that grows while it is open.
A colleague running saci init in a clone of a repository that is already set up only gets the
clone registered (nothing is written or proposed again), and saci repo add refuses a repository
nobody set up yet and points to saci init. Both
files saci writes carry only what differs from the defaults: a default saci changes later is not
frozen in your files as if you had chosen it (saci docs config lists every default).
saci repo show and saci doctor tell where the configuration in force comes from.
Everything else lives in your user folder (~/.saci, or the folder in SACI_HOME):
~/.saci/
config.json repositories and settings
state/<owner>/<repo>/sessions.json Claude sessions per issue and pull request
state/<owner>/<repo>/runs.jsonl every Claude turn with its cost (what `saci stats` reads)
runs/<owner>/<repo>/<time>-<profile>-…/ prompt, system prompt, result and transcript of every run
worktrees/<owner>/<repo>/… task worktrees
bugs/ saci bug reports that could not be filed yet
logs/saci-<date>.log what saci did, one file per day (`saci logs`)
Providers, models and effort
Sessions run on Claude Code by default. claude.provider: "copilot" runs them on GitHub
Copilot CLI instead (copilot, logged in with copilot login), globally, per repository or per
profile like every other claude setting — a triager on Copilot and a developer on Claude is one
line each. Copilot has no system-prompt or answer-schema flags, so saci writes the protocol, the
task and the schema to files in the run folder and tells Copilot to read them; it reports premium
requests rather than dollars and no subscription windows, so saci stats shows no cost for its
sessions and the usage guard does not see them. claude.maxBudgetUsd does not apply to it;
allowedTools/disallowedTools take Copilot's tool names. saci session resume continues a
Copilot session with Copilot.
Without a claude.model, every session uses the provider's own default model (the one the CLI
would pick for you). claude.model (an alias such as sonnet, opus, haiku, or a full model
name) and claude.effort (low, medium, high, xhigh, max) are passed to every session,
and can be set globally, per repository and per profile — the triager and the tester read and
reproduce, the developer writes code, so they rarely deserve the same model. A setup that keeps the
subscription for the work that needs it, in the repository's .saci/config.json:
"triager": { "claude": { "model": "sonnet", "effort": "medium" } },
"reviewer": { "claude": { "model": "sonnet" } },
"tester": { "claude": { "model": "sonnet", "effort": "medium" } },
"developer": { "claude": { "model": "opus", "effort": "high" } }
Every session says which model and effort it runs with (Starting Claude session … (model sonnet, effort medium)), and saci stats shows what each profile costs.
saci repo add writes a repository entry with the folder; the behaviour comes from the
repository's .saci/config.json and the defaults. saci config show prints the effective
configuration. An example of the per-installation file:
{
"claude": { "model": "opus", "permissionMode": "bypassPermissions", "timeoutMinutes": 60 },
"github": { // act as a bot instead of you (optional)
"tokenEnvironmentVariable": "SACI_GH_TOKEN", // name of the variable holding the token, never the token
"gitUserName": "saci-bot",
"gitUserEmail": "saci-bot@example.com"
},
"trust": { "associations": ["*"] }, // everyone (default); a public repository narrows it to ["OWNER", "MEMBER", "COLLABORATOR"]
"dailyBudgetUsd": 20, // stop starting Claude sessions once a repository spent this today
"usage": { // share of your Claude subscription the agents may use
"maxFiveHourPercent": 60,
"maxSevenDayPercent": 80,
"fillWindowPercent": 95, // how far --fill-window may take the 5-hour window
"fillWeekPercent": 95, "fillIdleWait": "30m", // --fill-week: 7-day limit, and the wait when there is no work
"activeHours": "20:00-07:00" // only at night, for example (null = always)
},
"maxParallel": 2, // issues or pull requests worked on at the same time
"notifications": { "webhookUrlEnvironmentVariable": "SACI_CHAT", "format": "slack", "on": ["failed", "pullRequestOpened"] },
"customProfiles": [
{
"name": "test-writer",
"labels": ["needs-tests"],
"workspace": "branch", // readOnly | branch
"instructions": "Write the unit tests the issue asks for, following the conventions of the existing tests.",
"outcomes": {
"done": { "addLabels": ["tests:written"], "removeLabels": ["needs-tests"] },
"notPossible": { "addLabels": ["tests:skipped"] }
}
}
],
"repositories": [
{
"slug": "my-org/my-repo",
"paths": ["C:\\git\\my-repo", "C:\\git\\my-repo-2"],
"project": { "owner": "my-org", "number": 5, "statusField": "Status" },
"triager": {
"kinds": ["Bug", "Feature"], // issue types or labels to triage
"order": "oldest", // oldest first (default: nothing waits forever) or newest first (the inbox)
"assignType": { "enabled": true, "subIssues": false }, // give untyped open issues a type; sub-issues too?
"rewrite": { "enabled": true, "useTemplates": true, "templates": { "bug": "bug.yml" } }, // rewrite poor descriptions of confirmed bugs
"createSlices": false, // true: a split feature becomes linked child issues
"projectStatus": "Todo", // only issues in this column
"outcomes": {
"confirmed": { "moveToStatus": "In Progress" },
"duplicate": { "addLabels": ["duplicate"], "close": true }
}
},
"developer": {
"kinds": ["Bug"], // what it takes on its own; "@saci take" bypasses this
"branchPrefix": "saci/", "branchName": "{prefix}issue-{number}-{slug}", // or e.g. "i{number}" for an iNNNN rule
"readyLabels": [],
"projectStatus": "In Progress",
"onPullRequest": { "moveToStatus": "In Review" },
"fixFailingChecks": true, "resolveConflicts": true, "answerComments": true
},
"reviewer": {
"browserTest": "required", // required | optional | off
"gates": { // what a pull request must satisfy besides working
"acceptanceCriteriaTested": true, // every criterion / the bug scenario has an automated test in the PR
"changedLineCoverage": null, // e.g. 80: percent of changed lines the tests must cover
"docsUpdated": true, // user-visible changes update README/docs/help
"noNewWarnings": true, // no compiler, analyzer or linter warning the base did not have
"compatibility": false, // data, API, config and wire changes keep old data and clients working
"reportFlakyTests": true, // re-run a failing test once; report it as flaky if it then passes
"custom": [ { "name": "migrations", "instruction": "Every schema change ships a reversible migration." } ], // or plain strings
"suggestions": ["acceptanceCriteriaTested"] // gates that are reported but never withhold approval
}
},
"tester": {
"enabled": true,
"interval": "1d", // a full round explores at most this often
"maxIssues": 3, // issues filed per exploration; the rest wait for the next one
"areas": [], // e.g. ["checkout", "admin"]; empty = everything, guided by the persona
"checks": ["console", "links"], // + accessibility, performance, security, or a check named in the persona
"performanceBudgetMs": 3000,
"labels": ["saci:found"],
"categoryLabels": {}, // e.g. { "accessibility": "a11y" }: an extra label per finding category
"issueType": "Bug"
},
"language": "pt-BR", // the team's language: saci writes in it and puts foreign issues/PRs into it
"branches": { // release lines maintained next to the default branch
"rules": [ { "label": "12.3", "branch": "releases/12.3" }, { "label": "12.4", "branch": "releases/12.4" } ]
// or: "labelPattern": "^\d+\.\d+$", "branchTemplate": "releases/{label}"
},
"learning": { "enabled": true, "lookbackDays": 14 }
}
]
}
Defaults when nothing is configured: the triager handles Bug and Feature issues; confirmed
bugs get saci:ready, ready features saci:needs-approval (duplicate, saci:cannot-reproduce,
saci:needs-info, saci:not-a-bug, saci:already-fixed, saci:needs-design, saci:split,
saci:out-of-scope for the other outcomes; an already fixed bug is also closed as completed, its
comment naming the pull request or commit that fixed it, and a duplicate is closed as such, its comment
naming the original and where it stands (Duplicate of #5310 (open, saci:ready): the work is tracked there.); while an issue is being triaged it carries
saci:triaging, so a saci on another machine leaves it alone — a claim older than triager.claimTimeout,
3 hours, belongs to a dead process and is ignored — and a verdict another saci posted meanwhile wins:
the second one posts nothing); the developer takes saci:ready bugs, works on
saci/issue-<n>-<title> branches and opens pull requests labelled saci (drafts only for unfinished work, or always with
developer.draftPullRequest: true); the reviewer
requires a browser test and marks approved pull requests ready for review.
The full reference is in specs/001-saci-agent-cli/contracts/config.schema.md and specs/002-autonomy-extensions/contracts/config.md.
Keeping the subscription for yourself
On a Claude subscription the limit is not money but the 5-hour and 7-day usage windows you share
with the agents. Claude reports how full both windows are during every session; saci stores the
last report and stops starting sessions when a window is above usage.maxFiveHourPercent (60) or
usage.maxSevenDayPercent (80), until it resets. usage.activeHours keeps the agents to hours
when you are not working, and saci usage pause 2h (or pause until saci usage resume) stops
them on the spot. saci usage, saci stats and saci doctor show where you stand. Set a limit to
null to turn it off.
Acting as a bot
By default saci acts as whoever is logged in to gh, and GitHub does not let an author approve
their own pull request, so the reviewer can only comment. Give saci an identity of its own (a bot
user's token, or a GitHub App installation token) through github.tokenEnvironmentVariable: every
GitHub call, push and agent session then runs as that identity, the reviewer files real
approvals and change requests, and saci doctor shows who saci is.
Who is trusted
Issue and comment text reaches Claude, which runs unattended, so text from people outside the
project is the main way to attack it. By default saci trusts everyone who writes in the
repository: it is made for teams working on their own repositories, where every author is staff or
a partner and a list of names would have to follow people joining and leaving. On a public
repository, where strangers open issues, set "trust": { "associations": ["OWNER", "MEMBER", "COLLABORATOR"] }: saci then only acts on issues and pull requests by those, never shows Claude
comments or review threads written by anyone else (the prompt says how many were left out), and
trust.authors lists the outside logins to trust anyway (a bot that files issues, support people).
The repository's .saci/config.json overrides the global rule.
Several saci at once
You can run as many saci processes as you like on one machine (shells, saci run --watch, the
web server), on different repositories or on the same one, from the same clone or from different
clones of it. They share ~/.saci and coordinate through it:
- an issue or pull request is claimed by the process that starts it; another process that
reaches it reports
busyand moves on to the next item, so work is split, never duplicated; - shared read-only checkouts (
triage,learn, custom profiles) are leased per process: the second process getstriage-2, and so on; - sessions, costs, usage, lessons and the log are written under cross-process locks; every log line carries the process id;
saci statusshows what each process is working on and which checkouts are in use.
Across machines, the coordination is on GitHub: the triager labels an issue saci:triaging while it
works on it and skips issues another saci claimed, and a verdict another saci posted meanwhile wins.
Every comment saci writes ends with its release (saci 0.41.0 · Claude session …); when a colleague's
recent comments come from an older release — which ignores those claims — saci says so and asks for
saci update on their side.
Two folders of the same repository are the same repository to saci (identity comes from the remote), so they share sessions and worktrees; each process creates worktrees from its own clone. Personas and the shared configuration come from the repository itself.
Using saci from a program or a language model
saci is built to be driven by an agent as much as by a person:
--jsonon every command: one JSON object per line (log,item,result,error,done), nothing else on standard output, same exit codes.saci run --dry-run --json,saci status --json,saci doctor --json… Seesaci docs json;saci schema eventsis the JSON Schema of the lines.saci docs [overview|commands|config|workflow|json|operator|all]prints the documentation of the installed version as Markdown, so an agent can load exactly what it needs without this repository.saci schema config|repo-configprint the JSON Schemas of the two configuration files (editors validate them; an agent writes them right the first time).saci mcp: saci as a Model Context Protocol server over stdin/stdout. Add it to Claude Code (claude mcp add saci -- saci mcp), Claude Desktop or any MCP client and it gets typed tools:saci_status,saci_usage,saci_pause/saci_resume,saci_repositories,saci_repository_config,saci_stats,saci_run,saci_triage,saci_develop,saci_review,saci_test(all dry-run by default),saci_sessions,saci_logs,saci_docs,saci_doctorandsaci_commandfor anything else. Each tool runs the corresponding command in--jsonmode and returns its events, so the rules and the output are exactly those of the command line.llms.txtat the root of this repository points an agent at the right entry points.
Controlling saci remotely
Three ways, from the simplest up:
- From claude.ai, through Claude Code Remote Control. Run
saci remoteon the machine: it starts a Claude Code session with Remote Control on, briefed to operate saci through its own command line (and allowed to run nothing else without your approval). Open the session from claude.ai on your phone or another computer and talk to it: "what is saci doing?", "pause it for two hours", "run the triager on powerplanning, dry run first". It runs thesacicommands and reports back. saci itself keeps running independently of this session. - From GitHub. Trusted people can comment on any issue or pull request of a registered
repository:
@saci pause 2h,@saci pause,@saci resume,@saci status,@saci run. saci answers in a comment at the start of its next round (immediately when the web server receives webhooks). No open port needed, works from the GitHub app. - Over the network, through the web API. Set
web.bindAddressto0.0.0.0and the token variable named byweb.apiTokenEnvironmentVariable(SACI_API_TOKEN); the server refuses to bind beyond localhost without a token. Thencurl -H "Authorization: Bearer $TOKEN" http://host:5080/api/status,POST /api/pause?duration=2h,POST /api/resume,POST /api/run; the dashboard works too after opening it once with?token=<token>.
Web server and webhooks
saci web start starts a local web server in the background with a dashboard (repositories,
activity, what waits on a person, the timeline of any issue, charts of cost and Claude turns per
day and per profile, subscription meters, quality trends, sessions, runs with their transcripts,
bug reports), a JSON API, a GitHub webhook endpoint so agents react when something happens, and a
background runner that works like saci run --watch. saci web status tells whether it runs and
where, saci web stop stops it; --foreground keeps it in the terminal, --dashboard-only runs
no agents in it. From source, saci-web.ps1 runs the same server; it can also be installed as a
Windows service with scripts/install-service.ps1. See docs/web.md.
When a run fails right away
Claude exited with code 1 without a result: --json-schema is not valid JSONhappened whenclaudewas the npm install on Windows (claude.cmd): the shim hands its arguments tocmd.exe, which re-parses them, and a JSON schema full of quotes does not survive. saci now runs such shims asnode <script>directly, so this no longer occurs;saci doctorshows whichclaudeis used.Ignoring N permissions.allow entries from .claude/settings.json: this workspace has not been trustedis a warning from Claude Code, harmless withbypassPermissions(the default). To silence it, openclaudeonce in the clone and accept the trust dialog.
Safety
- Claude runs unattended with
bypassPermissionsby default, because nobody is there to answer permission prompts. Setclaude.permissionMode,allowedToolsanddisallowedToolsglobally, per repository or per profile to restrict it,maxBudgetUsd(per session) ordailyBudgetUsd(per repository and day) to cap the cost, andusageto cap the share of the subscription. - The trust rules above keep strangers' text away from the agents, but instructions to a model are not a security boundary: run saci on repositories where you trust who can open issues, or restrict the permission mode.
- saci never pushes to the default branch, never force-pushes and never merges.
- The agents' protocols forbid opening credential stores for their own use and ask for disposable databases; in a shared one they may only create what the persona allows, marked with the issue number and removed afterwards.
- The web server binds to localhost and has no authentication. Do not expose it directly.
Finding problems before users do
The tester explores the application without an issue to start from. Its persona
(saci persona edit tester) tells it how to build, test, start and sign in to the application,
which flows matter most and which areas to leave alone. A full round (saci run) explores once per
tester.interval (a day by default); saci test explores right away, optionally focused on an
area (--area checkout). Findings are ordered by severity and filed as issues labelled
saci:found, at most tester.maxIssues per exploration, with a `` marker;
findings that match an open issue, or that the tester itself traced to an existing one, are only
logged. A new issue then follows the normal path: the triager reproduces it and, if it is confirmed,
the developer fixes it. Set "tester": { "enabled": false } on repositories that are not meant to
be run, or "enabled": true only where the persona says how.
Besides behaviour, the tester runs the checks in tester.checks on the pages and flows it
visits: console (browser console errors, failed requests), links (broken links, 404s),
accessibility (axe-core rules, labels, keyboard use, focus, alternative text), performance
(page loads and requests over performanceBudgetMs) and security (security headers, cookie
flags, secrets in URLs, stack traces in error pages). Any other name is a check of your own,
described in the persona. Each finding carries its check as a category, shown in the issue and,
with categoryLabels, as a label.
Quality gates
A pull request that builds, passes its tests and solves the issue is not necessarily good. The
reviewer also checks the gates in reviewer.gates and reports each with evidence in its
review comment: tests for every acceptance criterion or for the bug scenario, coverage of the
changed lines, documentation for user-visible changes, no new warnings, compatibility of data and
APIs, plus any gate of your own (custom: a name and what must be true). A failed gate turns an
approval into a change request with one finding per gate, which the developer answers like a
review comment. Gates named in gates.suggestions (the automated-test gate by default) are checked
and reported but never withhold approval: their failure is a suggestion. The reviewer also re-runs
a failing test once and reports flaky tests instead of failing the review for them.
What a review looks like
The review comment is short: a few sentences, one table of checks with a coloured circle per row
(🟢 passed, 🔴 failed, 🟡 a suggestion gate that failed, ⚪ not run: build, tests, solves the issue,
breaking changes, browser test, whether GitHub can merge it into the base branch, one row counting the passed gates and one per gate that did not pass), an Acceptance criteria checklist with the issue's own criteria, each 🟢 met or 🔴 not met, a table To fix or check with every item,
its severity (🔴 Critical, 🟠 Required, 🟡 Suggestion, 🔵 Question) and where it is, plus the review
threads still open from earlier rounds (⚪ Open thread), the evidence
of how each gate and the browser test were checked folded under "Evidence", and the verdict at the
end, easy to spot: 🟢 Verdict: approved — 92% sure it can be merged (🟡 changes requested,
🔴 blocked). Critical and required findings withhold approval, suggestions and questions never do. Every finding becomes a review thread the author can
answer: on its line of the diff, with a GitHub "suggested change" (one line or a block) whenever the
reviewer knows the correct code, or, for a finding without a line of its own, on the first changed
line of the first changed file; the answers are read on the next round. Breaking changes are checked
in every situation the change reaches (APIs, stored data, configuration, other screens,
integrations, scripts) and listed. When a change depends on a sibling repository (a package built
from another clone), the reviewer may read the other repositories registered with saci, read-only;
nothing else on the machine is in scope. A fix split across repositories is reported as a
dependency: the review links the sibling pull request under "Depends on" and tells people to
approve this one only after it.
Talking a review over before it is posted. saci review --pr n --interactive reviews the pull
request but posts nothing: the items come numbered in the terminal. ask 2 where does this break? has saci explain an item (what it saw, why it matters, how to reproduce) without changing
anything, and ask <text> asks about the review as a whole. why 2 the column is an integer by contract tells saci why an item is not a problem, and it withdraws the item or keeps it
and says why; fix 3 has saci fix the item on the pull request branch, commit, push and check
again; check tells saci you pushed a fix yourself, and it checks every item again; post
publishes what is left (threads, summary, labels, the review event) and quit posts nothing. Every
turn continues the Claude session that made the review, so it remembers what it already looked at.
A pull request by someone else gets GitHub's own review process: a change request when changes
are needed, and an approval only when saci has nothing to say (no finding of any severity, no
breaking change, no dependency) and is 100% sure, because that approval is filed in the name of
whoever runs saci. Anything less is a comment, and the review says a person approves. saci's own
pull requests get comments only, everything else being equal. The verdict is also a
label: saci:needs-changes while the pull request waits for its author (reviewer.onChangesRequested),
saci:reviewed once saci would approve it (reviewer.onApproved); each replaces the other. When
saci:reviewed is already there because a colleague's saci approved that commit, a second review
only comments. On a second round the reviewer reads its earlier review and the answers: a finding
or question the author answered, or explained away, is not raised again. New commits after an
approval start the review over from scratch (a fresh session) and take the label back first.
Quality trends
Every agent records what says how the software is doing, next to what it cost: reviews with
their gates and flaky tests, pull requests opened, merges with lead time and review rounds,
triage verdicts by source (tester, saci, people), tester explorations with their findings
(~/.saci/state/<owner>/<repo>/quality.jsonl). The Quality page of the web dashboard turns it
into weekly trends: merged pull requests, bugs confirmed, lead time from issue to merge, review
rounds per pull request, gates passed, findings per exploration, the gates that fail most, the
flaky tests seen and what the tester finds by category, so you can see whether the software is
getting better, not only how much Claude it used.
When a run is interrupted
A saci process can die mid-way (power, memory, a kill). What it leaves behind is designed to be
safe and visible: the developer comments "saci started working on this" (branch, time) the
moment it takes an issue and saves its session record before Claude starts, so a later run
resumes the same Claude session — nothing Claude did is lost — and comments that it resumes.
An issue carrying the in-progress label with no pull request is taken again by the next
developer run, and the dashboard's Waiting page lists it under Started but no pull request
yet. On Ctrl+C or saci stop the developer puts the labels back and comments interrupted.
To start over instead of resuming: saci reset --issue n (forgets the session and worktree,
restores the ready label, comments reset).
Triage and review sessions are resumed too — a reporter's answer continues the conversation that
asked for it — with two exceptions: --force asks for a fresh look and starts a new session, and
a session whose protocol or persona changed since it started is replaced by a new one, so the new
rules are actually read instead of the old conclusion being repeated. The developer keeps its
session across such changes: the work on its branch is worth more than a fresh reading.
When saci finds a bug in itself
An unexpected failure inside saci is filed automatically as an issue in t6-enterprise/saci
(selfRepository in the configuration). The report contains the exception, the saci version and
the environment, with tokens and your home folder removed; it never contains issue, pull request
or prompt content. The same bug is filed once and later occurrences are added as comments. When the
repository cannot be reached the report waits in ~/.saci/bugs (saci bug submit). Turn it off
with "selfBugReporting": false or SACI_NO_SELF_REPORT=1. Register the saci repository itself
(saci repo add) and its developer agent will pick those issues up once they are triaged.
Development
dotnet build Saci.slnx
dotnet test Saci.slnx
src/Saci.Core: configuration, repositories, personas, the git / GitHub / Claude clients and the agentssrc/Saci.Cli: thesacicommand linesrc/Saci.Web: the web server (ASP.NET Core + Drapo)tests/Saci.Tests,tests/Saci.Web.Tests: xUnit tests; external tools are replaced by hand-written fakes, git is exercised against a local bare repository
The project follows Spec Kit: principles in .specify/memory/constitution.md, the specification, plan and tasks of each feature under specs/. CI builds and tests every pull request.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
This package has no dependencies.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.66.0 | 35 | 10/9/2026 |
| 0.65.0 | 32 | 10/9/2026 |
| 0.64.0 | 34 | 10/9/2026 |
| 0.63.0 | 31 | 10/9/2026 |
| 0.62.0 | 36 | 10/9/2026 |
| 0.61.0 | 37 | 10/9/2026 |
| 0.60.0 | 39 | 10/9/2026 |
| 0.59.0 | 35 | 10/9/2026 |
| 0.58.0 | 39 | 10/9/2026 |
| 0.57.0 | 47 | 10/8/2026 |
| 0.56.0 | 48 | 10/8/2026 |
| 0.55.0 | 45 | 10/8/2026 |
| 0.54.0 | 42 | 10/8/2026 |
| 0.53.0 | 40 | 10/8/2026 |
| 0.52.0 | 52 | 10/8/2026 |
| 0.51.0 | 45 | 10/8/2026 |
| 0.50.0 | 45 | 10/8/2026 |
| 0.49.0 | 45 | 10/8/2026 |
| 0.48.0 | 47 | 10/8/2026 |
| 0.47.0 | 45 | 10/8/2026 |