What changed
Read from CHANGELOG.md at build time, so this page cannot drift from the record. Entries say what changed and why — the why is usually the part worth reading.
Unreleased
Not published yet. These land in the first release.
Standards 0.1.0 — Next.js and Express stacks
- `nextjs` (Next.js, App Router), extending `react`. The framework's defining hazard is that the server/client boundary is invisible in the source, so most of the rule set is that boundary seen from different angles: a Server Action is a public POST endpoint and re-checks authorisation in its own body whatever called it;
NEXT_PUBLIC_inlines a literal into the bundle at build time and cannot be rotated afterwards; a server-only module imported beneath'use client'ships its source to the browser; per-user data rendered into a cached segment serves the first visitor's data to the second;middleware.tsruns on a matcher and is a redirect for humans, never the enforcement point. Written for the App Router, with a Pages Router note only where the two disagree about whether something is a bug. New profileweb-next=javascript · react · nextjs. - `express` (Express 5). Where Express 4 differs in a way that changes the verdict the rule says so — an
asynchandler's rejection is the main one: Express 4 does not forward it, so the response is never written and the connection leaks. Also the four-argument error middleware whose absence hands out stack traces on every box whereNODE_ENVis notproduction,cors({ origin: '*' })withcredentials: true, traversal intosendFile, and an unbounded body limit. New profileservice-express=javascript · microservices · express. Explicitly not for NestJS, which runs on Express but validates and handles errors its own way. - Detection order is the whole mechanism, in both directions.
web-nextis tried *before*web-react, because every Next.js repo also depends on React and the general rule would win on ordering alone.service-expressis tried *last*, below every web framework, becausedepreads devDependencies andexpresssits there in a great many front-end repos as a dev server or mock API — a React app with an express dev server is a React app. A Nest service listing express directly staysservice-node. - Seed corpora.
seeded/nextjs/(18 seeds across a page/action file and a route handler) andseeded/express/(16 seeds).scripts/validate.mjsrejected two of these on first run for citing a severity above the rule's own — the corpus and the standard cannot disagree.
Docs — one owner per fact, and five claims that were no longer true
- Five stale claims, corrected against the source. Onboarding said
redline verifyruns eight checks;cli/commands/verify.tsemits eleven, and the sample quoted three messages the CLI never prints. The Workflows page saidredline-sync.ymlandverify-onboarding.ymlcarryif: falseand cannot run — neither file does, andlib/workflows-info.tssaid both "Active" and "never ran" in the same record. The Cursor adaptor said the vendor ships disabled and that no re-sync command exists; the manifest ships it enabled andredline synchas existed since 0.0.3. The custom-vendor page said there is no push-based distribution. The Skills page linked/docs/adaptors/skills, deleted with the skills vendor. - Each fact has one page, and every other mention links to it.
verifystatuses,pendingAdminand--repair, the enforcement ladder's thresholds, the severity table, the profile list, the required-check name, the sync register. The Standards page had been re-teaching the output contract and the profile mechanism in full; it now says what a standard is and links. - Installation merged into Onboard a repository. Prerequisites, the once-per-org gate publish and the limitations list now sit on the page documenting the command they are prerequisites for.
/docs/installationredirects permanently; the header CTA, the site nav and the footer point at the page that answers them. - Telemetry gained the dashboard. The tiles, what an unhealthy one means, the rule tuning queue and the seed-score numbers moved off "What success looks like", which was addressing a repository owner and a platform team on one page. Success keeps the four checkpoints a repository owner runs.
[0.0.4](https://github.com/moelzanaty3/redline/compare/v0.0.3...v0.0.4) (2026-09-12)
Standards — core/type-checker-suppression could not fire on four of the stacks it ships to
- Swift, Kotlin and C# are recognised —
// swiftlint:disable,@Suppress(...)and#pragma warning disable.@Suppress(is matched separately from@SuppressWarnings, because Kotlin's annotation is not a prefix of Java's and one pattern cannot stand for both. - The Go directive is matched as Go writes it. The list carried
// nolintwith a space. golangci-lint only honours//nolintwithout one, so the single Go spelling the checker looked for was the one spelling Go tooling ignores. Pinned by its own test. - `@ts-nocheck` and `# pylint: disable` join the dialects already covered, which were the two remaining gaps in stacks the list already claimed.
- Matching is by pattern, not substring. Whitespace is what broke the Go case, and a substring list has no way to say "optional space" — so every dialect is a pattern and the spacing of a real directive cannot silently miss again.
Standards — a ticket reference was matched case-insensitively, in every stack
Standards — the JavaScript rules were scoped away from Vue and Svelte components
Validation corpus — the deterministic tier had no seed coverage at all
Docs
- A new Verification page. Nine scenarios, run as steps, that take an onboarded repository from "the gate is installed" to "I have watched it pass and watched it block". Every row is a real run recorded against a real pull request rather than a description of what the workflow should do.
- Onboarding covers the whole sequence, including what happens after the pull request merges: the admin capabilities a non-admin run leaves in
pendingAdmin,--repair, the CODEOWNERS owner that has to resolve, and the approval trap a solo maintainer hits the first time the ruleset lands. - The merge gate page says how to add a check of your own — stand a Redline job down and run your own beside it, or add a job to the gate and to the aggregate's
needs, with the required-check naming rule that makes the difference between a gate and a permanently pending one.
A finding can now say where its rule is documented
- `redline init --docs-url <base>` records where your organisation publishes the standard, as
docsBaseUrlin.redline.json. Set it and every finding — deterministic tier and model review alike, through one shared renderer — carries→ <base>/r/<rule-id>on the next line. - Unset by default, and unset prints nothing. There is no honest default: an organisation running Redline internally wants findings pointing at its own copy of the standard, and a link to somebody else's is worse than no link. Anything that is not
http(s)is refused, because a finding is rendered into a comment on somebody else's host. - Every repository onboarded so far prints exactly what it printed before. The field is absent from their config, which reads back as no link.
The catalogue truncated ten rules mid-sentence
Docs — the rule page, and the page for when it is broken
- `/r/<rule-id>` is the canonical address of a rule — 335 static pages, one per rule, carrying its severity and what that obliges, whether a checker or a reviewer decides it, the globs it applies to, the profiles it reaches, and where it is defined. It is what
--docs-urlpoints at, and what a rule id typed into search now resolves to. Deliberately not a redirect into the stack page: someone arriving from a review comment has one question about one rule, and a table of eighty with one row tinted is the second question. - A Troubleshooting page. The failures that actually happen — a pull request stuck on "Expected — waiting for status", a green gate that checked nothing,
partially onboarded, an exemption that did not exempt, a rule that never fires — each with the string it prints and the command that resolves it. The material existed, scattered across five pages, none of which was where a stuck reader lands. - Search reads the documentation, not just its navigation. The index was page titles, descriptions and hand-written keywords, so
javascript/var-in-new-code— the single most likely thing a reader arrives with — returned nothing, as didpendingAdmin,swiftlintand "Expected — waiting for status". It now carries every rule as its own result and a token index of each page's prose and of the artifacts the reference pages render. - The docs are navigable and reachable. Every heading is anchored and listed in an on-this-page rail (Verification had seventeen headings and no ids, so nobody could link to scenario 6); the reading measure is capped at about 72 characters, down from 87–95; docs pages have a
mainlandmark and a skip link, replacing 46 presses of Tab; one global focus ring replaces an engine default drawing 1px of near-black on a near-black ground; breadcrumbs are links; and--faint, which failed AA in both themes, does not.
[0.0.3](https://github.com/moelzanaty3/redline/compare/v0.0.2...v0.0.3) (2026-09-11)
redline init — a gate for a repository whose organisation has not agreed to one yet
- `redline init --gate-source local` vendors the gate into the repository at
.github/workflows/redline-gate.ymland points the caller at it withuses: ./.github/workflows/redline-gate.yml. It is the same file the organisation copy is published from, with a header and the CLI version stamped in — not a second implementation, and a test pins it to the byte so it cannot become one. - The required check does not move. A local reusable workflow still reports as
<caller job id> / <called job id>, and both job ids are unchanged, so the context stays exactlyredline-gate / gate. Rulesets, branch policy andverifyneed no change, and a repository can move between the two sources with a re-run. - The wizard offers it when the organisation has no gate. The planning pass — which runs before a single byte is written — now reports whether the refusal is one vendoring would solve, and a run at a terminal is asked. A scripted run gets the denial it always got: vendoring is a weaker control, and nothing unattended chooses it for an operator. A refusal that is merely an unreadable host (a 401, a 500) is never offered the fallback — answering a transient failure with a standing security decision is the wrong trade.
- `.redline.json` records `gateSource` and `gateVersion`. Absent reads back as
org, which is what every repository onboarded before this has. An unrecognised value reads back asorgtoo: a hand edit must not be able to makeverifystop asking whether the organisation gate resolves. - `redline verify` reports a stale vendored gate. An org gate updates itself — one merge reaches everything pointing at it — and a vendored one does not.
gate-vendoredcompares the stamp against the CLI running the check and namesredline init --repair. A missing file is drift; an unrecorded version isunknown, not stale. - `.github/workflows/redline-gate.yml` is now protected from being clobbered the way the caller has always been. It is YAML, so it can take no marker-block merge, and a repository with an unrelated workflow already at that path would have had it destroyed.
- The attribution check recognises a local caller. It required a trailing
@— a reusable-workflow ref — which a localuses: ./...cannot carry, so Redline read a caller it had written itself as somebody else's workflow and refused to touch it.
redline init — the run that hung, and the one that writes nothing you did not ask for
- git no longer hangs on a prompt nobody can see. A real onboarding sat on "committing and opening the pull request" until it was killed:
git pushreached an ssh wanting a passphrase, stdin isignoreso the answer could not be typed and stderr is captured so the question could not be printed. The prompts are switched off rather than hidden —GIT_TERMINAL_PROMPT, sshBatchModeand a connect timeout — so git fails at once andpush()gets to print the error it always had, naming the branch the work is committed on. An operator's ownGIT_SSH_COMMANDstill wins. - `--no-commit` writes the files and stops. There was nothing between
--dry-run, which writes nothing at all, and a full run, which changes repository settings, commits to a branch and opens a pull request. It resolves the repository from the clone, skips the preflight and the label writes, and so needs no credential and works offline. The menu offers it as a third answer toReady?. What it gives up is stated in the report: without the preflight, an org-sourced caller is written without confirming the organisation publishes the workflow it references. - `remove` takes the vendored gate away with the caller that ran it. It deleted the caller and left
.github/workflows/redline-gate.ymlbehind — a reusable workflow nothing calls, in a repository just told Redline was gone. The path is read out of the caller, and accepted only when it is the one path Redline writes: a caller is a file anybody can edit, and resolving an arbitrary./…out of it would let a hand-edited workflow nominate any file in the repository for deletion. - A multi-select says it can be answered with nothing.
0/2 selectedbesideenter confirmread as a form refusing to submit until something was ticked. It never was. Only the word changed; escape is still the cancel key, because it is the only way out of a wizard. - The menu asks where the gate should live, straight after
What should Redline install?and only when the gate was kept. It is asked blind — the wizard runs before the platform is resolved — so the mid-run offer stays for anyone who picksorgand turns out not to have one. - `verify` called the gate broken on repositories where it was working. The machinery parser required a trailing
@on the gate reference, which a localuses: ./…cannot carry, so every locally-sourced repository read back as a caller whose job id had been edited away. That is the one direction this check must never fail in — it is the check that tells a real outage from a slow run. - The home page listed six wizard questions for a wizard that asks ten. Every other number on that page is derived from the repository and throws rather than degrade; the question list was hand-written and drifted silently. It is parsed out of
cli/ui/wizard.tsat build time now, and a disagreement fails the build naming both sides.
Vendors — Cursor on, Codex named, skills gone
- Cursor is enabled. It shipped disabled and rendered nothing; the renderer was complete the whole time. Onboarded repositories now get
.cursor/rules/redline-*.mdc— core always applied, one file per stack scoped by globs. - Codex is selectable by name. It renders
AGENTS.md, the same file theagentsvendor writes, because that is the file Codex actually reads. It is a separate id because an operator looking for Codex has to find it in the menu, and "it is covered by the one called agents" is not something a list of checkboxes can say. Selecting both writes the file once. - The `skills` vendor is removed, with its renderer, its adaptor page, its seed of
.claude/skills/redline-*/, andmetrics context— the command that existed to measure whether it paid for itself. It shipped disabled, so no onboarded repository rendered it and nothing has to be cleaned up anywhere. - Two vendors can now own one file, and the deselect path could not cope. Rendering with only
codexselected queuedAGENTS.mdfor stripping on behalf of the unselectedagentsvendor — deleting the file that had just been written. A path another selected vendor plans is no longer treated as deselected. - Several tests pinned the org-manifest ceiling using `cursor` as the disabled example, which stopped being true the moment it was enabled. They now build a manifest with a vendor switched off rather than borrowing whichever one happens to be off this month — the invariant is about the ceiling, not about Cursor.
Standards — the web is more than React
- Four new stack rule sets.
angular(RxJS teardown, change detection, thebypassSecurityTrust*sink, guards mistaken for authorisation),vue(reactivity lost on destructure,v-html, module-scope state shared across SSR requests),svelte({@html}, private env reaching the browser, universalloadrunning twice) anddomfor framework-free browser code (innerHTML,postMessageorigin checks, listeners and observers that outlive their widget). Each ships with a seed corpus; the recall corpus is now 16 stacks and 117 BLOCKER seeds. - `web` is now five profiles:
web-react,web-angular,web-vue,web-svelteandweb-vanilla. Naming the framework is the whole point — a profile is how Redline avoids installing two rule sets that contradict each other in one repo. - `web` is kept as an alias of `web-react`. A repository onboarded before this renders the same rules it rendered before, with no edit to
.redline.json. The one difference is the generated marker line, which names the resolved profile: it now readsprofile: web-reactwhere it readprofile: web. Expect that line, and the version, to be the whole diff of its next sync PR. - Detection proposes the framework it can see:
@angular/coreorangular.json,svelte/svelte.config.js/.svelte,vue/nuxt.config.ts/.vue, each ahead of the broader React rule so a Svelte repo that pulls React in transitively still lands onweb-svelte.web-vanillais never proposed, only chosen: plain browser JavaScript and a build script are indistinguishable from the outside, and guessing wrong installs DOM rules on a repo with no DOM.
CLI — two new commands, and four decisions that were being made for you
- `redline explain <rule-id>`. What a rule means, whether a checker or a reviewer decided it, which files it is scoped to and which profiles receive it. Findings have carried ids since 2.1 precisely so they could be looked up, and nothing looked them up.
--listprints the catalogue;--jsonfor tooling. A mistyped id suggests the near misses rather than reporting the rule as missing. - `redline status`. What is installed here, how hard it bites, what an administrator still owes you, and whether the standards have moved on — one screen, no credential, no host call. All of it was knowable; none of it was answerable without reading
.redline.jsonby hand. - `--integrations <list>`, and a menu question to match. Redline has always detected what a repository already runs — SonarQube, Snyk, Mend, Dependabot, Renovate, gitleaks, TruffleHog, CodeQL — and used it to avoid installing a second scanner beside the first. That survey was *printed at* the operator, after the decision it should have informed, with no way to correct it. It is now a question: what was detected arrives ticked, every other probe is offered, and the answer is recorded — so a scanner wired through a shared pipeline template, which no checkout can reveal, is stated once instead of every run.
- `--review-owners <list>`, and the bug it exists for. The team seeded into CODEOWNERS was the constant
platform-engineering, in every organisation. GitHub silently ignores an owner it cannot resolve, so every repository outside the one this was written for got a CODEOWNERS that reported as installed and enforced nothing. Owners are now stated, and taken by shape:@personis a user and is left alone,@org/teamand an email address are already complete, and only a bare word gets the organisation put in front of it. The menu asks only when review-ownership was actually selected. - `--branches <patterns>`. Which branches the merge policy governs. The ruleset hardcoded the default branch, so a
release/*line was ungoverned. The default is unchanged and deliberately not detected: widening what a branch ruleset covers is a change to an enforcement boundary, and it happens when somebody names the branches. - `--json` on `verify` and `status`. A tool whose claim is auditability was readable only by a human; anything wrapping it had to scrape prose for what it already knew.
CLI
- `redline init`'s menu no longer repeats its own question. The redraw rewound one line short of the frame it had drawn, so every keypress left the title line behind and a few arrow presses filled the screen with copies of "Which standards apply here?".
- The menu was redrawn. A block wordmark opens the run; the keys are printed under the question rather than past the end of a twelve-row list, where they were below the fold exactly when someone who had never seen the prompt needed them; a multi-select counts what it has (
3/12 selected) instead of asking you to count marks; and each option's explanation moved to a full-width line under the list, so the text that says what a profile *is* no longer gets truncated to fit beside the longest label. Red throughout, because that is the product's colour and the menu was wearing someone else's. - Long lists filter and scroll. From nine options up, the menu grows a search field: type to narrow it,
ctrl-ato take everything still showing — filter toweb, take all five web profiles, leave the rest untouched. Selection is indexed against the whole list, so a row ticked before a search is still ticked after it and still in the answer. Below nine options the letters stay shortcuts instead (j/kmove,atakes all), because a short list is faster to read than to filter. - The frame can no longer outgrow the terminal. Options past the fold become a window that follows the cursor and says what it is hiding (
↓ 9 more). This is the same class of bug as the repeated title: the redraw rewinds by the number of lines it drew, so a frame taller than the screen scrolls it first and the rewind then erases rows that have already moved. A short terminal drops the separators, then the key hints, before it will overflow. - The menu no longer ticks its own guess. Detection now grades its evidence: a manifest naming the framework (
go.mod,angular.json,@angular/corein dependencies) is the repository saying what it is, and arrives preselected; a bare file extension is a guess, and arrives offered but unticked. This is what made a Capacitor or Cordova web app open withmobile-iosalready chosen — it ships anios/App.xcodeproj, and pressing enter through the menu would have installed Swift rules on a codebase with no Swift in it. A ticked row is consent; a guess does not get to collect it by default. What is already recorded in.redline.jsonstill wins over both. - The red is the brand red.
#e60000, the value the site already calls--red, sent as a 24-bit colour where the terminal supports it, the nearest 256-colour neighbour where it does not, and the terminal's own red on sixteen colours — where the operator's theme outranks ours anyway. - All of it degrades: a non-UTF-8 console loses the wordmark and gets
up/downfor the arrows,NO_COLORloses the chip, and a narrow terminal drops the wordmark rather than wrapping it.
redline init — the run that lost its own pull request
- The plan and the install disagreed, and the pull request paid for it.
installGateruns twice — a planning pass, then the real one — and only the real one asked the host whether the reusable workflow it was about to reference existed. So the plan listed.github/workflows/redline.yml, the install refused to write it, and the file list came from the plan:git adddied on a pathspec matching no file, after every host mutation and.redline.jsonhad already succeeded. The work was done, on a branch, with no pull request and an error that named a git command instead of a cause. The planning pass of a live run now runs the same preflight the install does; a dry run still contacts nothing. - `stagePaths` was the one mutating git call not wrapped. Every other one converts git's output into a stated failure; this one leaked
Command failed: git add --and thirty paths. - A tool that already covers a gate job now stands that job down. This was a sentence in the report — "narrowing the gate to match is not wired yet, so for now deselect it yourself with
--skip gate" — which took the entire gate away to silence one job of it. The caller workflow carries astand-downlist, the reusable workflow guards thepolicy,dependency-reviewandsecretsjobs on it, and the aggregate reads a skipped job as a pass. Detected tools and stated ones narrow it identically: the wizard hands its own findings back through--integrations, so treating them differently would make one repository behave two ways depending on which entry point ran it. - The gate aggregate would have failed every repository that narrowed it. The first jq that treated
skippedas a pass read.value.resultfrom inside a pipe whose input was the pass list, which exits 5 — and underset -ethat is a gate that fails every pull request. Caught before it shipped; the expression binds the result first.
redline init — output you can read
- A spinner. Between the last menu answer and the first line of the report,
initrenders a dozen files, makes six host calls and pushes a branch, and said nothing at all while it did. Thirty seconds of silence reads as a hang, and the move after a hang is Ctrl-C in the middle of a run that is writing to your repository. The line now names the phase it is in. No TTY, no animation — a spinner in a build log is noise with no reader. - The report is sections, not a column. Files, repository settings, the pull request, and what is left — each with a heading and a count, a fixed status column, and the pull request URL somewhere it can be found rather than buried between two wrapped paragraphs about repository properties.
- The fix is no longer inside the problem. A capability outcome carries
detailandhintseparately; they were being joined into one string, so the one part of the report that asks the operator to do something was a clause halfway through a sentence in a narrow column. Denials now collect at the end as a numbered list, each with the command to run. - A capability this repository cannot have is not a capability it is missing.
unsupportedis grey, not red, and counted apart from a refusal — a fully onboarded repository was reporting itself as two-sevenths done and looking broken doing it.
[0.0.2](https://github.com/moelzanaty3/redline/compare/v0.0.1...v0.0.2) (2026-09-08)
CLI — redline init asks before it writes
- `redline init` with no flags at a terminal now walks a menu. Profile, host, pipeline, vendors, contexts, capabilities and rung, one question at a time, with the detected answer preselected and every term explained beside the choice that uses it. The last question is Dry run or Apply, defaulting to Dry run. CI, pipes and any run carrying a flag take exactly the path they took before: a prompt in a pipeline is a hang with nobody there to answer it. Zero new dependencies — the prompt kit is
node:readlineand ANSI, because a tool that runs vianpxinside other organisations' CI should not hand them a supply-chain edge for a menu. - The gate no longer writes a workflow that cannot resolve.
initused to render.github/workflows/redline.ymlreferencing<org>/.github/.github/workflows/redline-gate.yml@main, commit it, and reportapplied gate— on an organisation with no.githubrepository. Every pull request in that repository then failed with "Unable to find reusable workflow". The reference is now resolved before the caller is written; a failure suppresses the caller only, keeps the pull request template and labels, and reports the gate asdeniedso it reaches pendingAdmin with the org-level fix named. - `--pipeline github-actions|azure-pipelines`, asked in the menu. The host and the thing that runs the checks come apart: a repository can live on GitHub and be built entirely by Azure Pipelines. Deriving one from the other is what put an Actions workflow into a repository that runs no Actions. Choosing
azure-pipelineson a GitHub host writes.azuredevops/redline-gate.yml— with thepr:trigger GitHub-hosted repositories honour, the same pinned diff secret scan as the other two gates, and no Azure Repos status POST, because on GitHub the build result is the check. - `verify` and `init` no longer contradict each other about the security floor. GitHub includes a key in a visible
security_and_analysisblock only when the feature exists on the repository's plan. An omitted key now reads asunsupported, notdenied—initsaid "not available on this repository" whileverifysaid "FAIL disabled" about the same three settings, sending the operator to enable something no administrator of that repository can. - `check-name-reported` summarises. A mature repository reports twenty-five checks, and printing all of them put one unreadable line in the middle of the report with a real security-floor failure directly underneath it.
- An applied run says where its changes went. Onboarding commits to
redline/onboardand pushes, sogit statusstays clean; the run now says so. Without it the first real onboarding looked like it had done nothing, and the operator raninitagain and was told "already onboarded" by a repository they believed was not. - Cancelling is not a failure. Ctrl-C at a prompt exits 130 with no
errorline. - A repository can be more than one profile.
--profile web,infra— and the menu's standards question is a multi-select — because a React application with its own Terraform beside it was previously forced to pick the half that fitted worst..redline.jsonis unchanged:profilestays a single string, it just may now readinfra,web. Every reader of that field already hands it straight toresolveProfile, which resolves the union and records the list sorted, so the order it is typed in cannot change the rendered artifacts.
Standards 0.0.2 — optional context sections
speckit.md— the repository works spec-first. On by default, and dropped automatically where the repository already runs Spec Kit, which is a separate tool with its own installer and templates that Redline neither creates nor edits.tmf.md— the repository implements TM Forum interfaces. Off unless--tmfasks for it.
Published as redlinegate
Review pass — fourteen defects found before merge
- Every exemption was rejected. The pull request template explains each field in a comment that necessarily contains the words
reason:,until:andscope:, and the parser read the instructions instead of the author's answer. A correctly filled exemption parseduntilas the sentence describing it. Both parsers now strip HTML comments, and the regression test reads the real shipped template rather than a fixture — a fixture is exactly what would have kept passing. - `redline review` with no flags reviewed nothing uncommitted. It ran
git diff base...HEAD, which compares two commits and cannot see the working tree — so the daily command failed at the case it exists for. It now diffs against the merge base directly, which keeps the property three dots was chosen for and includes uncommitted work. - A deleted file became a phantom added line.
+++ /dev/nulldid not match the file header, fell through to the added-line branch, and was attributed to the *previous* file — so deleting a file could raise a finding on a file the change never touched. The/dev/nullguard that was supposed to prevent this was unreachable. - Ingested scanner counts were multiplied by pull request volume. Code-scanning alerts are a fact about a repository; stamping the snapshot on every merged pull request and summing them turned 12 alerts across 40 merges into 480. Counted once per repository now.
- A scoped exemption waived everything. The gate called
redline exemptwithout--scope, so the field was recorded, displayed, and never evaluated. - `block-high` did nothing `block-blocker` did not. The rung was never passed to
redline policy, so the strictest rung on the ladder was behaviourally identical to the one below it. - An unrecognised rung enforced in the gate and observed in the CLI. Two halves disagreeing meant a typo blocked every pull request in a repository the CLI reported as observing. Both now fail toward not enforcing.
- Smaller: a partly unreadable security floor verified as healthy; sync computed stale artifacts and discarded them, reporting a drifted repository as current; the deterministic checker flagged its own correct
parseIntcalls and fired on the word "var" inside comments; an exemption exactly at the 90-day maximum was refused as 91; a mistyped--providersilently becameopenai. - Process:
actionlintsilently skips shell linting whenshellcheckis absent, which it was in the environment this branch was developed in. Local runs reported clean on a strictly weaker check than CI's.
The documentation site catches up with the product
- The reference layer was complete — 61 per-item pages, every command, every rule — and the conceptual layer had stopped at Phase 1. Someone importing the framework read about a merge gate and profiles and never learned that exemptions, an enforcement ladder, scanner ingestion, cost measurement, a deterministic tier, local review or a distribution loop existed. Seven concept pages now cover them, each leading with the decision rather than the mechanism.
- A quickstart that onboards one repository in four steps and ends with nothing blocked, and an adoption path — day one to month two — whose answer to "when do we start enforcing" is later than most people expect and on evidence rather than a date.
- A what changed page, parsed from
CHANGELOG.mdat build time so it cannot drift. A release-notes page maintained by hand is accurate the day it ships and misleading a month later. - The homepage and the introduction stop describing Phase 1. The introduction now states the boundary out loud — Redline governs a change while it is still a diff, has no opinion on delivery or cloud spend, and its data ends at merge — and names the property that runs through the whole system: it refuses rather than approximates. A check that could not run reports
??, an unmeasurable figure is absent with its reason, a correlation below its sample threshold is withheld. - One homepage claim had become false and is gone: telemetry and scoring no longer "run outside the CLI".
redline metrics — the estate runners get a front door
- Running a measurement meant cloning the metrics repo, knowing the file path, and knowing that
SPEND_GRAINexisted at all. Env-var-only configuration is the genuinely old-fashioned part, not the file extension: there is no--help, so the only way to discover an option was to read the source.redline metrics <command>andredline registryown configuration now, with a flag surface declared as data — which is what makes it testable, and what guarantees help and validation cannot disagree, because they are generated from the same table. - Every refusal here used to be silent. A mistyped
--spend-grain per-seatbecameunknownand made the resulting number quietly less trustworthy than it looked; a non-numeric--daysbecameNaNand produced an empty window; a missing token surfaced as a 401 halfway through an org walk. All three are now refused by name before anything runs. - The metrics repo no longer needs a copy of `scripts/`. It checked itself out and ran
node scripts/build-dashboard.mjs, which meant keeping the runners duplicated there. Those workflows callnpx redline-cli@<version> metrics …and own nothing but their owndata/. The inbox workflow, which runs in the source repo, deliberately uses the local build instead: a broken command there should fail before the release, not after it. - A runner that throws is reported as a host failure with a hint, not as "redline failed unexpectedly" — that catch-all is for internal defects, and telling a reader the tool is broken when their token is wastes an afternoon.
- Credentials are never forwarded on a command line, even for the runner that parses its own argv. A command line is visible in the process table and lands in shell history.
- The four build internals stay scripts —
validate,assign-rule-ids,render-self,check-pins. They act on this repository's ownstandards/, and a command that exists but cannot work on your repository is a worse promise than one that does not exist.
Ignored-finding correlation — research, reported with its confidence
- Redline could say a rule was ignored and not that ignoring it mattered. Where a finding was left unresolved and the same repository later attracted a revert or a hotfix, that is evidence the rule earns its place — computed from merged-pull-request history alone, with no incident feed, which keeps it inside the roadmap's non-goals.
- The refusal is the feature. The roadmap calls this the most speculative item on the list and says to cut it without regret if the signal is too weak, so a rule below the sample threshold gets no rate at all — just a statement of how many ignored findings it has and how many are needed. Below the threshold the rate exists arithmetically and means nothing, and publishing it anyway is how a coincidence becomes a rule nobody can argue with.
- The output carries a verdict on the *experiment*, not only on each rule. "Not reportable" is the honest result of a weak sample rather than a failure, and no ignored findings at all is reported as a good result rather than as an empty one.
- Where a rate is reported the caveat travels with it: correlation, not causation, and a weak one. It is for prioritising which rules to examine, never for justifying a rule on its own.
- A remediation's own ignored findings are not attributed to anything. Counting them would let a single incident inflate every rule that happened to fire on the fix.
redline review — the daily command, bounded to what applies
- The last of v3's four commands, and the answer to every request for "catch it before I push". It reviews the working tree, staged changes or any diff against only the rules that apply to the files it touches. That bound is the product: anyone can ask an assistant to review a diff, and a model handed the composed standard for a twelve-stack profile spends most of its attention on languages the change never touches — the findings get worse, not better.
- Two engines, and `embedded` is the default because it calls no model at all. It emits the bounded prompt for the assistant already running the command, which is the common case in Claude Code, Copilot or Cursor. That is the design rather than a stub: calling a second model from inside the first one's session pays twice for a worse answer.
--engine apispeaks OpenAI-compatible and Anthropic, and the OpenAI-compatible half covers the fully local case for free — Ollama, LM Studio and vLLM all expose it, and a local endpoint needs no key. A review that must send a diff to a third party is one several markets cannot run. - The output contract is rendered by code, never free-typed by the model. The model returns JSON against a published schema; the CLI validates it and writes the
Redline/<SEVERITY> [rule-id]:line itself. A model that writes that prefix will eventually write a severity that does not exist or an id it invented, and every aggregate keyed on that line becomes fiction. A finding citing a rule the prompt did not carry is discarded and the reason is said out loud. - Local findings never reach rule-tuning telemetry, and the report says so on every run. A local run has no thread to resolve, no reviewer to attribute, and no way to tell a finding that was fixed from one the author never read — counting it would compute acted-on rate partly from runs nobody can verify.
scripts/validate.mjsfails the build if that exclusion is ever removed. - It always exits 0. A non-zero exit would invite someone to wire it into CI as a second gate, where it would enforce nothing while looking like it did — the pull request review remains the system of record.
- A changed file no stack covers is reported rather than dropped: that is a gap in the standard, and reviewing it against the core rules alone while saying nothing hides it.
Graduated enforcement — a ladder a repository climbs on evidence
- Enforcement was binary: advisory, or blocking with
--blocking. Neither end works across hundreds of repositories. Rolling blocking to all of them in one step is not achievable, and leaving everything advisory means the organisation can never state a guarantee about any of them. Four rungs now:observe,warn,block-blocker,block-high, recorded in.redline.json, carried in the register, and written into each repository's caller workflow so the gate needs no second source of truth. - Answers v3 §15 Q2: evidence-gated self-service. A repository promotes itself when the recorded evidence supports it — seed BLOCKER recall at 100%, zero false positives on the clean corpus, an acted-on rate above the rung's threshold, and a sample large enough that the rate is not a coincidence. It cannot promote on assertion, it cannot skip a rung (the rung it would skip is where the evidence for the next one is gathered), and a refusal names the specific blocker rather than saying no.
- Demotion never needs evidence. The safe direction never needs permission: a repository whose gate is misfiring at 3am must be able to step back without waiting for anyone, and a ladder that made that hard would be switched off entirely rather than stepped down.
- Answers open question 3: per repository, with a per-market floor. A market may raise its minimum rung; it may not push a repository below the rung it has already reached. A repository under its market's floor is reported as out of policy rather than as drift, because a different person has to act.
- A run that says nothing about enforcement never changes the rung, and an unrecognised or hand-edited rung reads back as
observe. Both failure directions point the same way: a typo must never be able to make a repository stricter than anyone chose. - The security floor is not on the ladder. Dependency review and the secret scan block at every rung including
observe. The ladder governs how strictly a repository's own standards are enforced, never whether the organisation's security minimum applies to it. - The dashboard reports how much of the estate is actually enforcing rather than watching — the question the ladder exists to answer, and one no per-repository view can show. An unreadable register leaves it absent rather than zeroed: zero blocking repositories and an unreadable register look nothing alike to whoever has to act.
Cost and DORA — what review cost against what it caught
- Redline could prove review works and could not say what it cost.
scripts/build-roi.mjsnow produces the page the roadmap's exit condition asks for: one page a finance stakeholder can read, sourced entirely from collected data. The headline is cost per BLOCKER caught — a figure nobody else in the toolchain can compute, because a cost-management tool knows spend and has no findings, and a DORA tool has neither. - Value is what was acted on, not what was reported. A finding nobody acted on caught nothing, and counting it would let the return be inflated by producing more noise — the exact behaviour the guardrails exist to prevent.
- Answers open question 2 by refusing to guess. Spend carries its grain, and an org-level figure will not answer a per-repository question: it says to publish org-level cost against org-level value instead. Org spend divided by repository count and presented as per-repository cost looks precise, is invented, and is the number a stakeholder would act on.
- Lead time and change failure rate come from merged-pull-request data the collector already pulls. Deployment frequency is reported as unknown, not zero, where a repository does not use the deployments API — assuming one deploy per merge reports a trunk-based team and a quarterly-release team as identical, which is the exact distinction the metric draws.
- MTTR is refused by name, so nobody wonders whether it was forgotten. It needs an incident feed Redline does not have and should not acquire, and a wrong MTTR is the number most likely to be quoted at someone who will act on it.
- The change-failure caveat travels with the number rather than living in a doc: it is a floor, not the true rate, because a revert or hotfix is evidence of a failed change rather than proof, and a team that fixes forward without saying "hotfix" scores better than one that labels honestly.
- The collector now records each pull request's title and first commit timestamp, which is what lead time and change failure rate are derived from. A pull request whose first commit the API did not return is left absent rather than defaulted to the merge time, which would report a lead time of zero and drag the median toward a number no team achieved.
Pre-release standards iteration — the deterministic policy tier
- A share of what the standard asserts needs no model. A ticket reference is present or it is not; a type-checker suppression carries one or it does not. Sending those to an LLM costs tokens and invites a false positive on a *fact*, which is the worst kind — an author cannot argue with a model about whether the word TODO appears on a line.
redline policyevaluates them directly, and the gate runs it on every pull request. - The classification lives in `standards/manifest.json`, not in the markdown. The roadmap rates this the highest-risk item because it changes the shape of the source of truth, and this is the change that removes most of that risk:
standards/*.mdis what a reviewer reads, and deleting a rule from it because a checker also covers it would narrow what the model considers. Every rule is classified by construction — listed means deterministic, absent means judgement — and not one rule id changed, because every historical telemetry record is keyed on them. - No rule changed meaning. Where a check can only decide part of a rule it decides that part and the model still sees the whole rule.
javascript/unsafe-numeric-coercionis checked for a missingparseIntradix; whether aNumber()coercion is applied to user input is judgement and stays with the model. - Only ADDED lines are examined, which is a rule rather than an optimisation. Flagging an existing
varin a file the author merely renamed is exactly what the standard's "what NOT to flag" section forbids, and an author who is right to ignore one finding learns to ignore the next one too. scripts/validate.mjsfails the build if a rule is classified deterministic and has no implementation. That failure — a rule everyone believes is machine-checked and which is in fact checked by nobody — is worse than leaving it to the model, because the model would at least have looked.- The floor is BLOCKER, not HIGH. A deterministic tier that failed merges over a missing ticket reference on day one would be switched off by week two, and then nothing it decides is enforced at all. A repository can raise it with
--fail-on. core/hardcoded-secretsis deliberately NOT in the tier. A regex over added lines is how a secret scanner earns a reputation for false positives; the gate already runs a real one against verified secrets, and the model keeps the rule for what a scanner misses.- Verified against the seeded corpus: the tier flags
seeded/javascript's SEED 8 at the right line, and seed recall is unaffected because nothing was removed from what the model is asked to consider.
Claude skills — per-stack rules, and an honest measurement of what they cost
- A fifth render target closes a real asymmetry. Copilot gets
applyToglobs and loads a stack's rules only when that stack's files are in play; Claude gotCLAUDE.md→@AGENTS.md— the whole composed standard, every turn, for the life of every session. Theskillsvendor renders one.claude/skills/redline-<stack>/SKILL.mdper stack plus a core skill, and a skill loads on its description, so the description names the stack and its file extensions. - `standards/` is unchanged by this work. It is packaging, not authoring: each skill body is the stack's own markdown byte for byte, and no Claude-shaped concept leaks backward into how a rule is written.
- The measurement, including where it loses.
scripts/measure-context.mjsreproduces it: multi-stack profiles save 19.7% to 48.3%, and single-stack profiles cost about 5% more — with one stack there is no second one to avoid loading, so the frontmatter is pure overhead. Both directions are pinned by tests. The roadmap makes this piece conditional on paying for itself, so quoting only the wins would have been marketing. - It ships disabled at the org level and is selected *instead of*
claude, never alongside it: the two render the same rules in different shapes, and a repository with both loads every stack twice. A render target that changes what every Claude session loads should not switch itself on across an estate in a patch release. PruneRulegrew adirectoriesflag, because a skill is a directory rather than a file. Without it every skill directory looked unplanned on every render and would have been deleted and rewritten each time.
SARIF ingestion — one severity contract across every producer
- Redline was one more finding producer competing with scanners it should have been consuming. Its own diff secret scan and dependency review are weaker than a real scanner stack and always will be. The collector now ingests code-scanning alerts from repositories that already run one, puts them through the same three-severity contract, and reports them beside Redline's own — so the estate has one picture instead of five dashboards.
- Ingested findings are always distinguishable from Redline's, everywhere. This is the one way this piece could make things worse than not doing it: rule tuning reads the finding stream, and a view that could not tell a CodeQL finding from a Redline one would tune Redline's rules on another tool's noise. So an ingested finding keeps the producer's own rule id — never rewritten into a Redline id, which would make every rule aggregate in the estate fiction — carries its tool, and aggregates in its own bucket. Acted-on rate is computed within each source and never across: Redline's is a resolved review thread, a scanner's is a closed alert, and averaging two definitions describes neither.
- Answers roadmap open question 4: ingested findings never gate a merge. They are measured only. Gating on another tool's output makes Redline responsible for that tool's false positives, and
scripts/validate.mjsfails the build if the gate ever starts reading code scanning. - Severity mapping is configurable per repository and visible in every record. The roadmap names this as Phase 1's risk and it is right — it is a judgement call that will be wrong somewhere. So each finding carries both the mapped severity and the producer's own word, a severity the map does not know is reported rather than absorbed, and an unrecognised one falls back to SUGGESTION and never BLOCKER: a wrong BLOCKER blocks a merge and teaches people the gate is noise, a wrong SUGGESTION is a line in a report.
- Redline never runs a scanner and no repository is asked to change which ones it runs. A repository with code scanning disabled, or a token that cannot see security data, is a fact about that repository rather than a failed collection run.
- The dashboard gains a Finding sources view showing both catalogues side by side. When it is empty, that is the answer to the roadmap's open question 1 — and the signal that SARIF ingestion was not where the next effort belonged.
Pre-release standards iteration — structured exemptions
redline-exemptwas a bare label. It downgraded the process checks to warnings and recorded nothing: not who accepted the failing check, not why, not until when. An exemption nobody has to justify and nobody revisits is not an exemption, it is an opt-out. The gate now reads a## Redline exemptionblock carrying a reason (at least 20 characters — "needed for release" tells a later reader nothing), a scope, and an expiry of at most 90 days. Longer than 90 days is a standards change, not an exemption.- This is a behaviour change for every onboarded repository, and it ships behind a grace. The gate's new
exemption-enforcementinput defaults towarn: a label without a valid block is accepted and told what is missing. A repository moves torequireone standards version later, so nobody's open pull request is failed by a rule that did not exist when they opened it. - Unchanged, and load-bearing: an exemption still touches the process checks only. It has never been able to waive dependency review or the diff secret scan, and it still cannot.
- Redline's own sync pull requests carry a real exemption block rather than being a special case in the gate — one rule for everyone is worth more than a convenience for the tool that wrote the rule. The generated exemption expires after 30 days, so a sync pull request nobody merges starts failing its own gate, which is exactly what should happen to a standards change a repository is quietly refusing.
- The pull request template gains the section, but only where Redline is already writing a block. A team whose own template already answers the gate keeps it untouched: no gate job fails for the section's absence, so it must never be the reason a marker block appears in a file somebody else wrote.
verifydoes not report its absence as drift either. - The collector records the parsed exemption per pull request, so standing exemptions trend and a team routing around the gate shows up as the same scope recurring — the guardrail the roadmap asks for, and one a per-pull-request view can never show. The block is parsed twice on purpose (the CLI enforces, the collector audits, and the collector has no build step to import from), and
scripts/validate.mjsfails the build if the two ever diverge. scripts/build-baseline.mjscomputes the Phase 0 baseline — the numbers every later roadmap phase is judged against. The roadmap says plainly that the ordering of Phases 1-4 is a hypothesis until this exists and that the baseline is allowed to reorder them, so the instrument is built here even though only the owner can run it with org credentials.- An unmeasurable figure is `null` with its reason, never `0`. This is the whole design rule. A zero that actually means "nobody measured this" reads as a finding, and makes every later comparison look like progress that did not happen: 0% coverage is a crisis, an unreadable register is a Tuesday. So a merge rate with no pull requests yet is absent rather than 0%, cost per BLOCKER names which half is missing rather than dividing by an assumption, and a repository whose workflows the token cannot list is skipped rather than counted as running no scanner.
scripts/has unit tests for the first time.npm testnow coversscripts/**/__tests__/*.test.mjsalongside the CLI suite, and the bundle self-check fails the build if the baseline instrument goes missing.redline verify --repo owner/nameverifies a repository over the API, with no checkout. This is whatworkflows/verify-onboarding.ymlwas missing, and it is no longer gated off: the weekly sweep walks the register and opens one tracking issue — updated in place, never one per run — naming every repository that drifted and quoting its failing checks.- A check that could not run reports `??`, never `ok`. Some assertions genuinely need a working tree, and a token can be structurally unable to read a setting without that being a refusal. Reporting either as a pass produces a false all-clear across the whole estate at once, which is worse than not checking. An
??also never fails a repository on its own: failing on the absence of evidence trains an operator to ignore the weekly issue. - Both verify paths parse the gate caller with the same function. Two independent answers to "what check does this file publish" would eventually disagree, and that disagreement is exactly the difference between a repository reported healthy and one reported broken.
--gateand--repotogether are refused rather than one being silently ignored:--gatepublishes *this* repository's merge status and cannot speak for another one.scripts/validate.mjsfails the build if either distribution or drift detection is gated off again or stops calling its command. Both failures are silent by nature — a repository that never received a change looks exactly like one that did.- Azure DevOps remote verification is outstanding, as its sync is. An Azure entry in the register is reported as unsupported rather than skipped silently.
redline sync— the third of v3's four commands, and the one Phase 0 exists for. A standards change now reaches every onboarded repository as a pull request instead of waiting for someone to re-runredline initthere by hand. Targets come fromregistry.json; each target's profile and vendors are read from its own.redline.jsonlive rather than from the register, so a repository that changed since the last nightly walk is rendered correctly rather than confidently wrong.- Three properties sync holds, each of which is a way this could have gone wrong. It seeds the render with the target's current files, so everything a team wrote above a
REDLINE:BEGINmarker survives — rendering into an empty directory would have produced a correct-lookingAGENTS.mdthat deleted every repository's own context section at once. It branches from the default branch, never from a stale sync branch, so an unmerged pull request from an older standards version cannot carry its changes forward. And it updates an open sync pull request rather than opening a second, because a scheduled job that opens a new pull request every night is one nobody reads. - A repository already carrying the current render gets nothing — no branch, no empty pull request. One unreachable repository is reported and the rest of the estate still syncs, but the run exits non-zero: a distribution that reports success while quietly missing repositories is exactly how coverage rots.
workflows/redline-sync.ymlis no longer gated off, andscripts/validate.mjsfails the build if it is gated off again or stops calling the command. The failure it guards is silent by nature — the repositories that did not receive a change look exactly like the ones that did.- Azure DevOps sync is outstanding. A registered Azure repository is reported as unsupported rather than skipped silently, and Phase 0's exit condition is not met until it exists. Sync also never merges and never pushes to a default branch: it makes the change available, and the dashboard's coverage figure is what makes an ignored one visible.
- Every reference item on the documentation site is its own page, listed in the sidebar under its category rather than reachable only through a wall of cards, and each answers the same four questions in the same order: how to onboard it, how to use it, what output to expect, and how to edit it. The edit loop is derived from the CI steps that actually guard each kind of file rather than restated per item, a standard's page shows the output contract filled in with one of its own rule ids and its real severity counts, and prev/next now walks items in reading order instead of skipping between categories. Templates and workflows gained an expected-output field they had no equivalent of — several templates are reference shapes that produce nothing at all, which is worth stating rather than leaving to inference.
- Three things the site had no page for: the CLI commands, documented only inside the onboarding walkthrough; the 13 seeded corpora that decide whether the reviewer still works; and the roadmap, specs and plans under
docs/. A seed page parses its own markers, so its defect table and rule links are the file's current contents rather than a copy that drifts.redline syncandredline revieware listed and marked not built — two of the four commands v3 fixes the surface at, and whether they exist is a question the docs should answer. - The install page asks the npm registry at build time instead of asserting a version.
npx redline-cli initcould not resolve for any reader: the package has never been published, and with nov*tag the release workflow's own first-release guard refuses to publish. Published, unpublished and could-not-check are worded apart deliberately — a network blip must not read as a missing release — and the lookup has a timeout, never fails a build, and honoursREDLINE_NPM_VERSIONandREDLINE_NPM_REGISTRYfor an air-gapped runner or a private mirror. This also separates the two version lines that were being conflated: the npm package version, whichpackage.jsonnever carries because semantic-release computes it at publish time, and the standards version instandards/manifest.json, which is what rendered artifacts name. - CI builds the documentation site. It reads
standards/manifest.json, the rule catalogue andscripts/lib/rules.mjsat build time, so a standards change can break it — and nothing in CI touchedweb/, so it would have broken silently. - Two permission tests asserted nothing when the suite runs as root. Both stage an unreadable path with
chmod 000and check the code degrades rather than throwing; root ignores permission bits, so the read succeeded, the degradation never happened, and the suite was red for a reason unrelated to the code under test. They now probe whetherchmodcan deny this process a read at all and skip with that reason when it cannot — verified as a non-root user, where both run and pass rather than skipping. - A register of onboarded repositories exists again, and it is derived rather than maintained.
registry.jsonis discovered nightly from the.redline.jsoneach onboarded repository already carries, so an entry exists exactly as long as that file does and a repository that removes Redline leaves the register on the next run. Nothing hand-edits it andredline initdoes not write it — the previous register,sync-targets.txt, was appended to byscripts/setup-repo.sh, lost its only writer when that script was deleted, and then went with it; the dashboard's coverage figure has been absent ever since. The dashboard reads the register in its place, andscripts/validate.mjsnow fails the build if either the runner or its workflow goes missing, so the same silent loss cannot repeat. - The register's schema is deliberately the minimum its consumers need today — host, org, repo, default branch, profile, standards and CLI versions, onboarding date. Because it is re-derived from scratch on every run, adding a field later costs one nightly walk and no migration, so fields are added when a consumer needs them rather than designed ahead.
- Two caveats this does not close. Azure DevOps discovery is outstanding: the walk is GraphQL and GitHub-only, Azure has no equivalent and needs a per-project repository walk, so Phase 0's exit condition — sync working on both hosts — is not met until that exists.
RegistryEntry.hostand its optionalprojectalready carry the Azure shape so the schema will not need changing. Andworkflows/dashboard.ymlruns in the metrics repo, not this one: untilredline synccan distribute it, restoring the live coverage figure needs that file copied across by hand. - The registry workflow commits to this repository's default branch. That is a deliberate exception for a derived artifact in Redline's own repository and not a precedent: Redline still never pushes to the default branch of a repository it governs. If this repo is ever onboarded to its own ruleset the push is refused and the job fails loudly rather than quietly ceasing to refresh.
- A deselection never deletes what an earlier run installed, so the output says what remains rather than describing a state the repository is not in.
redline init --skip gatenames the workflow still on disk and still firing, andredline verify'sgate-machineryfinding says it is still present and what it still publishes instead of "Redline installs none at <path>" — a sentence that was false beside an installed gate, and that invited the operator to delete it by hand. - A blocking merge policy Redline applied and then stopped maintaining is named rather than passed over.
--skip gate --skip merge-policywalks past the deadlock guard, which only sees a policy the current run would apply, and leaves a live blocking ruleset requiring the Redline check.redline initsays so on that run;redline verifyreads the policy back for exactly that case and reports two different states — while something in the repository still publishes the check nothing is blocked and the hazard is named, and once nothing does, every pull request is blocked forever and the finding fails. --skip gatereportslabelsas off with it. The gate install is what creates Redline's labels — GitHub pre-declares the gate's soft-fail labels there, Azure creates them on use — so with the gate deselected nothing ever creates one. The effective state is whatinitandverifyreport, and both say why;.redline.jsonkeeps the operator's own choice, so re-selecting the gate brings the labels back without a second flag.- Per-capability selection at onboarding.
redline init --skip <list>deselects a capability the repository already has its own answer for —gate,merge-policy,labels,review-ownership— and--with <list>selects one again. A deselected capability is not attempted, not written and not reported byredline verifyas missing:verifygains acapabilitiesfinding naming the whole selection, and the checks a deselection governs (merge-policy,gate-machinery,pull-request-template,check-name-reported) sayoff by choiceinstead of failing, so a reader can tell "off because we chose to" from "off because it broke". The selection is recorded in.redline.jsonundercapabilitiesand survives a re-run; flag precedence is the existing rule — a typed flag overrides the record, an omitted flag keeps it. A config written before the field reads back with everything selected, and only an explicitfalsedeselects, so neither a missing key nor a mistyped one is a way to fall below the standard.review-ownershipmaps onto themenu.sensitivePathReviewerskey that was already exactly that switch rather than growing a second one to disagree with it. - The security floor is not optional. Secret scanning, push protection and dependency alerts are the organisation-wide minimum, and unlike a gate pipeline or a branch policy there is no "we already have our own" to respect — they are additive host settings that displace nothing.
--skip security-flooris refused by name with an exit 2 saying why, rather than recorded or quietly ignored, so an operator can never come away believing they opted out of it. --skip gatewith a blocking merge policy is refused before any host call or write: with no gate machinery nothing in the repository publishes the check a blocking policy requires, and the result would block every pull request in the repository forever. The refusal names--skip merge-policyas the way a repository keeps both its own gate and its own policy.redline initnow says what else is already in the directory this host runs pipelines from, before it installs its own gate there, and offers--skip gate— and the refusal both adapters raise over a gate machinery file they cannot attribute to Redline now offers--skip gatealongside--adopt-caller. Detection informs the operator; it does not decide for them, and it is only offered while Redline's own gate is still absent..redline.jsonrecords a content identifier for each command file Redline owns whole, and a later run recognises its own earlier output by that identifier rather than by recomputing what it would write there now. This closes the residual left by the previous fix: the byte-exact match against the installed CLI's command text stops recognising a repository the momentcommands/<name>.md's body or description changes here, and that repository then takes the merge path and gets its prompt appended to itself. The byte-exact match remains as the fallback for repositories onboarded before the field existed. A file Redline merely merged its block into is never given an identifier — recording one for a human's file is how a later run would come to overwrite it — and gaining the field is not by itself work to do, so a settled repository is not given a pull request just to record a hash.- Repository-local rules that Redline renders and never overwrites. A repository can now state a rule that *overrides* an org rule by writing
.redline/local.md. Redline reads that file, renders it into every enabled vendor's artifact inside theREDLINE:BEGIN…REDLINE:ENDblock under a# Repository-local rulesheading that states the precedence in words the tool acts on — the repository's own rules win where they conflict with the org standard — and never writes, rewrites or prunes the file itself, on any path including a vendor deselect. Landing inside the block is the whole point: an edit a human makes to the block does not survive the next render, and this does. The section is absent entirely when the file is missing or empty — no heading, no placeholder — and deleting the file removes the section on the next render while leaving the rest of the block byte-identical. Nothing in the file can fail a run: aREDLINEmarker line in it is escaped to the characters it renders as, and content thatcli/render/markers.tscannot read a block back through (an unclosed code fence, which would swallow theENDmarker) is quoted rather than refused — a human's file is not Redline's to validate. Readability is asked ofmarkers.tsitself, not of a second parser..redline.jsonrecordslocalRules, whether the file was present at the last run, soredline verifycan tell "never had one" from "had one and it went away"; a config written before the field reads back asfalse.redline verifyreports a changed local file as work to do rather than as drift the repository is failing at —artifacts-currentstays green and names.redline/local.mdand the re-run that folds it in. The excuse is bounded three ways, because an unbounded one is a drift bypass in the oversight product itself: it is per-path (only the four artifacts the section is actually rendered into can be explained by it — a hand-edited.github/instructions/redline-*.instructions.mdcan never carry the section and is drift), it must account for the *whole* stale set, and a stale removal is never explained by a rules file at all. A hand edit to an artifact that already carries the current section is still drift. - Closed the command-file clobber.
cli/render/commands.tswrites.github/prompts/<name>.prompt.md,.claude/commands/<name>.md,.opencode/command/<name>.mdand.cursor/commands/<name>.md, where<name>is only the filename incommands/— nothing reserves that name in a consumer repository, so addingcommands/review-pr.mdhere would have silently overwritten a team's own.claude/commands/review-pr.mdin every onboarded repository. Today's two command files happen to beredline--prefixed, so this was latent rather than live. Redline now merges its block into whatever is already at the path, reusingwrapBlock/stripBlockrather than a second merge implementation, and creates the file only when nothing is there. A.claude/commands/<name>.mdis a single prompt body, so/<name>on a file a team already owned now runs both texts concatenated; that is the accepted cost of not destroying their prompt, and the markers are what keep Redline's half removable and re-renderable. The frontmatter header stays outside the block, because the tools that read these files parse it at byte zero; a file whose only content outside the block is that header is one Redline created, so itsdescription:still trackscommands/<name>.md, while a header or any prose a human wrote is never rewritten: the header carries a# Managed by Redlineline and that line, not the shape of the frontmatter, is what attributes it — a team's own command file can carry an identical lonedescription:key, so shape was an indeterminate read and claimed their bytes. A marker-less file that is byte-for-byte what the previous CLI rendered at that path is Redline's own earlier output and is replaced rather than appended to; without that, the first upgradedredline initin every already-onboarded repository would have appended a block carrying the same body and made/<name>run the prompt twice. Deselecting a command host now takes Redline's block back out — deleting a file that was only ever Redline's, and otherwise returning the repository's own bytes plus at most the single newlinewrapBlockinserted as the block's paragraph separator, which is indistinguishable on disk from one the human typed and is the same documented non-inversestripBlockalready carries for the shared artifacts (a file that already ended in a blank line round-trips exactly). A file carrying no Redline block anywhere is never touched. - The gate machinery files get the opposite treatment, deliberately: appending a marker block to YAML gives the file a second
name:/on:(ortrigger:/steps:) key and it stops running at all, so there the only honest answers are "replace Redline's own file" and "stop". Both adapters now refuse rather than clobber —.github/workflows/redline.ymland.azuredevops/redline-gate.yml, in the plan phase as well as the real run, so--dry-runcannot promise a write the run would refuse. The refusal also fires before the render:redline initplans the host file diffs ahead of writing a single vendor artifact or command file, so its "Nothing was written" is true rather than leaving a half-onboarded tree behind. Attribution is positive, not a substring match: a# Managed by Redlineline (now carried by both templates), the reusable-workflow reference every v3 caller has, or Azure's ownADR_DIFF_THRESHOLDvariable block. A workflow that merely mentions Redline — a repository's ownRedline lintjob at that path — is a repository's file and is refused. A Redline 2.1 caller carries none of those and cannot be told apart from such a file by guessing, so it too is refused, andredline init --adopt-calleris the human decision that hands the path to Redline; the refusal names the flag. - Final whole-branch review fixes. `redline init` now converges whenever a security capability's write answer and its read answer disagree.
pendingAdminrecords exactly one fact — a writeredline initattempted was refused — and the settled path used to revise that record fromreadSecurityState, which answers a different question ("is the setting on?"). The two chased each other: the read cleared the entry, the refused write re-recorded it, and every run re-applied all four host mutations and pushed another commit onto the onboarding pull request, exiting0throughout, whileredline verifytold the operator to re-run. Reachable on both hosts — a fine-grained GitHub token withadministration: readreadsGET /vulnerability-alertsas 204 whilePUT /automated-security-fixesanswers 403; an Azure PAT withvso.advsecbut no Project Administrator role readsadvSecEnabled: truewhile thePATCHis refused, oscillating all three capabilities at once. No read now revises the record, for any capability, so the security three carry the same standing costmerge-policyand the four write-only capabilities already carried, andredline init --repairis the one sanctioned way to clear a recorded refusal. The comment that claimeddependency-alertsis never read back — the false premise the settled path's whole safety argument rested on — is corrected; both adapters emit it fromreadSecurityState. The settled path is one host GET lighter as a result. redline verify'spending-adminfinding no longer contradicts its ownsecurity-floorfinding.unsupportedandunknownare both "not answered" but need different *advice*, not just different wording:unsupportedis definite (the feature is not licensed here, so no administrator action would ever clear it), whileunknownonly means the token could not see the setting — an administrator enabling it is exactly what clears it. On one repository state (pendingAdmin: ["secret-scanning"]plus a token that cannot seesecurity_and_analysis)security-floorsaid "not visible to this token" whilepending-adminsaid "no administrator action would clear it". They are now separate clauses with separate remedies.- Azure gained GitHub's rejected-payload guard. A 400 from
POST/PUT _apis/policy/configurations— Azure rejecting the Redline branch-policy payload — used to fold intooutcome()asmerge-policy: denied, printing "an administrator must still enable: merge-policy" and exiting0: a Redline defect misdiagnosed as a missing permission. It now throwsRedlineError('host', …)naming the method and path, exactly ascli/platforms/github/install.tsdoes for a 422 on the ruleset write. - Two untested behaviours pinned. The
notEnforcedReasonconsumer incli/commands/verify.tshad a tested producer and no test at all on the consumer side: a GitHub ruleset switched fromenforcement: "active"to"evaluate"keeps every field readable while none of its rules apply, and deleting the consumer left the suite green. The rendered command files (cli/render/commands.ts) — written into every onboarded repository — had their paths, frontmatter prefixes and cross-host body equality asserted but never their bytes, so a changed trailing shape broke nothing. Both are now killed by a test. renderCommandsis held to the org vendor ceilingrender()already enforced. Withstandards/manifest.jsonas it stands (cursor.enabled: false),redline init --vendors copilot,cursorcorrectly skipped.cursor/rules/and still wrote.cursor/commands/ redline-{init,verify}.md, delivering half of a vendor the org had switched off. The ceiling is applied at the call site rather than insiderenderCommands, becauseCOMMAND_HOSTScarries hosts the vendor manifest has no entry for at all (opencode) — which is not the same thing as a vendor the org disabled.- Per-repository vendor selection.
redline initused to render every org-enabled vendor (standards/manifest.json→copilot,agents,claude) into every repository, so a Copilot-only team was handedAGENTS.mdandCLAUDE.mdit never asked for. It now detects a default from what the repository already contains (.github/copilot-instructions.mdor.github/instructions/→ copilot;CLAUDE.mdor.claude/→ claude;AGENTS.md→ agents;.cursor/rules/→ cursor; none of them → the org default), a repository's recorded.redline.jsonselection overrides detection on a re-run, and a typed--vendors copilot,agentsoverrides both.render()(cli/render/standards.ts) enforces the org manifest as a ceiling on every call it makes, includingredline verify's: a repository may select a subset of what the org enables, never a superset, and a vendor the org later disables stops rendering regardless of what was recorded — a stale selection cannot resurrect it. Deselecting a vendor removes exactly what it wrote: aredline--owned file is deleted outright (reusing the existing prune machinery, now run for every vendor rather than only the currently-selected ones, so a vendor dropped entirely is cleaned up too, not just a stack dropped within one still selected); a sharedmerge: truefile (CLAUDE.md,AGENTS.md,.github/copilot-instructions.md) has only itsREDLINE:BEGIN…REDLINE:ENDblock cut out, reusingcli/render/markers.ts'sfindBlockrather than a second marker parser, and is deleted only when nothing but whitespace remains — a file carrying the team's own content keeps it byte-identical, mirroringwrapBlock's own contract on the append path: the bytes outside the block are never trimmed or reformatted, and the only newline ever removed is the block's own trailing one. The removal rides out through the existing rendered-file diffing, so it lands in a pull request like any other change and prints as a removal, not a write, under--dry-run. A vendor selection change that has nothing left on disk to remove no longer reads as "nothing to change" on a settled repository — the same failure mode a menu change already had to be guarded against.cli/render/__tests__/vendors.test.tsnow pins, at the source, the CLI constantsweb/components/journey.tsxhardcodes with source comments because its.tsspecifiers do not resolve through Next's bundler: exactly which vendors rendermerge: true, thatPROFILEstays a profile the manifest can resolve, and the literal values ofGATE_CHECK,ONBOARD_BRANCHandSYNC_LABEL(newly exported fromcli/commands/init.ts, replacing a duplicated literal) — so a fourth merged vendor, an unresolvable profile, or a changed literal fails CI instead of silently falsifying the page. - The value-case card's "With Redline" column — proof chips, column header, per-row tag — now carries a restrained green accent instead of reading identically to the "Today" column. New
--ok-ink/--ok-line/--ok-bgtokens carry it, defined in both:rootand[data-theme="light"]:--green(#45de83) is not reused directly, since it is tuned for the dark surfaces.tk-greenand.gate-check .st.oknever leave, and reads too pale once the light theme puts a chip on a light background. Chip text holds 9.2:1 on the dark surface and 5.6:1 on the light one. CapabilityOutcomegained a fourth status,unknown, closing the Azure half of a defect fixed for GitHub earlier: Azure's Advanced Security enablement read mapped a 401/403 todenied, so a token that could WRITE the setting but not READ it back made a re-run overwrite a correct.redline.jsonwith a falsependingAdminlist, open a pull request, and exit 0. It now maps tounknown— Azure DevOps does not document this endpoint telling "you cannot see this" apart from a genuine refusal — andisPendingtreats it exactly likeunsupported: never enteringpendingAdmin, never clearing an entry already recorded there. The same audit on GitHub found its ownunsupportedmappings (an invisiblesecurity_and_analysisblock, a 403 onvulnerability-alerts) were the same indeterminate case wearing the wrong label — genuinely unlicensed and merely unobserved used to share one status — so both now read asunknowntoo, andunsupportedis reserved for a definite "this does not exist here" (only Azure's Advanced Security-unlicensed 404 has one today).redline verify'ssecurity-floorfinding splits accordingly: anunknowncapability still fails a plain run and reports without failing under--gate, exactly asunsupportedused to; a genuinelyunsupportedone — no administrator remedy exists, so failing it forever was the bug — now passes a plain run outright. The pull-request-template candidate-folder order, previously three private copies in bothinstall.tsfiles andcli/platforms/pull-request-templates.ts, now has one source; only the order moved, not the per-adapter discovery or merge logic.- Stack detection scans breadth-first, so root-level manifests (
go.mod,pom.xml) are always seen even when one large subtree alone exceeds the 5000-file scan budget..xcodeprojdirectories are now emitted as path markers, so an Xcode project without Swift sources is detected asmobile-ios. - The HTTP transport no longer retries a
POSTon a 5xx response: the request may already have committed on the host, and a retry could open a duplicate pull request or policy. 429 responses retry for every method; 5xx retries are limited to idempotent methods (GET, PUT, PATCH, DELETE, HEAD). - Release flow hardened.
semantic-releaseis pinned exactly (25.0.9) indevDependencies, so the publish job runs the lockfile-resolved version instead of whatevernpx --yesfetches that day. The release job now fails loudly if nov*tag exists — the history must be seeded withv0.0.0once so the first computed release is 0.0.1 rather than semantic-release's default 1.0.0 — and prints the exact seed command. Same-repo pull requests runsemantic-release --dry-runwith no secrets in the job: because semantic-release exits early on PR context before verifying credentials, this proves the pinned toolchain resolves from the lockfile, no more (fork PRs are skipped entirely). The full publish path is rehearsed by a manualworkflow_dispatchthat publishes a throwaway build to the npmcanarydist-tag without movinglatest. The two version axes (CLI vs standards) are now documented in the README. - GitHub admin write endpoints (repo PATCH, ruleset create/update, vulnerability-alerts, automated-security-fixes) now report a 404 as
denied, so it reaches the pending-admin list — fine-grained tokens without the administration scope get 404, not 403, on a repository they can otherwise read. Read endpoints keep 404 = unsupported. - A 422 from the ruleset create/update now fails loudly as a host error naming the endpoint, instead of leaving the repository silently policy-less with exit 0.
redline initis now safe on a brownfield working tree. It refuses to run (exit 2, with a hint) when the git index already has staged changes, because the onboarding commit would sweep them into the Redline PR. A successful run — and a failed one — returns the operator to the branch they started on instead of leaving them onredline/onboard. "Nothing to commit" is reported as an already-onboarded no-op instead of exiting 1. A non-fast-forward push (a previous partial run left a staleredline/onboardon origin) now says exactly that, with the delete-or-merge fix, instead of misdiagnosing it as missing push access. A repository with nooriginremote gets the friendly "add one with: git remote add origin <url>" usage error instead of "redline failed unexpectedly".- A 401/403 while resolving the repository (both GitHub and Azure DevOps) now exits 3 (permission, with a token hint) instead of 4 (host), so CI can tell "token lacks scope" from "the host is down".
- The Azure merge gate now actually runs. Azure Repos ignores YAML
pr:triggers (a GitHub-only feature), so writing.azuredevops/redline-gate.ymlalone ran nothing and, with--blocking, the requiredredline/gatestatus would have blocked every pull request forever.redline initon Azure DevOps now creates two more host objects: aredline-gatebuild definition pointing at the gate YAML (an existing definition matching that name and YAML file is reused; one with the same name but a different YAML file is human-owned — reported and left untouched), and a "Redline: gate build" Build Validation branch policy on the default branch that queues the pipeline on every pull request. Build policies without theRedline:displayName marker are never written to. Without Build Administrator rights the registration degrades to a pendinggatecapability instead of failing the run — and theredline/gateStatus policy is then written advisory, never blocking, since no pipeline could publish the status it would require. The deadpr:block is removed from the gate template, andredline verifynow reads the Build Validation policy back as part of the gate check: required checks are derived only from a blocking Status policy, and the gate only counts as blocking when the Build Validation policy exists. Gate definitions are registered in their own\Redlinepipeline folder, and a definition is only ever reused when its folder, name, YAML file *and* repository all match: build-definition names are unique per folder but the lookup filter is project-wide, so without this a second repository in the same project would have adopted the first repository'sredline-gatedefinition and pointed its Build Validation policy at a pipeline building the wrong repository. A same-named definition bound to another repository is reported and left untouched. Two more ways the gate can fail to run are now handled without leaving a repository requiring a status nothing publishes: a rejected Build Validation policy write (the policy is written before the Status policy, and the Status policy then goes out advisory), and a token that cannot read build definitions on a re-run — an existingRedline: gate buildpolicy keeps the gate blocking instead of silently downgrading an enforcing repository to advisory. When a blocking Status policy has no Build Validation policy behind it,redline verifynow names that as the cause in the merge-policy finding rather than only reporting the gate as advisory. - The web home page gained a value-visualization block after the hero: the onboarding mechanism (repo →
init→ floor installed → every PR checked → drift caught) plus a severity split of the rule catalogue and a stat strip. All numbers are loaded fromstandards/at build time viaweb/lib— the hardcoded stats-band counts were rewired to the same loaders, so they can no longer drift. The seeded-findings tile is counted fromseeded/using the marker conventionscripts/score-seeds.mjsparses (it read 105; the corpus holds 106), and the100%tile now says on its face that it is the target the corpus is scored against, not a measurement. - The web home page now states the value proposition it was missing, in a section after the mechanism block: four problem/answer pairs (the rules never reach the AI writing the code; every reviewer draws the line somewhere else; a check gets switched off and forgotten; a standard nobody can revise is one nobody follows), each with what it costs today and what Redline changes. The argument is made from mechanism and real counts only: every figure on it (rule sets, project types, AI-tool formats, severity levels, rule and id totals, standards version) is read from
standards/at build time throughweb/lib/manifest.tsandweb/lib/rules.ts. No time-saved, defect-reduction or adoption figure appears, because Phase 1 ships no telemetry to support one. - Azure DevOps onboarding is now brownfield-safe and at parity with the GitHub gate. Branch policies are matched by ownership, not by type: every policy Redline writes carries a
Redline:displayNamemarker (theredline/gategenre/name pair serves the same purpose for the Status policy), and a policy of the same type without one is a human's — it is reported and left untouched, never PUT over. Matching also honours the scope'srefName, so aRedline:-marked policy an operator scoped to a release branch is no longer adopted by the default-branch write and silently rescoped. Where Azure models a control as one setting per branch (minimum reviewers, comment resolution) Redline adds nothing beside a human's policy; where Azure runs several side by side (Status, Build Validation) it installs its own and leaves the neighbours alone. Analreadyoutcome — a settled branch — no longer reads as failure and zeroes the policy the run reports. The onboarding pull request now actually gets itsredline-synclabel (Azure has no label field on the create call, so it is a second request); a refused label degrades into alabelsoutcome instead of costing the pull request. The gate template gained the soft-fail escape hatch GitHub already had: it reads the pull request's labels and, when one is inSOFT_FAIL_LABELS, publishesredline/gateas succeeded with a warning instead of blocking — one-way, and with the access token still off argv. The template'sredline-cli@latestis pinned to the installing CLI's version at write time, so an npm publish can no longer change gate behaviour across the org without a pull request anywhere; an unpublished0.0.0-developmentbuild keeps@latest, since that version does not exist on the registry. - The site's home page now leads with the product instead of a caveat. The fold carries the headline, one sentence,
No servers. No SaaS. No per-seat fee.on its own strip, the two commands a reader should remember (npm i -g redline-cli, thenredline init, each with its own copy button) and a live proof rail; the roadmap disclaimer it used to contain moved to a "what is not built yet" note beside the feature grid, where it now also names the missingredline sync. A new before → command → after section replays a real onboarding: every terminal line and file name in it is a literal string fromcli/core/log.ts,cli/bin/redline.ts,cli/render/vendors.ts,cli/render/commands.ts,cli/commands/init.ts,cli/commands/verify.tsandcli/platforms/github/install.ts, and the rendered artifact list is derived fromstandards/manifest.jsonat build time. The transcript is real text in the server-rendered HTML — the replay is an enhancement that never gates reading it, andprefers-reduced-motiongets it whole and static. The value section was tightened (column headers replace the badge repeated on every row, row 01 leads, fact strips fill their width), and its fourth claim was corrected: it said every repository using Redline gets a pull request when a standard changes, which is Phase 3 work — it now says a repository picks the change up the next time someone runsredline initthere. "Count which ones keep firing" lost its implied built-in report. redline initis now honest about a re-run. The menu is resolved as defaults ← what.redline.jsonalready recorded ← only the flags the operator actually typed, so a plainredline initon a repository onboarded with--blockingno longer demotes its live ruleset back to advisory while the config still claims blocking (the same reset hit--no-a11yand--speckit). The whole file diff — rendered standards, command files, the gate workflow and CODEOWNERS — is now computed *before* the first host mutation, so a settled repository re-run changes zero repository settings instead of rewriting four of them to discover it had nothing to do. Conversely, a repair (someone deleted CODEOWNERS or the gate workflow) and a CLI-version bump that repins the gate template now open a pull request instead of reporting "already onboarded — nothing to change" and leaving a modified tracked file behind:installGateandensureReviewOwnershipcompare content before writing and report only the files that actually changed, and a menu change alone is a real run.- "Settled" is now decided against the host, not only against the working tree. When a run has no file and no menu change of its own, the plan phase makes two reads — the merge policy and the security state — and no writes; a run that already has work to do makes neither, so a read-side outage cannot abort it. A ruleset an administrator loosened or deleted by hand no longer reads as "already onboarded — nothing to change" from
redline init*or* fromredline init --blocking(the recorded menu already said blocking, so nothing looked changed); it is re-applied. The only recovery that used to exist was deleting.redline.json, which destroysonboardedAt, silently reverts every unrecorded menu selection, and makes the next run look like a 2.1 migration.redline verifyalso names the capability and the command that actually clears it: "<capability> now granted on the host, but the record is of a refused write and no read clears it — runredline init --repairto retry the write". A plain re-run deliberately does not revise a recorded pending entry in either direction, so--repairis the true instruction and the one every clause gives. Where the re-run does short-circuit, the pending-admin line is marked "(as recorded at the last run)" rather than asserted as this run's finding. - A read only changes what
.redline.jsonrecords about a capability when it says something definite about that capability. Two consequences.redline initon a repository onboarded *without* repository-admin rights — the normal partial-permission path — now converges: the refused ruleset it never created reads back as absent, which is the recorded state rather than drift, so the re-run settles instead of rewriting four host settings, and from the second re-run a non-fast-forward push toredline/onboardno longer maderedline initexit 1 forever. And GitHub omitssecurity_and_analysisentirely for a requester without admin permission, which was being read as "both capabilities are off": a write-but-not-admin re-run overwrote a correct.redline.jsonwith a falsependingAdminlist and opened a pull request, exiting 0. An absent block is now reportedunsupported— "not visible to this token" — which neither files pending-admin work nor clears what is already recorded. A block that is present and not enabled is stilldenied. - Nothing reads back whether this token may *write* the merge policy, so a recorded
merge-policyrefusal is never cleared by a read.GET /rulesetsproves a ruleset exists;PUT /rulesets/{id}is the admin call that gets refused, and the two come apart for exactly the write-but-not-admin token above — on a repository whose ruleset an administrator had already created, the plan phase cleared the entry and the refused write re-recorded it on every run, so it never settled andredline initexited 1 from the second re-run onward. The entry now persists, likelabels,review-ownership,repo-property,gateanddependency-alerts, and the already-onboarded output marks the list "(as recorded at the last run)". A ruleset that is absent where the record says it was applied, or that does not match the requested menu, is still drift and is still re-applied — which is also what clears the entry once an administrator grants the rights. redline verifyno longer reports "security floor enabled" for a token that cannot see the setting. The finding has three states — enabled, disabled, and not confirmed, the last naming the capabilities nobody observed (GitHub omits the block for a non-admin token; Azure returns 404 where Advanced Security is unlicensed). Plainredline verifyfails on it: an operator asking whether the floor is on must not be told yes on no evidence.redline verify --gatereports it without failing, because that flag's only caller is the Azure gate template, whose step runs as the build service identity — not a repository administrator, so the Advanced Security enablement endpoint is routinely invisible to it — and a gate that always fails is a gate nobody keeps. A capability that is present and off still fails in both modes.redline verifyno longer fails a healthy repository for four different reasons. - A required check missing from a pull request the gate never ran on is reported as "no gate run observed yet", not as a broken contract: the host is asked about the newest pull request of any state, which is routinely one that predates the gate. The failing finding now needs a gate run to have published *something* on that head commit, and it only claims every pull request is blocked when the policy actually blocks. - Stale rendered artifacts are split by version. Where the installed CLI's standards version differs from the one.redline.jsonrecords, the finding reports "standards updated upstream (vX → vY) — re-run redline init to adopt" and does not fail; adopting a release isinit's job. A CLI pinned to an *older* standards version than the repository recorded is told so instead, rather than sent to re-render the repository backwards. Where the versions match, staleness is local drift and still fails. Publishing standards used to failverifyin every onboarded repository at once and, through the Azure gate, on every open pull request. - The merge-policy finding now compares the approvals count, code-owner review and thread resolution as well as the blocking flag, so an administrator loosening the review requirements is caught. Approvals are a floor, not an equality: a team requiring three is stricter, not drifted. A host that cannot attribute a setting to Redline names it (Azure applies no code-owner requirement at all, and backs off a reviewer or comment policy a human already owns) andverifyreports those without comparing them. - The pending-admin finding scopes "an administrator must still enable" to the capabilities a read can actually answer, and lists the rest as "recorded as pending; not verifiable with this token".redline verifynow observes the pull request template the host would actually serve, which nothing checked before: a deleted template, one whoseREDLINE:BEGIN/REDLINE:ENDpair a human half-edited (whichredline initrefuses to write to), and an Azure branch-specific template added after onboarding are all failing findings. A marker-less template that answers the gate on its own is reported and left alone, because that is exactly whatinitdoes with it.redline verifyobserves the gate machinery itself, as agate-machineryfinding read from the local checkout. Deleting.github/workflows/redline.yml(or.azuredevops/redline-gate.yml), or renaming the caller jobtemplates/redline.ymlwarns not to rename, leaves a blocking repository where no pull request can ever satisfy the required check — and nothing observed it, becauserender()covers only rendered standards artifacts. It now fails, which is also what makes the softened "no gate run observed yet" report safe: that path may only mean the gate has not run yet once the machinery that would run it is known to be in place.- Three inputs
gate-machineryandmerge-policystill reported as fully green are now caught. An Azure blocking Status policy with no Build Validation policy to queue the pipeline failsmerge-policyinstead of passing while its own detail said "every pull request will sit blocked". The GitHub caller-job scan now anchors on thejobs:block structurally instead of taking the last two-space key seen anywhere in the file, so an inline comment, a quoted or differently-indented job id, or a YAML anchor on the job-id line no longer falls through to a key from theon:block and names a job that does not exist; where the scan genuinely cannot attribute a job id it now reports the file as unparseable rather than fabricating one. And a caller workflow present and correctly named but with itspull_requesttrigger removed no longer reports healthy — nothing can ever publish the check, sogate-machineryfails rather than telling the operator to go open a pull request. Agate-machinerymismatch is now three distinct messages (renamed job, policy no longer requiring the gate, file unreadable) instead of one that told a correctly-named repository to "rename the job back". redline verify'smerge-policyfinding also catches a GitHub ruleset switched out ofactiveenforcement — the cheapest loosening on that host, and one every field this CLI reads back survives unchanged — and an approvals policy that no longer dismisses stale approvals on push. On Azure, a Redline-owned reviewer or comment policy that has been DELETED is drift and fails; one a human owned all along is still reported without being compared. The difference is whether a policy of that type is present but unmarked (a human's, whichinitdeliberately backed off from) or absent altogether (Redline's, removed).pending-adminno longer files capabilities nobody can act on as work for an administrator. A capability the host reported as unavailable — Advanced Security on an unlicensed Azure tenant — and one nothing reads back at all are each named in their own clause, and only a capability a read answered with "off" is listed as work to chase. Under--gate, which runs as a build service identity that can enable nothing, only that actionable list fails; plainredline verifystill fails on any recorded entry, because an operator asking whether onboarding finished must be told no.readSecurityStatecovers dependency alerts on both hosts — GitHub throughGET /repos/{o}/{r}/vulnerability-alerts(204 enabled, 404 disabled, 403 unobserved), Azure off the same Advanced Security flag its install writes — so the capability can be observed to be off, and adependency-alertsentry recorded in.redline.jsoncan be answered rather than persisting forever.redline init --dry-runprints the plan — the files it would write, the files it would remove, the repository settings it would change and the resolved menu — and exits 0 having written nothing, contacted no host and required no credential. The repository identity it plans against is derived from the git remote and the local clone, not from a live read, and the host client is built lazily so the token is never resolved on this path: the most useful moment for a preview is before anyone has gone and got an admin-scoped PAT. Every other command, and a non-dry-runinit, still resolves the credential up front inresolvePlatformand still exits 3 there with the same message. Deletions print aswould remove, not as writes.--no-a11yand--speckitnow say in the usage text what they really do: they are recorded in.redline.jsonfor later phases and change nothing in Phase 1.accessibilitydefaults from the resolved stacks rather than beingtrueeverywhere, so a Terraform or Go repository no longer records a commitment to rules that are never rendered there..redline.jsongainedlastRunAt, andonboardedAtnow records when the repository joined rather than being overwritten on every run. A config written beforelastRunAtexisted still parses; the field reads back asonboardedAt.- A pull request that cannot be opened after the host settings and
.redline.jsonare already written is now reported instead of thrown: the CLI names the cause and says the changes are on theredline/onboardbranch, to push it if it is not already on origin and open the pull request by hand, and exits 1 (failed) rather than 4. The hint does not claim the push succeeded, because the same wrap also catches a git step that failed before it. The onboarded state is still recorded, because the host mutations really happened. A dirty git index still refuses up front with exit 2. - Migrating a 2.1 repository now stages the removal of what 2.1 left behind that v3 does not write —
.github/workflows/redline-sync.ymlandscripts/redline-*.sh— into the same pull request, so a 2.1 sync workflow cannot keep running against a repository that has moved on. Two guards keep that from destroying a human's files: the 2.1 marker (.github/workflows/redline.yml) is a path v3 writes too, so a repository is only treated as a migration when that file is *not* v3's own caller — otherwise a v3 repository whose.redline.jsonwas deleted would have its scripts removed — andscripts/redline-*.shis a glob, so a script is only deleted when its content carries a Redline ownership marker. A hand-writtenscripts/redline-deploy.shis left alone. - Azure branch-policy scope matching lowercases
matchKindbefore comparing: Azure echoes it back with the casing the object was created with, so a human'srefs/heads/-prefix policy created in the portal comes back asPrefix, was not recognised as covering the default branch, and got a second Redline policy stacked beside it. - The home page's before/after demo now explains what the run does to a repository that already has files. The after-band names each written path by what happens to it:
CLAUDE.md,AGENTS.mdand.github/copilot-instructions.mdare merged (Redline owns only the span between itsREDLINE:BEGIN/ENDmarkers, the rest of the file is kept);.github/pull_request_template.mdis merged the same way, into the first template the host would resolve, with a template that already carries its own## Launch readinesssection left untouched; the generated artifacts and.github/workflows/redline.ymlare Redline's own and rewritten whole;.github/CODEOWNERSis seeded only when the repository has none..redline.jsonis described as the recordredline verifyreads back, and the installed slash commands are listed with the descriptions parsed out ofcommands/at build time — which now fails the build rather than rendering a heading over an empty list. The transcript'swriteprefix is re-derived fromcli/bin/redline.ts:130:writeis padded to the width ofremove, so three spaces separate it from the path, not two, and 13 of the 33 transcript lines were previously one space short of what the CLI prints. The file lists in the after-band are derived from the same arrays the transcript is built from rather than written asredline-*globs the renderer does not enforce. Also: the lead no longer claims every transcript line is CLI output (one is a#annotation, now named), the honesty caveat moved off--faintonto--mutedso it passes AA in both themes, and the hero proof rail readsmanifest.versioninstead of a hardcoded site constant that could drift from it. Re-derived against the current CLI after the pull-request-template and verify work landed: the transcript gained thegatecapability outcomeinstallGatenow returns second, andredline verify's findings gainedgate-machineryandpull-request-templateand a rewrittenmerge-policydetail — eight findings, two of which read the local checkout rather than the host. The template row says what the code does now (only the gated sections the template does not already answer, and a template that answers both left untouched), and the merged row names the refusal pathwrapBlockgrew for a malformed marker pair.web/app/page.tsxdeclaresdynamic = "force-static"so the page's build-time throws stay build-time. - A repository's own pull request template is no longer destroyed.
installGatewrote.github/pull_request_template.md/.azuredevops/pull_request_template.mdthrough a plain content compare, so the firstredline initon a repository that already had a template silently replaced it — the last host-writing path in either adapter with no coexistence guard. Leaving it untouched is not the fix either:redline-gatefails any pull request whose body has no## Launch readinesssection, so an untouched template would block the repository's own pull requests. Now: with no template present the packaged one is written whole, as before; with a template present the file is kept byte for byte and only the gated sections (## Launch readiness, which thechecklistjob enforces, and## Architecture decision, which theadrjob reads for adocs/adr/link) are appended insideREDLINE:BEGIN/ENDmarkers through the samewrapBlockthe instruction files use; with markers already present only that span is regenerated. A template that already carries its own## Launch readinesssection is left alone rather than given a second, duplicate one. Both adapters, identical semantics; the run reports which of the four it did, and--dry-runwrites nothing on every path. This supersedes the "one shared-name file that is replaced" line above: the template is now merged. - The template Redline merges into is the one the host would actually serve. Both hosts resolve the default pull request template from several folders and match the filename case-insensitively (GitHub:
.github/, the repository root,docs/; Azure DevOps:.azuredevops/,.vsts/,docs/, the root). Writing the canonical lower-case path regardless left a repository whose template lives at.github/PULL_REQUEST_TEMPLATE.mdordocs/pull_request_template.mdwith two templates and ambiguous precedence — and if the host served the human's, the Redline block was invisible and the gate blocked every one of that repository's pull requests.installGatenow searches the candidate list in the host's precedence order and merges into the first file that exists, creating the default path only when the host would resolve none. APULL_REQUEST_TEMPLATE/directory of alternate templates is deliberately not adopted and never written into: those are reachable only through a?template=link and are not the default body, so the default path is still written beside them — without it, a plain pull request there opens empty and fails the checklist job. - The packaged pull request templates now ship with the gated sections already inside
REDLINE:BEGIN/ENDmarkers, so the ownership rule is the same on every repository: Redline owns its marked block, everything outside it is the team's.# Summary,## Change typeand## Automated revieware outside the markers and are the team's to edit;## Launch readinessand## Architecture decisionare inside and are refreshed on every run. Previously a greenfield repository got a marker-less file on its first run and was then treated as a human template forever, which meant Redline could never update the gated sections anywhere — not even in a repository it created the file in — andredline verifydoes not observe the pull request template at all, so that drift was silent. Accepted cost: repositories onboarded before this change keep their marker-less template and take the brownfield branch, so they are never rewritten and never receive gated-section updates. No data loss, no propagation; adding the markers by hand opts a repository back in. wrapBlocknow locates an existing Redline block by the<!-- REDLINE:BEGINprefix instead of the whole marker line. The rest of that line is attribution prose, and matching it in full made every future correction a live migration hazard: blocks already on disk in every onboarded repository would stop matching, and the next run would append a second block beside the first instead of replacing it. With the prefix match in place, the marker text was corrected — it claimedgenerated by scripts/render.mjs, which was never true of the blocks the CLI's install path writes, and now readsgenerated by Redline.REDLINE:ENDis unchanged.- A file whose Redline markers are malformed is now refused instead of silently corrupted.
wrapBlocksplices by byte offset and never checked thatREDLINE:BEGINcame beforeREDLINE:END, or that there was one of each. Two reachable states, both reproduced: with END before BEGIN the two slices overlapped, so the span between the markers was emitted twice and the file grew by a whole block on every run (measured 2 → 3 → 4 marker pairs over three runs); with a BEGIN and no END, the first run appended a second block and the second run deleted every byte between the orphan marker and it. A half-deleted marker line is the ordinary accident — the marker's own text tells humans to edit *around* the block — andwrapBlockwritesCLAUDE.md,AGENTS.mdand.github/copilot-instructions.mdin every onboarded repository, so the blast radius was the whole estate rather than one file. Redline cannot tell which span it owns in such a file, so it does not guess: no silent repair, no second block, nothing deleted. The run fails (exit 1) with a message naming the file and what is wrong with it, and a hint telling the operator to restore a single pair or delete both marker lines. Each adapter'smarkedprecondition is the same check, so the two can no longer disagree. - A
REDLINE:BEGINthat is quoted rather than written is no longer a splice point. A marker now counts only when it opens a line and is not inside a fenced code block, which is exactly where a document that explains the marker convention puts one on a line of its own. Previously the first occurrence anywhere in the file won, so a template or instruction file documenting Redline lost every byte between the quotation and the real block. Residual, stated rather than hidden: a marker written at the start of a line outside any fence is indistinguishable from a real one by any textual rule — that file now has two BEGIN markers and is refused with a message, which is the safe end of the trade rather than silent loss. - Only the gated sections a template does not already provide are appended.
## Launch readinesscounts as present when the heading matches the way thechecklistjob's own awk matches it — by prefix, so## Launch readiness checklistis not given a competing second section — and## Architecture decisioncounts as present when the body already contains adocs/adr/link, which is what theadrjob greps for. Before this, a brownfield template carrying its own Launch readiness section was left completely untouched and so never received the ADR affordance, and a pull request larger thanadr-diff-thresholdfrom that repository failed the adr job with no line in the template to fill in. The outcome detail names which sections were appended. - Azure branch-specific templates are merged too. Azure DevOps serves
<root|.azuredevops|.vsts|docs>/pull_request_template/branches/<branch>.md*in preference to* the default template, so a repository with one was getting a pull request body Redline had never merged into, and the checklist job failed every pull request into that branch. All four folders are searched (Azure searches them all, not only the first), branch names nested up to ten levels are followed, and both.mdand.txtare recognised. This is deliberately not the same treatment as GitHub'sPULL_REQUEST_TEMPLATE/directory or Azure's "additional" templates inpull_request_template/: those are opt-in through a?template=link and are never the default body, so they are neither adopted nor rewritten. Branch templates are automatic, so leaving them alone would leave the repository no better than before Redline ran. - Candidate resolution corrected against the hosts' documentation. GitHub documents only
pull_request_template.md, so.txtand extension-less names are no longer adopted as the merge target — merging into a file GitHub may not serve, while creating no.mdat all, is the worse of the two failures. Azure documents.mdand.txtand states that filenames and folder locations are not case sensitive, so both are candidates there. The candidate *directory* is now matched case-insensitively as well, by listing rather thanexistsSync, which was case-insensitive on macOS and case-sensitive on Linux — a.GitHub/checkout was missed on CI and a second template written beside the served one. The same listing answersisDirectory(), so a plain file nameddocsno longer crashes the run withENOTDIRafter the gate workflow has already been written. Where two case variants of the template coexist in one directory the tie-break is now deterministic (canonical spelling, then preferred extension, then byte order) rather than filesystem order. - The append path is byte-for-byte on the existing file.
wrapBlockno longer appliestrimEnd()to the human's content; it adds only the blank-line separator needed to make the appended block a paragraph of its own, and an empty file gets the block with no leading blank lines. - The Azure adapter reports one
gateoutcome frominstallGateinstead of two. The pipeline definition and the pull request template are both gate machinery, and emitting the same capability twice was a trap for any caller that folds outcomes per capability —worstOutcomekeeps one and drops the other's detail. They are folded at the source now, where both details survive. - An unclosed code fence no longer hides the Redline block. CommonMark says an unterminated fence runs to the end of the document, so it is valid markdown rather than a mangled file — but it made every marker below it invisible to the fence-aware marker scan added above,
findBlockreported no block, andwrapBlockappended a second one. Measured through the real renderer on anAGENTS.md: 10042 → 20018 → 29994 bytes, one → two → three marker pairs, unbounded and silent, in the same three files written in every onboarded repository. Reading the hidden markers as absent grows the file; reading them as real splices inside what renders as a code block. Both are guesses about a human-owned file, so the run refuses (exit 1) and names the line the fence was opened on. Refusal is limited to the case that is actually undecidable: when the unterminated region holds no marker the two readings agree, nothing is being guessed, and the file is written normally. - A closing code fence carrying an info string no longer closes the fence, per CommonMark ("The closing code fence ... may not have an info string"). A ```
`md``line inside an open`` fence is content, so treating it as the close exposed markers that are really inside a code block, and was one of the ways a document could end in an unterminated fence. - A brownfield template no longer gains a second
## Launch readinesson the second run. The marked branch refreshed the block with every gated section unconditionally, so a template that kept its own checklist outside the block got Redline's inside it on the next run — and the gate's awk enforces both, meaning the repository could no longer pass its own gate, andredline initopened a second onboarding pull request for a change nobody asked for. The marked branch now applies the samesatisfied()test the append path uses, measured against the content *outside* the block; when the file satisfies both gate jobs on its own, the block is left exactly as it is. Verified stable across three consecutive runs. wrapBlocknow validates the file it is about to WRITE, not only the one it read. The previous guard checked whether an unterminated code fence hid any marker, and skipped the refusal when it hid none — on the reasoning that both readings of the fence then produce the same marker set. That is true of the marker set and false of the write: "no markers" is not a neutral observation, it is the instruction *append at end of file*, and end of file is inside the fence. So a five-line brownfield pull request template ending in an open ``— a common idiom, and valid CommonMark — had Redline's block written inside the code block on the first run, after which everyredline initand everyredline init --dry-run` refused in the plan pass: the repository was left less onboardable than before Redline ran, by Redline's own write. The result of every merge is now re-scanned and must come back as the one well-formed REDLINE block, at the offset it was placed at, or the run refuses before writing. The narrow case the old reasoning did hold for — an unterminated fence *below* an intact block, where the write is a replace and never enters the undecidable region — still writes, and is pinned by its own test.- The same output check closes a second failure with the same root: an unbalanced code fence authored into
standards/is emitted verbatim into the generated body, so the block Redline wrote could not be found again. Previously the first render wrote the file and every render afterwards refused — in every onboarded repository, with a hint naming a line inside Redline's own generated block that the repository owner could not act on. The first render now refuses instead, before writing, and the hint says that a fence inside the generated block means the generated content is unbalanced and its source is what needs fixing. - Three fence-parser corrections. A code fence opened on a list-item line (
-+ a rail) and closed at the item's content indent is now recognised as a pair, instead of reading the closing rail as a lone opener and refusing a document that renders correctly. A rail inside a multi-line HTML comment or an HTML block is no longer treated as a fence — CommonMark says it is literal content — which had let a pair of spurious fences hide a real marker block so that a second one was silently appended beside it and the first abandoned. HTML tracking suppresses fence detection only, never marker detection: not seeing a marker is the dangerous direction. - A marker line prefixed by leading whitespace is no longer invisible to the scan. A real Redline block a human tab-indented — a paste, an autoformatter, a manual edit — used to read as absent, so the next run silently appended a second, correctly-indented block beside it. Detection now tolerates leading spaces or tabs before
REDLINE:BEGIN/END; writing does not — a found indented block is replaced and re-emitted at column zero, same as any other replace. An indented look-alike that leaves the file ambiguous (a second BEGIN or END that only widened detection makes visible) is refused rather than guessed at, the same rule already applied to every other malformed marker pair. redline init --repaircloses the last onboarding path that never converged on its own.labels,review-ownership,repo-property,gateandmerge-policy's own null branch (a repository refused admin rights, so no ruleset was ever created) have no read side, so a plain re-run treats a recorded refusal for any of them as settled by design and never tries again — even after an administrator grants the rights.--repairskips that verdict, re-applies every capability and recomputespendingAdminfrom the fresh outcomes instead of the record. It is notrm .redline.json && redline init:onboardedAt, the recorded menu (unless an explicit menu flag overrides it, same precedence as every other run) andmigratedFromare all untouched, because it still reads the existing config rather than starting from nothing.--repair --dry-runcomposes as everywhere else — it prints the plan and makes zero host mutations.redline verify'smerge-policyfinding and thepending-adminfinding now nameredline init --repairas the remedy for the capabilities a read can never answer, instead of pointing at a plainredline initthat would not have retried them.- The home page's "What it did to files you already had" rows were tightened: each now leads with a plain sentence answering "does this touch my file", with the mechanism trimmed to one or two short sentences after it rather than a single dense paragraph. The "Redline's own — rewritten in full" row now says plainly that every instructions file it writes carries the
redline-prefix (cli/render/vendors.tsPREFIX), so a repository's own.github/instructions/*.instructions.mdfile is never written or pruned by it — the prune rule matches only that prefix and extension.
Development history — 2026-09-02 (never published)
- `redline` CLI, published as the
redline-clinpm package (redlinealone is taken on the public registry by an unrelated package — every invocation isnpx redline-cli <command>, or plainredline <command>once installed withnpm i -g redline-cli; never a barenpx redline@latest).redline initdetects the repository's stack and host, renders the right standards profile, installs the merge-readiness template and gate, turns on the security floor, and opens a pull request — never a direct push.redline verifyre-checks a repository against what it claims in.redline.json. - Both GitHub and Azure DevOps, detected from the git remote.
platforms/holds one adapter per host behind a shared interface; no file outside it calls a host API or CLI directly. - The merge gate is advisory by default.
redline initreports; it does not block, unless--blockingis passed. Phase 1 installs the policy in advisory mode only and posts no automated review findings — it onboards and verifies, nothing more. - `pendingAdmin` and partial onboarding. When the operator running
redline initlacks the rights to enable a capability (push protection, a branch policy, repository properties), onboarding still completes and opens its pull request; the capability is reported in apendingAdminlist for an administrator to finish, rather than failing the whole run. - Renderer ported to TypeScript (
cli/render/), proven byte-identical against the retiredscripts/render.mjsacross all 12 profiles × 4 vendors before the oracle was deleted.scripts/setup-repo.sh,scripts/sync.shandscripts/render.mjsare removed;scripts/render-self.mjsis the two-line shim this repo's own CI uses to re-render its artifacts.
Known limitations — read before onboarding on Azure DevOps
Development history — 2026-09-01 (never published)
- Vendor-neutral standards in
standards/(core + 12 stacks, 249 rules, each with a permanent<stack>/<slug>rule id), rendered to Copilot, AGENTS.md, Claude and Cursor byscripts/render.mjs. - Severity output contract: every finding is prefixed
Redline/<SEVERITY> [<rule-id>]: …— parseable, aggregatable per rule. - Profiles instead of glob negation; a repo installs exactly one.
- Readiness gate (
redline-gate / gate): checklist, ADR-for-big-diffs, dependency review, diff secret scan (SHA-pinned).redline-exemptsoft-fails process checks only. - Org + per-repo rulesets: one human approval, thread resolution, name-verified required check. CODEOWNERS protects the enforcement surface.
- One-command onboarding (
scripts/setup-repo.sh) with--verify; sync distributes everything as PRs, never pushes. - Telemetry measures acted-on findings per rule id; weekly Teams digest, org inbox and dashboard on Pages; weekly seed canary and onboarding re-verification.
- Validation corpus: 82 BLOCKER + 23 HIGH seeds across 12 stacks plus a clean precision corpus. Target: 100% BLOCKER recall, zero findings on clean code.