MINOR. Three new plugin skills and a new vendored file are additive per
LIFECYCLE.md, and the skill rename below — while a real break
for anyone with the old command in their fingers — cannot turn a consumer's CI
red, which is the test that makes a release MAJOR.
Fixed
-
make coverage-diffdid not source.colormath/ci.env
(Makefile.colormath). CI'sdiff-coveragegate sources it before running
pytest; the local mirror of that gate did not. So a consumer whose app needs
test configuration to import at all — a session secret, a provider key —
got a passing gate in CI and a collection error locally, from the same
commit.make preflightis only useful if it runs what CI runs.Found while bumping intendent to v3.0.0:
make testhad been fixed to
mirror CI, andcoverage-diffwas the one remaining target still running
pytest with the wrong environment. The fix uses the sameif [ -f … ]; then set -a; . …; set +a; fiform as the CI step, so the two are literally the
same idiom.This closes the half colormath owns, and only that half. Whether the app
also reads a local.envis the consumer's business: a.envholding
container-only paths still leaks into host runs, and neutralizing that stays
with the consumer. So a consumer workaround can shed its ci.env-sourcing
half at this release, but not necessarily all of it. -
The two stamped refs could drift, and had (
Makefile.colormath,
.github/workflows/ci.yml,LIFECYCLE.md).gates.yml'scolormath-ref
default is how CI fetches its gate scripts;Makefile.colormath's
COLORMATH_REFis howmake preflightfetches the same ones. The release
checklist stamped only the first, soCOLORMATH_REFsat atv2.0.0while
CI moved tov3.0.0— preflight fetching scripts from a tag three releases
behind the workflow.Harmless so far purely by luck:
audit-deps.sh,migrations-sync.shand
diff-coverage.share byte-identical betweenv2.0.0andv3.0.0. The
first script change would have made local and CI disagree with no signal.The
refs-lockstepjob added here was the right instinct and the wrong
check. It compared the two stamps to each other and never asked whether the
ref resolved — so when this release was stampedv3.1.0before the tag
existed, it passed while every consumer'smake preflightwould have 404'd
on every gate script. See the release-machinery entry below, which replaces
it and makes hand-stamping impossible in the first place. -
Releases are now atomic (
release/,.github/workflows/release.yml,
.github/workflows/ci.yml,LIFECYCLE.md). Releasing was a six-step
checklist, and the steps came apart. An audit of all sixteen published tags
found nine internally inconsistent:v2.1.0throughv2.4.0each ship a
gates.ymlthat fetches its gate scripts fromv2.0.0, andv3.0.0's
Makefile.colormathpoints three releases back. This release was itself
stamped intomainand written up here without ever being tagged, leaving
mainadvertising a ref that 404s.The root cause was stamping forward: a human wrote the next version into
the tree days before the tag existed, and the window between the two closed
only if they remembered. Stamps now move only in the release commit, which is
tagged with the version it stamps in a singlegit push --atomic— git
updates both refs or neither, so the window is gone rather than merely
shortened. In steady statemainis stamped at the last released tag and
every ref in it resolves.release/cut.shis the one gesture: it refuses to start unless the tree is
clean, synced withorigin/main, and green in CI; stamps; re-verifies;
commits; creates an annotated tag (the history alternates between
lightweight and annotated); pushes atomically; then publishes the GitHub
Release from this file's section for that version. A rejected push rolls the
local commit and tag back, and a failure after the push is resumable, because
publishing is idempotent.release/verify.shreplacesrefs-lockstepin CI and is a strict superset:
it coversplugin.json— a stamp site nothing checked, and which had been
missed twice — asks whether the stamped ref resolves, and requires
example/'s vendored copies to stay byte-identical to the root ones.
--audit-allproduced the drift table above and stays advisory, because
published tags are never rewritten. -
Documentation no longer names a version (
README.md,
.github/workflows/gates.yml,.github/workflows/review.yml). Nine
copy-paste pins had rotted — the README told consumers to pin@v2.0.0and
the two workflow usage-comments said@v1.1.0and@v1.0.0, while the repo
was onv3.1.0. They now read@vX.Y.Zand point at
/releases/latest, andverify.shfails the PR if a concrete version
reappears. Automating the stamping of prose would have worked; deleting the
data was cheaper and cannot regress. -
Every tag has a GitHub Release. All sixteen were bare; the notes existed
only here.release/backfill-releases.shcreated them retroactively from
this file, andcut.shcreates them going forward, so
/releases/latestis now a real answer to "what should I pin to?"
Changed
-
/colormath:review-ticketis now/colormath:refine-ticket
(plugin/skills/refine-ticket/). Same skill, same behavior, same contract
surfaces — only the command name moves, so that the two grooming skills read
as the pair they are:refine-ticketfor a ticket,refine-initiativefor
the initiative above it. "Review" also collided with the other review in
this plugin — the Thermonuclear Review thatshipwaits on — which is an
adversarial audit of a diff, not grooming.This breaks muscle memory and any docs that name the old command.
/colormath:review-ticketstops existing at this release; there is no alias.
MINOR rather than MAJOR under LIFECYCLE.md's test, which is
about a consumer's CI going red without them editing anything — a skill is
invoked by a person, and no gate, workflow input or Makefile target moves
here. Grep your consumer repos forcolormath:review-ticketwhen you take
this release; product copy that tells users to run it is the likely hit.
Added
-
AGENTS.colormath.md— a third vendored file, alongside
Makefile.colormathandeslint.config.colormath.mjsand refreshed by the
samemake colormath-update. It carries the half of a consumer's agent docs
that is identical in every colormath app: what being a colormath app means,
the sixteen gates with their localmakemirrors and how to pass each, the
preflight rhythm, the/colormath:shippipeline, the layering and
design-token and docstring conventions, the ADR practice, and the guardrails
(never hand-edit the vendored files, never push to the default branch, never
touch.env, justify every CVE ignore).The problem it solves is drift, and the drift was already there. Surveying
four consumers, the gate suite was documented four ways — sixteen gates, nine
gates, nine gates, seven gates — and one repo's doc named two disabled gates
that were not the two itsgates.ymlactually disables. Each copy was right
when written, and nothing marks the ones that stopped being right.Consumers keep an
AGENTS.mdof their own and import this one from it
(@AGENTS.colormath.md), so app-specific facts stay app-specific. Two things
deliberately do not move here: which gates a consumer disables — that is
enable-<gate>: falseplusCOLORMATH_PREFLIGHT_SKIP, and prose about them
is exactly what went stale — and where a consumer's design tokens live. The
shared file states the rule and points at the local file for the value.Additive: nothing reads it in CI and no gate, input or target changes. A
consumer that never vendors it is unaffected; one that does gets the file on
its nextcolormath-update, and wires up the import in that same PR.
example/carries the wiring as the reference. -
/colormath:refine-initiative(plugin/skills/refine-initiative/) — the
layer aboverefine-ticket. Takes an initiative, reads its feature
definitions and the tickets already under it, investigates the architecture
and decision records those features land in, interviews the filer in batched
concrete rounds, then rewrites the initiative's description and every feature
so a team could build from them.It is deliberately bounded at both ends. It stops short of code: no
file-by-file steps, no signatures, no DDL — that altitude belongs to
refine-ticket, per ticket, later. And it never starts building, because
that transition is one-way, locks the feature list and cuts a ticket per
feature; there is no MCP tool for it and the skill hands back instead of
asking for one.Contract surfaces it depends on (Abacus MCP):
get_ticketreturning
type,initiative_status,featuresandchildren;update_ticket;
add_feature,update_feature,move_feature;add_comment. A rename of
any of them must ship with a skill update in the same release. It also relies
on two current asymmetries, and names both rather than working around them:
there is no delete tool for feature definitions, andupdate_feature
replaces both fields.Second skill to require the Abacus MCP server, after
refine-ticket. -
/colormath:plan-initiative(plugin/skills/plan-initiative/) — runs
refine-ticketover every ticket in an initiative, one at a time, in build
order, injecting what that skill cannot see on its own: the initiative and its
settled decisions, the ticket's position in the sequence, what came before it
and what those plans decided, and what comes after it.The reason is the seams. Run by hand seven times,
refine-ticketgrooms seven
strangers — re-deriving the same background, asking the same question seven
times, and producing plans that each make locally sensible choices that
contradict each other where they meet. Answers carry forward, so the questions
thin out as the run goes.A ticket counts as planned only when both
planandqa_planare set,
re-read from the tracker rather than assumed from the sub-skill returning.
Tasks are skipped by design — that type has no plans and the tracker
refuses to write them. It holds noupdate_tickettool, because writing plans
isrefine-ticket's job, and noEdit/Write, so it cannot touch the repo.Contract surfaces (Abacus MCP):
get_ticketreturningtype,
initiative_status,children,planandqa_plan;add_comment. Plus the
refine-ticketskill itself — the two ship together and a change to either's
contract is a change to both. -
/colormath:implement-ticket(plugin/skills/implement-ticket/) — takes
a groomed ticket from its plan to a shipped PR, closing the chain the other
skills start:refine-initiativedesigns,plan-initiativeplans every
ticket,refine-ticketplans one, this one builds it.It executes the plan rather than rewriting it, and the step that earns its
keep is the one before any code: the plan was written against the codebase
as it was, so every step is walked against the repo first. Where it no
longer holds, that is a finding for the user — silently improving a plan is
how a reviewed decision gets replaced by an unreviewed one, and sometimes the
right outcome is "this plan no longer holds" rather than a PR.Then: build on a branch at the layer the plan names, execute the ticket's QA
plan against the running stack (every item observed,⚠️when a UI item has
no browser, failures fixed and re-run rather than shipped with the document
claiming they passed),make preflight, and hand off to/colormath:ship.
Deviations land in the PR body and a ticket comment; theplanandqa_plan
fields are left alone as the record of intent. It does not move tickets
between lanes, because lane meaning is per board.A ticket with no plan is sent back to
refine-ticketrather than planned and
implemented in one breath, which would mean nobody ever reviewed the plan. A
task is refused: that type carries no plans by design and is not code work.Contract surfaces (Abacus MCP):
get_ticketreturningtype,plan,
qa_planand the parent initiative;add_comment. Plus/colormath:shipand
/colormath:qa's recon discipline.