User guide: contents

Keep it running

Watchers only run while scf ui (or scf serve) runs. When a new version of Spaghetti Code Foundry is built (npm run build), the running server restarts itself as soon as no run is active — no need to stop and start it. To keep the Foundry running in the background on macOS, also after a restart of your Mac:

scf service install     # uninstall | status

If you installed the service before, run scf service install once. It replaces the old com.claude-factory.server agent with com.spaghetti-code-foundry.server, and puts the old one back if the new one can't start.

The model analyst#

A separate process that reads your own runs every 6 hours and says, per step, how the model does and whether another level fits the work. It reads run files and writes a report, and it does not need the server. By default it only advises; with self-tuning on it changes the model or effort of a step by itself (see below).

scf analyst install     # run every 6 hours as its own macOS login agent; the first time now
scf analyst run         # write a report now (--no-web: do not look up published figures)
scf analyst report      # print the last report
scf analyst status      # uninstall removes the agent
scf analyst auto on     # let it change models and efforts by itself; auto off: only advise
scf analyst reset       # put back everything it changed

The report is analyst/report.md in the data folder; the same advice as data is in analyst/latest.json. It looks at the last 14 days and needs 20 visits of a step before it says anything; visits stopped by a usage limit do not count towards the 20. Per step and model it shows how often the step worked, ran into its time limit, was repaired by a fix_* step, or was sent back by a review, and its share of all tokens. From that:

  • Try a lower level. The step works nearly always and its work is rarely repaired. The advice is one effort level down when the step runs on high or above, otherwise the next smaller model. For a review: almost none of the reviews led to a change, and the review step itself works at least 97% of the time.
  • Try a higher level. The step fails or its work is repaired often. A review step that fails 15% of the time or more is treated the same way: it is never advised to go lower.
  • Time limit, not model. 5% or more of the visits ran into the time limit. A stronger model is usually slower, so look at the limit and the size of the work first.
  • Look at this. The step got clearly worse in the last two days.

Visits stopped by a usage limit are not counted against a model. The advice rests on what happened, not on a trial of the other model. Self-tuning does that trial.

Self-tuning (scf analyst auto on; off by default). At every check the analyst may start a trial: it takes the advice with the largest token share that says "effort …" or names a Claude model, and sets it for that step. There is one trial per flow at a time, so the effect of a change can be told apart. The change is written to analyst/overrides.json; the engine reads that file at the start of every agent step, so it takes effect at the next step, also in a run that is under way. No flow file is changed.

Some advice stays advice, and the report says why under "Advice not tried by itself":

  • Another model is only tried when the published figures (see below) hold at least one figure for that model. The analyst makes the name of the next level from the name of the current one, and this is its check that such a model exists. With --no-web, or before the first lookup worked, no model is tried. Effort changes need no figure.
  • A review none of whose reviews led to a change. A trial of it could not be judged.
  • A step inside a sub-flow.
  • A change back to what the flow itself says. scf analyst reset does that.

What the analyst set never wins over you:

  • A router rule of yours for the step wins, for the model and for the effort.
  • The override follows the flow. It stores what the flow said for the step (model and effort) when the analyst set it, and applies only while the flow still says that. When you edit the step's model or effort, your edit counts from the next step on.
  • Only the model is replaced. The step keeps its agent and its provider, so a step on a local model or on a proxy stays there. The model of a step that does not run on Claude Code is never changed.

A trial is judged on the visits since it started, against the numbers of the step before it:

  • After 20 visits a lower level is kept unless the step does worse: it works more than 5 points less often, its work is repaired more than 10 points more often, it times out more than 5 points more often, or reviews send its work back more than 15 points more often. For a review step: unless it leads to a change less than half as often as before.
  • A higher level is kept only when the step works at least 5 points more often or is repaired at least 10 points less often. Otherwise it is not worth its tokens and is put back.
  • After 8 visits a trial that works 25 points less often is stopped at once.
  • A trial with fewer than 20 visits in 7 days is put back.

A change that was put back is not tried again for 30 days, and a step is left alone for 3 days after any trial. The run log of a step says "set by the analyst" when an override applied. The report lists what is set now, what happened at this check, and the last ten trials; analyst/tuning.json holds the full record, and it owns the overrides: an entry in overrides.json that no running or kept trial stands for is removed at the next check, so do not add entries by hand. scf analyst auto off stops new trials and keeps what is set. scf analyst reset removes it all and ends the running and the kept trials as "put back", so the same changes are not tried again for 30 days.

Published figures. Once a week the analyst asks Claude Code (with the Claude sign-in of this machine) for published scores, prices and speeds of the models you use and the levels next to them, and keeps them in analyst/reference.json with the source of each figure. Figures a third party measured itself are used as they are. Figures from the makers, and figures whose origin is not given, are lowered by a tenth. They are only used to hold back advice for a smaller model that scores far lower, and to know that a model exists before it is tried; your own runs always decide. Coding scores are only compared when both name the same test. That session has the two web tools (search and fetch) and no others, no MCP servers, none of your personal or project settings, and runs in an empty temporary folder. When the search fails or finds nothing, the earlier figures are kept and it is tried again a day later. On Linux, run scf analyst run from cron.

The command line for a running Foundry#

Everything the web interface does goes through the server's API, and every call of that API is also a command. These commands talk to the running Foundry, with your account and its rights. (The older commands such as scf run, scf resume and scf user work on the files of the data folder and need no server; they are unchanged.)

scf login                 # asks for your e-mail address and password; --server names another Foundry
scf whoami                # who is signed in, and where
scf runs list --status failed
scf turn list             # what waits for you
scf repos                 # an area without an action lists its commands
scf flows --help          # flows, blocks and monitor: without an action they are the older file commands
scf runs list --help      # the options of one command
scf logout

Signing in. scf login uses the same accounts as the web interface. The password is typed without echo, sent to the server and not kept. What is kept is the session, in cli-session.json in the data folder, readable by your user only. It ends after 7 days, at scf logout, or when the account's password changes. A script can pipe the password in as one line; without --email it pipes two lines, the e-mail address and then the password. Only a Foundry on this machine (localhost, 127.0.0.1, [::1]) is reached over plain http. Any other address is reached over HTTPS: --server foundry.example.com means https://, and an http:// address of another machine is refused, so the password never travels in clear text. A redirect is not followed; the command says where the server points to and stops.

The areas.

AreaWhat it covers
runsList, show, start, answer, approve, reject, resume, cancel and archive runs; a step's log and the diff
turnWhat waits for you, and answering it: answers, defaults, approve, reject, retry
board, findThe board, the next steps, what changed since a time; search
reposYour repositories: add, sign-in method, test the connection, remove
refineRefinement sessions: story drafts, suggestions, reviews, the publish plan, publishing
askAsk SCF: ask a question, try again, stop; rate an answer (feedback); your conversations (list, show, complete with --resolved, archive, restore, delete); what a page context is worth (context); the usage and quality numbers (admin)
flows, blocksList, show, check, save and remove flows and blocks
settings, providersThe settings file; the AI tools and model providers
monitorThe self-monitor: findings, mutes, bug stories
users, admin, auditAccounts and their limits, all repositories and their watchers, the audit log (admin)
account, secretsYour own password; your stored secrets
watchers, serverWatchers; server information, queue, statistics, clean-up

What every command has.

  • --json prints the server's answer as it is, for scripts. Without it you get a table or a few lines.
  • --data '{…}' or --data @file.json adds fields to the call that have no option of their own. It only adds: a field that an option, an argument or the command itself sets is refused, and so is a field that another command asks about first (the role in scf users set, the sign-in method in scf repos add). A command that only reads takes no --data.
  • Text that starts with - goes after --: scf flows save my-flow --scope repo -- "$(cat my.yaml)".
  • --server <address> talks to another Foundry (sign in there first).
  • --as <account id> reads as that user would, for an admin who started scf admin view-as.
  • Exit code 0 means done; 4 means not signed in or not allowed (also for scf whoami); 1 is any other refusal by the server. Errors are printed on standard error; with --json the server's whole refusal is printed there as JSON.
  • A call may take two minutes (a sign-in half a minute); then the command gives up. An answer that is not from a Foundry counts as an error, whatever its status.

Safety.

  • A command that deletes or replaces something asks you to type yes. --yes answers for you; without a terminal and without --yes it stops. scf flows save and scf blocks save ask only when a file with that name is there already; scf users add asks only for --role admin.
  • A password, token or other secret is never an option or an argument: the command asks for it without echo (or reads one line of standard input each, in the order it would ask), so it is not in your shell history. Given inline in --data it is refused, for every command (the fields token, password, secret, current, key, privateKey); in a file (--data @file) it is accepted, unless the field is one the command does not let --data set.
  • An argument that becomes part of the address (an id, a name) cannot be . or .. and cannot contain / or \.
  • scf settings set sets all settings: a setting you leave out goes back to its default, so it insists on --data. The Slack webhook is the exception: it stays unless you give notify.slack_webhook: null, which removes it. Start from scf settings show --json; it does not show the webhook, only notify.slack_webhook_set.

Any call directly. scf api routes [word] lists every call the server knows and who may make it; scf api GET runs (or POST, PUT, PATCH, DELETE) makes one. A body is sent only with --data: without it nothing is sent, so add --data '{}' for a call that wants an empty one. DELETE, PUT and PATCH ask you to type yes first (--yes answers; without a terminal and without --yes they stop). One call has no command: the live event stream of a run, which never ends; scf api refuses it too.

Helper processes#

Five small processes that run beside the Foundry, each started by macOS by itself once installed. They read the data folder and write their results in their own folder there. Two of them also set something outside it: the governor and the limits helper each keep their cap on the runs at once in a file of their own in governor/caps/, and the governor writes test-slots/cap. None needs the server, and the server does not need them. Every one has the same commands:

scf governor install    # also: flaky, limits, blockage, digest, housekeeping
scf governor run        # one check now
scf governor report     # the last result
scf governor status     # uninstall removes it and what it set

status says "installed" only when macOS really runs the helper. "Installed but not loaded" means the file is there and nothing runs: run install again.

HelperRunsWhat it does
governorevery minuteLowers the number of runs at once while the machine is overloaded, and raises it again when it is calm.
flakyevery 6 hoursFinds tests that fail in unrelated stories or pass only on a retry.
limitsevery hourSays when the weekly Claude and Codex limits are reached at the current pace.
blockageevery hourFinds the stories that hold up the most others, and says when run slots are empty.
digestdaily at 07:30One short report: what was done, what failed, what waits for you.
housekeepingevery 3 hoursRecords the memory of the system's file-change tracker and of the Foundry server, and once a night removes the workspaces of old runs.

Caps on the runs at once. The governor and the limits helper can each lower the number of runs at once for a while. The lowest cap that still holds counts, never below 1 and never above the configured number. A cap holds back new runs only: work that runs is never stopped, and a run that already started and is resumed (after a restart, an answer or a pause) is not held back, because it may hold the turn to merge that others wait for. A new run that waits only for a cap is marked so in the queue (capped, and the queue names the cap), and the monitor does not report it as a queue that stands still.

The load governor. It reads the load average per core and the memory pressure. Above 2.5 load per core, or under critical memory pressure, it allows one run fewer per check, down to half the configured number (at least 2). Memory pressure "warning" counts only together with a load above 1.5 per core: macOS reports it for hours on a full but idle machine. After five calm checks in a row (1.5 per core or less) it allows one more, until the configured number is back. In between (a load between 1.5 and 2.5, or a memory warning without load) nothing moves at first, but after 15 checks in a row without overload it allows one more as well, so a cap does not stay for hours on a machine that is only a little busy. Above 4 per core, or under critical memory pressure, only one test run works at a time. Every change is a line in governor/changes.log. Its limits end by themselves four minutes after the last check — the cap on runs, the cap on test runs, and what the governor remembers — so a governor that stops can never hold the Foundry down, and the first check after the machine slept starts fresh. scf governor reset takes them away at once. The daily release check asks for a test slot like every test run, but waits five minutes at most.

Unreliable tests. A test is listed when it failed, or passed only on a retry, in at least three different stories of one repository in the last 7 days: one story can break its own test, several cannot. A run without an issue is not a story. Where it can see them:

  • A test that failed in a test log (Vitest and Gradle output).
  • A test that failed and passed when the flow ran the tests once more: the flows keep the first log as tests.first.log for that.
  • A test that Vitest retried by itself — only when the test command uses --reporter=verbose. Vitest's default reporter prints nothing for a test that passed on a retry.

The report names a likely cause when the error of that test shows one (a port in use, a time limit, a full disk), and says whether the test was also seen to pass. With scf flaky auto on it makes one bug story per test file and repository through the GitHub sign-in of this machine, at most 2 per check and never twice for the same file. A story is made only when at least one listed test of the file was also seen to pass in those 7 days (on a retry, on the second run, or named as passed in a verbose log); a test that never passes is broken, not unreliable, and stays in the report only. Off by default.

Usage limits. The weekly limits are not published, so the forecast needs one reading from you:

scf limits set claude 34 2026-10-19T23:00     # 34% used now; the week resets then (local time)

A reset without a time zone is local time, also a date alone (midnight). A reset more than 7 days ahead is refused. From the tokens used since the reset and that share it works out the size of the week, and from the pace of the last 24 hours when the limit is reached. It is an estimate: it holds while the mix of work stays about the same, and a reading below 5% is too coarse. With scf limits auto on it lowers the runs at once so that the limit lasts until the reset — down to 1 if needed, lower than the governor goes. It works from the number of runs that are allowed at that moment, lowers its cap as far as needed in one check, and raises it by one per check at most. The cap ends by itself two hours after the last check. Off by default.

What holds work up. It follows two kinds of waiting: a story that stepped aside for files another run holds, and a story that waits for one it depends on (from the service log of the last 6 hours, until the log shows that the story started or ended). The report lists the stories at the root, the number they hold up through the chain, and their state; stories are named with their repository. Stories that wait for each other in a circle are listed first, as one entry: that never ends by itself. A run that says "running" while its process is gone is shown as interrupted. The report says so when run slots are empty while stories wait.

Housekeeping. Every run installs and deletes very many small files, and macOS keeps track of every change. On a machine that runs for days, the system's file-change tracker (fseventsd) once grew to tens of gigabytes this way and left too little memory for the work. So every 3 hours this helper writes one line to housekeeping/memory.log with the memory of fseventsd and of the Foundry server. Above 8 GB for the tracker or 6 GB for the server it sends one notification that day, and the digest names it. The tracker only shrinks with a restart of the machine. Once a night, at the first check after 03:00, it removes the workspaces of runs that ended more than 2 days ago, as scf clean --older-than 2 does: run records, logs and branches stay, and active or paused runs are not touched.

The digest. For the last 24 hours, from the newest run of each story: done, split into smaller stories, failed (with the reason) and in progress; what waits for you is listed however old it is, and so are runs that were interrupted (they say "running", but their process is gone). Below that come a few lines from the other helpers and the analyst; a result of a helper that is older than 26 hours is left out. It is digest/report.md, and one file per day, named by the local date; a desktop notification gives the counts.

Self-update#

Off by default, because it lets merged code run on this machine without a person looking. For a Foundry that runs from a git checkout of its own repository. Switch it on in Settings → Safety and name the repository (self_update: { enabled: true, repo: owner/name } in config.yaml). After upgrading, stop and start the Foundry once, so that it can go back after a bad update.

Every 5 minutes it checks, in this order, and stops at the first reason that applies (the Health line shows it):

  1. The record self-update.json can be read, and no earlier go-back failed.
  2. The Foundry runs from a git checkout, under the supervisor (scf ui / scf serve, or the service).
  3. The origin of the checkout is the repository in the setting.
  4. main can be read from GitHub. Nothing new: nothing to do.
  5. The checkout is on main and has no local changes (untracked files count). Only then does it fetch.
  6. The same commit did not fail after its last try. It waits for a newer commit. (A failed test run is tried again 20 minutes later, up to 3 tries in all; the wait is shown in the Health line.)
  7. The checkout can fast-forward to main.

Then it builds and tests the new commit in a separate folder (npm ci, npm run build, npm test). The tests take a test slot like the tests of stories: 2 run at once, and they wait at most 15 minutes for a free one, then run anyway; their own 15-minute limit starts when they start, and a run that hits it counts as a failed try. If the tests fail, they run again after 20 minutes, up to 3 tries in all, and the Health line says which try it is. The log and the Health line after the third failure name the failing tests (at most 20 are kept). If the install or the build fails, it stops after one try. Until the last try ends, the old version keeps running; a failure after the last try gets a monitor finding. When all pass, no new runs start (queued runs wait and start after the restart) and the server restarts when the active runs are done. Right before the install it checks everything again, including the tip of main. It never updates from a branch other than main or from another repository.

Skipped tests. When the daily release check (release-daily) already passed the same tests on the same code, self-update does not run them again. The log then says self-update: <sha> is not tested again: the release check passed these tests on this code (run <id>, <time>); install and build still run. It skips only when all of these hold: the test command is the updater's own test step (npm test, or npm ci && npm test when node_modules was missing), the release check recorded a green run of that command on the same tree in the data folder (tested/), and the record has exactly the form the check writes. Every other case runs the tests as usual: a story's record, another tree or command, no detected command, an unreadable record, or a test_cmd pinned in .claude-factory/config.yaml of the code, even when a record matches. A release check with its own test_cmd records under that command, so it matches only when that command is the updater's own test step. Only the release check's record counts. Anything that can write tested/ in the data folder can make self-update skip its tests, so keep that folder as private as the rest of the Foundry.

If the new server stops or does not answer within 3 minutes, the supervisor puts the previous version back once and reports it (the monitor finding is critical). An install that was cut off is finished or undone at the next start. If you switch the setting off while an update waits, the server restarts on the unchanged version when the runs are done.

When the Health line says "Self-update is stopped", the checkout needs a person: run git status, npm ci, npm run build, then delete self-update.json in the data folder. Going back does not undo changes the bad version made in the data folder.


Improve this page on GitHub