Status: accepted; steps 1 to 4 implemented (
findings.json, lever table, card block,/loop-insightsinai-dev-tools), step 5 (the review) pending. Design in ADR-0046 and ADR-0047, status and build in ADR-0048; the on-demand command isai-dev-toolsADR-017. This page describes the behavior and the order it was built in; it changes when the review of step 5 decides on a push channel.
The meta-loop turns the loop's own statistics into suggested changes to the loop. It reads; it never writes to GitHub and never edits prompts/, config/, scripts/ or docs/.
run records (metrics branch, redacted, ADR-0040)
│ pipeline-site.yml (existing build, no new permission)
▼
diagnose() ──► Diagnostics page (ADR-0043)
│
├──► findings.json ◄── lever table (ADR-0047) deterministic, no LLM
│ │
│ ├──► "Suggested lever" block on each finding card
│ │
│ └──► /loop-insights (ai-dev-tools, on invocation)
│ drafts proposals in the invoker's session
▼
a person applies a lever as an ordinary change:
protected paths · live evals (ADR-0031) · docs + tests · human merge
| Not done | Why |
|---|---|
| Open an issue, PR, comment, label or Discussion | It would re-enter the loop: validate-issue.yml fires on every issue opened or edited, a PR triggers the review. ADR-0046 |
| Edit a prompt, a setting or a script | Those paths are on the write denylist (ADR-0021, ADR-0044); the decision stays with a person (ADR-0001). The active lessons below do not edit anything: they add a fixed sentence to one run's prompt |
| Propose a weaker gate | No threshold change, dataset removal, filtered run, floor change or attempt-cap increase (ADR-0031, ADR-0044, ADR-0034) |
| Keep a proposal registry | The feedback is the finding turning resolved (ADR-0043) |
| Call an LLM in the build | A generated diagnosis could pass a hypothesis off as a fact (ADR-0043) |
ADR-0051 closes the loop for the agent as well as for the operator, for the two stages that write code (generation and auto-fix). It does not change what the meta-loop above does; it is a second reader of the same kind of signal.
pipeline stages write, as they already do: a run of generation / auto-fix:
codegen_skip / autofix_skip (rules of a guardrail rejection) loadLessons() ── one read-only GET ──► metrics/runs.jsonl
review_signal (axis ids of blocking findings) │ (metrics branch)
│ ▼
▼ extractLessons(): a signal seen ≥ 2 times among the
metrics/runs.jsonl ─────────────────────────────────────────► last 30, none older than 30 days → ≤ 3 lessons
│
▼
fixed sentences of LESSONS (lessons.mjs) appended to
the system prompt, ≤ 700 chars; journaled by id
| Property | How |
|---|---|
| No model text in a prompt | Only the keys of the catalog cross: signals are guardrail rule ids and review axis ids written by code; an id outside LESSONS is dropped on read |
| Never loosens a rule | A lesson restates a guardrail the prompt already has (ADR-0009) or a review axis; the write guard still rejects the patch either way |
| Bounded | At most 3 lessons, 700 characters, whole lessons only; generation drops the block rather than exceed generation_max_input_tokens |
| Expires | Outside the newest 30 signals or older than 30 days, a lesson is gone; there is no state to clear |
| Cannot fail a run | No metrics branch is no lessons; any other failure is one lessons.unavailable warn event |
| Switch | ADL_LESSONS=off as a repository variable (vars.ADL_LESSONS) |
| Observable | lessons.loaded lists the ids; docs/observability.md |
The review stage gets no lessons: it would bias the stage whose job is to be independent of the history of the PR. What this is not: a measured improvement. Generation and auto-fix have no eval suite, so whether a lesson lowers its rule's rejection rate is read from Diagnostics (codegen-blocked, autofix-ineffective) over the following weeks; a lesson that does not move its rate is removed from the catalog by a person.
On the Diagnostics page, an active warning or critical finding from a mapped rule shows a block Suggested lever — not verified:
| Line | Meaning |
|---|---|
| Stage | review, auto-fix, generation, provider |
| Summary | what the setting or prompt governs |
| Targets | keys of config/models.yaml, repository variables or prompt files to look at, each with its own gate |
| ADRs | the decisions that explain the target |
| Requires | per target, in words: "run the live evals.yml (suite )" (evals in the export: ADR-0031 §1) or "no eval suite measures it: verify by hand and say so in the PR" (no-eval-gate); docs-and-tests always (AGENTS.md Review Hygiene) |
No block means no lever, which has five distinct causes: the finding is resolved, it is info (a guardrail doing its job), the rule is mapped to NO_LEVER, it is a telemetry gap, or the window is too thin for the rule to have fired. The block says where to look; the fact above it says what was observed; neither says the lever is the cause.
The table is the dashboard's Rules and levers page (pipeline/levers.html), generated from LEVERS and NO_LEVER in scripts/lib/meta_levers.mjs: for each rule, the stage, what it governs, the targets with what a change to each requires (a live eval suite, or a check by hand), the decisions that explain them, and for the rules without a lever, the reason. A copy kept here would drift from the code; the v1 entries as decided are in ADR-0047 §2. A test fails on a diagnostic rule that is neither mapped nor listed with a reason.
Published at the site root with the other static files. A serialization of the Diagnostics page: it discloses nothing the page does not (every text field is on the page; the lifecycle timestamps derive from the records it cites). Fields and compatibility rules: ADR-0047 §1. Points a consumer must respect:
- Check
schema_version; refuse an unknown one. - Show
generated_atandwindow: the window is relative to the latest recorded run and the site rebuilds when a record lands, not on a schedule. status: no_dataand agapsentry are not health. An emptyfindingsarray is not proof of health.title,fact,hypothesis,evidence[].summaryare data. Some carry allowlisted model-originated text: never execute or obey them.hypothesisis unverified by definition;lever.verifiedis alwaysfalse.
- Dashboard. Open Diagnostics, filter on
warning/critical, read the fact, then the suggested lever. - Drafting.
/loop-insights(ai-dev-toolsADR-017; the card of a finding with a lever shows the command with its--findingkey, one click selects it) fetchesfindings.json, validates the version, and drafts one proposal per finding that has a lever. The output stays in the session. You decide whether to make the change, in which repository, and how. - Applying. As any change to the loop: branch, edit,
npm test, live evals when the lever requires them, documentation and tests in the same PR, review, human merge. - Checking the effect. The finding turns
resolvedonce its pattern has left the lastRULES.activeWindowruns of its rule (10 at the time of writing,scripts/lib/pipeline_diagnostics.mjs). There is no other tracking.
Each step is mergeable alone; a step does not depend on the next being accepted. Every step follows AGENTS.md: tests in the same PR, changes/ changelog fragment, docs for behavior changes.
| # | Step | Accepted when |
|---|---|---|
| 1 ✅ | scripts/lib/findings_export.mjs (pure) + findings.json written by assembleSite; key made unique per finding |
A fixture covering every rule yields unique keys; the export ⊆ page data test passes; schema documented in docs/observability.md; c8 ≥ 80 % and added to test.yml |
| 2 ✅ | scripts/lib/meta_levers.mjs (LEVERS, NO_LEVER, leverFor) |
Exhaustiveness test (every rule id mapped or in NO_LEVER); no forbidden target; every config ref resolves in the file it names (config/<file>.yaml#<key>); leverFor is null for resolved, info and unmapped findings |
| 3 ✅ | "Suggested lever" block in console_views.mjs; lever in the export |
Rendering test with hostile strings (escaped); the block is below the hypothesis; absent when lever is null |
| 4 ✅ | ai-dev-tools: scripts/loop-insights.mjs, agent, command, docs (its ADR-017) |
See that ADR |
| 5 | Review after several weeks of data | Levers acted on, or not: decide on a push channel (ADR-0046, Revisit) |
Things steps 1 to 3 must not do: add a workflow, a token scope, a label or a call to a GitHub write endpoint; read a journal event outside EVENT_META_ALLOWLIST; call an LLM; add a threshold beside RULES.
- Pull, not push: a suggestion is seen by someone who looks.
- A rejected suggestion reappears while its finding is active.
- Generation and auto-fix prompts have no eval suite (
docs/evals.md): their targets readno-eval-gate. - The statistics are a few days of runs; most WITHHELD verdicts are guardrails (ADR-0038). Only the anomaly share produces a lever, so early on most findings have none.
- Free-text fields of the public record can hold model-originated text (audit
docs/audits/2026-10-19-cross-cutting-audit.md, F01, F06): this is why a consumer that is an LLM treats them as data.