Skip to content

Blog

A Green Pipeline Is Not Proof

Alongside the audit sweep, 1.5 folds a batch of hard-won lessons into the architecture and process standards. They came out of real development on a TypeScript / Rust / Swift local-first monorepo — the kind of project these standards are meant for. Each one is a bug that a reasonable person would not have predicted, and that a green checkmark actively hid.

A green release that never reached production

Section titled “A green release that never reached production”

Twice, a release was cut, promoted, and reported green — while production never changed.

The first time, the deploy platform’s build token had been silently rolled. Every in-repo check passed because the failure lived entirely outside the repo: the git host had no idea the token was dead. The second time, a force-push protection on the release branch blocked the fast-forward promote. Same symptom: pipeline exits zero, nothing ships.

The lesson is uncomfortable because it undercuts the thing we most want to believe: that a passing pipeline means the work is done. It doesn’t. A pipeline proves the steps it ran exited zero. It cannot prove that a system it doesn’t control — the deploy platform, its credentials, its branch rules — did what you asked. So arch-08 and proc-02 now require the release to end by fetching the live deployment and asserting the new version is actually serving. Necessary is not sufficient; the only proof that production updated is production.

The local-first app used a shared-worker leader/follower model — one tab owns the database, the rest coordinate through it. After a deploy, a browser tab left open from the previous version still held the leader lock. Every freshly loaded tab became a follower, probed the incompatible old leader, timed out, and retried the same doomed probe forever. The app was wedged, and nothing in the running code knew how to recover.

Two fixes, now in arch-05. Version the coordination protocol, so a new follower recognizes an incompatible leader and triggers takeover instead of trusting it. And distinguish transient failure from persistent failure — retry is for hiccups; a persistent leader timeout means re-elect, not retry. A follower that retries a permanent condition forever isn’t resilient, it’s stuck.

This one is subtle. The repo commits generated cross-platform artifacts — wasm blobs, UniFFI bindings. To catch drift, CI regenerates them and diffs against what’s committed. Straightforward, except it kept passing while the artifacts were genuinely stale.

The culprit was the warm build cache. An incremental target/ directory happily produced the already-committed output from stale intermediate state; the drift only reproduced after a cargo clean. So the “regenerate and diff” gate was diffing against a lie. arch-07 now requires the drift gate to rebuild from a cold state — clean first, or run in a fresh checkout — because a warm target masks exactly the drift the gate exists to catch.

The one that fails in seconds vs. the one that waits

Section titled “The one that fails in seconds vs. the one that waits”

A smaller, sharper one for arch-08: two self-hosted runners sharing a machine produced collisions — duplicate listener stacks fighting over _diag paths, package-manager setup dirs racing on big fan-out PRs. The fix is isolation (each runner gets its own HOME, tool-cache, and work directory). But the lesson worth writing down was diagnostic: an offline runner leaves a job queued and waiting; a misconfigured one fails at “Set up job” in seconds. Same red check, opposite cause. Knowing which you’re looking at saves an hour of debugging the wrong thing.

None of these are exotic. They’re the failure modes you only meet once you ship something real across more than one platform — and once you’ve met them, they’re obvious. The point of a standards repo is to let the next team meet them as a paragraph in a document instead of as a production incident.

That’s the trade every entry in this batch makes: someone already paid for the lesson. The standard is just the receipt, filed where the next person will look.

Standards 1.5: The Audit Sweep

Every few releases we stop shipping features and turn the audit on ourselves. This is one of those releases. We ran a full-repo sweep — scripts, CI, docs, and the standards documents themselves — and it surfaced about thirty distinct issues. 1.5 lands the fixes.

The interesting part isn’t the count. It’s what the count was made of: not one big broken thing, but thirty small ones, each individually easy to wave off, that together added up to a repo quietly drifting from the standards it publishes.

A few themes ran through almost everything we found.

Portability assumptions that only held on Linux. The compliance linter (lint-standards.sh) crashed on --format json and --format sarif when there were zero findings — but only under bash 3.2, which is exactly what ships on macOS, and exactly the path the PR-review action runs. gh-task used grep -oP, a GNU-only flag that silently returns empty on a Mac. Both had lived in the tree for months because CI runs on Ubuntu and nobody hit them locally until they did.

Docs describing a repo that no longer existed. The agent configs still told assistants to “strictly follow” arch-03 — a standard we had deleted. Go and Elixir shipped as full language standards but were missing from every agent config’s detection map. The README said the standards-review workflow was “installed automatically by make setup” when in fact it required an explicit --workflow flag. None of these break a build. All of them mislead a reader — or an agent — at exactly the moment they’re trying to trust the document.

Gates that didn’t gate. The Definition-of-Done workflow ran linting, coverage, and security scans and then swallowed every failure with || true. It looked like a quality gate and enforced almost nothing. The lifecycle-sync automation queried the first 100 project items and quietly stopped working past that. A “green” pipeline that proves nothing is worse than no pipeline, because it manufactures false confidence.

A source that would run anything. gh-task read its state file with source .gh-task-state — executing whatever a tampered or mis-merged file put there, with your shell’s privileges. That one we treated as a security fix, not a cleanup.

Thirty issues is too many for one pull request and too many to merge one-at-a-time without losing the plot. We grouped them by kind — script bugs, doc consistency, CI hardening, chores, dependency triage, standards enrichment — and shipped each as its own reviewable PR that closed a handful of issues at once. Each batch stated what it fixed, how it was verified, and what it deliberately left for later.

That structure paid off when the batches started depending on each other. The new “dogfood” CI job — which runs our own linter against our own repo and asserts the JSON and SARIF output is valid — is a direct regression guard for the zero-findings crash. It literally cannot pass until the crash fix is present. So the batches had a merge order, and the order was part of the plan, not an afterthought.

The headline items:

  • The linter now dogfoods. CI runs lint-standards.sh against this repo on every push and fails if the tool crashes or emits malformed output. The bug that would have shipped a broken SARIF file to every consumer is now caught here first.
  • Secret scanning that teams won’t turn off. We ship a TruffleHog config with a documented allowlist, verification off by default, and merge-blocking on real findings. Noisy scanners get disabled; a scanner with good false-positive hygiene stays on.
  • Release automation. A release-drafter workflow keeps a categorized draft release current from merged PRs and auto-labels them from their commit type. The changelog stops being a manual chore.
  • Actions pinned to SHAs, Dependabot grouped. Every workflow now pins actions to an exact commit, and Dependabot batches the updates so they stop piling up as one-PR-per-bump.
  • Standards enrichment from real cross-platform work — the subject of the companion post.

There’s a satisfying symmetry to fixing your own linter’s crash by making your CI run your own linter. The repo is a little more like the thing it tells everyone else to build. That’s the whole point of doing this on a schedule.

Standards 1.2: Safe Setup

Last release was about governance and drift. This one is about a quieter promise we were breaking: installing the standards into your project should never destroy the work you’ve already done there.

The feedback that kicked off 1.2 came from a real adoption. Someone ran setup.sh against a repo that already had an AGENTS.md, a CLAUDE.md, and a CI workflow. When the dust settled, their AGENTS.md had been overwritten, the new CLAUDE.md was full of literal {{PROJECT_NAME}} tokens, configs for five agents they don’t use had been dropped into the root, and a standards-review.yml workflow had appeared without anyone asking for it. They cherry-picked out what they wanted and sent back a note: here’s what we rejected, and why.

Four problems. One underlying mistake — defaults that assumed more than they should. This release fixes all four and ties them together around a single idea: stage, don’t clobber.

setup.sh now routes every file it wants to write through the same gate that sync-standards has used for months: should_assemble(). If the target doesn’t exist, write it. If it exists and still matches the last assembled hash, overwrite it. If it exists and has been customized, write the new version to .standards-pending/<file> instead and leave the original alone.

The infrastructure for this wasn’t new — we’d built it for re-syncs. What was new was pointing setup.sh at it. The assembly loop is now a single function in scripts/lib/assembly.sh, shared by both scripts. First-run and re-sync are the same flow. That one change closes the “clobbered AGENTS.md” complaint completely.

--agents now defaults to detect. When you run setup.sh, it probes for CLAUDE.md, .cursorrules, .github/copilot-instructions.md, .gemini/GEMINI.md, AGENTS.md, .aiderrc — and only installs configs for the agents it finds. A greenfield project gets a short message listing what’s available, not six unused dotfiles.

The old “install everything, let the user delete what they don’t need” default was well-intentioned but wrong. The escape hatch is still there — --agents all if you really do want everything, or --agents claude-code,cursor for an explicit list — but the default respects what’s already in your tree.

The base CLAUDE.md template has three placeholders: {{PROJECT_NAME}}, {{PROJECT_OVERVIEW}}, {{KEY_COMMANDS}}. Until this release, the assembler wrote them through as literal {{...}} tokens. If you weren’t paying attention, your repo ended up with a CLAUDE.md that greeted the next agent with # {{PROJECT_NAME}} — Claude Code Guide.

Now: {{PROJECT_NAME}} resolves from package.json, Cargo.toml, pyproject.toml, or the directory name — whichever comes first. The two content placeholders become <!-- TODO(standards): --> markers that the merge skill fills in. The shipped file never contains {{.

The standards-review.yml workflow is useful, but it’s also opinionated — it can conflict with a project’s existing CI. 1.2 gates its install behind --workflow. When you skip it, setup prints the one-liner to install it later. No more workflow showing up unannounced.

Here’s where it gets interesting. When setup.sh stages anything to .standards-pending/, it also writes a MERGE_PLAN.md alongside it — a short briefing that lists the files to reconcile, the agents detected in the project, any unresolved <!-- TODO --> markers, and three ways to finish the job:

  • /merge-standards in Claude Code
  • the merge-standards command in Cursor
  • make merge-standards at the CLI

All three read the same MERGE_PLAN.md. The design goal is that setup.sh doesn’t finish the install — it hands a complete, agent-agnostic briefing to whichever LLM the user prefers, and that agent finishes the install against the user’s actual preferences. You pick the tool; we give it the context.

This release isn’t a rewrite. The pending-mode infrastructure already existed — sync-standards.sh had been using it for months. The 1.2 change lifts that loop into scripts/lib/assembly.sh and points setup.sh at it. Most of the new code is in four small libs (one per concern: assembly, detection, template vars, merge plan), the 17 functional tests that enforce each fix, and the docs that explain it.

Four rejections from one adoption. Four commits. Four test groups that will keep them from regressing. That’s the whole release.

Standards 1.1: Reliability and Agent Coverage

Standards 1.1 is a maintenance-and-maturity release. Most prior posts have been about adding something — a new linter, a new skill, a new framework. This release is about the layer underneath: the governance, infrastructure, and drift-prevention that make the project trustworthy as a long-running dependency. It also closes a gap that was overdue: cross-agent coordination conventions for Google Antigravity, so Missions and design-fidelity reviews work the same way whether the agent in front of you is Antigravity, Claude Code, Cursor, Aider, or Codex.

Three files most projects don’t notice until they’re missing:

  • .github/CODEOWNERS — default code ownership for review routing. Aligns with proc-03 code review expectations.
  • .github/dependabot.yml — automated dependency updates for npm and GitHub Actions. This is distinct from the 72-hour age gate: dependabot proposes upgrades, the age gate decides whether they’re old enough to install.
  • A P0/P1/P2 checklist in the PR template. The security framework defined the severity model; the PR template is where reviewers and authors actually use it. One short checklist beats a 200-line standards doc no one re-reads per PR.

docs/adr/0001-unified-standards-repository.md is the first Architecture Decision Record in the repo. It captures why the standards live in one repo per organization (rather than per-language or per-team) and what the alternatives were. ADRs aren’t documentation in the usual sense; they’re the trail of receipts for choices that later look obvious.

Two new READMEs join it: standards/README.md (a map of the directory layout and naming conventions) and bin/README.md (an overview of the gh-task CLI with links to its full guide). Neither adds anything functionally new; both compress the time-to-orient for someone landing in the repo cold.

When the block-based agent config assembly shipped, every agent config file except one was migrated to it. The exception was .aiderrcsync-standards.sh treats it as customized (its checksum diverged from the canonical template) and skipped it on every sync. The result: that file carried a 97-line inline P0/P1 security list that the rest of the project had stopped using six weeks earlier (#34).

1.1 resyncs .aiderrc to its canonical template and adds a new make doctor check — check_aiderrc_template_sync — that cmp -s compares the two files and surfaces drift before another six weeks pass. Fix is small; the generalizable lesson is bigger: when you build an assembly system, audit the files that aren’t in it.

The default Aider model also moves from claude-sonnet-4-20250514 (a dated Sonnet 4.0 preview) to the family alias claude-sonnet-4-6, which auto-tracks point releases without requiring further template churn.

Reliability fix: the standards-review action

Section titled “Reliability fix: the standards-review action”

The standards-review composite action failed to load in consumer repos with:

while scanning a simple key
could not find expected ':' (line 91, col 1)

Root cause: a Python heredoc inside a run: | block was indented at column 0. YAML’s literal block scalar terminates the moment a content line has less indentation than the block, so the parser ended the scalar at the first heredoc line and tried to interpret Python as YAML. It choked on the colon in if len(sys.argv) > 1 else "{}".

The fix moves the formatter to a sibling format-results.py invoked via ${{ github.action_path }}. The YAML stays trivial, and the Python is independently testable. Two-file change (#64).

The website itself caught up in this release:

  • A “How It Works” page covering the architecture and the standards-sync pipeline end-to-end — useful for anyone evaluating the project before adopting it.
  • A Security section in the sidebar, surfacing sec-01 rather than burying it under “Standards”.
  • The build pipeline now syncs security standards into the rendered docs automatically, so the website can’t drift from the canonical files.
  • The blog backfilled posts covering release history back to 0.1.

Google Antigravity groups work into Missions — long-running, agent-driven tasks scoped to a feature or fix. The problem when multiple agents touch the same project: Claude Code, Cursor, and Aider have no native concept of an active Mission, so they don’t know whether their work is in scope or scope creep.

1.1 ships a cross-agent advisory mechanism — .gemini/active_mission.log — and two helper scripts:

Terminal window
./scripts/mission-set.sh https://antigravity.google.com/missions/<id> # at start
./scripts/mission-clear.sh # on completion

Other agents check the log before starting work; if a Mission is active, they reference it in commits and avoid expanding scope. The full convention (feature bracketing, lifecycle, what “stale” looks like — i.e. a non-empty log more than ~7 days old) lives in proc-04 § 5, with the read protocol mirrored in GEMINI.md so every agent that consumes the standards picks it up automatically.

Alongside it, .gemini/settings.json ships a Postgres MCP entry — opt-in via POSTGRES_MCP_DATABASE_URL env var. When set, Gemini gets live schema introspection for migration and query work; when unset, the server fails to start gracefully and Gemini continues without it. make doctor warns when the entry is present but the env var is missing.

Antigravity, Claude Code (via Playwright MCP), and Cursor all have browser tools that can render and screenshot a UI. Until now there was no convention for what to compare it against — agents would render, agree it looked “fine”, and merge. 1.1 introduces assets/designs/ as a per-project reference directory, plus proc-04 § 7: UI Change Validation defining the protocol:

  1. Render via dev server or the project’s Devloop /rebuild endpoint.
  2. Capture the rendered output with the agent’s browser tool.
  3. Compare against assets/designs/<route-or-component>/<state>.png.

The deliberate non-default: pixel-diff is not the gate. Font rendering, anti-aliasing, and sub-pixel layout drift swamp real changes. The agent surfaces the diff (side-by-side, overlay, or per-region delta) and a written summary; a human approves whether the change matches design intent. Pixel-diff gating is opt-in for projects that want to enforce it, with thresholds documented in assets/designs/NOTES.md.

The assets/designs/ directory is opt-in per projectsetup.sh doesn’t auto-create it. Projects taking on UI work add it manually and copy in templates/assets-designs-README.md.example, which documents naming conventions, common state vocabulary (default, loading, error, mobile, dark, etc.), and the cross-agent invocation table.

For agents without native browser tools (Aider, Codex), the protocol relies on the project’s Devloop GET /snapshot HTTP endpoint to normalize capture across all agents.

72-Hour Age Gate: Our Response to the Axios Supply Chain Attack

At 00:21 UTC on March 31, 2026, a compromised npm maintainer account published axios@1.14.1 – a trojaned version of the most popular JavaScript HTTP client, a package downloaded over 100 million times per week. Thirty-nine minutes later, axios@0.30.4 followed. Both versions injected a hidden dependency, plain-crypto-js@4.2.1, whose sole purpose was to drop a cross-platform remote access trojan onto developer machines (Socket, The Hacker News).

Today we are adding a mandatory 72-hour (3-day) dependency age gate to every language standard in this framework.

The attacker compromised the npm credentials of the primary Axios maintainer, bypassing the project’s GitHub Actions CI/CD pipeline entirely (StepSecurity). According to Socket’s analysis, the malicious dependency was staged 18 hours in advance, three separate RAT payloads were pre-built for macOS, Windows, and Linux, and both release branches were hit within 39 minutes of each other (Socket).

The dropper used a dual-layer obfuscation scheme – reversed Base64 encoding plus XOR cipher – to evade static analysis (Snyk). On install, npm’s postinstall hook executed automatically, detected the developer’s OS, and downloaded a platform-specific RAT from a command-and-control server. The payloads beaconed to C2 every 60 seconds, accepting commands for arbitrary code execution, credential harvesting, SSH key exfiltration, and filesystem enumeration (Socket).

The malicious versions were live for roughly two to three hours before detection and removal. Socket’s scanner flagged the compromise within about 6 minutes, but the npm registry take-down took longer. Any npm install or CI pipeline that ran during that window – without a lockfile, or with loose version ranges – pulled the trojan automatically.

For the full breakdown, see the Malwarebytes write-up. Additional technical analysis is available from Huntress, Wiz, and SANS.

The axios attack was detected and reverted within hours. Most supply chain attacks follow this pattern: a malicious version is published, the community detects it, and the registry pulls it – usually within 1-3 days. A 72-hour waiting period before adopting any new dependency version means your team never installs a package during that critical window.

This is not a novel idea. Google’s SLSA framework and the OpenSSF Scorecard project both recommend evaluating package freshness as a risk signal. We are making it a hard rule.

What the age gate catches:

  • Compromised maintainer accounts (like axios) – malicious versions are typically reverted within hours to days
  • Typosquatting packages – most are flagged and removed quickly once published
  • Accidental secret leaks – maintainers who publish credentials in a release and yank it

What it does not catch:

  • Long-lived, subtle backdoors that evade detection for weeks (these require deeper supply chain controls like reproducible builds and code review of dependencies)

We updated three layers of documentation:

A new Dependency Age Gate subsection under Dependency Management:

All 3rd-party dependency versions must be at least 3 days old before adoption. Do not upgrade to or add a dependency version published less than 72 hours ago. CI should enforce age verification against the registry publish date.

A new P1-severity rule under Dependency & Supply Chain Security. P1 means this is merge-blocking – CI must fail if a dependency version is younger than 3 days. The rule includes:

  • A registry API reference table covering PyPI, npm, crates.io, RubyGems, Maven Central, pub.dev, Swift packages, and Zig
  • An exception process for emergency security patches (team lead approval + documented justification + 24-hour follow-up review)

Language Standards (lang-01 through lang-10)

Section titled “Language Standards (lang-01 through lang-10)”

The core language standards (lang-01 through lang-10) now include an Age Gate bullet in their Package Management sections, with the language-specific registry to check and a cross-reference to the full exception process in sec-01. Ruby on Rails (lang-11) inherits the rule from its Ruby base standard (lang-10). Remaining standards, including Go and Elixir, will adopt the same rule in their next revision.

The registry publish date is available via API for every major ecosystem:

Terminal window
# npm - check publish date of a specific version
curl -s https://registry.npmjs.org/axios | jq '.time["1.14.1"]'
# PyPI
curl -s https://pypi.org/pypi/requests/json | jq '.releases["2.31.0"][0].upload_time'
# crates.io
curl -s https://crates.io/api/v1/crates/serde/versions | jq '.versions[0].created_at'
# RubyGems
curl -s https://rubygems.org/api/v1/versions/rails.json | jq '.[0].created_at'

A CI check that compares lockfile versions against registry timestamps is straightforward to build. We recommend running it as a required status check on every PR that modifies a lockfile.

Sometimes you genuinely need a fresh dependency version – typically a critical CVE fix. The exception requires:

  1. Explicit approval from a team lead or security owner
  2. Documented justification in the PR description explaining why the bypass is necessary
  3. A follow-up review within 24 hours to confirm the version was not subsequently reverted or flagged

This keeps the gate meaningful without blocking legitimate emergency responses.

If you use Coding Standards in your projects, run make sync-standards to pull the updated rules. If you maintain your own standards, consider adopting the 72-hour age gate – the axios attack demonstrated that even the most trusted packages in the ecosystem can be weaponized overnight.

For the axios incident specifically: if you ran npm install between 00:21 and ~03:29 UTC on March 31, 2026 without a committed lockfile, check whether axios@1.14.1 or axios@0.30.4 was installed. If so, assume compromise and rotate all credentials on the affected machine.


See the full rule in sec-01: Security Standards and the updated core standards.