Skip to content

feat(loop): an agentic OS for this repository - #284

Merged
gplanchat merged 7 commits into
mainfrom
feat/agentic-os
Sep 7, 2026
Merged

feat(loop): an agentic OS for this repository#284
gplanchat merged 7 commits into
mainfrom
feat/agentic-os

Conversation

@gplanchat

Copy link
Copy Markdown
Owner

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 claude CLI.

The decision, the seats, the rollout and the exceptions are recorded in WA007.

What is here

Layer Files
1 Constitution CLAUDE.md — 66 lines, only rules a script can check
2 Walls and gate loop/contract.md, loop/guardrails/verify.sh
3 Heartbeat loop/loop.sh, triage.md, conductor.md, workers/*.md
4 Trust ledger loop/scripts/trust-log.sh
5 Standing goals loop/verify-goals.sh, loop/goals/
6 Budget loop/scripts/log-cost.sh, cost-check.sh
7 Optional loops loop/retro.sh, loop/loops.md
8 Ops Makefile, loop/RUNBOOK.md
9 Rollout WA007, the 30-day table

The 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) and Analyse statique jobs run.

Departures from the article, and why

  • No llm CLI on this machine, so every seat is claude -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 -p has no --max-tokens flag. The article's script does not run as printed.
  • The CLI reports total_cost_usd, so cost logging records real dollars rather than 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.
  • 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.
  • The trust tier table in the source is a published image. Only auto (20 runs, 95%) survives as text; the watch/queue thresholds are this repository's own and say so where they are defined. Same for the runbook, whose rows are built from contract.md and loop.sh's exit codes.
  • Layer 0's pricing is two months stale. No figures from it are transcribed into the repository.

Repository-specific additions

  • psalm-baseline.xml may 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.

  1. "${3:-{}}" — bash closes the expansion at the first brace, so jq received the envelope with a literal } appended and killed every tick just before the conductor's cost was logged.
  2. Both seats intermittently fence their output in markdown. A fenced QUIET never equals QUIET, so a quiet repository woke the expensive seat every tick.
  3. Gating execution on the auto tier deadlocked the ledger — a skill reaches auto by accumulating runs and could only accumulate them by executing. The tier gates the PR, not the attempt.
  4. git diff reports 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 judging done_when against a diff with the test cut out.
  5. An empty diff wrote an unearned pass — vacuously satisfying the verifier and passing the gate. Twenty of those promote a skill that has done nothing to auto.

Checks

Check Result
0 Every scripted call checks stop_reason before using a result
1 wc -l CLAUDE.md = 66, under 150
2 A conductor asked to write a file has the attempt recorded in permission_denials and produces no file. Gate rejects a planted CS violation (exit 8) and a widened psalm-baseline.xml; silent on a clean tree
3 Hand tick exits 0, logs triage and conductor costs, writes one dispatch row, and queued a contract-sensitive item rather than acting on it. Execute path ran end to end: worker went off-spec, the fresh-context verifier caught it, the ledger recorded 1 run / 0 passes / tier queue
4 21 passes → auto; two fails → watch with ALERT demoted in STATE.md
5 Goals with predicate true and false → exit 1, exactly one flipped. Both shipped goals tested against their fixed and broken states
6 Cache reads dominate conductor input — 54127 cached against 4 uncached on the first tick; thinking_tokens nonzero
7 Every installed loop carries its install condition; three are specified and deliberately not installed

Two things this branch does not do

  • The gate cannot go green locally. illuminate/cache is in composer.lock but missing from the local vendor/, so 13 tests error. This pre-exists on origin/main in the primary checkout and is unrelated to this branch — CI installs fresh, so it should not appear here. Worth a composer install locally.
  • No crontab is installed. The cron lines are text in the runbook. LOOP_PUSH defaults to 0, 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.

gplanchat and others added 7 commits September 6, 2026 23:24
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>
@gplanchat
gplanchat merged commit 823849c into main Sep 7, 2026
34 checks passed
@gplanchat
gplanchat deleted the feat/agentic-os branch September 7, 2026 08:38
gplanchat added a commit that referenced this pull request Sep 7, 2026
Both merged (#284, #285). A claim left behind lies for as long as it stays,
and prises-check.sh would flag them stale on its next daily sweep.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant