feat(loop): an agentic OS for this repository - #284
Merged
Conversation
Only rules a script can check. The supervised-only list is this repo's own, not the article's: generated protobuf, the ADR record, the prise registry, CI and splitsh. psalm-baseline.xml joins the never-weaken-a-test law because a baseline entry silences a finding without fixing it. CHECK 1: wc -l CLAUDE.md = 66, under the 150 ceiling. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The gate runs exactly what CI's "QA (CS + tests)" and "Analyse statique" jobs run, so a tick that passes locally passes on the PR. Two additions the article does not have, both found by running it: - psalm-baseline.xml is diffed after the tools pass. Psalm exits 0 on a suppressed finding, so without this the gate green-lights a worker that "fixed" static analysis by widening the baseline. - vendor/ is provisioned into a fresh worktree by hardlink copy from the primary checkout. vendor/ is gitignored, so every tick would otherwise die 127 before running a single test. Composer's path repositories are relative symlinks, so the copy resolves against its own worktree and tests the branch's src/ — verified, not assumed. CHECK 2: the conductor invocation physically cannot write. Asked a model with --allowedTools "Read,Grep,Glob" to write a file: the attempt is recorded in permission_denials and no file appears. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The split the whole architecture exists for: the expensive seat decides, cheap seats execute, a fresh context judges, and a shell script votes last. Three departures from the article, each forced by what is actually installed: - There is no `llm` CLI here, so every seat is `claude -p --model`. One CLI, no new dependency, and the seats are variables so a model outage is a config edit rather than an incident. - `claude -p` has no --max-tokens flag. Dropped; the CLI manages the ceiling. - The CLI reports total_cost_usd per call, so log-cost.sh records real dollars instead of the article's token-arithmetic estimate. thinking_tokens and cache_read are still logged: the first is where surprise spend hides, the second is the only proof the cached-prefix ordering still holds. One check the article does not have. The read-only wall holds, but silently: a denied Write returns stop_reason "end_turn" and a cheerful message about having requested permission, so nothing in the happy path notices. loop.sh now reads permission_denials and stops the tick (exit 4) when the conductor reaches outside its seat — that is either injection or drift, and neither should have its decision acted on. The trust tier table in the source is a published image. Only the top row survives as text (auto = 20 runs at 95%); the watch/queue thresholds are this repository's own, and trust-log.sh says so where they are defined. CHECK 4: 21 passes -> auto; two fails -> watch, with ALERT demoted in STATE.md. CHECK 5: goals with predicate true and false -> exit 1, exactly one flipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
retro.sh is the one loop installed, because its install condition is "always". Quorum, ratchet and red team ship as written conditions with the reason each is not installed here — the article's rule is that a loop whose condition you cannot state should be uninstalled, and three uninstalled loops with triggers beat three installed ones nobody can justify. The runbook maps every exit code and every ALERT line to a response. Its rows come from contract.md's "pages me" list and loop.sh's exits; the article's own runbook table is a published image. Cron ships as text in the runbook. Installing a crontab is a decision, not a side effect of a merge. WA007 records the decision, the seats, the rollout, and one exception that needed writing down rather than assuming: loop/* branches are exempt from the prise registry, because a loop that opens and abandons draft PRs would either churn main with prise commits or fill the registry with stale entries. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…g goals
CHECK 3 does not pass as written until these are fixed. All three were mine or
the article's, none were the model's:
- log-cost.sh died on every call. "${3:-{}}" reads as a sane default but bash
closes the expansion at the first brace, so jq received the envelope with a
literal "}" appended and exited 5 after already emitting its line — killing
the tick just before the conductor's cost was logged.
- Both seats intermittently fence their output in markdown. Unfenced, a fenced
"QUIET" never equals QUIET, so a quiet repository wakes the expensive seat on
every tick; and a fenced work order dies in jq. The prompts ask for bare
output, but a prompt is an instruction, not a guarantee.
- Gating execution on the `auto` tier deadlocked the ledger: a skill reaches
`auto` by accumulating runs and can only accumulate runs by executing. What
the tier gates is the PR, not the attempt — below `auto` the work still runs
in a throwaway worktree and the branch is left for a human, which is what
weeks 1 and 2 of the rollout describe.
CHECK 3: a hand tick now exits 0, logs triage and conductor cost lines, and
writes one dispatch row. The conductor queued a contract-sensitive item rather
than acting on it. Cache reads dominate conductor input (54127 cached against 4
uncached), which is CHECK 6's caching criterion visible on the first tick.
Gate, both directions: a planted CS violation is rejected (exit 8); a widened
psalm-baseline.xml is rejected; a clean tree passes both guards silently.
The two goals are born from this branch's own finished work, and each predicate
was tested against the fixed and the broken state as Layer 5 requires — padding
CLAUDE.md past 150 lines and pointing the gate at a non-existent composer script
both flip their goal to VIOLATED.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…out of it Two bugs in the six lines between the worker and the ledger — the stretch that decides whether a skill earns autonomy, and the only stretch no check had run. - `git diff` reports neither untracked files nor anything the worker committed. The worker prompt mandates TDD, so the new failing test is exactly the untracked file that goes missing, and the verifier was being asked to judge done_when against a diff with the test removed. Now captured against the branch point: `git add -A && git diff "$BASE"`. Proved before fixing — touch a new file and modify a tracked one, and `git diff` reports one of the two. - An empty diff wrote an unearned pass. It satisfies the verifier vacuously (nothing in it exceeds the spec) and passes the gate (the tree is unchanged). Twenty of those promote a skill that has done nothing to `auto`, poisoning the one ledger that decides autonomy. A no-op is no longer recorded as a run. First live run of the execute path, with these fixes in: conductor chose execute, the worker went off-spec and wrote an IMPLEMENTATION.md instead of touching the target file, and the fresh-context verifier caught it — "the diff does not touch the target file at all". Ledger recorded 1 run, 0 passes, tier queue; the skill did not earn autonomy. Maker and checker separation paying for itself on the first run it was asked to do anything. Four stages, ~$0.60. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements an agentic OS over this monorepo — the permanent layer of files, scripts and ledgers that lets a model work here unattended and be checked while it does. Nine layers, after Linas Beliūnas' How to Build an Agentic OS with Claude Fable 5 (2026-07-10), ported to this repository's PHP toolchain and to the
claudeCLI.The decision, the seats, the rollout and the exceptions are recorded in WA007.
What is here
CLAUDE.md— 66 lines, only rules a script can checkloop/contract.md,loop/guardrails/verify.shloop/loop.sh,triage.md,conductor.md,workers/*.mdloop/scripts/trust-log.shloop/verify-goals.sh,loop/goals/loop/scripts/log-cost.sh,cost-check.shloop/retro.sh,loop/loops.mdMakefile,loop/RUNBOOK.mdThe split the architecture exists for: an expensive seat decides, cheap seats execute, a fresh context judges, and a shell script votes last. The conductor is physically unable to write a file, and the gate runs exactly what CI's
QA (CS + tests)andAnalyse statiquejobs run.Departures from the article, and why
llmCLI on this machine, so every seat isclaude -p --model. One CLI, no new dependency. The seats are variables: a model outage or a price change is a config edit, not an incident.claude -phas no--max-tokensflag. The article's script does not run as printed.total_cost_usd, so cost logging records real dollars rather than the article's token-arithmetic estimate.thinking_tokensandcache_readare still logged: the first is where surprise spend hides, the second is the only proof the cached-prefix ordering still holds.vendor/is provisioned into a fresh worktree by hardlink copy from the primary checkout. It is gitignored, so every tick would otherwise die 127 before running a test. Composer's path repositories are relative symlinks, so the copy resolves against its own worktree — verified, not assumed.auto(20 runs, 95%) survives as text; thewatch/queuethresholds are this repository's own and say so where they are defined. Same for the runbook, whose rows are built fromcontract.mdandloop.sh's exit codes.Repository-specific additions
psalm-baseline.xmlmay never grow. A baseline entry silences a finding without fixing it — test-weakening in another coat. The gate diffs the file; the verifier fails a diff that touches it.documentation/adr/is supervised-only: an ADR records what was decided when it was written (DUR000).loop/*branches are exempt from the prise registry, which is a deliberate exception argued in WA007 rather than an oversight.Five bugs found by running it
None were the model's.
"${3:-{}}"— bash closes the expansion at the first brace, sojqreceived the envelope with a literal}appended and killed every tick just before the conductor's cost was logged.QUIETnever equalsQUIET, so a quiet repository woke the expensive seat every tick.autotier deadlocked the ledger — a skill reachesautoby accumulating runs and could only accumulate them by executing. The tier gates the PR, not the attempt.git diffreports neither untracked files nor worker commits. The worker prompt mandates TDD, so the new failing test was exactly the file going missing, and the verifier was judgingdone_whenagainst a diff with the test cut out.pass— vacuously satisfying the verifier and passing the gate. Twenty of those promote a skill that has done nothing toauto.Checks
stop_reasonbefore using a resultwc -l CLAUDE.md= 66, under 150permission_denialsand produces no file. Gate rejects a planted CS violation (exit 8) and a widenedpsalm-baseline.xml; silent on a clean treequeueauto; two fails →watchwithALERT demotedinSTATE.mdtrueandfalse→ exit 1, exactly one flipped. Both shipped goals tested against their fixed and broken statesthinking_tokensnonzeroTwo things this branch does not do
illuminate/cacheis incomposer.lockbut missing from the localvendor/, so 13 tests error. This pre-exists onorigin/mainin the primary checkout and is unrelated to this branch — CI installs fresh, so it should not appear here. Worth acomposer installlocally.LOOP_PUSHdefaults to0, so nothing opens a PR until that is deliberately set — week 3 of the rollout.One item needs a human decision before week 3: a GitHub OAuth token sits in the global Composer configuration on the development machine and is readable by any process the loop spawns. WA007 makes moving the loop to a low-privilege user a precondition for granting it push rights.