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
Workflows: the frozen entry shim, the poller, the Run's three jobs and the nudge, each held to its shape #196
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 live workflows: the frozen entry shim, the poller, the Run with its three jobs, and the nudge from this repo. Each shape is a rule the workflow lint must hold (docs/standards/workflows.md, decision 0006, §2.4 rule 1), with a counter-example each, before any of them runs.
Done when
The entry shim runs from the default branch and has jobs that each uses: a pinned reusable workflow and nothing else; no step, no run: block, only a job-level if: (§2.4 rule 1). tools/check_workflows.py refuses a shim with a step (examples/workflows/invalid/shim.a-step-in-the-shim.yml).
The poller workflow: several staggered schedule lines, workflow_dispatch with no inputs, one concurrency group with one pending entry, actions: write on the poller job alone (§4.9), environment factory-poller.
The Run workflow: run on factory-run with no write permission, verify with no secret and no cache action and a time limit, apply on factory-apply with no model credential; a concurrency key per Work item at job level (§2.4 rule 4); a final step that writes outcome/1 under if: always().
A workflow in this repo nudges the poller on pull_request and push with the default token (P-9), carrying nothing (§3.2). Fork runs wait for approval (P-27).
Each rule has a counter-example under examples/workflows/invalid/ and a boundary test in tests/test_tools.py; .github/ is a protected path, so the Maintainer approves each edit.
Needs. P-9, P-14, P-27 (batch 2c) and P-30 for the shim's pinned reference. The Harness and the Controller for what the jobs call.
design §2.2 (the shim "may not contain a step or a run: block"), §2.3, §2.4 rules 1 to 4, §3.2, §3.10 (which run records count, the outcome, the lane under P-44), §4.7 (the Pause), §4.9 (the threats and the credentials table), §7.3, §9.1, §9.4 ("the entry shim is frozen and should have its final shape from the start"), §9.6 and §9.12;
docs/standards/workflows.md; tools/check_workflows.py, its Workflows tests in tests/test_tools.py, its mutants in tools/mutants.py, and the 40 counter-examples under examples/workflows/invalid/;
.github/workflows/ on main: verify.yml (factory), ci.yml (ci, also on a push to main) and sonarcloud.yml;
decisions 0002, 0003, 0004, 0006, 0011, 0035, 0036, 0039 and 0043, and lessons L-35 to L-40, L-43, L-45, L-46 and L-58;
What this story decides. What the Factory's workflow files are, what shape each holds, and how tools/check_workflows.py holds each shape before anything runs. It is also the switch that makes the Factory live on its own repo, so it says when that happens and what holds it dormant until then.
1. Three pull requests, in order.
PR
Holds
Live?
Waits on
1
The rules: tools/check_workflows.py's new rules, their counter-examples, tests and mutants, the standard's new sections, and the record
Nothing runs
Nothing. It can start now
2
The called workflows: .github/workflows/pass.yml and .github/workflows/run-jobs.yml, on workflow_call alone; config/controller.json's account and Run workflow; harness/verify.py's slice-1 form
Dormant: nothing calls them
PR 1; #193 PR 2 (the pass's entry and the effects port, the modules pass.yml runs); #194 PR 2 (harness.apply, harness.outcome)
3
The shims: .github/workflows/poller.yml and .github/workflows/run.yml, frozen; the records port's two readings of ruling 10
Live at merge, under a Pause
PR 2; #197's first Release cut from a main holding PR 2, and next moved to it; the canary trial of ruling 9
Why: the rules can be reviewed, and can fail, before any file they hold exists. A called workflow does nothing until a shim calls it, so PR 2 can land and be cut into a Release without running. A shim resolves its uses: when its run is created, so it can land only once the tag it names exists. The last PR is then a small one, and it is the switch.
2. The files. Four, and no others.
File
name:
Runs on
Is
poller.yml
sf-poller
schedule, workflow_dispatch, workflow_run
The poller's shim
pass.yml
sf-pass
workflow_call
The pass: one job, pass
run.yml
sf-run
workflow_dispatch
A Run's shim, the one Role workflow the records port lists
run-jobs.yml
sf-run-jobs
workflow_call
The Run's three jobs: run, verify, apply
There is no separate nudge workflow (ruling 5). The act Run's file is #195's, held by these rules: since it runs on workflow_dispatch, it is a shim calling a workflow at the channel, and its one job, approve on factory-approve, is the called workflow's (ruling 14). Decisions 0002, 0003 and 0043 already name poller.yml, and 0036 names run.yml, so the names stay.
3. The entry shim, and how it is held. A workflow that runs on schedule, workflow_dispatch or workflow_run is a shim, since each of those events runs the default branch's file (§2.4 rule 1). A workflow on push alone, such as ci.yml and sonarcloud.yml, runs the pushed commit's own file and is not one.
Shape. Each job has the keys uses, permissions and concurrency, and may have if and with. Nothing else: no steps, runs-on, env, secrets, environment, strategy, needs or timeout-minutes (a calling job cannot have one; the called jobs do). The top holds name, on, permissions, jobs and, for run.yml alone, run-name.
What it calls.uses: is combsmith/software-factory/.github/workflows/<file>@factory/v0-next, the channel tag of decision 0006, never a local ./.github/workflows/ path, which would run main's copy and not the Release. Every shim names the same channel. A with: value is exactly ${{ inputs.<the same name> }}.
Frozen. Each shim is held word for word by its hash, as EXEMPT holds ci.yml: a SHIMS table in the check, so a change to a shim fails until the same pull request records its new hash. .github/ is a protected path, so the Maintainer approves that change too.
Environments. A job names an environment: only in a workflow on workflow_call alone. A shim, a pull request's job or a push's job names none.
New rule shim, with examples/workflows/invalid/shim.a-step-in-the-shim.yml (the issue's), shim.the-default-branchs-own-copy.yml (uses: ./.github/workflows/pass.yml), shim.a-secret-handed-to-the-release.yml (a secrets: mapping on the calling job) and shim.a-key-beside-a-pull-request.yml (an environment: on a pull_request job). Tests: tests/test_tools.py::test_a_shim_holds_no_step_and_calls_the_release_at_its_channel, test_a_shim_is_held_word_for_word, test_every_shim_calls_one_channel. Why: a file on main is not in any Release, so it is reviewed by no Release and rolled back by no channel move. Holding it to a call and an if: leaves nothing in it to roll back.
The schedule. Three lines, hourly, twenty minutes apart, none at minute 0 and none on a quarter hour. It is a backstop of hours (decision 0043): three lines five minutes apart got 19 of 335 due runs (trials/P-8.ticks.json), and more lines are the same scheduler more times. Three, not one, so a line the scheduler drops is not the whole backstop.
The dispatch.workflow_dispatch with no inputs. It serves the successor, sent by the pass job's own token (started_bysuccessor), and the Maintainer's hand (dispatch).
The restart.workflow_run, on completed alone, naming factory, ci and sf-run, never sf-poller (decision 0043; ruling 5 adds the first two). Nothing in poller.yml or pass.yml reads github.event: the pass learns what started it from its own run record through the records port, as pass_.py does now.
One running, one pending. One concurrency group, sf-poller, on the job, never cancelling the pass that runs. A newer start replaces the one that waits, so any number of nudges during a pass become one more pass (§7.3, P-44).
Permissions.actions: write and contents: read, on the pass job alone, in the shim and in pass.yml (a called job gets no more than its caller grants). actions: write is the pass's own token, which starts the Runs and the successor (§4.9, P-9). The job id pass is reserved for poller.yml and pass.yml, as eval is for eval.yml, and no other job anywhere is granted a write. The check's rule text says this today ("grant nothing that can write"), but its code reads only the top-level block, so a job-level write passes now; this closes that gap.
pass.yml. One job, pass, on factory-poller, runs-on: ubuntu-24.04, timeout-minutes: 15 (the three-minute wait of decision 0043 and a pass; revisited from the first passes' durations). Its steps: the Release's checkout (ruling 14); the pass, python3 -m the entry Controller: one pass reads, decides, records, dispatches, writes the view and raises the reconciliation alert #193 PR 2 names, given CONTROLLER_APP_KEY on that step alone; the upload of pass/1; then the writes, python3 -m the effects entry, given CONTROLLER_APP_KEY and WORKFLOW_TOKEN: ${{ github.token }} on that step alone. The record is uploaded before anything is dispatched (§2.3 steps 5 and 6), so a Run never starts from a Decision with no record.
New rules wake and permissions (widened), with wake.reads-the-run-that-woke-it.yml (an if: on github.event.workflow_run.conclusion), wake.woken-by-its-own-end.yml, wake.a-poller-that-takes-inputs.yml, wake.on-every-stage-of-a-run.yml (no types:, so requested too) and permissions.actions-write-beyond-the-pass.yml. Tests: test_the_poller_wakes_only_on_what_it_names_and_reads_nothing_from_it, test_only_the_poller_runs_on_workflow_run, test_the_pass_job_alone_is_granted_a_write, test_the_pass_uploads_its_record_before_it_dispatches. docs/standards/workflows.md gains workflow_run "for the poller alone, on these conditions", as decision 0043 asked, and its sentence on events is brought up to EVENTS (it omits push and workflow_call today).
5. The nudge. This repo nudges the poller by the end of a workflow, never by a workflow of its own.
Events.factory ends on every pull_request and merge_group; ci on those and on every push to main, so every merge; sf-run at every Run's end. So the issue's "on pull_request and push" holds, through the completion of the workflows those events already run.
The default token. The pass job's, as P-9 tried it, and no other job's.
It carries nothing. The poller takes no inputs and reads nothing from the run that woke it (§3.2). Its started_by is own_repo (decision 0043).
Forks. A fork's run of factory waits for a person's approval before it runs (P-27, the setting first_time_contributors). When it ends it causes one pass and directs nothing, as decision 0043 accepted for a run of the same name. No file a fork's event runs holds a Factory secret: the shims run only on events a fork cannot start (P-27: a fork's dispatch was refused and no schedule ran), and a pull request's job names no environment (ruling 3). The setting is asserted by the readiness check (§3.12), which waits on its story.
Why not a nudge workflow on pull_request and push: its job would need actions: write, and §4.9 gives that to the poller job alone. On pull_request it would run the head's own file and code, and that token can delete run records, which would undo the Run ceiling. And a nudge as a pull request opens finds its checks pending and decides nothing; the pass that can decide is the one after factory ends (W-8). This widens decision 0043's row "listens to the completion of the Run workflows" to the Lens workflows of this repo; the record says so (ruling 12).
Inputs. Three, required, and final: name, the run-name/1 the pass gave the Run, which holds its Run id; item, its repo and number; and pass, the id of the pass record that holds its Decision. So every Role dispatch carries an item (decision 0036). The effects port's dispatch sends all three from the pass's own record. name gives the run its name and item its lane, which no expression can cut out of the name; the draft rulings on Release: cut an immutable tag, move next, and roll back in one action #197 name the Run id and the pass alone, and this story keeps all three. The frozen shim and the Release it calls must agree on them, so none is added after PR 3 (L-46).
The if:. Two conditions, and nothing else. The dispatcher is the pass's token, by its account id from P-9, so a person's dispatch runs nothing the records port would not count (I-34). And a Pause of the repo or the org starts no Run, even when Factory code is broken (decision 0003). The id is held to config/controller.json's accounts.workflow_token by a test.
What the Pause here does not do. GitHub gives vars.FACTORY_PAUSED as one value, the repo's over the org's, so a repo's false hides an org's true here (L-58's failure). The pass reads both, so this is a second layer, not the first. It covers Runs, and so the approval, which is a Run; it does not cover the Controller's own acts in the pass (an enqueue, a dequeue, the status). Those wait on whichever story makes the enqueue (P-2).
New rules lane and expression (widened). expression allows exactly three new places, each the whole value: run-name: ${{ inputs.name }}, a concurrency group: ${{ inputs.item }}, and a job's outputs: value ${{ steps.<id>.outputs.<name> }} (ruling 7). Counter-examples: lane.cancels-the-run-in-progress.yml, lane.one-lane-for-every-item.yml (a group written in, or concurrency at the top), lane.a-run-with-no-lane.yml, expression.text-around-the-run-name.yml. Tests: test_a_runs_lane_is_its_item_on_the_job_and_cancels_nothing_running, test_every_run_dispatch_carries_an_item_and_the_poller_takes_none, test_a_run_starts_only_from_the_poller_and_never_under_a_pause.
7. The Run's three jobs.run-jobs.yml, on workflow_call with the inputs name, item and pass. Each job has runs-on: ubuntu-24.04 and permissions: contents: read, which on a public repo grants nothing anyone lacks, and nothing else. The only uses: allowed are the listed checkout and the listed actions/upload-artifact and actions/download-artifact, each pinned by hash and held to one version as CHECKOUT is, looked up from its release and never guessed. No cache action, and no actions/setup-*.
The Engine's step sets CLAUDE_CODE_SUBPROCESS_ENV_SCRUB: "1" in its own env:, written in, never left to the CLI's default under GITHUB_ACTIONS.
The model credential is given only as CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}, on the Engine's step, a name on the scrub's list. The environment secret has that name too. The metered fallback (decision 0033) may be used only under a name the scrub is shown to list.
verify, needs: run, no environment:, no secrets. anywhere, timeout-minutes: 10. Steps: the Release's checkout, the Result's download by its id, python3 -m harness.verify, the verdict's upload. For an Intent that pushes nothing, as the review Intent, harness/verify.py records not_run and checks only the Result's digest, never passed (L-54). The checks for code come with the fix Intent (slice 2).
apply, on factory-apply, needs: [run, verify], the job-level if: always(), env: NEEDS: ${{ toJSON(needs) }} and nothing else, timeout-minutes: 10. Steps: the Release's checkout; the Result's download by the id run gave; python3 -m harness.apply, given PR_REVIEW_APP_KEY on that step alone, which mints no token and writes no Effect unless NEEDS shows run and verify succeeded, and checks the Result's digest against run's output; the upload of applied/1; then, each under the step's if: always(), python3 -m harness.outcome and the upload of outcome/1, the last step. No model credential is named in apply (0004; held by Harness: run and apply for the review Intent, and the model holds no credential that can write #194 PR 3's boundary rule).
Why apply runs when run fails. A failed run must still leave outcome/1: a capacity fault is run's to find, and the pass reads outcome/1 only for Runs that did not succeed (§3.10). So apply is the one job that always runs, and its code, not the workflow, decides that nothing is written. That rule gets a test and a mutant in Harness: run and apply for the review Intent, and the model holds no credential that can write #194 PR 2. A Run killed in apply itself has no outcome and is read as item, as §3.10 says; whether a final step runs after a timeout is P-18, and nothing here depends on it.
New rules verify and outcome, with verify.a-secret-in-verify.yml, verify.a-cache-in-verify.yml, outcome.a-final-step-a-failure-skips.yml and outcome.apply-skipped-when-run-fails.yml. Tests: test_verify_holds_no_secret_and_no_cache, test_the_runs_last_step_writes_its_outcome_whatever_failed, test_a_runs_jobs_use_only_the_listed_actions. One more, outside lint(), since it needs the tree: test_every_module_a_workflow_runs_is_in_the_repo, so a made-up python3 -m fails before it first runs.
8. The CLI on the runner. No decision says how the pinned CLI reaches a runner, and docs/standards/workflows.md lets nothing be fetched but the secret scan's install. So run's install is the second exempt block, held as the scan's is: one literal block, word for word, given CLI_VERSION and CLI_CHECKSUM from its own env:, each quoted and held to its shape, fetched over HTTPS only with redirects held to HTTPS, the binary's sha256 checked before it is moved onto the path. The version is the Engine's pin (L-35), held equal by a test, and a pull request that moves the pin moves the checksum. The step holds no secret. The source and its checksum are looked up from the vendor's release, never guessed. This is a risk the Maintainer takes or not (his line below).
9. One canary trial before the shims. Six things this story rests on are untried, and the first sign of any of them would otherwise be the live Factory:
whether workflow_run starts the poller when a run the pass's token dispatched ends, and how long a chain of dispatches and workflow_run starts runs before GitHub stops it (it documents three levels of workflow_run);
whether a concurrency group on a calling job holds the lane across the called workflow's three jobs (P-44 tried one plain job);
whether a called job's environment: gives it that environment's secrets on a dispatch from main;
how github.actor_id compares in the shim's if:;
what a run concludes when an if: skipped its only job;
which value, if any, names a called workflow's own commit (ruling 14).
It runs these exact files at a test tag in combsmith/software-factory-canary, with no secret but a throwaway environment's, through the skill run-a-platform-trial, and leaves one fixture under trials/. It takes the next free trial number when filed, and its row joins §6.
10. The configuration and the records port.
config/controller.json's accounts.workflow_token becomes {"type": "Bot", "id": 41898282}, from trials/P-9.json, in PR 2. It is who a successor is (started_by), and the dispatcher the records port counts a Run by. adapters/records.py's docstring says the Controller's App dispatches a Run; it cannot (actions: read in config/apps.json), and §4.9 gives the dispatch to the pass job's token. PR 2 corrects the docstring and gives records.runs that account. Since every job's token is that account, ruling 4's "the pass job alone holds a write" is what keeps a counted Run the poller's (I-34). A person with write access can still write a workflow that dispatches one; the check keeps the Factory's own files from doing so.
Two readings in adapters/records.py, before PR 3 merges (in PR 3, or a records follow-on): a Run concluded skipped, as the shim's if: may leave it, is no attempt and reads as superseded, like a Run cancelled before it had a runner (§3.10); today CONCLUSIONS has no skipped, so such a row would leave the list not whole for its 90 days and admit nothing anywhere. And startup_failure, which a missing or broken Release gives every Run, is environment, not item; today it is read as failure, and with no outcome file it would spend every item's attempts. Each gets a test and a mutant.
11. Live or dormant. Live only from PR 3, and live under a Pause.
PR 1 adds no workflow. PR 2's files run on workflow_call alone, and nothing calls them until PR 3.
Before PR 3 merges, the Maintainer sets FACTORY_PAUSED to true on this repo. The first passes then read, record and admit nothing, and the Run shim's if: starts nothing. He clears it when he chooses. It is his hand, since a session changes no setting.
12. Record. One, numbered at enqueue with PR 1: "The entry shim calls the Release and holds nothing else; the poller wakes when a workflow of this repo ends and reads nothing from it; a Run holds its item's lane from run to apply". Its table holds rulings 3 to 8 and 10. It widens decision 0043's row on what the poller listens to, and rewords decision 0006's row on thin callers to "every workflow that runs on schedule, workflow_dispatch or workflow_run is a thin caller; one on push runs its own commit's file and names no environment". It names #287's rules as the workflow holds them. PR 2 and PR 3 add rows.
13. Rows and lessons re-pointed.tools/check_context.py fails when poller.yml and run.yml appear, so PR 3 re-points every row waiting on them; PR 1 and PR 2 re-point what they hold.
0003: a human Pause holds when Factory code is broken; a Run ceiling outside kernel/
waiting on poller.yml
Split. The Pause, for Runs: PR 3, test_a_run_starts_only_from_the_poller_and_never_under_a_pause. The ceiling outside kernel/, and the Pause for the Controller's acts: waiting on controller/brakes.py
0003: a Run's lane, on the job, cancelling nothing running
L-46: the dispatcher and what it dispatches ride one Release
waiting on release/
Its first half, PR 3: test_every_shim_calls_one_channel. Its second, a new field read at every live channel, stays on release/
L-35 to L-40 stay held as they are; the new pins join L-35's listed versions.
14. What the design leaves undecided. Each is named here so that a builder does not settle it silently.
How the CLI reaches a runner (ruling 8).
How a called workflow learns its own Release's commit. github.workflow_sha is the caller's, main's. The draft rulings on Release: cut an immutable tag, move next, and roll back in one action #197 hand this to this story, by trial: ruling 9's trial answers it. GitHub documents the OIDC claim job_workflow_sha, but reading it needs id-token: write, which ruling 4's rule refuses to every job but pass; a context that names it needs no grant. If none does, each job checks out factory/v0-next once and reads every Factory file at the commit it got. If next moves mid-Run, the jobs read different Releases, and apply refuses a Result whose release is not the Task's (Harness: run and apply for the review Intent, and the model holds no credential that can write #194 ruling 5), so it fails closed.
Which channel the poller rides once a Consumer is on stable, and a Run shim for stable. Slice 3's. A second file, run-stable.yml, sharing the item's lane by the same group, leaves run.yml as it is.
The relay's public event on the poller (P-20 passed; P-31 not run). Slice 3 adds it, by a change to the shim the Maintainer approves.
CodeQL's default setup has no workflow file to name, so its end wakes nothing; factory, the slowest Lens, ends after it on nearly every head.
The platform Pause sees one value, and does not cover the Controller's own acts (ruling 6).
A conflict with the draft rulings on The approving act: the machine user approves from a Run with no Engine, bound to the admitted commit #195. They give approve.yml a job with steps on workflow_dispatch, run from main with no Release. Under ruling 3 that file is a shim: approve.yml calls the act's workflow at the channel, and every rule those drafts give the job (one environment, one secret, one checkout, one module) holds on the called job instead. Its checkout is then the Release's, not "the dispatched commit of the default branch". The two drafts must agree before either PR 1 lands.
15. Tests and mutants. Every rule above gets a boundary test in tests/test_tools.py's Workflows and a mutant in tools/mutants.py naming it, checked by the capped filtered run on each new path. Every counter-example breaks its rule and no other, as run() already holds. docs/standards/workflows.md gains three sections, "The entry shim", "The poller" and "The Run's three jobs", each with its example and its "Held by:" line; its first block stays examples/workflows/verify.yml word for word.
16. Gates. Export verify on each PR. A fresh Claude review and a security pass on each; the security pass reads first the permissions and environments (PR 1), run's steps, the CLI's install and where each secret reaches (PR 2), and the shims' if: (PR 3). AGY on Argon on all three, since they are workflow PRs and tools/check_workflows.py, and PR 2 places the keys. "Muse: not run (Maintainer, 2026-10-20: Kernel/hook only)". The capped filtered mutant run. .github/ is protected, so the Maintainer approves each edit of PR 2 and PR 3; tools/check_workflows.py is a Lens input, so once #140 holds, PR 1 needs a model Lens or his approval too.
17. Out of scope. The Release, its tags, its cut and the ring file (#197); only the trial of which value names a called job's own commit is this story's. The ruleset switch (#198). The act Run's workflow and its environment (#195), held by these rules. harness/ beyond verify.py's slice-1 form (#194). The pass's entry and the effects port (#193 PR 2). The relay and its event (slice 3). The eval job, eval.yml (reserved). §4.9's rewording and the Linux /proc/<pid>/environ trial (#287). The readiness check (§3.12). Verify's checks for code (slice 2).
Needs the Maintainer? Yes, on four points; the rest is buildable as ruled.
Risk: the CLI is fetched at run time (ruling 8). A second exempt install, held word for word with its checksum. The other ways are a runner image of our own, pinned by digest, or a self-hosted runner, each with more to keep.
Scope: the nudge is the poller's workflow_run, on the ends of factory, ci and sf-run, not a workflow on pull_request and push holding actions: write (ruling 5). It meets the issue's fourth criterion another way, and widens decision 0043's row.
An amendment to rulings 7 and 10 above, so they agree with #193's rulings for the effects port (#193).
Ruling 10.config/controller.json's run_workflows list is replaced by a map of workflow files by kind, run, approval and poller, plus home: {repo, branch}. The effects port picks a dispatch's file by its kind, and the records port lists the files of kinds run and approval as the Role workflow files. This also covers The approving act: the machine user approves from a Run with no Engine, bound to the admitted commit #195's amendment, where approve.yml is of kind approval.
Ruling 4's pass.yml. It gains one step, after the pass's writes, that uploads written/1. That is the record of each write the effects port made, kept apart from pass/1, which is uploaded before any write and never rewritten.
It is the second exempt block, held word for word as the secret scan's install is.
The version and the checksum move together, in one pull request, held equal to the pin by a test.
The other two ways (a runner image of our own, or a self-hosted runner) are not taken.
(b) Two secrets in run (ruling 7, rule 3): separate steps.
The reader App's key is used in a step of its own, which mints the reader's token, reads what the Task needs, and revokes the token before the step ends.
tools/check_workflows.py holds it: in run-jobs.yml's run job, the step that runs the Engine names CLAUDE_CODE_OAUTH_TOKEN alone, and the reader key's step comes before it and revokes in a finally. Each has a counter-example.
The fourth job, task, on an environment of its own, is not taken.
Activity
don-petry commentedon Oct 21, 2026
Working rulings (overturnable in review)
Solution Architect, delegated. On main at 7e03db6. Read from:
runandapply(lane s). The workflows"; "Last: the Release. Redesign the machine user's permissions: approve everywhere, own no rules #89, then the ruleset switch");run:block"), §2.3, §2.4 rules 1 to 4, §3.2, §3.10 (which run records count, the outcome, the lane under P-44), §4.7 (the Pause), §4.9 (the threats and the credentials table), §7.3, §9.1, §9.4 ("the entry shim is frozen and should have its final shape from the start"), §9.6 and §9.12;docs/standards/workflows.md;tools/check_workflows.py, itsWorkflowstests intests/test_tools.py, its mutants intools/mutants.py, and the 40 counter-examples underexamples/workflows/invalid/;.github/workflows/on main:verify.yml(factory),ci.yml(ci, also on a push tomain) andsonarcloud.yml;config/apps.json(factory-pollerholdsCONTROLLER_APP_KEY,factory-runholdsREADER_APP_KEY,factory-applyholdsPR_REVIEW_APP_KEY),config/controller.json(accounts.workflow_tokennull) andschemas/controller.schema.json,schemas/outcome.schema.json,schemas/run-name.schema.json;controller/as Controller: the pass as a pure function of what it read (#193, PR 1) #275 left it (pass_.py'sstarted_by,dispatchas{run, name}), andadapters/records.py(what counts,CONCLUSIONS);github-actions[bot], id 41898282, type Bot), P-13 at 2.1.306, P-14, P-20, P-27 and P-44;runandapply, and its amendments), and the issues of The approving act: the machine user approves from a Run with no Engine, bound to the admitted commit #195, Release: cut an immutable tag, move next, and roll back in one action #197 and The ruleset switch: main requires the machine user's approval of the most recent commit, and slice 1's event is recorded #198;rulings-195.md,rulings-197.md), read for their tag, inputs and act file.What this story decides. What the Factory's workflow files are, what shape each holds, and how
tools/check_workflows.pyholds each shape before anything runs. It is also the switch that makes the Factory live on its own repo, so it says when that happens and what holds it dormant until then.1. Three pull requests, in order.
tools/check_workflows.py's new rules, their counter-examples, tests and mutants, the standard's new sections, and the record.github/workflows/pass.ymland.github/workflows/run-jobs.yml, onworkflow_callalone;config/controller.json's account and Run workflow;harness/verify.py's slice-1 formpass.ymlruns); #194 PR 2 (harness.apply,harness.outcome).github/workflows/poller.ymland.github/workflows/run.yml, frozen; the records port's two readings of ruling 10mainholding PR 2, andnextmoved to it; the canary trial of ruling 9Why: the rules can be reviewed, and can fail, before any file they hold exists. A called workflow does nothing until a shim calls it, so PR 2 can land and be cut into a Release without running. A shim resolves its
uses:when its run is created, so it can land only once the tag it names exists. The last PR is then a small one, and it is the switch.2. The files. Four, and no others.
name:poller.ymlsf-pollerschedule,workflow_dispatch,workflow_runpass.ymlsf-passworkflow_callpassrun.ymlsf-runworkflow_dispatchrun-jobs.ymlsf-run-jobsworkflow_callrun,verify,applyThere is no separate nudge workflow (ruling 5). The act Run's file is #195's, held by these rules: since it runs on
workflow_dispatch, it is a shim calling a workflow at the channel, and its one job,approveonfactory-approve, is the called workflow's (ruling 14). Decisions 0002, 0003 and 0043 already namepoller.yml, and 0036 namesrun.yml, so the names stay.3. The entry shim, and how it is held. A workflow that runs on
schedule,workflow_dispatchorworkflow_runis a shim, since each of those events runs the default branch's file (§2.4 rule 1). A workflow onpushalone, such asci.ymlandsonarcloud.yml, runs the pushed commit's own file and is not one.uses,permissionsandconcurrency, and may haveifandwith. Nothing else: nosteps,runs-on,env,secrets,environment,strategy,needsortimeout-minutes(a calling job cannot have one; the called jobs do). The top holdsname,on,permissions,jobsand, forrun.ymlalone,run-name.uses:iscombsmith/software-factory/.github/workflows/<file>@factory/v0-next, the channel tag of decision 0006, never a local./.github/workflows/path, which would runmain's copy and not the Release. Every shim names the same channel. Awith:value is exactly${{ inputs.<the same name> }}.EXEMPTholdsci.yml: aSHIMStable in the check, so a change to a shim fails until the same pull request records its new hash..github/is a protected path, so the Maintainer approves that change too.environment:only in a workflow onworkflow_callalone. A shim, a pull request's job or a push's job names none.New rule
shim, withexamples/workflows/invalid/shim.a-step-in-the-shim.yml(the issue's),shim.the-default-branchs-own-copy.yml(uses: ./.github/workflows/pass.yml),shim.a-secret-handed-to-the-release.yml(asecrets:mapping on the calling job) andshim.a-key-beside-a-pull-request.yml(anenvironment:on apull_requestjob). Tests:tests/test_tools.py::test_a_shim_holds_no_step_and_calls_the_release_at_its_channel,test_a_shim_is_held_word_for_word,test_every_shim_calls_one_channel.Why: a file on
mainis not in any Release, so it is reviewed by no Release and rolled back by no channel move. Holding it to a call and anif:leaves nothing in it to roll back.4. The poller.
poller.yml, in full:trials/P-8.ticks.json), and more lines are the same scheduler more times. Three, not one, so a line the scheduler drops is not the whole backstop.workflow_dispatchwith no inputs. It serves the successor, sent by the pass job's own token (started_bysuccessor), and the Maintainer's hand (dispatch).workflow_run, oncompletedalone, namingfactory,ciandsf-run, neversf-poller(decision 0043; ruling 5 adds the first two). Nothing inpoller.ymlorpass.ymlreadsgithub.event: the pass learns what started it from its own run record through the records port, aspass_.pydoes now.sf-poller, on the job, never cancelling the pass that runs. A newer start replaces the one that waits, so any number of nudges during a pass become one more pass (§7.3, P-44).actions: writeandcontents: read, on thepassjob alone, in the shim and inpass.yml(a called job gets no more than its caller grants).actions: writeis the pass's own token, which starts the Runs and the successor (§4.9, P-9). The job idpassis reserved forpoller.ymlandpass.yml, asevalis foreval.yml, and no other job anywhere is granted a write. The check's rule text says this today ("grant nothing that can write"), but its code reads only the top-level block, so a job-level write passes now; this closes that gap.pass.yml. One job,pass, onfactory-poller,runs-on: ubuntu-24.04,timeout-minutes: 15(the three-minute wait of decision 0043 and a pass; revisited from the first passes' durations). Its steps: the Release's checkout (ruling 14); the pass,python3 -mthe entry Controller: one pass reads, decides, records, dispatches, writes the view and raises the reconciliation alert #193 PR 2 names, givenCONTROLLER_APP_KEYon that step alone; the upload ofpass/1; then the writes,python3 -mthe effects entry, givenCONTROLLER_APP_KEYandWORKFLOW_TOKEN: ${{ github.token }}on that step alone. The record is uploaded before anything is dispatched (§2.3 steps 5 and 6), so a Run never starts from a Decision with no record.New rules
wakeandpermissions(widened), withwake.reads-the-run-that-woke-it.yml(anif:ongithub.event.workflow_run.conclusion),wake.woken-by-its-own-end.yml,wake.a-poller-that-takes-inputs.yml,wake.on-every-stage-of-a-run.yml(notypes:, sorequestedtoo) andpermissions.actions-write-beyond-the-pass.yml. Tests:test_the_poller_wakes_only_on_what_it_names_and_reads_nothing_from_it,test_only_the_poller_runs_on_workflow_run,test_the_pass_job_alone_is_granted_a_write,test_the_pass_uploads_its_record_before_it_dispatches.docs/standards/workflows.mdgainsworkflow_run"for the poller alone, on these conditions", as decision 0043 asked, and its sentence on events is brought up toEVENTS(it omitspushandworkflow_calltoday).5. The nudge. This repo nudges the poller by the end of a workflow, never by a workflow of its own.
factoryends on everypull_requestandmerge_group;cion those and on every push tomain, so every merge;sf-runat every Run's end. So the issue's "onpull_requestandpush" holds, through the completion of the workflows those events already run.started_byisown_repo(decision 0043).factorywaits for a person's approval before it runs (P-27, the settingfirst_time_contributors). When it ends it causes one pass and directs nothing, as decision 0043 accepted for a run of the same name. No file a fork's event runs holds a Factory secret: the shims run only on events a fork cannot start (P-27: a fork's dispatch was refused and no schedule ran), and a pull request's job names no environment (ruling 3). The setting is asserted by the readiness check (§3.12), which waits on its story.Why not a nudge workflow on
pull_requestandpush: its job would needactions: write, and §4.9 gives that to the poller job alone. Onpull_requestit would run the head's own file and code, and that token can delete run records, which would undo the Run ceiling. And a nudge as a pull request opens finds its checks pending and decides nothing; the pass that can decide is the one afterfactoryends (W-8). This widens decision 0043's row "listens to the completion of the Run workflows" to the Lens workflows of this repo; the record says so (ruling 12).6. The Run's shim.
run.yml, in full:name, therun-name/1the pass gave the Run, which holds its Run id;item, its repo and number; andpass, the id of the pass record that holds its Decision. So every Role dispatch carries an item (decision 0036). The effects port's dispatch sends all three from the pass's own record.namegives the run its name anditemits lane, which no expression can cut out of the name; the draft rulings on Release: cut an immutable tag, move next, and roll back in one action #197 name the Run id and the pass alone, and this story keeps all three. The frozen shim and the Release it calls must agree on them, so none is added after PR 3 (L-46).runtoapplyand never cancels what runs (§2.4 rule 4, §7.3, P-44). GitHub's groups span the repo, so The approving act: the machine user approves from a Run with no Engine, bound to the admitted commit #195's act Run, given the same group, shares the item's lane.if:. Two conditions, and nothing else. The dispatcher is the pass's token, by its account id from P-9, so a person's dispatch runs nothing the records port would not count (I-34). And a Pause of the repo or the org starts no Run, even when Factory code is broken (decision 0003). The id is held toconfig/controller.json'saccounts.workflow_tokenby a test.vars.FACTORY_PAUSEDas one value, the repo's over the org's, so a repo'sfalsehides an org'struehere (L-58's failure). The pass reads both, so this is a second layer, not the first. It covers Runs, and so the approval, which is a Run; it does not cover the Controller's own acts in the pass (an enqueue, a dequeue, the status). Those wait on whichever story makes the enqueue (P-2).New rules
laneandexpression(widened).expressionallows exactly three new places, each the whole value:run-name: ${{ inputs.name }}, a concurrencygroup: ${{ inputs.item }}, and a job'soutputs:value${{ steps.<id>.outputs.<name> }}(ruling 7). Counter-examples:lane.cancels-the-run-in-progress.yml,lane.one-lane-for-every-item.yml(a group written in, or concurrency at the top),lane.a-run-with-no-lane.yml,expression.text-around-the-run-name.yml. Tests:test_a_runs_lane_is_its_item_on_the_job_and_cancels_nothing_running,test_every_run_dispatch_carries_an_item_and_the_poller_takes_none,test_a_run_starts_only_from_the_poller_and_never_under_a_pause.7. The Run's three jobs.
run-jobs.yml, onworkflow_callwith the inputsname,itemandpass. Each job hasruns-on: ubuntu-24.04andpermissions: contents: read, which on a public repo grants nothing anyone lacks, and nothing else. The onlyuses:allowed are the listed checkout and the listedactions/upload-artifactandactions/download-artifact, each pinned by hash and held to one version asCHECKOUTis, looked up from its release and never guessed. No cache action, and noactions/setup-*.run, onfactory-run,timeout-minutes: 30. Steps: the Release's checkout; the CLI's install (ruling 8); the Task,python3 -m harness.task, givenREADER_APP_KEYon that step alone; the Engine,python3 -m harness.run; the Result's upload, whose id and digest are the job's twooutputs:. Design: the CLI's sandbox limits connections but passes the parent's environment through (P-26 failed); the run job relies on the scrub and --tools "" #287's rules, as the workflow holds them:CLAUDE_CODE_SUBPROCESS_ENV_SCRUB: "1"in its ownenv:, written in, never left to the CLI's default underGITHUB_ACTIONS.CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}, on the Engine's step, a name on the scrub's list. The environment secret has that name too. The metered fallback (decision 0033) may be used only under a name the scrub is shown to list.env:. The reader's key is on the Task's step alone, and the Task's step revokes the token it minted before it ends, so no reader token is live while the model runs. Read so, Design: the CLI's sandbox limits connections but passes the parent's environment through (P-26 failed); the run job relies on the scrub and --tools "" #287's "no other secret" is the environment the CLI and any tool inherit (the Maintainer's line below).--tools ""on every Run that readsoutsideoradvisorytext, and the review Intent always. This isharness/engine.py's, held by Harness: run and apply for the review Intent, and the model holds no credential that can write #194 PR 1's test, not by the workflow.runchecks out anything but the Release.The check that holds 1 to 3 in the
runjob is Harness: run and apply for the review Intent, and the model holds no credential that can write #194 PR 3's (its ruling 9 and the amendment's rule 5). If this story's PR 1 lands first, it carries them, withrun.no-scrub.yml,run.the-model-credential-under-another-name.ymlandrun.a-second-secret-beside-the-model.yml, and Harness: run and apply for the review Intent, and the model holds no credential that can write #194 PR 3 does not add them again.verify,needs: run, noenvironment:, nosecrets.anywhere,timeout-minutes: 10. Steps: the Release's checkout, the Result's download by its id,python3 -m harness.verify, the verdict's upload. For an Intent that pushes nothing, as the review Intent,harness/verify.pyrecordsnot_runand checks only the Result's digest, neverpassed(L-54). The checks for code come with the fix Intent (slice 2).apply, onfactory-apply,needs: [run, verify], the job-levelif: always(),env: NEEDS: ${{ toJSON(needs) }}and nothing else,timeout-minutes: 10. Steps: the Release's checkout; the Result's download by the idrungave;python3 -m harness.apply, givenPR_REVIEW_APP_KEYon that step alone, which mints no token and writes no Effect unlessNEEDSshowsrunandverifysucceeded, and checks the Result's digest againstrun's output; the upload ofapplied/1; then, each under the step'sif: always(),python3 -m harness.outcomeand the upload ofoutcome/1, the last step. No model credential is named inapply(0004; held by Harness: run and apply for the review Intent, and the model holds no credential that can write #194 PR 3's boundary rule).applyruns whenrunfails. A failedrunmust still leaveoutcome/1: a capacity fault isrun's to find, and the pass readsoutcome/1only for Runs that did not succeed (§3.10). Soapplyis the one job that always runs, and its code, not the workflow, decides that nothing is written. That rule gets a test and a mutant in Harness: run and apply for the review Intent, and the model holds no credential that can write #194 PR 2. A Run killed inapplyitself has no outcome and is read asitem, as §3.10 says; whether a final step runs after a timeout is P-18, and nothing here depends on it.New rules
verifyandoutcome, withverify.a-secret-in-verify.yml,verify.a-cache-in-verify.yml,outcome.a-final-step-a-failure-skips.ymlandoutcome.apply-skipped-when-run-fails.yml. Tests:test_verify_holds_no_secret_and_no_cache,test_the_runs_last_step_writes_its_outcome_whatever_failed,test_a_runs_jobs_use_only_the_listed_actions. One more, outsidelint(), since it needs the tree:test_every_module_a_workflow_runs_is_in_the_repo, so a made-uppython3 -mfails before it first runs.8. The CLI on the runner. No decision says how the pinned CLI reaches a runner, and
docs/standards/workflows.mdlets nothing be fetched but the secret scan's install. Sorun's install is the second exempt block, held as the scan's is: one literal block, word for word, givenCLI_VERSIONandCLI_CHECKSUMfrom its ownenv:, each quoted and held to its shape, fetched over HTTPS only with redirects held to HTTPS, the binary's sha256 checked before it is moved onto the path. The version is the Engine's pin (L-35), held equal by a test, and a pull request that moves the pin moves the checksum. The step holds no secret. The source and its checksum are looked up from the vendor's release, never guessed. This is a risk the Maintainer takes or not (his line below).9. One canary trial before the shims. Six things this story rests on are untried, and the first sign of any of them would otherwise be the live Factory:
workflow_runstarts the poller when a run the pass's token dispatched ends, and how long a chain of dispatches andworkflow_runstarts runs before GitHub stops it (it documents three levels ofworkflow_run);environment:gives it that environment's secrets on a dispatch frommain;github.actor_idcompares in the shim'sif:;if:skipped its only job;It runs these exact files at a test tag in combsmith/software-factory-canary, with no secret but a throwaway environment's, through the skill
run-a-platform-trial, and leaves one fixture undertrials/. It takes the next free trial number when filed, and its row joins §6.10. The configuration and the records port.
config/controller.json'saccounts.workflow_tokenbecomes{"type": "Bot", "id": 41898282}, fromtrials/P-9.json, in PR 2. It is who a successor is (started_by), and the dispatcher the records port counts a Run by.adapters/records.py's docstring says the Controller's App dispatches a Run; it cannot (actions: readinconfig/apps.json), and §4.9 gives the dispatch to the pass job's token. PR 2 corrects the docstring and givesrecords.runsthat account. Since every job's token is that account, ruling 4's "the pass job alone holds a write" is what keeps a counted Run the poller's (I-34). A person with write access can still write a workflow that dispatches one; the check keeps the Factory's own files from doing so.config/controller.jsongainsrun_workflows: ["run.yml"], the Role workflow files the records port lists, so the pass's own runs are never a Role's (decision 0036).controller/1gains the field, with its example and a counter-example. The approving act: the machine user approves from a Run with no Engine, bound to the admitted commit #195 adds its act file there.adapters/records.py, before PR 3 merges (in PR 3, or a records follow-on): a Run concludedskipped, as the shim'sif:may leave it, is no attempt and reads assuperseded, like a Run cancelled before it had a runner (§3.10); todayCONCLUSIONShas noskipped, so such a row would leave the list not whole for its 90 days and admit nothing anywhere. Andstartup_failure, which a missing or broken Release gives every Run, isenvironment, notitem; today it is read asfailure, and with no outcome file it would spend every item's attempts. Each gets a test and a mutant.11. Live or dormant. Live only from PR 3, and live under a Pause.
workflow_callalone, and nothing calls them until PR 3.mainthat holds PR 2, and onfactory/v0-nextpointing at it. The tag is Release: cut an immutable tag, move next, and roll back in one action #197's,factory/v0-nextin its draft rulings; the shim names whatever it cuts. Until the tag exists, a shim fails at startup, so it cannot be held dormant by itsif:.FACTORY_PAUSEDtotrueon this repo. The first passes then read, record and admit nothing, and the Run shim'sif:starts nothing. He clears it when he chooses. It is his hand, since a session changes no setting.12. Record. One, numbered at enqueue with PR 1: "The entry shim calls the Release and holds nothing else; the poller wakes when a workflow of this repo ends and reads nothing from it; a Run holds its item's lane from
runtoapply". Its table holds rulings 3 to 8 and 10. It widens decision 0043's row on what the poller listens to, and rewords decision 0006's row on thin callers to "every workflow that runs onschedule,workflow_dispatchorworkflow_runis a thin caller; one onpushruns its own commit's file and names no environment". It names #287's rules as the workflow holds them. PR 2 and PR 3 add rows.13. Rows and lessons re-pointed.
tools/check_context.pyfails whenpoller.ymlandrun.ymlappear, so PR 3 re-points every row waiting on them; PR 1 and PR 2 re-point what they hold.poller.ymltests/test_tools.py::test_the_pass_uploads_its_record_before_it_dispatcheskernel/poller.ymltest_a_run_starts_only_from_the_poller_and_never_under_a_pause. The ceiling outsidekernel/, and the Pause for the Controller's acts: waiting oncontroller/brakes.pyharness/examples/workflows/invalid/lane.*,test_a_runs_lane_is_its_item_on_the_job_and_cancels_nothing_runningharness/test_only_the_run_job_names_the_model_credentialrunandverifydeclare no permissions and use no cacheharness/contents: readalone and use no cache":verify.*,test_verify_holds_no_secret_and_no_cacheharness/release/shim.*,test_a_shim_holds_no_step_and_calls_the_release_at_its_channelrun.ymltest_every_run_dispatch_carries_an_item_and_the_poller_takes_nonepoller.ymltest_the_poller_wakes_only_on_what_it_names_and_reads_nothing_from_it; the relay's half waiting oncontroller/relay.pyworkflow_run, never its own end, reading nothingpoller.ymlwake.*,test_only_the_poller_runs_on_workflow_runrelease/test_every_shim_calls_one_channel. Its second, a new field read at every live channel, stays onrelease/L-35 to L-40 stay held as they are; the new pins join L-35's listed versions.
14. What the design leaves undecided. Each is named here so that a builder does not settle it silently.
github.workflow_shais the caller's,main's. The draft rulings on Release: cut an immutable tag, move next, and roll back in one action #197 hand this to this story, by trial: ruling 9's trial answers it. GitHub documents the OIDC claimjob_workflow_sha, but reading it needsid-token: write, which ruling 4's rule refuses to every job butpass; a context that names it needs no grant. If none does, each job checks outfactory/v0-nextonce and reads every Factory file at the commit it got. Ifnextmoves mid-Run, the jobs read different Releases, andapplyrefuses a Result whosereleaseis not the Task's (Harness: run and apply for the review Intent, and the model holds no credential that can write #194 ruling 5), so it fails closed.stable, and a Run shim forstable. Slice 3's. A second file,run-stable.yml, sharing the item's lane by the same group, leavesrun.ymlas it is.factory, the slowest Lens, ends after it on nearly every head.approve.ymla job with steps onworkflow_dispatch, run frommainwith no Release. Under ruling 3 that file is a shim:approve.ymlcalls the act's workflow at the channel, and every rule those drafts give the job (one environment, one secret, one checkout, one module) holds on the called job instead. Its checkout is then the Release's, not "the dispatched commit of the default branch". The two drafts must agree before either PR 1 lands.15. Tests and mutants. Every rule above gets a boundary test in
tests/test_tools.py'sWorkflowsand a mutant intools/mutants.pynaming it, checked by the capped filtered run on each new path. Every counter-example breaks its rule and no other, asrun()already holds.docs/standards/workflows.mdgains three sections, "The entry shim", "The poller" and "The Run's three jobs", each with its example and its "Held by:" line; its first block staysexamples/workflows/verify.ymlword for word.16. Gates. Export verify on each PR. A fresh Claude review and a security pass on each; the security pass reads first the permissions and environments (PR 1),
run's steps, the CLI's install and where each secret reaches (PR 2), and the shims'if:(PR 3). AGY on Argon on all three, since they are workflow PRs andtools/check_workflows.py, and PR 2 places the keys. "Muse: not run (Maintainer, 2026-10-20: Kernel/hook only)". The capped filtered mutant run..github/is protected, so the Maintainer approves each edit of PR 2 and PR 3;tools/check_workflows.pyis a Lens input, so once #140 holds, PR 1 needs a model Lens or his approval too.17. Out of scope. The Release, its tags, its cut and the ring file (#197); only the trial of which value names a called job's own commit is this story's. The ruleset switch (#198). The act Run's workflow and its environment (#195), held by these rules.
harness/beyondverify.py's slice-1 form (#194). The pass's entry and the effects port (#193 PR 2). The relay and its event (slice 3). The eval job,eval.yml(reserved). §4.9's rewording and the Linux/proc/<pid>/environtrial (#287). The readiness check (§3.12). Verify's checks for code (slice 2).Needs the Maintainer? Yes, on four points; the rest is buildable as ruled.
runjob holds no secret but the model's; decision 0035 put the reader's key onfactory-runfor the same job. Recommended: each key on its own step, the reader's token revoked before the Engine starts, under--tools "". The other way is a fourth job,task, on an environment of its own, with the reader's key stored again: one more runner start per Run.workflow_run, on the ends offactory,ciandsf-run, not a workflow onpull_requestandpushholdingactions: write(ruling 5). It meets the issue's fourth criterion another way, and widens decision 0043's row.FACTORY_PAUSEDon the repo before PR 3 merges, and clears it once The approving act: the machine user approves from a Run with no Engine, bound to the admitted commit #195's act file is in a Release (ruling 11).don-petry commentedon Oct 22, 2026
An amendment to rulings 7 and 10 above, so they agree with #193's rulings for the effects port (#193).
config/controller.json'srun_workflowslist is replaced by a map of workflow files by kind,run,approvalandpoller, plushome: {repo, branch}. The effects port picks a dispatch's file by its kind, and the records port lists the files of kindsrunandapprovalas the Role workflow files. This also covers The approving act: the machine user approves from a Run with no Engine, bound to the admitted commit #195's amendment, whereapprove.ymlis of kindapproval.pass.yml. It gains one step, after the pass's writes, that uploadswritten/1. That is the record of each write the effects port made, kept apart frompass/1, which is uploaded before any write and never rewritten.don-petry commentedon Oct 22, 2026
The Maintainer's answers (2026-10-21, relayed by the orchestration session) on two points above.
(a) The CLI on the runner (ruling 8): a second exempt install, checksummed.
CLI_CHECKSUM. A mismatch fails the Run.(b) Two secrets in
run(ruling 7, rule 3): separate steps.CLAUDE_CODE_OAUTH_TOKEN: no other secret is in itsenv:, and the reader's token is already revoked. That keeps Design: the CLI's sandbox limits connections but passes the parent's environment through (P-26 failed); the run job relies on the scrub and --tools "" #287's rule, that the model's step holds no secret but its own, under the scrub and--tools "".tools/check_workflows.pyholds it: inrun-jobs.yml'srunjob, the step that runs the Engine namesCLAUDE_CODE_OAUTH_TOKENalone, and the reader key's step comes before it and revokes in afinally. Each has a counter-example.task, on an environment of its own, is not taken.apply reads again with the Role App's own token, read-only, for the n…
Harness: apply's refusals, the identity port and a Role's writes (#194,…
Records: the rows #194 holds that wait on the run job's workflow wait…