Skip to content

Workflows: the frozen entry shim, the poller, the Run's three jobs and the nudge, each held to its shape #196

Open

Description

@don-petry

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, §2.4, §4.9, §7.3; decisions 0003, 0004, 0006; docs/standards/workflows.md; L-35 to L-40.

Activity

  1. don-petry commented on Oct 21, 2026

    @don-petry
    ContributorAuthor

    Working rulings (overturnable in review)

    Solution Architect, delegated. On main at 7e03db6. Read from:

    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.

    4. The poller. poller.yml, in full:

    name: sf-poller
    on:
      schedule:
        - cron: "13 * * * *"
        - cron: "33 * * * *"
        - cron: "53 * * * *"
      workflow_dispatch:
      workflow_run:
        workflows:
          - factory
          - ci
          - sf-run
        types:
          - completed
    permissions: {}
    jobs:
      pass:
        permissions:
          actions: write
          contents: read
        concurrency:
          group: sf-poller
          cancel-in-progress: false
        uses: combsmith/software-factory/.github/workflows/pass.yml@factory/v0-next
    • 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_by successor), 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).

    6. The Run's shim. run.yml, in full:

    name: sf-run
    run-name: ${{ inputs.name }}
    on:
      workflow_dispatch:
        inputs:
          name:
            required: true
            type: string
          item:
            required: true
            type: string
          pass:
            required: true
            type: string
    permissions: {}
    jobs:
      lane:
        if: github.actor_id == '41898282' && vars.FACTORY_PAUSED != 'true'
        permissions:
          contents: read
        concurrency:
          group: ${{ inputs.item }}
          cancel-in-progress: false
        uses: combsmith/software-factory/.github/workflows/run-jobs.yml@factory/v0-next
        with:
          name: ${{ inputs.name }}
          item: ${{ inputs.item }}
          pass: ${{ inputs.pass }}
    • 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 lane. One concurrency group per Work item, on the one calling job, so it holds from run to apply and 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.
    • 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-*.

    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.
    • config/controller.json gains run_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/1 gains 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.
    • 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.

    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.

    Row or lesson Now To
    0002, 0039: the pass record's upload waiting on poller.yml PR 2: tests/test_tools.py::test_the_pass_uploads_its_record_before_it_dispatches
    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 waiting on harness/ PR 1: examples/workflows/invalid/lane.*, test_a_runs_lane_is_its_item_on_the_job_and_cancels_nothing_running
    0003: one workflow, the Run's, names a model credential waiting on harness/ #194 PR 3's check, or PR 1's if it lands first: test_only_the_run_job_names_the_model_credential
    0004: run and verify declare no permissions and use no cache waiting on harness/ PR 1, reworded "are granted contents: read alone and use no cache": verify.*, test_verify_holds_no_secret_and_no_cache
    0004: both credentials in one job; a write job runs the item's code waiting on harness/ #194 PR 3, as its ruling 9 says
    0006: only thin callers run from the default branch waiting on release/ PR 1, reworded (ruling 12): shim.*, test_a_shim_holds_no_step_and_calls_the_release_at_its_channel
    0036: every Role dispatch carries an item; the pass's run is no Role's waiting on run.yml PR 3: test_every_run_dispatch_carries_an_item_and_the_poller_takes_none
    0043: the restart on a Run's end, the relay's nudge or a tick waiting on poller.yml PR 3: test_the_poller_wakes_only_on_what_it_names_and_reads_nothing_from_it; the relay's half waiting on controller/relay.py
    0043: the one workflow on workflow_run, never its own end, reading nothing waiting on poller.yml PR 1: wake.*, test_only_the_poller_runs_on_workflow_run
    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.
    • Risk: two secrets in one job (ruling 7, rule 3). 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 says the run job holds no secret but the model's; decision 0035 put the reader's key on factory-run for 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.
    • 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.
    • Scope and his hand: going live. One more canary trial before the shims (ruling 9). He sets FACTORY_PAUSED on 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).
  2. don-petry commented on Oct 22, 2026

    @don-petry
    ContributorAuthor

    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.
  3. don-petry commented on Oct 22, 2026

    @don-petry
    ContributorAuthor

    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.

    • The install downloads the pinned build, 2.1.306 (the Engine's pin, L-35, as STRING_KEYS no longer names model, which the run at 2.1.306 never showed (closes #247) #300 left it), and checks its sha256 against CLI_CHECKSUM. A mismatch fails the Run.
    • 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.
    • The step that starts the model comes after it, and holds only CLAUDE_CODE_OAUTH_TOKEN: no other secret is in its env:, 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.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.
  4. added a commit that references this issue on Oct 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions