You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Controller: one pass reads, decides, records, dispatches, writes the view and raises the reconciliation alert #193
Part of the epic "Epic: slice 1, first approval. A Release on next reviews a pull request to this repo, and the machine user's approval stands".
Why. The first commit of controller/: one pass (design §2.3, steps 1 to 6 and 14 to 16). Twelve rows and four lessons wait on it, and one of them, I-33, is the design's "most useful control": the enqueues of 2026-10-18 that no session made are the event it would have alerted on.
Done when
controller/pass.py reads every open pull request through the facts port and the run records through the records port, calls kernel.decide with the context (paused, active_runs, model_deferred_until, passes_since, unidentified_runs, all keys always given), uploads pass/1 with every read's date and age, dispatches an admitted Run named by run-name/1 with a minted run and writes that run beside its Decision, writes view/meta.json and view/items.json from config/feed.json and nothing else, and posts the status check run on the item only when the Decision changed (I-17).
The brakes outside the Kernel: FACTORY_PAUSED on the org or the repo read as exact true, exact false or nothing (L-58), and the Run ceiling, each stopping Admission when the Kernel is broken (decision 0003).
Alerts, one cause one alert, on one tracking issue: a Factory identity's write that matches no recorded Effect (I-33); a pass that did not run (L-48); a credential near its limit (L-37); an automatic pause needs agreeing faults from Runs on different items (L-59). A pass deferred by an unidentified Run starts no successor; otherwise it starts one only when something is due sooner than the next tick (W-7).
tests/test_controller.py runs a pass against recorded facts and run records with a fake effects port, and asserts each of the above; tools/check_purity.py holds kernel/ apart; controller/AGENTS.md carries the rules of that place. Every row waiting on controller/ is re-pointed.
Needs. The two ports. The Controller App's key is stored (factory-poller); the poller workflow is the workflows story's.
Solution Architect, delegated. On main at ab53e4f. Read from:
the issue (it counts "twelve rows"; there are 17 decision rows and four lessons waiting on controller/);
the slice-1 epic's Order ("Then: the Controller", waiting on the ports);
design §2.2 and §2.3 (the pass's six steps), §3.4 (the context's five keys), §3.10 (the run-name), §4.7 (the Pause), §4.9 (reconciliation, I-33), §7.3 as decision 0043 left it, and §8.4 (the view's files);
kernel/decide.py:decide, kernel/issue.py:decide and kernel/brakes.py;
schemas/pass.schema.json, view-meta, view-items, run-name and config/feed.json;
sim/world.py:poll (the same steps, with faults and invariants mixed in, which the simulator keeps);
What this story decides. What one pass is, as code, and what of it can be built and tested before anything runs on GitHub.
1. Two pull requests, after both ports.
PR 1 is the pass as a function of its inputs, built and tested on recorded facts and records.
PR 2 is its writes: dispatching a Run, the status on the item, a dequeue and the alerts' tracking issue, through the effects port, whose first commit it is. Why: every decision the pass makes can be tested with no I/O, as the Kernel's are. The writes need a port that writes, and it should be reviewed apart from what decides.
2. PR 1: controller/pass_.py. One function takes the reads, the configuration, now, the previous pass records and a mint for ids. It returns:
the pass/1 record, validated, holding the context, the runs, each read's date and age, and each item's facts and Decision with the Run id minted for an admission or an approval;
the list of Runs to dispatch, each named by run-name/1;
view/meta.json and view/items.json from one pass, with next_actor and decision_required from config/feed.json alone (0039);
view/health.json;
the alerts;
whether to start a successor.
It calls kernel.decide and kernel.issue.decide and decides nothing itself.
3. What PR 1 holds.
The context. It is built from the reads: paused from FACTORY_PAUSED on the org and the repo, read as an exact true, an exact false or nothing, and anything else unreadable (L-58). Also active_runs, model_deferred_until, unidentified_runs and passes_since. passes_since is the first pass after the last gap of the not-run alert's second reading (0043).
started_by. It is read from the run that started the pass, an account by its type and id in the configuration (0043). The configuration gains the relay's account as null until slice 3.
The successor. One starts only for an item due at the pass's end. Every pass reads no sooner than three minutes after the last that read began. A pass deferred by an unidentified Run starts none (0036, 0043).
The not-run alert's second reading. Six hours with no pass is one alert, which clears at the next pass. The relay's reading waits on the relay, controller/relay.py.
One cause, one alert (L-48). An automatic pause needs agreeing faults from Runs on different items (L-59).
The rebuild. A view rebuilt from GitHub equals the view updated pass by pass (0002), held over a sequence of recorded passes.
The three thresholds of 0011, computed from run records and written to view/health.json, whose schema health/1 is new, with an example and counter-examples.
4. What waits, and on what.
I-33, reconciliation (0005's two rows, 0007's override row). It needs a read of every write each Factory identity made, which facts/1 does not hold. It waits on controller/reconcile.py, a story of its own once the facts port's PR 2 reads the timeline's acts.
0020's base-branch hooks.json read waits on adapters/settings.py.
L-37's credential age waits on harness/.
0011's W-2 with the relay waits on controller/relay.py.
5. Tests and mutants.tests/test_controller.py uses recorded facts/1, run-record and issue-facts/1, from examples/ and the ports' replays, and a fake mint. Each rule gets a mutant naming its test, checked by the capped filtered run. controller/AGENTS.md. The rows held are re-pointed; the rest name ruling 4's files.
6. Record. None for PR 1: 0002, 0003, 0036, 0039 and 0043 decided it. PR 2's effects port may need one, at its own ruling.
7. Gates. Export verify; a fresh Claude review and a security pass. AGY on Flash, since it calls the Kernel on every item. "Muse: not run". The capped filtered mutant run.
8. Out of scope. The workflows (#196); the effects port (PR 2); the relay (slice 3); the page (slice 4); calling the Kernel any way but its two entry points.
Needs the Maintainer? No, for PR 1. One configuration key, the relay's account, is added as null. Its value is his once the relay exists.
Amendment to ruling 8 (the orchestration session's ruling (a), 2026-10-21), for "Kernel: an unidentified Run older than P-17's window raises one alert instead of deferring every admission" (#270).
Ruling 8 had the pass ask the Kernel through its two entry points alone, kernel.decide.decide and kernel.issue.decide. #270 needs one reading of the whole scope that no single item's Decision can give: which unnamed Runs are past max_unnamed_run_seconds (the run_unnamed alerts the pass writes to view-meta/1's alerts), whether every Run waits on an unnamed Run, and whether more are past the window than max_unnamed_runs. The pass needs these with no item to decide on, to raise run_unidentified or runs_unnamed and to hold back a successor.
So the Kernel has a third entry point, kernel.decide.unnamed_scope(context, config, now). It returns {defers, alerts, bounded}, read as each Decision reads them, and a test holds the two to each other. tests/test_controller.py::test_the_pass_asks_the_kernel_through_its_entry_points_alone and controller/AGENTS.md name all three. Carrying the alerts on every Decision instead would have changed decision/1 for every item, and was the costlier option.
Working rulings for PR 2, the effects port (overturnable in review)
Solution Architect, delegated. On main at 32be214. Read from:
the issue, "Controller: one pass reads, decides, records, dispatches, writes the view and raises the reconciliation alert", its Done-when ("posts the status check run on the item only when the Decision changed (I-17)"; "Alerts, one cause one alert, on one tracking issue"), its rulings 1 to 8 and the amendment to ruling 8 (unnamed_scope);
adapters/ on main: github.py (request, Live, Replay; the one redirect of decisions 0044 and 0045; EXTRA headers alone), records.py (runs(transport, repo, branch, workflows, dispatcher); its docstring's "a dispatch the Controller's App made"), facts.py (snapshot returns (facts, title), no head), pull.py, checks.py, artifacts.py (the 64 KiB storage bound), and adapters/AGENTS.md (effects.py planned);
config/apps.json (the Controller's App: actions: read, checks: write for write:status_on_item, contents, metadata and pull_requests read; write:tracking_issue and effect:dequeue_merge waiting; no need that reads a variable), config/controller.json and schemas/controller.schema.json (accounts.workflow_token null), config/defaults.json (status_on_item is factory);
design §2.2 (the effects port "applies one Effect and reads it back"), §2.3 (the last arrow: "update the status on the item if the decision changed"), §3.3 (the status's one fixed name), §3.4 (the pass record), §3.6 (recovery keys, the re-read, the two bindings), §3.10 (an act has no run record), §4.1 ("Leaving the merge queue": the Controller's App dequeues), §4.7 (one tracking issue; notified once per item per reason), §4.9 (the threats and the credentials table), §6 (P-2, P-9, P-12, P-42), §8.5, and I-4, I-7, I-13, I-17, I-21, I-22, I-33, I-34 and I-38. There is no §3.x on the status: §3.9 is "State: derived";
decisions 0002 (its row on the status, waiting on harness/), 0003 (its issue-227 dequeue row, waiting on adapters/effects.py), 0005, 0035, 0036, 0039 (the envelope, ENVELOPE), 0043, 0044 and 0045. No decision is on the Controller's own writes;
trials/, every write in every fixture: P-9 exchange 8 is the one write by the workflow's own token (a dispatch, {"ref": "main", "inputs": {...}}, 204, its run's actor github-actions[bot], id 41898282, Bot). Every other dispatch (P-1, P-3, P-7, P-14, P-17, P-22, P-24, P-27) is the Maintainer's. No fixture holds a dequeuePullRequest, a check run or commit status written, an issue comment, or an issue opened or edited by an App; P-46 opens and closes an issue as the Maintainer. P-44 grounds the lane, not a write. P-2 and P-12 have not run;
lessons L-41, L-48, L-50, L-51 and L-58, and sim/grounding.py (a_pass_starts_its_successor on P-9; an_app_leaves_the_queue waiting on P-2);
What this story decides. What the Controller writes after a pass, as code: which credential each write uses, what may be sent through it, in what order, how a write already made is recognised, and how each write is recorded. It is the first commit of adapters/effects.py, and it names the two entries that #196's pass.yml runs.
1. Two pull requests, after PR 1.
PR
Holds
Waits on
2
The pass's entry; the plan of writes; the effects port's first commit, with the dispatches (a Run, an approval, the successor) and the status on the item; written/1; the record
P-12, run with the Controller's key at the Maintainer's terminal (ruling 12); adapters/identity.py, from #194 PR 2 or written here, whichever is enqueued first
3
The dequeue, the tracking issue, and the Pause read again before an act or a dispatch; the Controller's new grants in config/apps.json
PR 2; the Maintainer's change to the Controller's registration and P-39 again (ruling 3); P-2, in #185's window, with the dequeue by the Controller's App; the trial of the Controller's issue writes and its read of the Pause (ruling 12)
Why: PR 2's writes need no grant the Controller's App lacks: the dispatch is the pass job's token, which P-9 tried, and the status is checks: write, which the App holds. PR 3's writes each need a grant it does not hold, and a trial no one has run. Splitting there lets #196 PR 2 build on PR 2 while PR 3 waits on the Maintainer's hand.
2. The modules.
controller/writes.py, pure.plan(out, heads, config) takes what pass_.run returned and the head commit read beside each pull request's facts, and returns writes/1: the writes of this pass, in ruling 5's order, each with an id w1, w2 and so on. It reads no clock and reaches nothing, as pass_.py does (tests/test_controller.py::test_the_pass_asks_the_kernel_through_its_entry_points_alone is widened to name it).
adapters/effects.py.apply(writes, record, transports, config) sends each write through the transport its type names and returns written/1. It holds one table: for each write type, its credential, the requests it may send, its recovery key and its read-back (ruling 4). The approving act: the machine user approves from a Run with no Engine, bound to the admitted commit #195 PR 1 adds approve to that table, with the machine user's token as its own credential.
Two entries.python3 -m controller is the pass: it mints the Controller's token, reads through every port, waits out wait_before_reading, runs the pass and writes pass.json, the view's files and writes.json to the directory it is given. It is the one module of controller/ that reads the environment or the clock. python3 -m adapters.effects is the writes: it reads pass.json and writes.json, builds one Live transport for each credential, writes written.json, and exits non-zero if any write failed.
Held to the record before anything is sent.writes/1 must name the record's pass; each dispatch must carry a Run id that a Decision in the record holds with outcome admit, or act and issue_approval, on the same item and content, under the run-name pass_.py gives it; each dequeue must match a dequeue Decision on the same item. Otherwise the whole list is refused and nothing is sent (I-22). Held by tests/test_adapters.py::test_a_write_list_that_is_not_the_records_is_refused_whole.
New schemas.writes/1 and written/1, documents of identity pass in ENVELOPE, each with an example and counter-examples (tools/check_examples.py).
3. Which credential each write uses.
Write
Credential
Grant
Held now?
Dispatch a Run (run.yml), an approval (approve.yml), the successor (poller.yml)
The pass job's own token, WORKFLOW_TOKEN: ${{ github.token }}, on the effects step alone (#196 rulings 4 and 10)
actions: write, declared on the pass job alone
Yes. P-9: 204, and the run's actor is id 41898282
The status on the item
The Controller's App, a token narrowed to checks: write and metadata: read
checks: write (write:status_on_item)
Yes
The dequeue
The Controller's App (§4.1)
Not known: P-2 records the least that dequeuePullRequest needs
No.effect:dequeue_merge waits on P-2
The tracking issue
The Controller's App
issues: write, a new need write:tracking_issue → issueswrite
No. No Factory App holds issues: write
The Pause, read by the pass and again before an act
The Controller's App
GitHub's repository variables permission, read. GitHub documents that it reads both the repo's variable and the org's variables shared with the repo; the trial records the least that works
No. No need in config/apps.json reads a variable
Two transports, each held to a list. Each Live transport is wrapped so that it sends only its credential's requests: the workflow token, a POST of /repos/{home}/actions/workflows/{file}/dispatches for the three files alone; the Controller's token, the reads and writes of rulings 6 to 8 by method and path, and for GraphQL the one dequeue document word for word. Any other request is refused before it leaves (tests/test_adapters.py::test_each_write_goes_through_its_one_credential_and_its_allowlist).
The tokens. Each is read from the environment by the entry alone, handed to its transport, never written or logged; the Controller's token is revoked in a finally (DELETE /installation/token, as P-1 shows). The workflow token is never handed to the Controller's transport, nor the reverse.
Flagged: permissions to add. Three, all on the Controller's App and all in PR 3: the variables read, issues: write, and the dequeue's grant once P-2 names it. Each is a change the Maintainer makes to the registration, and tools/check_apps.py holds the data to P-39's fixtures, so the data, P-39's new fixtures and the trial land together. If the dequeue needs pull_requests: write, tools/check_apps.py's "no write need is held by two Apps" still holds: the need is the dequeue's, not the review's.
Why: §4.9 gives the dispatch to the poller job's token and nothing else, and P-9 shows that token starts a run the records port can tell by its account. Every other write is the Controller's App, so that reconciliation (I-33) can tell the Factory's writes by one account. The default token is github-actions[bot] for every workflow, so a write by it is no one's in particular.
4. The port's interface: idempotence, recovery keys and the re-read. GitHub writes carry no idempotency key (§3.6), so each write type says how a write already made is recognised, and what is read back.
Write
Recovery key
Read back
Re-read before
A Run's or an approval's dispatch
None: GitHub answers 204 and no id. Sent once a pass and never retried, whatever it answers
By the next pass's records port, by its run-name; until then the Run may be unidentified, which defers every admission (0036)
None: the Run asks the Kernel again as it starts (I-38)
The successor
None. Sent once
Not read back: a successor is no Run and takes no run-name
None
The status
The Controller's App's check run named status_on_item on that head, whose external_id is the Decision's digest (P-12)
GitHub's answer to the write: name, head, App and external_id as sent
None: it is bound to the commit it describes (ruling 6)
The dequeue
Queue membership, and the entry's head (§3.6)
The pull request read again, out of the queue
The entry and its head (ruling 7)
The tracking issue
The one open issue the Controller's App opened in the home repo; its body as rendered
GitHub's answer: number, state, body
The open issues the App opened (ruling 8)
A claim is only what was read back (L-41). A dispatch is sent, never applied, since nothing in its answer says a run exists. Held by tests/test_adapters.py::test_a_claim_is_only_what_was_read_back.
No retry of a dispatch. A dispatch GitHub answered with 5xx, or did not answer, may still have started a run; a second would start a second run under the same Run id. It is recorded failed, dispatch_unanswered, and the next pass reads the run list and decides again. Held by tests/test_adapters.py::test_a_dispatch_is_sent_once_and_never_retried.
No Run the records port could not count. While accounts.workflow_token is null, nothing is dispatched: such a Run would be counted by no one, and the ceilings would not see it (I-34). Held by tests/test_adapters.py::test_nothing_is_dispatched_that_the_records_port_could_not_count.
5. The order of writes in a pass, and what a failed write does. The record is uploaded first, by pass.yml (§2.3, #196 ruling 4). Then the effects step writes in this order, held by tests/test_controller.py::test_the_writes_go_in_their_order:
The dequeues. A stop comes first: the queue can merge at any moment (§4.1).
The dispatches of Runs and approvals, in the record's item order, so a Run waits for a runner from as early as it can.
The statuses.
The tracking issue, last but one, so it can say that a write before it failed.
The successor, last, so the next pass starts after this one's writes.
A failed write:
is recorded in written/1 with a reason code, and the next write goes on. A 401 or 403 ends every later write on that credential, each recorded refused, token_refused;
fails the step: once written.json is written, the entry exits non-zero, so the pass's run concludes failure where the Maintainer sees it;
adds the cause write_failed to the tracking issue's body in the same pass (PR 3). If the issue's own write fails, the failed run and written/1 are the report;
never changes the pass record. pass/1 is uploaded before any write and is never written again.
Nothing reads written/1 to decide. The next pass derives every write again from GitHub (0002): a status not written is written, a dequeue not made is made, and a Run whose dispatch failed is admitted again only if the run list shows none. Held by tests/test_adapters.py::test_a_failed_write_is_recorded_and_fails_the_step.
6. The status on the item. One check run for each head, edited in place.
Where. A check run by the Controller's App, named status_on_item from the configuration (factory), on the head commit read beside the pull request's facts. It is posted on that commit, so it describes the content the Decision was made on, even if the pull request has moved since. A new head has none, so the first pass to read it posts one.
When (I-17). The effects port lists the App's check runs of that name on the head. If one carries this Decision's external_id, nothing is written (already_applied). If one carries another, it is edited in place by PATCH. If there is none, one is created. The external_id is d and the first 32 hex digits of the SHA-256 of the Decision's canonical JSON, sorted keys, so any change to the Decision, its evidence among it, changes it. Held by tests/test_adapters.py::test_the_status_is_written_only_when_the_decision_changes.
Not from the pass records. Comparing with the previous pass's Decision would leave a status that once failed to write stale for ever. GitHub holds the key, so the status is derived like everything else (0002).
Its conclusion. Always completed and neutral. A status that never passes or fails cannot be read as a gate, and no ruleset could be satisfied by it (§4.9; tests/test_tools.py::test_the_status_on_the_item_never_carries_a_required_lens_name already holds its name). Held by tests/test_adapters.py::test_the_status_neither_passes_nor_fails.
Its text: codes and identifiers alone. The title is <state>: <reason>. The summary lists the attention, the outcome and action, next_actor from config/feed.json, the content_id, the pass and the Release, and the evidence. An evidence item is shown only if it matches ^[A-Za-z0-9_.:/#@-]{1,200}$; any other is counted, never shown. No title, no login, no branch name and no comment reaches it. The summary is at most 4 KiB and shows at most 20 evidence items, then "and N more". details_url is the pass's run, built from its id. Held by tests/test_adapters.py::test_the_status_carries_codes_and_identifiers_alone.
On which items. An open pull request whose Decision is not hands_off and whose State is not not_served. Hands-off is never acted on, and an item not served gets no change (§4.1, §4.7). An issue gets none in slice 1: it has no commit, and the issue Lifecycle's labels are its status. Held by tests/test_adapters.py::test_no_status_is_written_on_an_item_left_alone.
Under a Pause the status is written: it is not an act (§4.7 lists what a Pause stops), and it is how a person sees why nothing moves.
A bound on one pass. At most max_status_writes in controller/1, 50, in item order; the rest wait for the next pass. GitHub limits requests that create content, and a first pass or a Pause changes every status at once.
7. The dequeue (PR 3).dequeuePullRequest, by the Controller's App, bound to the head it was decided on.
The re-read. One GraphQL read of the pull request: its node id, headRefOid, and mergeQueueEntry { id headCommit { oid } }.
Not in the queue:already_applied, and nothing is sent (§3.6's key).
In the queue at another head than the one read beside the Decision:superseded, head_moved, and nothing is sent. Only a person or the Factory enqueues, and the next pass decides on that head.
In the queue at that head: the mutation, with the pull request's node id, by one fixed document the allowlist holds word for word. GitHub's mutation takes no head, so the binding is the read just before it.
Read back. The pull request read again; it is applied only if its mergeQueueEntry is null.
The Pause, read again. Before the first dispatch or act of a pass, the effects port reads FACTORY_PAUSED on the org and on every repo it acts in, as controller/pass_.py:human_pause reads it (L-58). Unless every scope reads exactly not paused, no dispatch and no dequeue is sent, each recorded refused, paused or pause_unreadable. This is a brake outside the Kernel (0003): it holds if the Kernel is broken, and it reads both scopes where the shim's if: sees one value (Workflows: the frozen entry shim, the poller, the Run's three jobs and the nudge, each held to its shape #196 ruling 6). It lands in PR 3, since it needs the variables grant; until then the pass's own read is refused and admits nothing. Held by tests/test_adapters.py::test_no_act_or_run_is_sent_unless_the_pause_reads_exactly_not_paused_on_every_scope.
Held by tests/test_adapters.py::test_a_dequeue_is_bound_to_the_head_it_was_decided_on and test_a_pull_request_out_of_the_queue_is_already_dequeued.
8. The tracking issue (PR 3). One issue in the Factory's repo, opened when a fault first stands and closed when none does.
Found again by its opener. The open issues the Controller's App opened, listed by its bot login and kept only where the author's id and type are accounts.controller's, never by a title or a label anyone can set. controller/1 gains accounts.controller, null until the trial records the bot account's id; while it is null, no issue is written.
What opens it. The pass's alerts, less paused: a Pause the Maintainer set is not a fault. pause_unreadable is one. Plus write_failed, from this step (ruling 5).
Open, edit, close. With causes and no open issue, one is opened, titled Factory alerts, with the Maintainer assigned (his third point below). With causes and an open issue, its body is edited in place only if it differs from the rendering (already_applied otherwise). With no cause and an open issue, it is closed, state_reasoncompleted. A cause later opens a new issue; a closed one is never reopened.
Two open. If two open issues are the App's, neither is written, the write is refused, tracking_issue_twice, and the step fails. A person closes one.
Its body. A fixed first line, then one line for each cause code, sorted, then the pass id, its now and the Release. Codes only: no text from outside reaches it.
Notification. GitHub notifies the assignee once, when the issue opens. A new cause while it is open changes its body and notifies no one. That is "one tracking issue" and "notified once" (§4.7). The other way, a comment for each new cause, is the one write with no recovery key (§3.6); see the Maintainer's points.
Held by tests/test_adapters.py::test_one_tracking_issue_is_opened_once_edited_in_place_and_closed_when_clear, test_the_persons_own_pause_opens_no_tracking_issue and test_a_second_open_tracking_issue_is_never_written.
9. The record of each write: written/1, not pass/1.pass/1 is uploaded before any write (§2.3), so it cannot hold what the writes did, and it keeps its major version. Each write goes in written/1, uploaded by pass.yml's last step under if: always():
type: dispatch_run, dispatch_approval, dispatch_successor, status_on_item, dequeue, tracking_issue, and not_built for an act no write serves yet;
state: applied, already_applied, sent (a dispatch alone), superseded, refused or failed, as applied/1's, with sent added;
key: what was read back, a check run's id, a queue entry's or pull request's node id, an issue's number; null for a dispatch;
reason: a code or null. No text.
It is public, as every artifact is, and holds ids and codes alone. It is what controller/reconcile.py will match the Controller's own writes against (I-33); without it, every status and dequeue would be an alert. An enqueue, and the issue Lifecycle's accept, remove_label and place_hold, are recorded refused, not_built, and raise nothing. Held by examples/written.json, its counter-examples, and tests/test_adapters.py::test_every_write_sent_is_in_written_with_what_was_read_back.
10. The pass's entry, and what it cannot read yet.#196 PR 2 runs python3 -m controller, so this PR builds it. Three of its inputs have no port today:
The Pause. No port reads FACTORY_PAUSED, and no grant allows it. PR 2 adds adapters/pause.py, one function reading the org's and each repo's variable as GitHub holds it, for the entry and for ruling 7. Until the grant exists, GitHub refuses the read, the Pause is unreadable, and every pass admits nothing (I-13). That fails closed.
The pass records before this one. No port reads them, and 14 days of passes is about a thousand artifacts. The entry reads the last two passes' records, which is all that the read gap, passes_since, the not-run alert and the successor read. The view's age_hours and the health window then cover only those two until the feed story keeps them cheaper. A pass record is likely larger than decision 0045's 64 KiB bound on what the storage serves; see the Maintainer's points.
Held by tests/test_controller.py::test_the_entry_reads_through_the_ports_and_hands_the_pass_nothing_else on replays, and test_only_the_entry_reads_the_clock_or_the_environment.
Under the go-live Pause the pass records, posts statuses, and keeps the tracking issue. It dispatches and dequeues nothing: the Kernel defers, ruling 7's re-read refuses, and the shim's if: skips.
12. Tests, and the trial behind each write.tests/test_adapters.py gains Effects, and replays each write from a fixture, varying a copy of the recorded exchange as the existing port tests do (0044). tests/test_controller.py holds writes.py and the entry. A write with no fixture is not tested on a hand-made answer (L-51).
Write
Grounded by
Ask
A dispatch by the pass job's token, with inputs
trials/P-9.json exchange 8
None. #196 ruling 9's canary trial covers a dispatch of another file than the job's own
The successor
trials/P-9.json exchange 8
None
The status: create, list by name and App on a head, edit in place, external_id read back, what GitHub keeps of the output
None. P-12 asks the external_id
P-12, widened to these, run before PR 2 merges with the Controller's key at the Maintainer's terminal through tools/trial_as_app.sh. No token window
The dequeue, and the least grant it needs
None
P-2, with the dequeue by the Controller's App, not the pr-review App as #185's issue has it. In #185's window
The tracking issue: open with an assignee, edit, close, list by creator; the Pause's read on the repo and the org
None. P-46 opens and closes an issue as the Maintainer
A new trial, the next free number when filed, after the registration change; also at his terminal
L-50 ("Each write a Role may make is tried once on a canary repo") is held by tests/test_adapters.py::test_every_write_the_port_sends_has_the_shape_a_trial_recorded. It lists each write type with the fixture whose exchange it was built from, and make verify prints any without one, as it prints sim/grounding.py's unverified rules. an_app_leaves_the_queue in sim/grounding.py names P-2's fixture when it lands.
13. Mutants, and the rows re-pointed. Each rule gets a mutant in tools/mutants.py naming its test, checked by the capped filtered run on adapters/effects.py, adapters/pause.py, controller/writes.py and controller/__main__.py. Among them:
a dispatch sent through the Controller's transport;
a dispatch retried after a 5xx;
the null workflow_token guard dropped;
the record check skipped;
the status written whatever its external_id, or concluded success on a converged item;
the evidence filter dropped;
applied recorded without the read-back;
the step exiting 0 after a failed write;
the dequeue sent at another head;
the Pause re-read skipped, or reading the repo's value alone;
the tracking issue opened on paused, or found by its title.
tools/check_context.py fails every row still waiting on a file once it exists, so PR 2 re-points every row waiting on adapters/effects.py, PR 3's among them. No row waits on controller/ now.
Row or lesson
Now
To
0002: the status on a Work item is rewritten only when the decision changes
waiting on harness/
PR 2: tests/test_adapters.py::test_the_status_is_written_only_when_the_decision_changes. If #194 PR 1 lands first, it re-points this row to adapters/effects.py
0003: on a person's hold or close of an issue, the Controller dequeues the Factory's pull request for it (issue 227)
waiting on adapters/effects.py
Waiting on adapters/issues.py, from PR 2. No Kernel rule makes a pull request's Decision read its issue's hold or close (kernel/issue.py says such a merge "is never done"), so the reading is still to build, and an issue is filed for it. PR 3 adds the write's half, test_a_dequeue_is_bound_to_the_head_it_was_decided_on
L-48: an item that needs a person is assigned to one
waiting on adapters/effects.py
Waiting on controller/notify.py, the notification story that config/apps.json names, added to controller/AGENTS.md's list
L-50: each write a Role may make is tried once on a canary repo
0003: the Pause for the Controller's acts, if #196 PR 1 splits it to controller/brakes.py
waiting on controller/brakes.py
PR 3: test_no_act_or_run_is_sent_unless_the_pause_reads_exactly_not_paused_on_every_scope; the Run ceiling's half stays
0005 and I-33: every write by a Factory identity matches a recorded Effect
waiting on controller/reconcile.py
Unchanged; written/1 is what it will read
adapters/AGENTS.md and controller/AGENTS.md gain a row for each rule above.
14. Record. One, numbered at enqueue with PR 2: "The Controller writes through two credentials, each held to what it may send; its status is one check run per head that neither passes nor fails; and every write it sends is recorded in written/1". Its table holds rulings 2 to 6 and 9 to 11. PR 3 adds rows for rulings 7 and 8 and the grants. It rewords §4.9's credentials row for the Controller's App key: "Read the facts, the run records and the Pause; post check runs, the status on the item; take a pull request out of the merge queue; keep the tracking issue. It does not start Runs". It amends decision 0045's bound if the Maintainer takes his fourth point.
15. What the design leaves undecided. Each is named so that a builder does not settle it silently.
The status's form. §2.3 and §3.3 give a check run with one name, and nothing on its conclusion, its text, a new head, or an issue. Ruling 6 settles each.
Which head a status or a dequeue binds to.facts/1 carries none (ruling 10).
The item input's form,owner/repo#number, as the run-name writes it (ruling 4).
The tracking issue. How it is found again, whether a Pause is a fault, whether a new cause notifies, and what two open issues mean (ruling 8).
Writes under a Pause. §4.7 lists what a Pause stops; the status and the issue are not in it (rulings 6 and 8).
The Pause's read, and the pass records' read. No port and no grant for either (ruling 10).
A dispatch's read-back. None in the step, since the records port is the one reader of runs (ruling 4).
GitHub's limit on content creation. It is not measured; ruling 6's bound is a guess, revisited from the first passes.
A transient failure. It opens the tracking issue once, and the next clean pass closes it. Damping it would need the pass before's written/1, which nothing reads today.
16. Gates. Export verify on each PR. A fresh Claude review and a security pass on each. The security pass reads first the two transports' allowlists, where each token goes from the environment to its transport, and what text can reach the check run and the issue. AGY on Argon on both, since they place credentials and the Controller's writes, and PR 2's entry calls the Kernel on every item. "Muse: not run (Maintainer, 2026-10-20: Kernel/hook only)". The capped filtered mutant run on each new path. PR 3 changes config/apps.json, a protected path, so the Maintainer approves it.
17. Out of scope. The workflows and their steps (#196); PR 2 names the two entries and #196 PR 2 runs them. The approval's write (#195 PR 1 adds approve to the port's table). The enqueue, which has no story yet, and the issue Lifecycle's label acts, each recorded not_built. Assignment and review requests (controller/notify.py). Reconciliation (controller/reconcile.py, I-33). The view's write (the feed story, combsmith-sf-view). The relay (slice 3). Backpressure (controller/backpressure.py).
Needs the Maintainer? Yes, on five points; the rest is buildable as ruled.
Risk: the Controller's App gains three grants (ruling 3): the variables read, issues: write, and the dequeue's grant, which P-2 names. A stolen Controller key could then also open, edit, close and label issues and pull requests where the App is installed, and, if the dequeue needs pull_requests: write, comment and review. The other way for the issue is the pass job's token with issues: write. That is not recommended: github-actions[bot] writes for every workflow, so I-33 could not tell the Factory's writes from any other's. He changes the registration, and P-39 is run again.
Cost: three trials. P-12 widened, at his terminal with the Controller's key, before PR 2 merges, with no token window. A new trial of the issue writes and the Pause's read, after the registration change. P-2 with the dequeue by the Controller's App, inside Trials P-2, P-5, P-23 and P-25: an App in the merge queue, and threads resolved by another App #185's window, which amends that story's P-2.
Scope: how the tracking issue notifies (ruling 8). Recommended: once, when it opens, by assigning him. That needs his login and id in controller/1. The other way is a comment for each new cause while it is open, the one write with no recovery key.
Risk: reading the pass records before this one (ruling 10). Recommended: the last two only, which is all the Kernel's context needs. The view's ages and the health window then cover two passes until the feed story. A pass record zipped is likely larger than 0045's 64 KiB storage bound. Recommended: a bound for the artifact named pass alone, 1 MiB zipped and 8 MiB read out, amending 0045. The other way is no read of earlier records at all, which breaks the read gap and passes_since.
Scope: statuses under the go-live Pause (rulings 6 and 11). The first passes post a factory check run, neutral, on every open pull request, saying it is paused. Recommended: yes, since it says why nothing moves. The other way holds every Controller write while the repo is Paused, and the first statuses appear only when he clears it.
The Maintainer's answers and the orchestration session's rulings (2026-10-21) on the effects port's five points.
(c) The Controller App's three grants: granted.
The grants are a repository variables read (for FACTORY_PAUSED, the org's and the repo's), issues: write, and the dequeue's permission once P-2 names it.
The Maintainer changes the App's registration and runs P-39 again when the PR that needs them, the effects port's next part, is ready. That PR gives him the exact steps then.
config/apps.json gains the three needs in the same PR, with P-39's new fixtures.
(d) The tracking issue assigns him.controller/1 gains accounts.maintainer, his login and account id, read from the fixtures. The issue opened for the first cause assigns him, once. A cause while it is open edits its body and notifies no one.
(e) Two rulings by the orchestration session:
The pass records. The pass artifact gets a download bound of its own: 1 MiB zipped and 8 MiB read out. It is recorded as a new row on decision 0045, since the record is never edited. Every other artifact keeps 0045's 64 KiB.
The go-live Pause. Every open pull request gets a neutral "paused" status, because it says why nothing moves.
Built so far. The first part of PR 2, held until decision 0048 is on main:
the plan of writes, rebuilt from the pass record and held to it exactly;
the dispatches through the workflow token alone, each body held;
written/1;
decision 0049.
The status, the dequeue, the tracking issue and the Pause re-read come in the next part, after P-12 and P-2 and the grants above.
Activity
don-petry commentedon Oct 21, 2026
Working rulings (overturnable in review)
Solution Architect, delegated. On main at ab53e4f. Read from:
controller/);kernel/decide.py:decide,kernel/issue.py:decideandkernel/brakes.py;schemas/pass.schema.json,view-meta,view-items,run-nameandconfig/feed.json;sim/world.py:poll(the same steps, with faults and invariants mixed in, which the simulator keeps);What this story decides. What one pass is, as code, and what of it can be built and tested before anything runs on GitHub.
1. Two pull requests, after both ports.
Why: every decision the pass makes can be tested with no I/O, as the Kernel's are. The writes need a port that writes, and it should be reviewed apart from what decides.
2. PR 1:
controller/pass_.py. One function takes the reads, the configuration,now, the previous pass records and a mint for ids. It returns:pass/1record, validated, holding the context, the runs, each read's date and age, and each item's facts and Decision with the Run id minted for an admission or an approval;run-name/1;view/meta.jsonandview/items.jsonfrom one pass, withnext_actoranddecision_requiredfromconfig/feed.jsonalone (0039);view/health.json;It calls
kernel.decideandkernel.issue.decideand decides nothing itself.3. What PR 1 holds.
pausedfromFACTORY_PAUSEDon the org and the repo, read as an exact true, an exact false or nothing, and anything else unreadable (L-58). Alsoactive_runs,model_deferred_until,unidentified_runsandpasses_since.passes_sinceis the first pass after the last gap of the not-run alert's second reading (0043).started_by. It is read from the run that started the pass, an account by its type and id in the configuration (0043). The configuration gains the relay's account as null until slice 3.controller/relay.py.view/health.json, whose schemahealth/1is new, with an example and counter-examples.4. What waits, and on what.
facts/1does not hold. It waits oncontroller/reconcile.py, a story of its own once the facts port's PR 2 reads the timeline's acts.workflow_run, and the Pause that holds when Factory code is broken, wait on.github/workflows/poller.yml(Workflows: the frozen entry shim, the poller, the Run's three jobs and the nudge, each held to its shape #196).hooks.jsonread waits onadapters/settings.py.harness/.controller/relay.py.5. Tests and mutants.
tests/test_controller.pyuses recordedfacts/1,run-recordandissue-facts/1, fromexamples/and the ports' replays, and a fake mint. Each rule gets a mutant naming its test, checked by the capped filtered run.controller/AGENTS.md. The rows held are re-pointed; the rest name ruling 4's files.6. Record. None for PR 1: 0002, 0003, 0036, 0039 and 0043 decided it. PR 2's effects port may need one, at its own ruling.
7. Gates. Export verify; a fresh Claude review and a security pass. AGY on Flash, since it calls the Kernel on every item. "Muse: not run". The capped filtered mutant run.
8. Out of scope. The workflows (#196); the effects port (PR 2); the relay (slice 3); the page (slice 4); calling the Kernel any way but its two entry points.
Needs the Maintainer? No, for PR 1. One configuration key, the relay's account, is added as null. Its value is his once the relay exists.
Controller: the fix round, after the Claude review, the security pass…
Controller: Codex's read of the fix round (#193, PR 1)
don-petry commentedon Oct 21, 2026
Amendment to ruling 8 (the orchestration session's ruling (a), 2026-10-21), for "Kernel: an unidentified Run older than P-17's window raises one alert instead of deferring every admission" (#270).
Ruling 8 had the pass ask the Kernel through its two entry points alone,
kernel.decide.decideandkernel.issue.decide. #270 needs one reading of the whole scope that no single item's Decision can give: which unnamed Runs are pastmax_unnamed_run_seconds(therun_unnamedalerts the pass writes toview-meta/1'salerts), whether every Run waits on an unnamed Run, and whether more are past the window thanmax_unnamed_runs. The pass needs these with no item to decide on, to raiserun_unidentifiedorruns_unnamedand to hold back a successor.So the Kernel has a third entry point,
kernel.decide.unnamed_scope(context, config, now). It returns{defers, alerts, bounded}, read as each Decision reads them, and a test holds the two to each other.tests/test_controller.py::test_the_pass_asks_the_kernel_through_its_entry_points_aloneandcontroller/AGENTS.mdname all three. Carrying the alerts on every Decision instead would have changeddecision/1for every item, and was the costlier option.don-petry commentedon Oct 22, 2026
Working rulings for PR 2, the effects port (overturnable in review)
Solution Architect, delegated. On main at 32be214. Read from:
unnamed_scope);controller/as Controller: the pass as a pure function of what it read (#193, PR 1) #275 and Controller: Argon's merged-early read of the pure pass (#193, PR 1 follow-on) #294 left it:pass_.py(runreturnsrecord,dispatchas{run, name},acts,view,alerts,cleared,left_out,successor),reads.py,feed.py,health.py,shared.py,controller/AGENTS.md, andtests/test_controller.py;adapters/on main:github.py(request,Live,Replay; the one redirect of decisions 0044 and 0045;EXTRAheaders alone),records.py(runs(transport, repo, branch, workflows, dispatcher); its docstring's "a dispatch the Controller's App made"),facts.py(snapshotreturns(facts, title), no head),pull.py,checks.py,artifacts.py(the 64 KiB storage bound), andadapters/AGENTS.md(effects.py planned);config/apps.json(the Controller's App:actions: read,checks: writeforwrite:status_on_item,contents,metadataandpull_requestsread;write:tracking_issueandeffect:dequeue_mergewaiting; no need that reads a variable),config/controller.jsonandschemas/controller.schema.json(accounts.workflow_tokennull),config/defaults.json(status_on_itemisfactory);harness/), 0003 (its issue-227 dequeue row, waiting onadapters/effects.py), 0005, 0035, 0036, 0039 (the envelope,ENVELOPE), 0043, 0044 and 0045. No decision is on the Controller's own writes;schemas/effect.schema.json,pass.schema.json,applied.schema.json,decision.schema.json(actionsissue_approval,enqueue,dequeue),issue-decision.schema.json(actionsaccept,remove_label,place_hold) andrun-name.schema.json;trials/, every write in every fixture: P-9 exchange 8 is the one write by the workflow's own token (a dispatch,{"ref": "main", "inputs": {...}}, 204, its run's actorgithub-actions[bot], id 41898282, Bot). Every other dispatch (P-1, P-3, P-7, P-14, P-17, P-22, P-24, P-27) is the Maintainer's. No fixture holds adequeuePullRequest, a check run or commit status written, an issue comment, or an issue opened or edited by an App; P-46 opens and closes an issue as the Maintainer. P-44 grounds the lane, not a write. P-2 and P-12 have not run;sim/grounding.py(a_pass_starts_its_successoron P-9;an_app_leaves_the_queuewaiting on P-2);actions: write, andpass.ymlruns the pass's entry, the record's upload, then "the effects entry", givenCONTROLLER_APP_KEYandWORKFLOW_TOKEN; ruling 6: run.yml's inputsname,item,pass; ruling 10: the dispatcher is the workflow token's account,run_workflows; ruling 11: live under a Pause), The approving act: the machine user approves from a Run with no Engine, bound to the admitted commit #195 (ruling 1:approveinadapters/effects.py; its amendment:approve.ymlis a shim on the item's lane), Harness: run and apply for the review Intent, and the model holds no credential that can write #194 (ruling 6:adapters/identity.py's mint), and Trials P-2, P-5, P-23 and P-25: an App in the merge queue, and threads resolved by another App #185's issue (P-2 as written has the pr-review App take the pull request out).What this story decides. What the Controller writes after a pass, as code: which credential each write uses, what may be sent through it, in what order, how a write already made is recognised, and how each write is recorded. It is the first commit of
adapters/effects.py, and it names the two entries that #196'spass.ymlruns.1. Two pull requests, after PR 1.
written/1; the recordadapters/identity.py, from #194 PR 2 or written here, whichever is enqueued firstconfig/apps.jsonWhy: PR 2's writes need no grant the Controller's App lacks: the dispatch is the pass job's token, which P-9 tried, and the status is
checks: write, which the App holds. PR 3's writes each need a grant it does not hold, and a trial no one has run. Splitting there lets #196 PR 2 build on PR 2 while PR 3 waits on the Maintainer's hand.2. The modules.
controller/writes.py, pure.plan(out, heads, config)takes whatpass_.runreturned and the head commit read beside each pull request's facts, and returnswrites/1: the writes of this pass, in ruling 5's order, each with an idw1,w2and so on. It reads no clock and reaches nothing, aspass_.pydoes (tests/test_controller.py::test_the_pass_asks_the_kernel_through_its_entry_points_aloneis widened to name it).adapters/effects.py.apply(writes, record, transports, config)sends each write through the transport its type names and returnswritten/1. It holds one table: for each write type, its credential, the requests it may send, its recovery key and its read-back (ruling 4). The approving act: the machine user approves from a Run with no Engine, bound to the admitted commit #195 PR 1 addsapproveto that table, with the machine user's token as its own credential.python3 -m controlleris the pass: it mints the Controller's token, reads through every port, waits outwait_before_reading, runs the pass and writespass.json, the view's files andwrites.jsonto the directory it is given. It is the one module ofcontroller/that reads the environment or the clock.python3 -m adapters.effectsis the writes: it readspass.jsonandwrites.json, builds oneLivetransport for each credential, writeswritten.json, and exits non-zero if any write failed.writes/1must name the record'spass; each dispatch must carry a Run id that a Decision in the record holds with outcomeadmit, oractandissue_approval, on the same item and content, under the run-namepass_.pygives it; each dequeue must match adequeueDecision on the same item. Otherwise the whole list is refused and nothing is sent (I-22). Held bytests/test_adapters.py::test_a_write_list_that_is_not_the_records_is_refused_whole.writes/1andwritten/1, documents of identitypassinENVELOPE, each with an example and counter-examples (tools/check_examples.py).3. Which credential each write uses.
run.yml), an approval (approve.yml), the successor (poller.yml)WORKFLOW_TOKEN: ${{ github.token }}, on the effects step alone (#196 rulings 4 and 10)actions: write, declared on thepassjob alonechecks: writeandmetadata: readchecks: write(write:status_on_item)dequeuePullRequestneedseffect:dequeue_mergewaits on P-2issues: write, a new needwrite:tracking_issue→issueswriteissues: writeconfig/apps.jsonreads a variableLivetransport is wrapped so that it sends only its credential's requests: the workflow token, aPOSTof/repos/{home}/actions/workflows/{file}/dispatchesfor the three files alone; the Controller's token, the reads and writes of rulings 6 to 8 by method and path, and for GraphQL the one dequeue document word for word. Any other request is refused before it leaves (tests/test_adapters.py::test_each_write_goes_through_its_one_credential_and_its_allowlist).finally(DELETE /installation/token, as P-1 shows). The workflow token is never handed to the Controller's transport, nor the reverse.issues: write, and the dequeue's grant once P-2 names it. Each is a change the Maintainer makes to the registration, andtools/check_apps.pyholds the data to P-39's fixtures, so the data, P-39's new fixtures and the trial land together. If the dequeue needspull_requests: write,tools/check_apps.py's "no write need is held by two Apps" still holds: the need is the dequeue's, not the review's.records.py's docstring says the Controller's App dispatches a Run. It cannot; PR 2 corrects it if Workflows: the frozen entry shim, the poller, the Run's three jobs and the nudge, each held to its shape #196 PR 2 has not.Why: §4.9 gives the dispatch to the poller job's token and nothing else, and P-9 shows that token starts a run the records port can tell by its account. Every other write is the Controller's App, so that reconciliation (I-33) can tell the Factory's writes by one account. The default token is
github-actions[bot]for every workflow, so a write by it is no one's in particular.4. The port's interface: idempotence, recovery keys and the re-read. GitHub writes carry no idempotency key (§3.6), so each write type says how a write already made is recognised, and what is read back.
status_on_itemon that head, whoseexternal_idis the Decision's digest (P-12)external_idas sentsent, neverapplied, since nothing in its answer says a run exists. Held bytests/test_adapters.py::test_a_claim_is_only_what_was_read_back.failed,dispatch_unanswered, and the next pass reads the run list and decides again. Held bytests/test_adapters.py::test_a_dispatch_is_sent_once_and_never_retried.accounts.workflow_tokenis null, nothing is dispatched: such a Run would be counted by no one, and the ceilings would not see it (I-34). Held bytests/test_adapters.py::test_nothing_is_dispatched_that_the_records_port_could_not_count.{"ref": <home branch>, "inputs": {"name": <run-name/1>, "item": "<owner>/<repo>#<number>", "pass": <pass id>}}, each copied from the record; the successor's is{"ref": <home branch>}.itemis written as the run-name writes it, so the lane's group is the same for a Run and an approval on one item (Workflows: the frozen entry shim, the poller, the Run's three jobs and the nudge, each held to its shape #196 ruling 6, The approving act: the machine user approves from a Run with no Engine, bound to the admitted commit #195's amendment).5. The order of writes in a pass, and what a failed write does. The record is uploaded first, by
pass.yml(§2.3, #196 ruling 4). Then the effects step writes in this order, held bytests/test_controller.py::test_the_writes_go_in_their_order:A failed write:
written/1with a reason code, and the next write goes on. A 401 or 403 ends every later write on that credential, each recordedrefused,token_refused;written.jsonis written, the entry exits non-zero, so the pass's run concludesfailurewhere the Maintainer sees it;write_failedto the tracking issue's body in the same pass (PR 3). If the issue's own write fails, the failed run andwritten/1are the report;pass/1is uploaded before any write and is never written again.Nothing reads
written/1to decide. The next pass derives every write again from GitHub (0002): a status not written is written, a dequeue not made is made, and a Run whose dispatch failed is admitted again only if the run list shows none. Held bytests/test_adapters.py::test_a_failed_write_is_recorded_and_fails_the_step.6. The status on the item. One check run for each head, edited in place.
status_on_itemfrom the configuration (factory), on the head commit read beside the pull request's facts. It is posted on that commit, so it describes the content the Decision was made on, even if the pull request has moved since. A new head has none, so the first pass to read it posts one.external_id, nothing is written (already_applied). If one carries another, it is edited in place byPATCH. If there is none, one is created. Theexternal_idisdand the first 32 hex digits of the SHA-256 of the Decision's canonical JSON, sorted keys, so any change to the Decision, its evidence among it, changes it. Held bytests/test_adapters.py::test_the_status_is_written_only_when_the_decision_changes.completedandneutral. A status that never passes or fails cannot be read as a gate, and no ruleset could be satisfied by it (§4.9;tests/test_tools.py::test_the_status_on_the_item_never_carries_a_required_lens_namealready holds its name). Held bytests/test_adapters.py::test_the_status_neither_passes_nor_fails.<state>: <reason>. The summary lists the attention, the outcome and action,next_actorfromconfig/feed.json, thecontent_id, the pass and the Release, and the evidence. An evidence item is shown only if it matches^[A-Za-z0-9_.:/#@-]{1,200}$; any other is counted, never shown. No title, no login, no branch name and no comment reaches it. The summary is at most 4 KiB and shows at most 20 evidence items, then "and N more".details_urlis the pass's run, built from its id. Held bytests/test_adapters.py::test_the_status_carries_codes_and_identifiers_alone.hands_offand whose State is notnot_served. Hands-off is never acted on, and an item not served gets no change (§4.1, §4.7). An issue gets none in slice 1: it has no commit, and the issue Lifecycle's labels are its status. Held bytests/test_adapters.py::test_no_status_is_written_on_an_item_left_alone.max_status_writesincontroller/1, 50, in item order; the rest wait for the next pass. GitHub limits requests that create content, and a first pass or a Pause changes every status at once.7. The dequeue (PR 3).
dequeuePullRequest, by the Controller's App, bound to the head it was decided on.headRefOid, andmergeQueueEntry { id headCommit { oid } }.already_applied, and nothing is sent (§3.6's key).superseded,head_moved, and nothing is sent. Only a person or the Factory enqueues, and the next pass decides on that head.appliedonly if itsmergeQueueEntryis null.FACTORY_PAUSEDon the org and on every repo it acts in, ascontroller/pass_.py:human_pausereads it (L-58). Unless every scope reads exactly not paused, no dispatch and no dequeue is sent, each recordedrefused,pausedorpause_unreadable. This is a brake outside the Kernel (0003): it holds if the Kernel is broken, and it reads both scopes where the shim'sif:sees one value (Workflows: the frozen entry shim, the poller, the Run's three jobs and the nudge, each held to its shape #196 ruling 6). It lands in PR 3, since it needs the variables grant; until then the pass's own read is refused and admits nothing. Held bytests/test_adapters.py::test_no_act_or_run_is_sent_unless_the_pause_reads_exactly_not_paused_on_every_scope.tests/test_adapters.py::test_a_dequeue_is_bound_to_the_head_it_was_decided_onandtest_a_pull_request_out_of_the_queue_is_already_dequeued.8. The tracking issue (PR 3). One issue in the Factory's repo, opened when a fault first stands and closed when none does.
accounts.controller's, never by a title or a label anyone can set.controller/1gainsaccounts.controller, null until the trial records the bot account's id; while it is null, no issue is written.paused: a Pause the Maintainer set is not a fault.pause_unreadableis one. Pluswrite_failed, from this step (ruling 5).Factory alerts, with the Maintainer assigned (his third point below). With causes and an open issue, its body is edited in place only if it differs from the rendering (already_appliedotherwise). With no cause and an open issue, it is closed,state_reasoncompleted. A cause later opens a new issue; a closed one is never reopened.refused,tracking_issue_twice, and the step fails. A person closes one.nowand the Release. Codes only: no text from outside reaches it.tests/test_adapters.py::test_one_tracking_issue_is_opened_once_edited_in_place_and_closed_when_clear,test_the_persons_own_pause_opens_no_tracking_issueandtest_a_second_open_tracking_issue_is_never_written.9. The record of each write:
written/1, notpass/1.pass/1is uploaded before any write (§2.3), so it cannot hold what the writes did, and it keeps its major version. Each write goes inwritten/1, uploaded bypass.yml's last step underif: always():{schema: "written/1", pass, release, writes: [{id, type, item, run, state, key, reason}]};type:dispatch_run,dispatch_approval,dispatch_successor,status_on_item,dequeue,tracking_issue, andnot_builtfor an act no write serves yet;state:applied,already_applied,sent(a dispatch alone),superseded,refusedorfailed, asapplied/1's, withsentadded;key: what was read back, a check run's id, a queue entry's or pull request's node id, an issue's number; null for a dispatch;reason: a code or null. No text.It is public, as every artifact is, and holds ids and codes alone. It is what
controller/reconcile.pywill match the Controller's own writes against (I-33); without it, every status and dequeue would be an alert. An enqueue, and the issue Lifecycle'saccept,remove_labelandplace_hold, are recordedrefused,not_built, and raise nothing. Held byexamples/written.json, its counter-examples, andtests/test_adapters.py::test_every_write_sent_is_in_written_with_what_was_read_back.10. The pass's entry, and what it cannot read yet. #196 PR 2 runs
python3 -m controller, so this PR builds it. Three of its inputs have no port today:facts.snapshotreturns(facts, title). It gains the head as a third value, read from the snapshot's own first read and held bypull.unchanged, beside the facts and never in them, as The approving act: the machine user approves from a Run with no Engine, bound to the admitted commit #195 ruling 4 also needs. Whichever of the two is enqueued first adds it.FACTORY_PAUSED, and no grant allows it. PR 2 addsadapters/pause.py, one function reading the org's and each repo's variable as GitHub holds it, for the entry and for ruling 7. Until the grant exists, GitHub refuses the read, the Pause is unreadable, and every pass admits nothing (I-13). That fails closed.passes_since, the not-run alert and the successor read. The view'sage_hoursand the health window then cover only those two until the feed story keeps them cheaper. A pass record is likely larger than decision 0045's 64 KiB bound on what the storage serves; see the Maintainer's points.Held by
tests/test_controller.py::test_the_entry_reads_through_the_ports_and_hands_the_pass_nothing_elseon replays, andtest_only_the_entry_reads_the_clock_or_the_environment.11. Dormant until #196's shims exist.
pass.yml, and nothing callspass.ymluntil Workflows: the frozen entry shim, the poller, the Run's three jobs and the nudge, each held to its shape #196 PR 3's shim. That merges under the Maintainer's Pause (Workflows: the frozen entry shim, the poller, the Run's three jobs and the nudge, each held to its shape #196 ruling 11).accounts.workflow_tokenis null (ruling 4), and Workflows: the frozen entry shim, the poller, the Run's three jobs and the nudge, each held to its shape #196 PR 2 sets it. No tracking issue is written whileaccounts.controlleris null. Until the variables grant, the Pause reads unreadable and nothing is admitted.if:skips.pass.ymlgains, as its last step underif: always(), the upload ofwritten/1. The configuration names the home repo and branch once,home: {repo, branch}, and the workflow files by what they carry:workflows: {run: "run.yml", approval: "approve.yml", poller: "poller.yml"}. The records port listsrunandapproval. This replaces Workflows: the frozen entry shim, the poller, the Run's three jobs and the nudge, each held to its shape #196 ruling 10'srun_workflowslist, since a dispatch must pick its file by kind. The two rulings must agree before either PR lands.12. Tests, and the trial behind each write.
tests/test_adapters.pygainsEffects, and replays each write from a fixture, varying a copy of the recorded exchange as the existing port tests do (0044).tests/test_controller.pyholdswrites.pyand the entry. A write with no fixture is not tested on a hand-made answer (L-51).trials/P-9.jsonexchange 8trials/P-9.jsonexchange 8external_idread back, what GitHub keeps of the outputexternal_idtools/trial_as_app.sh. No token windowL-50 ("Each write a Role may make is tried once on a canary repo") is held by
tests/test_adapters.py::test_every_write_the_port_sends_has_the_shape_a_trial_recorded. It lists each write type with the fixture whose exchange it was built from, andmake verifyprints any without one, as it printssim/grounding.py's unverified rules.an_app_leaves_the_queueinsim/grounding.pynames P-2's fixture when it lands.13. Mutants, and the rows re-pointed. Each rule gets a mutant in
tools/mutants.pynaming its test, checked by the capped filtered run onadapters/effects.py,adapters/pause.py,controller/writes.pyandcontroller/__main__.py. Among them:workflow_tokenguard dropped;external_id, or concludedsuccesson a converged item;appliedrecorded without the read-back;paused, or found by its title.tools/check_context.pyfails every row still waiting on a file once it exists, so PR 2 re-points every row waiting onadapters/effects.py, PR 3's among them. No row waits oncontroller/now.harness/tests/test_adapters.py::test_the_status_is_written_only_when_the_decision_changes. If #194 PR 1 lands first, it re-points this row toadapters/effects.pyadapters/effects.pyadapters/issues.py, from PR 2. No Kernel rule makes a pull request's Decision read its issue's hold or close (kernel/issue.pysays such a merge "is never done"), so the reading is still to build, and an issue is filed for it. PR 3 adds the write's half,test_a_dequeue_is_bound_to_the_head_it_was_decided_onadapters/effects.pycontroller/notify.py, the notification story thatconfig/apps.jsonnames, added tocontroller/AGENTS.md's listadapters/effects.pytests/test_adapters.py::test_every_write_the_port_sends_has_the_shape_a_trial_recordedcontroller/brakes.pycontroller/brakes.pytest_no_act_or_run_is_sent_unless_the_pause_reads_exactly_not_paused_on_every_scope; the Run ceiling's half stayscontroller/reconcile.pywritten/1is what it will readadapters/AGENTS.mdandcontroller/AGENTS.mdgain a row for each rule above.14. Record. One, numbered at enqueue with PR 2: "The Controller writes through two credentials, each held to what it may send; its status is one check run per head that neither passes nor fails; and every write it sends is recorded in
written/1". Its table holds rulings 2 to 6 and 9 to 11. PR 3 adds rows for rulings 7 and 8 and the grants. It rewords §4.9's credentials row for the Controller's App key: "Read the facts, the run records and the Pause; post check runs, the status on the item; take a pull request out of the merge queue; keep the tracking issue. It does not start Runs". It amends decision 0045's bound if the Maintainer takes his fourth point.15. What the design leaves undecided. Each is named so that a builder does not settle it silently.
facts/1carries none (ruling 10).iteminput's form,owner/repo#number, as the run-name writes it (ruling 4).written/1, which nothing reads today.16. Gates. Export verify on each PR. A fresh Claude review and a security pass on each. The security pass reads first the two transports' allowlists, where each token goes from the environment to its transport, and what text can reach the check run and the issue. AGY on Argon on both, since they place credentials and the Controller's writes, and PR 2's entry calls the Kernel on every item. "Muse: not run (Maintainer, 2026-10-20: Kernel/hook only)". The capped filtered mutant run on each new path. PR 3 changes
config/apps.json, a protected path, so the Maintainer approves it.17. Out of scope. The workflows and their steps (#196); PR 2 names the two entries and #196 PR 2 runs them. The approval's write (#195 PR 1 adds
approveto the port's table). The enqueue, which has no story yet, and the issue Lifecycle's label acts, each recordednot_built. Assignment and review requests (controller/notify.py). Reconciliation (controller/reconcile.py, I-33). The view's write (the feed story,combsmith-sf-view). The relay (slice 3). Backpressure (controller/backpressure.py).Needs the Maintainer? Yes, on five points; the rest is buildable as ruled.
issues: write, and the dequeue's grant, which P-2 names. A stolen Controller key could then also open, edit, close and label issues and pull requests where the App is installed, and, if the dequeue needspull_requests: write, comment and review. The other way for the issue is the pass job's token withissues: write. That is not recommended:github-actions[bot]writes for every workflow, so I-33 could not tell the Factory's writes from any other's. He changes the registration, and P-39 is run again.controller/1. The other way is a comment for each new cause while it is open, the one write with no recovery key.passalone, 1 MiB zipped and 8 MiB read out, amending 0045. The other way is no read of earlier records at all, which breaks the read gap andpasses_since.factorycheck run,neutral, on every open pull request, saying it is paused. Recommended: yes, since it says why nothing moves. The other way holds every Controller write while the repo is Paused, and the first statuses appear only when he clears it.don-petry commentedon Oct 22, 2026
The Maintainer's answers and the orchestration session's rulings (2026-10-21) on the effects port's five points.
(c) The Controller App's three grants: granted.
FACTORY_PAUSED, the org's and the repo's),issues: write, and the dequeue's permission once P-2 names it.config/apps.jsongains the three needs in the same PR, with P-39's new fixtures.(d) The tracking issue assigns him.
controller/1gainsaccounts.maintainer, his login and account id, read from the fixtures. The issue opened for the first cause assigns him, once. A cause while it is open edits its body and notifies no one.(e) Two rulings by the orchestration session:
neutral"paused" status, because it says why nothing moves.Built so far. The first part of PR 2, held until decision 0048 is on main:
written/1;The status, the dequeue, the tracking issue and the Pause re-read come in the next part, after P-12 and P-2 and the grants above.
Effects port: the step reads and writes inside the directory it runs …
Effects port: each of the step's three paths is held to its directory…
Effects port: the Controller's dispatches planned, held to the pass r…
Harness: apply's refusals, the identity port and a Role's writes (#194,…