kstrl lesson

PR 221, range 94fdbbd..79196de, merged into main as 72404e9. Taught on the day it landed and kept as it was; the register under docs/lessons/ records what it taught.

Six loops, one map: what kstrl becomes after R10

PR 221, "remodel kstrl as a control system", merged into main as 72404e9. It adds one document, docs/control-loop-design.md, and files thirteen issues, #222 to #234, tracked by #235. No code moved. This lesson teaches the system those issues build, as a picture you can click, for the person who built the rest of it. Click things; nothing here can break, and nothing is recorded.

1. The problem, before any file name

A story in a component's PRD is done when its passes flag is true. The only thing that ever sets that flag is the engineer agent, the same agent that wrote the code. Mechanical verification then reads the flag back and fails the component if any story is still false. Nothing in the harness writes it. The thing that acts is reporting its own result, and the harness has been treating that report as a measurement.

That is one defect, and it had nowhere to be seen from. kstrl is described everywhere as a pipeline of phases, and a pipeline has no word for "the thing that measures must not be the thing that acts". The design supplies the frame in which that rule is the first thing you see. Under it, kstrl is not one pipeline but six loops at different rates, and the phase chain everybody quotes is one tick of the middle two.

Two more defects become visible the moment the parts have names. The retry context handed to an agent on its third attempt still carries every failure from its first, with an instruction to fix them all, even the ones attempt two already fixed. And no sensor in kstrl can be run without a full paid factory run, which is why every threshold in the codebase is, in the source's own words, an unmeasured placeholder.

The plan fixes those in a fixed order, adds no phase, renames nothing, and edits no prompt body. What it adds is mostly small: one command, a few files, a handful of rules that make two sensors agree before a claim counts.

2. Words

Each plain word below is used the same way in the prose, the captions and the widgets. The register's glossary defines them; this table only maps each to the thing in kstrl.

Plain wordIn kstrlWhat it is
set pointpasses and the acceptance criteria; the PRD; the policy envelopeThe state a run steers toward.
sensorMechanicalVerifier, Reviewer, SecurityReviewer, ContractTester, FixturesOracle, CalibrationAnything that measures the tree independently of the agent that changed it.
error signalthe typed FindingThe gap between set point and measurement.
controllerPipeline, Scheduler, AutonomyLadderThe code that turns findings into the next action.
actuatorEngineerLoop driving the coding agentThe thing that changes the tree.
disturbancea transport failure, a moved base branch, model noiseA change the loop did not command.
winduptoday's RetryContextError accumulating while nothing discharges it.
level-triggeredRetryContext after R10.2Acting on what is failing now, not on the history of what failed.
advisory first[factory] setpoint_agreement, the dampener report, the demotion triggersA gate ships measuring and reporting, and blocks only by the operator's judgement.
flow controlFlowControl, [serve] max_open_prsA bound on work in flight, so the loop cannot outrun its reviewer.
dampenerDampener, ks sense --compare-baselineA stored baseline plus a report of what a branch added.
safe modeSafeModeOne predicate over the degraded states that already exist.
memory fileOperatorContext, scripts/kstrl/memory.mdStanding corrections, loaded right after the retry context.
human on the loopOperator, Inbox, SteeringThe loop runs itself; the human intervenes on exception.

3. The system, as one map you can click

This is the atlas's own drawing of kstrl, with the components the R10 plan reaches marked. It is the spine of this lesson: every later section points back into it by component name. Press TODAY to see what exists on main and END STATE to see the system the plan leaves. Then click any card. Its panel says what it does, its interface, its build state, what flows in and out, and the invariants it serves. Drag to pan, scroll to zoom, press zero to reset.

GITHUBissues in, pull requestsout, polled never pushedFACTORY PROCESSone process; every decision is codeOPERATORone person, on the loopCONTROL DIRECTORYXDG state outside the tree;the governor the agent...AGENT CLIsubprocess in its ownprocess group; killed on...WORKTREEthe only tree the agent maywrite1INTAKE2PLAN3BUILD4DECIDE5MEASURE6SHIP7OBSERVE8LEARN9TRUSTServeDaemonthe daemon that admitsworkIN REACHGitHubIntakethe issue readerIN REACHWorkQueuethe queueIN REACHSpendLedgerthe spend ledgerInboxthe decisions waiting foryouIN REACHFlowControlthe open-PR boundIN REACHSteeringyour PR comments, readbackIN REACHArchitectthe planner that attacksthe specManifestthe component graphPRDwhat done means, per storyIN REACHFeedforwardwhat the agent is toldbefore it startsKnowledgeInjectorfacts from earliercomponentsRetryContextwhat failed last timeIN REACHEngineerLoopthe inner loop around theagentIN REACHAgentAdapterthe wrapper around thecoding agentBreakerthe stall detectorPathGuardthe scope fenceOperatorContextyour standing instructionsIN REACHMechanicalVerifierthe checks that need nomodelIN REACHReviewerthe second opinionIN REACHSecurityReviewerthe security opinionContractTesterthe integration checkFixturesOraclethe answers the agentcannot rewriteAdequacyGatethe check on the teststhemselvesPolicyEnvelopethe written rules for amergeFindingsthe error signalIN REACHCalibrationthe check on the checkersIN REACHSenserun any check by handIN REACHPipelinethe decision per componentIN REACHSchedulerthe decision per runIN REACHWorktreesone working copy percomponentPullRequeststhe mergeIN REACHDistillerwhat was learned about theartifactDampenerthe regression guard onevery PRIN REACHReleaseStagethe deployAutonomyLadderhow much the factory maydo aloneIN REACHReplaywhat the ladder would havedoneStateDirthe governor's own files,out of reachSafeModeis the factory degraded,and whyIN REACHHealthTrendingis the factory gettingworseIN REACHEvolutionJournalthe record of everyoutcomeIN REACHProposalssuggested harness changesPlaybooklessons shared acrossprojectsRuntimeSignalswhat production saysEventBusthe event streamReducerthe current state, rebuiltfrom eventsDashboardthe live viewProgressLogthe older event log, keptcompatibleLinearMirrorthe tracker mirrorCLIthe ks commandIN REACHOperatoryouGitHubIssuesissues, the way inGitHubPRspull requests, the way outCodingAgentthe coding agent itselflabelled issuequeue itemnext ready itemadmission verdictopen-item capfactory runspeccomponent DAGstories + criteriaready componentsworktree per componentcomputed contextfactsfailures from last attemptpromptstdin promptstream + COMPLETE markernext failing storypasses flag (a claim)stall verdictscope verdictLoopResultworktree + PRDdiff + criteriadiffenvelope verdictadequacy findingsfixture resultscheck resultscriterion verdicts + concernsvulnerability findingsthe error signalretrypassed diffdurable factsmergepull requestmerged tiersbreaker component to resetcheckpoint / policy exception / budget overrunrun outcomeflag bundle (withhold only)violation -> demotionregression (R10.11)autonomy.jsonspend.jsoninbox.jsonlcomponent outcomes + signaturesrecurring patternsexperiments.tsvtyped eventstyped eventsv1 mirrorevents.jsonlRunStateks statusfailures, budget haltsboard, checkpoint modalapprove / reject / snoozepromote with ack

Scroll to zoom, drag to pan, click a component to frame it, Escape to go back.

in the plan's reachnot in the planWorkMeasurementFeedbackYouTrustLearningRecord
The system as the atlas describes it, with the components the R10 plan reaches marked. Drawn by scripts/atlas/schematic.py from docs/atlas/atlas.json, the same generator and data as docs/atlas/index.html, so this figure cannot disagree with the atlas. The marked set was read by hand from the modules each R10 issue (#222 to #234) names for editing, mapped through logical_model.implemented_by; it is a plan's reach, not a diff, and a component whose code has not landed is a dashed ghost until its entry point appears in the tree. Everything drawn is real; nothing here is invented.

Three things to read off the map before going on. The planned cards sit inside existing bands, never in a new one: that is doctrine 5, the frozen phase count, drawn. The one card outside every loop is Sense, the sensors run by hand, and it is the first item in the build order because every later item is cheaper once a sensor runs without a factory. And the only line that runs from the actuator back into the set point is passes flag (a claim), from EngineerLoop to PRD. Section 5.1 is about that line.

4. Walk one spec through it

The same map, one step at a time. Each step lights the components that act (amber) and the component that measures the step (steel). Where nothing measures a step, the step says so; those are the open loops.

jump to the map
Press Next.

Try this. Step to "the engineer builds one story" and look for a steel card: only the breaker and the path guard measure anything, and neither reads the code, so the fast loop is open. Then step to "release" and "runtime signals": nothing lights but a ghost, because the operate loop is not built.

The path is the one the design's section 2 table describes, placed on the atlas's components. Every card and edge it lights was checked against scripts/atlas/logical_model.py by docs/lessons/verify/pr-221/map_paths.py; a step that named a flow the model does not declare would light nothing.

5. Why it is built this way

Eight questions the operator would ask, each answered by a rule you can operate. Under every widget: two moves to try, then the first answer the design tried and what killed it. Every rule was pulled out of the page into a standalone script and swept over its whole input range before it was allowed in; the scripts live under docs/lessons/verify/pr-221/ and the register quotes their output.

5.1 Who is allowed to say a story is done?

Today: the engineer, alone. check_prd_stories at kstrl/verify.py:526 re-reads the PRD the agent just wrote and fails only if a story is still false. The reviewer already emits a verdict per acceptance criterion, CriterionReview.verdict at kstrl/review.py:65, and already knows which stories it covered. The second sensor exists and is not consulted. After R10.3 the two must agree.

Try this. Set criterion 2 to uncovered and criterion 1 to pass: still agreement, because a criterion with no verdict is ignored and the story counts as covered by the other. Now set both to uncovered: a finding, "not covered". Then, with a fail verdict showing, drag the level from 0 to 1 and watch the same finding turn from recorded to blocking without anyone touching the config.

The rule is issue #224 sections 2 to 5: fail dominates advisory dominates pass; a story with no criterion verdict was not covered; one finding per story claimed done and not confirmed; the gate blocks when the config says block or the resolved level is 1 or higher, mirroring layer0_blocks at kstrl/adequacy.py:721. Story and criteria are invented; the rule is the issue's. Swept by setpoint_agreement.py: 960 rows, 130 findings, 117 of them blocking, none with the default config.

The first answer, and what killed it. Keep the flag as the verdict and make the reviewer's disagreement a hard failure inside the review phase. That is what hard mode already does for a criterion marked fail, and it misses two cases the widget shows: a story the reviewer never covered, and an advisory verdict, both of which pass hard mode today. The second answer, the one the design's earlier draft held, was that requiring agreement meant editing the engineer prompt's step 14 and so crossing H3. Read step 14 at kstrl/init_cmd.py:81: "set that story's passes to true (only after tests/typecheck pass AND the self-critique is written)". It stays exactly correct. The flag was always a claim; the harness just starts treating it as one. That removed the supposed prompt cost and moved the item from expensive to cheap.

Two things about the end state are easy to miss. check_prd_stories stays; it still catches an agent that emitted COMPLETE with a story left false. And in blocking mode the harness becomes the first writer of passes in kstrl's history: it reverts the story to false, appends a notes line beginning "reverted by reviewer", and saves the PRD, so the engineer's own step 7 picks the story up again. The set point is corrected by the sensor, not by the actuator. That is invariant 12 on the atlas, enforced for the first time.

5.2 Why does the agent see only what is failing now?

The retry context is four lists whose only mutators append, kstrl/context.py:20-46. There is no clear, no dedup, no expiry. Its renderer ends with the line "Fix ALL issues listed above before completing." So on attempt 3 the agent is handed attempt 1's failures, which it may have fixed on attempt 2, unmarked, with an instruction to fix them all. Add failures one attempt at a time and watch where each lands.

Try this. Add verification four times. The current section keeps one entry while the old rendering carries four, and from the fourth attempt the new prompt is the shorter one. Then reset, add review, then verification: the review entry moves to "not re-measured", because attempt 2 stopped before review ran.

The rule is issue #223. Phases rank engineer 0, verification 1, review 2, security 3, contract 4. N is the latest attempt, Q the rank of its failing phase. Earlier entries ranked above Q were not re-measured; earlier entries ranked at or below Q are resolved or superseded and render as one count line. An entry from a context serialised by today's code has no attempt number and always renders under "not re-measured". Failure texts are invented and all the same length, so the character counts compare like with like and mean nothing on their own. Swept by retry_context_rank.py: 1560 sequences.

The first answer, and what killed it. Clear the context at the start of every attempt. The manifest already does that for a sibling field: begin_attempt at kstrl/pipeline.py:1035 resets comp.review_findings. Clearing throws away the entries whose sensors did not run in the latest attempt: an attempt that dies in verification never reaches review, so last attempt's review findings are neither fixed nor refuted. The rank rule exists to keep exactly those, and it is what the second move above shows.

The sweep also corrected an expectation. The new rendering is not smaller; it is bounded. For one or two attempts it is longer than today's, because its headings are longer. What changes is the growth: each further attempt at the same phase adds a full failure text to the old prompt and one history line to the new one. Windup is the growth, not the size.

5.3 What happens when the factory runs out of budget for reviewers?

Today the pipeline holds both answers at once. In hard mode with an unchunked review and max_adversarial_calls exhausted, kstrl/pipeline.py:2465 downgrades the review to SKIP, warns, and lets the component proceed on mechanical checks alone. A few lines later the chunked path refuses with an infrastructure_error finding. Same budget, same mode, two verdicts. Run components through the cap.

Try this. Cap 1, three components, hard mode, after R10.5: the second and third halt with check adversarial_budget. Flip the rule to today: they merge unreviewed, with phase_skipped recorded. Then drag the cap to 0: identical under both rules, which is the default and why the design calls this defect latent.

Each review that runs consumes one call; a cap of 0 is unbounded. Advisory mode keeps the skip under both rules, because advisory never blocks. Security review has the same shape at kstrl/pipeline.py:2696 and is left out: one rule per widget. Swept by budget_halt.py: under the old rule 20 components in the grid merge unreviewed, under the new hard rule none.

The first answer, and what killed it. Keep the downgrade because it is loud. It is: the warning prints and phase_skipped is recorded, so an empty finding list still means every phase ran. The design accepts that and still calls it a priority inversion. Under budget pressure the harness sheds the reviewer, and by this repository's journal the reviewer accounts for most of the failure signatures the factory ever catches. unconfirmed That count, fourteen of seventeen, comes from a journal that is not in the repository. What can be confirmed is the shape: the chunked path already fails closed, so R10.5 makes the unchunked path match it and leaves advisory mode alone. Invariant 6, enforcement reads artifacts: a component that was never measured cannot be treated as measured.

5.4 Why can the daemon refuse to start work?

The daemon, ServeDaemon, runs its admission gates in a fixed order, every one before the claim, because a gate evaluated after the spend is a post-mortem. R10.7 adds one more: refuse while a kstrl-authored pull request is still open. Flip conditions and read which gate refuses first, and whether the refusal is a wait, a pause, or a halt.

EVALUATED IN THIS ORDER, ALL BEFORE THE CLAIM ledgerhalt paused?skip poisonpause coveragepause budgetpause open PRswait, R10.7 inbox capwait lockwait claim one item recovery and the remote sync happen before the ledger gate; they never refuse

Try this. Set open kstrl PRs to 1 with the bound at 1: refused, as a wait, so the next cycle re-checks; nothing is paused and nobody is paged. Untick "gh answered": still refused, because an unknown number of open pull requests is not zero. Then set the poison streak to 3 as well and see the earlier gate win: the order is the mechanism.

The order is serve_cycle at kstrl/serve.py:1866 onward on 72404e9, with R10.7's gate placed where issue #228 puts it, in the gates tuple after check_budget. That is before the inbox cap and the factory lock, so the issue's sentence "a GitHub call happens only when everything else admits" holds for the three ledger gates and not for those two. Defaults: three consecutive poisons pause, the daily budget is off, the bound is 1. ks factory and ks run never enter this function: a human typing the command is the authorisation. Swept by admission_gates.py: 110592 rows; only the three ledger gates ever pause.

The first answer, and what killed it. Nothing bounded pull requests in flight at all. Grepping kstrl/ for max_open, open_prs, wip_ and concurrent_pr returns nothing; the bounds that exist are max_parallel = 4, one queue item per cycle, and an inbox cap of 50, none of which is about output. A loop that runs daily can open several unreviewed pull requests in a week, and then the reviewer is the thing that saturates. The design's earlier draft wanted a month of review-throughput data before choosing the bound; the merged draft ships it at 1, one line of config, reversible in seconds. unconfirmed The design says that default is proven in production elsewhere; the source is not named on main. The second dead end is the one the widget's second move shows: reading a failed count as zero.

5.5 How does the factory earn and lose autonomy?

The ladder, AutonomyLadder, is the controller's gain schedule: it selects how much the factory may do without a human from the evidence it has accumulated. Promotion needs evidence plus a human's acknowledgement; demotion is automatic and immediate; and the permissions a level grants are derived at run start, never stored, so a hand-edited flag cannot grant what the ladder never awarded. Drive it.

Try this. Press decisive run eight times and clean merge five times, then promote: L2. Press policy violation: back to L1 at once, counters reset, and promotion is locked for ten decisive runs, so the way up is slower than the way down. Then, at any level above L2, untick "policy envelope enabled": the earned level stays, the level a run may use drops to L2, because there is no envelope to auto-merge inside.

The rule is kstrl/autonomy.py on 72404e9: promote, demote, promotion_blockers, flag_bundle_for, resolve_runtime_level and manual_override_notes, plus R10.11's calibration trigger from issue #232. The thresholds (8 decisive runs, 5 merges for L2, 15 clean merges for L3, 30 for L4, a 10-run cool-down) are copied from the source, which calls every one of them an unmeasured placeholder; the widget teaches the shape, not the numbers. Swept by autonomy_ladder.py: eleven scripted sequences and 64 clamp combinations.

The first answer, and what killed it. Independent flags. Before the ladder, pause_before_pr_merge, review_mode and deps_allow_new could each be flipped alone in a config file, so "how much may this factory do without me" had no single answer and no audit record. The ladder makes autonomy one ordered level with a derived bundle that can only withhold. Tick "config says merge gate off" at L1 and read the note: the contradiction is recorded, and the bundle wins. That is invariant 7. The gap R10.11 closes is the other direction: five demotion triggers are declared and only the policy violation fires today, so a factory quietly getting worse without breaking policy kept its level. The calibration trigger ships advisory, an inbox item, with demotion behind a switch, for the same reason every other gate in this cycle does: the numbers are placeholders.

5.6 In what order is the agent's context assembled, and why does memory come last?

The engineer's prompt is one string, built once per attempt, and the order of its parts is a mechanism. Today the worker joins knowledge, feedforward and the retry context, kstrl/factory.py:1439-1450; then run_loop prepends the worktree's CLAUDE.md to the instructions and puts that whole prefix in front, kstrl/loop.py:529-541. R10.8 adds golden patterns after knowledge; R10.9 adds the memory file as the last part of the prefix, immediately after the retry context.

Try this. Untick the memory file: what follows the retry context is now CLAUDE.md, the project's general instructions, and if the worktree has none, the templated prompt itself. Switch the state to today: the golden and memory slots vanish whatever you ticked, because nothing loads them yet.

The rule is the parts list at kstrl/factory.py:1439-1450 and the prepend at kstrl/loop.py:529-541 on 72404e9, with the two insertions issues #229 and #230 specify. Presence toggles are the only inputs; nothing here is measured. Swept by prefix_stack.py: 80 combinations.

The first answer, and what killed it. kstrl already has three files that look like standing feedback: the Agent Learnings section of CLAUDE.md, per-directory AGENTS.md files, and codebase_map.md. The design says none of them is guaranteed to load after the controller's output, so standing feedback can be shadowed by the retry context. The code is subtler than that sentence: CLAUDE.md does land after the retry context today, as the widget shows, while the other two are never in the prompt at all; the agent reads them, or not, by opening files. What none of the three has is the memory file's contract: a fixed slot right after the retry context, read from the worktree, a character budget with a truncation warning, and a writer, since R10.10 appends reviewer comments to it. The ordering is the mechanism, and so is having exactly one file with that job. Invariant 9: the human is a role, and this is the file the role writes.

5.7 Why measure by hand before automating?

Every threshold in kstrl is a placeholder, and the design's diagnosis of why is one sentence: you cannot cheaply measure a sensor you cannot run. Pick a command and see how it reaches the mechanical sensors.

Try this. Pick factory: it reaches the sensors, through a PRD, a branch, a worktree and paid agent time. Pick status: it never reaches them. Then switch the tree to after PR #237 and pick sense: the first command that measures a tree and nothing else, exit 0 pass, 1 fail, 2 could not measure.

The command list is uv run ks --help on 72404e9, fifteen commands; the design document says fourteen, and the zero is the claim that matters. Whether a command reaches the sensors was read from each command's code path; whether any does so standalone was measured by grepping kstrl/ for callers of run_mechanical_verification: one, inside the pipeline. The "after PR #237" column is PR #237's description of itself; it was open, not merged, when this was written. Swept by sense_commands.py.

The first answer, and what killed it. Measure first. An earlier draft of the design gated seven mechanisms behind "measure this number first": how often the reviewer disagrees with the done flag, how much stale text attempt 3 carries, review throughput for the bound, and four more. Every one of those needed a factory run to collect, so measure-first delayed both the value and the data. What replaced it is advisory-first: ship the gate measuring and reporting, and the number arrives as a byproduct of running. The rule that survives is the repository's own: no threshold is chosen to make a feature shippable. So ks sense goes first, because the dampener is the same sensor pointed at a baseline and the inner-loop sensor is the same function called between iterations. Invariant 13.

5.8 Which loop does a change belong to, and how fast is it?

Six loops at different rates, and a seventh not built. A change belongs to the loop whose set point it moves, and the loop's rate says how soon its effect can be measured. Pick a proposed change; it lights the loop on the map and says what measures it.

Try this. Pick "a sensor between engineer iterations": the implement loop, seconds to minutes, and the only loop with no sensor today. Then pick "a safe-mode chip in the dashboard masthead": it belongs to no loop. The observe band is a view over every loop, and a sink is never control flow.

The loop table is section 2 of docs/control-loop-design.md, mapped onto the atlas's components by hand; the assignment of the plan step to the integrate loop and of feedforward to the accept loop is this lesson's reading, not the design's. Every component name was checked against logical_model.py by map_paths.py.

The first answer, and what killed it. Rename the modules to the loop's words: verify.py to sensor.py, [breaker] to [dampener], control-flavoured status values. Four things killed it, and the design lists them: doctrine 6 forbids retrofitting existing enums; a breaker interrupts and a dampener opposes rate of change, so the rename would make the vocabulary less accurate; the README's config sections are generated from the dataclasses and every rename is a regeneration; and any wording change inside an adversarial prompt is a calibration-gated change under H2 and H3. New code takes loop names. Nothing that exists is renamed.

5.9 What the design deletes

In code, nothing yet; the pull request is a document. In the system it defines, four things go, and each removal serves a numbered invariant on the atlas.

Nothing is renamed. No phase is added. No enum is retrofitted, with one argued exception: R10.11 adds HEALTH_BREACH to the inbox's ItemKind together with its emitter, which that enum's own docstring permits; a reviewer of that pull request decides whether the reading holds.

6. Making the next change here

The plan is thirteen issues in a fixed order. Each ships, and its advisory output is read, before the next starts. The order is the method's own phase order with the by-hand sensor pulled to the front, because every later item is cheaper once a sensor runs without a factory.

OrderIssueLands inNew file or keyDepends onAdvisory first
1#222 R10.1 ks senseSensekstrl/cli.py command; prd_path: Path | Nonenothingn/a
2#223 R10.2 level-triggered retry contextRetryContextFailureEntry in kstrl/context.pynothingexact, no gate
3#224 R10.3 set-point agreementReviewer, Pipeline[factory] setpoint_agreement; SETPOINT_DISAGREEMENT_CATEGORY#223 should land firstyes
4#225 R10.4 safe modeSafeModekstrl/safemode.py; a line in ks statusnothingn/a, a read
5#226 R10.5 budget halts in hard modePipelinenone; CHANGELOG marked breakingnothingn/a
6#227 R10.6 dampenerDampenerscripts/kstrl/sense-baseline.json; .github/workflows/sense-dampener.yml#222yes
7#228 R10.7 flow controlFlowControl[serve] max_open_prs; PR_FOOTER_MARKERnothingn/a
8#229 R10.8 golden patternsOperatorContextkstrl/operator_context.py; scripts/kstrl/golden-patterns.mdnothingn/a
9#230 R10.9 memory fileOperatorContextscripts/kstrl/memory.md, last in the prefix#229n/a
10#231 R10.10 polled steering, two PRsSteering, WorkQueueQueueItem.pr_urls; [intake_github] steer_enabled#228, #230n/a
11#232 R10.11 demotion triggersAutonomyLadder, Calibrationapply_demotion; the kstrl/health.py seam for #151#151 for the health halfyes
12#233 R10.12 iterate fasterEngineerLoop[verify] fast_iteration_checks; [factory] convergence_attempts#222, #223, and an entry criterionyes, and blocked
13#234 R10.13 reframe ARCHITECTURE.mddocsthe loop nest and a glossarynothingn/a

The graduation rule, stated once. A gate moves from advisory to blocking when the operator has seen its output on real runs and can name what it caught and what it flagged wrongly. That is a judgement, deliberately the operator's; no issue encodes a number in its place. The follow-ups it unlocks are listed on #235 and are deliberately not filed yet.

Three things nobody may do in this cycle. Edit a prompt body: no item touches DECOMPOSE_PROMPT, REVIEWER_PROMPT, SECURITY_PROMPT, DISTILL_PROMPT or DEFAULT_PROMPT, so H2 and H3 are not engaged, and the first item that does must bump the version, update the snapshot hash and re-run calibration in one pull request. Add a phase: new behaviour lands inside an existing phase or outside the chain. Add an outcome enum: new surfaces reuse the shared disposition.

Two things that will bite whoever picks up the first issue. Every issue ends with "tick its box in the tracker section of docs/control-loop-design.md", and on main at the time of writing that section does not exist; the pull request's last commit removed it before merge, and PR #239 restores it. And the line numbers in the issues were cited against 94fdbbd; kstrl/ did not change between that commit and the merge, so they resolve today, and every issue says they will drift.

7. Judge it: what would make this wrong

Answer before you press. Each button says why.

Q1. R10.3 has landed with its default configuration and the ladder off. The engineer marked story B done and the reviewer did not cover story B. What happens to the component?

Q2. Attempt 1 failed review on criterion X. Attempt 2 failed verification on a type error. What does attempt 3's prompt say about criterion X?

Q3. The daemon's max_open_prs is 1 and the gh pr list call fails with a network error. Does the daemon admit the next queue item?

Q4. An operator sets pause_before_pr_merge = false in kstrl.toml while the ladder holds L1. What does the run do at the merge?

Q5. Which change belongs to the loop that runs slowest today, and what is its sensor?

8. Close

The system on the map above is the one the plan leaves: the same bands, the same phase count, the same names, with a sensor that runs by hand, a set point corrected by a sensor rather than an actuator, a retry context that discharges, a daemon that will not outrun its reviewer, and a ladder with all its inputs wired. Most of it was already there without a word for it. The design supplies the word.

Anything here that did not land, ask your teacher. The map, the walk and every widget answer to a script in docs/lessons/verify/pr-221/; if a claim and a script disagree, the script is the one to read first.