Saci 0.61.0

There is a newer version of this package available.
See the version list below for details.
dotnet tool install --global Saci --version 0.61.0
                    
This package contains a .NET tool you can call from the shell/command line.
dotnet new tool-manifest
                    
if you are setting up this repo
dotnet tool install --local Saci --version 0.61.0
                    
This package contains a .NET tool you can call from the shell/command line.
#tool dotnet:?package=Saci&version=0.61.0
                    
nuke :add-package Saci --version 0.61.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 CLI gh (logged in: gh auth login) and claude (logged in) on the PATH
  • 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

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 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 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 by saci clean and at the end of every saci run. On Windows, set worktreeRoot to a short folder such as C:\w if 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, while slug, paths, worktreeRoot, maxParallel and dailyBudgetUsd always stay local. Only until the file is committed does the working tree's copy serve (right after saci 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 busy and 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 gets triage-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 status shows 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:

  • --json on 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… See saci docs json; saci schema events is 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-config print 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_doctor and saci_command for anything else. Each tool runs the corresponding command in --json mode and returns its events, so the rules and the output are exactly those of the command line.
  • llms.txt at the root of this repository points an agent at the right entry points.

Controlling saci remotely

Three ways, from the simplest up:

  1. From claude.ai, through Claude Code Remote Control. Run saci remote on 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 the saci commands and reports back. saci itself keeps running independently of this session.
  2. 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.
  3. Over the network, through the web API. Set web.bindAddress to 0.0.0.0 and the token variable named by web.apiTokenEnvironmentVariable (SACI_API_TOKEN); the server refuses to bind beyond localhost without a token. Then curl -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 JSON happened when claude was the npm install on Windows (claude.cmd): the shim hands its arguments to cmd.exe, which re-parses them, and a JSON schema full of quotes does not survive. saci now runs such shims as node <script> directly, so this no longer occurs; saci doctor shows which claude is used.
  • Ignoring N permissions.allow entries from .claude/settings.json: this workspace has not been trusted is a warning from Claude Code, harmless with bypassPermissions (the default). To silence it, open claude once in the clone and accept the trust dialog.

Safety

  • Claude runs unattended with bypassPermissions by default, because nobody is there to answer permission prompts. Set claude.permissionMode, allowedTools and disallowedTools globally, per repository or per profile to restrict it, maxBudgetUsd (per session) or dailyBudgetUsd (per repository and day) to cap the cost, and usage to 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.

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.

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 agents
  • src/Saci.Cli: the saci command line
  • src/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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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
Loading failed