Releases: Wizarck/ai-playbook
Release list
v0.24.0
v0.23.0 removed eleven skills and shipped a migration step: every consumer had to
run the materialiser once, on its current pin, before bumping. That step was
the defect. This playbook is pull-model — consumers bump on their own schedule
and nothing reaches into their repos — so a correctness requirement that must be
executed inside each consumer, in advance, cannot be relied on, and it presumes
the playbook knows who its consumers are. It does not.
Added
-
specs/skills-owned-history.yaml— every slug the playbook has ever
shipped, 87 of them. It exists so the materialiser can tell a skill the
playbook REMOVED from a skill the consumer wrote, which is the one thing a
fresh mirror cannot infer. Seeded frompresent ∩ desiredalone, a removed
slug is excluded from the owned set by construction and can never be
classified as stale; with the list, the seed becomes
present ∩ (desired ∪ ever_owned)and it is cleared on the consumer's first
ordinarybootstrap.py --update. A skill the consumer authored is in neither
set and is preserved.Append-only: removing a slug re-creates the orphan it exists to clear, and a
test fails if a shipped skill is missing from it. A playbook copy that predates
the file degrades to the old behaviour — deletes nothing — rather than failing.
Removed
-
scripts/bump_consumers.py,scripts/_bumper.py,tests/test_bumper.py
— 1 112 lines.bump_consumers.pywalked every project in the developer's
local registry, moved each one's submodule pin, and with--push --open-pr
pushed and opened pull requests across them. That is the push pipeline retired
in v0.19.0, still shipping as a supported script and contradicting
release.mdin the same repository: "No automation reaches into consumer
repos." Nothing else imported the two helpers — the "CI-side" user named in
their docstrings had already gone.The pull-model path is untouched: a consumer bumps itself with
scripts/rules/update-playbook.rule.py apply --execute.The registry itself stays and is unaffected.
~/.ai-playbook/projects.yamlis
per-dev, gitignored, and built by scanning the developer's own disk; the
playbook holds no list of who consumes it and now ships nothing that walks one.
Fixed
-
The
.gitignoretemplate ignored none of the three skills mirrors while
skills-distribution.md§3.2 stated that every consumer ignores all three. The
contract was documented and never implemented, so what a consumer ignored was
whatever it had hand-written. Repos onboarded under RFC-0001 carry a block that
ignores.claude/skills/and.gemini/skills/but keeps/skills/— correct
then, whenskills/was the source and the other two its copies; wrong since
v0.17.0 collapsed all three into mirrors. Two consumers were found committing
roughly 1 100 files of playbook content, taking a diff of files nobody there
wrote on every bump, and silently losing any hand-edit to the next materialise.The template now carries all three. Existing repos untrack theirs once with
git rm -r --cached skills/.
Migration
None. That is the point of the release — a consumer bumps normally and the
eleven skills v0.23.0 removed are cleared on that run. Step 0 of
upgrade-playbook-pin.md is deleted; consumers that already ran it lose nothing
by having done so.
A consumer that already crossed v0.23.0 with an older playbook copy still has the
eleven in its mirrors: bumping to v0.24.0 and re-running bootstrap.py --update
clears them.
v0.23.0
MINOR, not PATCH: eleven skills are removed from the surface every consumer
materialises, and consumers with no skills manifest need a one-time step
before they bump across this tag. See "Migration" at the end of this entry.
Removed
-
Eleven vendored BMAD skills, 1 056 671 B. An earlier audit proposed cutting
35 of the 71 on the strength of their being 50.20 % of the repository. That
number stopped existing when the testing knowledge base was deduplicated, and
the same 35 skills re-measured at 18.9 %. Re-justified one by one, ten of the
35 turned out to carry modifications made here — the list had been built from
reference counts, never from a diff against upstream — and one of them had been
improved the same week. What survived that scrutiny is a smaller cut on a
different criterion: not weight, but contradiction.The four BMad Builder skills (
bmad-agent-builder,bmad-workflow-builder,
bmad-module-builder,bmad-bmb-setup) build new BMad modules, which this
fork does not do; they held the skills tree's onlyuv rundependency and its
only installer. The story and sprint loop (bmad-create-story,
bmad-dev-story,bmad-sprint-planning,bmad-sprint-status,
bmad-quick-dev) was replaced by OpenSpec changes — a substitution the runbook
documents rather than an inference from disuse. And two had named successors:
bmad-generate-project-context(nowAGENTS.md+ the submodule) and
bmad-agent-tech-writer(its only output is reachable from
bmad-agent-analyst).The testing family stays, at 7.2 %. It holds most of this repository's
modifications to vendored skills, it is where upstream moves fastest, and one
of its skills had been improved the same week the list proposed deleting it.
The rest of the original candidates — the CIS coaches, the editorial
utilities,bmad-help,bmad-correct-course,bmad-create-epics-and-stories,
bmad-distillator,bmad-teach-me-testing— are 364 929 B together and are
left for a decision about whether the flow routes to them, which is a question
about use rather than about bytes.
Changed
-
One copy of the testing knowledge base instead of nine. The same 49
fragments and index sat byte-identical in nine skill directories and were
copied again into three mirrors per consumer, so a single fragment reached a
consumer twenty-seven times. It now lives once, inbmad-tea-knowledge, read
through its index. 6 153 384 B recovered with no skill deleted and no content
lost. -
Amelia executes a change, not a story.
bmad-agent-devrouted four of its
seven menu items at deleted skills, and told the agent to read a story file,
tick its tasks and update its File List. No skill produced that file any more.
The persona now readsopenspec/changes/<id>/proposal.mdandtasks.md; the
discipline is unchanged, only the artefact moved. -
A second UX spine, for how the product behaves.
docs/ux/DESIGN.mdis
about appearance, and everything about behaviour — state patterns, interaction
primitives, voice and tone, the accessibility floor — was being settled in the
head comment of whichever mock needed it first.EXPERIENCE.mdgives those an
address, and the track now states which artefact wins when two disagree.
Fixed
-
The test-review score depended on which dimension found the defect. Each of
the four review workers carried its own severity weights and computed its own
0–100 score, so the same violation cost a different amount depending on who
raised it, and the aggregator averaged the four. There is now one deduction
ledger — CRITICAL 10, HIGH 5, MEDIUM 2, LOW 1, subtracted from 100 — with
cross-dimension deduplication onfile:line:row, and the workers report
violations instead of scores. The criteria registry that fixes severity per row
was imported from test-architecture-enterprise 1.24.0.The aggregator also computed an A/B/C/D/F grade while the checklist documented
A+/A/B/C/F. They now agree. -
bmad-testarch-citold the agent to do three things the rest of the family
forbids — run tests serially, use a fresh database per test, and wait on hard
timeouts — each contradicting the isolation and determinism rules the same
family states elsewhere. -
The distribution spec described an algorithm the code no longer runs.
skills-distribution.md§4 documented whole-mirror wipe-and-recopy and promised
that skills removed upstream "disappear from the mirror in one shot — no orphan
accumulation". The materialiser has been provenance-gated since the additive
contract landed: it deletes only what its per-mirror manifest records it as
having installed. §4 now describes what runs, and §4.1 states the consequence
that makes the migration below necessary. -
The bootstrapped propagation workflow was not valid YAML. Three multi-line
strings sat at column 0 inside arun: |block, closing the block scalar. Every
consumer created from the template got a dead workflow and a red pre-commit. The
heredocs are nowprintfredirections, and the OpenSpec CLI is pinned and
version-checked instead of installed from two package names that do not exist. -
doctorwarned about environment variables it was reading from the wrong
file, then counted table rows whose "required" cell said things like
"yes (if X)" as unconditionally required, and treated another project's prefix
as this one's. -
The onboarding runbook documented two flags that do not exist in the
script's argument parser or its schema, and an expected output nobody had run. -
Six guardrails enforced decisions that never reached telemetry when invoked
directly, so Dashboard coverage was incomplete and the enforcement docs
promised more observability than existed. The remaining rule CLIs and both
sweep CLIs now route through the existingcli_emit/script_emitwrappers,
exit codes unchanged, andenforcement-status.mdstates where the
rule-event/v2stream ends — GitHub Actions gates stay outside it, because a
workflow check governs an asynchronous PR transition rather than an agent tool
call, and mixing the two would combine incompatible denominators.The regression guard that keeps this from silently regressing is bound to the
entry point it protects: acli_emitcall inside the__main__guard only
counts when it receivesmainitself, and a directmain()call in the same
guard is rejected — otherwise a decoy call satisfied the check while the real
path ran unobserved. Contributed by @WilliamPersico in #177.
Legal
-
NOTICEadded at the repository root. This is a public repository that
redistributes about nine megabytes of adapted third-party work, most of it the
skills/bmad-*tree, and carried no attribution outside the changelog. MIT
requires the copyright and permission notice to travel with any substantial
portion, so its absence was a defect, not a formality. The notice names every
origin, its licence and holder, what was taken, and whether it was vendored,
wrapped, or adopted as a pattern. It opens by disclaiming affiliation, because
a fork that keeps upstream's naming should say plainly that upstream did not
write it and is not answerable for it. -
The upstream version is named rather than guessed. The BMAD installation
manifest that a consumer still carries records installer 6.3.0 on 2026-04-19,
with core 6.3.0, bmm 6.3.0, bmb 1.5.0, cis 0.1.9 and tea 1.7.2; the import here
is dated 2026-04-26 and its skill set matches that installation exactly, one
own-work skill aside. The notice says "imported", which is a fact, and declares
that a byte-level comparison against the published packages was not run.
Fixed
-
Two licence claims were wrong. Graphify relicensed from MIT to Apache 2.0
and moved organisation; the README still called it MIT. And
skills/openspec-apply-parallel, which is this repository's own work, declared
Apache-2.0 inside an MIT repository. -
The hybrid-planning section named a dormant skill family. The README listed
bmad-testarch-*as part of the flow while the skills inventory marked the same
family as not participating in any documented phase. -
Ported skills now name the copyright holder they were ported from, and the
two rules shaped by third-party work carry that credit in the rule itself
instead of only in this file, so it survives being read on its own.
Migration
Consumers whose repository has no .ai-playbook-state/skills-manifest.json must
run the materialiser once on their current pin, before checking out this tag:
python .ai-playbook/scripts/materialise_skills.py --quiet
The materialiser deletes only what its manifest records it as having installed.
With no manifest it seeds ownership as present ∩ desired, which by construction
excludes a skill this tag no longer wants — so the eleven removed skills would
never be classified as stale, on that run or any later one, and would sit in all
three mirrors permanently. Seeding first records what you have today, so the bump
can remove them. Consumers that already have a manifest need nothing.
Full procedure: step 0 of docs/runbooks/upgrade-playbook-pin.md.
v0.22.22
Fixed
-
docker compose(v2, with a space) walked straight through the gate.
_KILL_PATTERNSonly carried the v1 hyphenated spellingdocker-compose, so
docker compose down -v— which destroys named volumes — was never matched.
Found the hard way: an agent ran exactly that against a live stack and the
gate stayed silent, while the same agent was correctly blocked on
docker rm. A safety rule with a hole is worse than a known-absent one,
because the blocks it does issue imply a coverage it does not have.Both spellings are now matched by one pattern, and the verb is reachable
behind global flags (docker compose -p proj -f a.yml down), which is the
form that actually slipped through. -
kubectl deletewas ungated entirely. Deleting a live workload is the
cluster-native equivalent ofdocker rm. Now matched.
Non-terminating neighbours stay allowed and are covered by tests:
docker compose up -d, docker compose ps, docker compose logs,
kubectl get, kubectl rollout restart.
v0.22.19 — rules using @dataclass could not be imported at all
Fixed
-
_load_rule_modulenever registered the module insys.modules.
@dataclasscombined withfrom __future__ import annotationsresolves its
field types throughsys.modulesat class-creation time, so any rule using
that pair — which is every rule in this repo — died with
AttributeError: NoneType has no attribute __dict__.except Exception: mod = Nonethen turned that into aNone, and the
caller'scontinuemade an unimportable rule indistinguishable from a rule
with no hook for this event.Measured cost:
jira-closure-evidencedeclares a dataclass. It was
status: enforced, appeared in--list, was not disabled, and matched its
trigger — and had NEVER ONCE RUN since it shipped in v0.22.14. Two later
explanations for it not firing (a matcher missingtransition, then absent
Atlassian credentials) were both true and both downstream of this one.Import failures are now recorded in
_LOAD_ERRORSinstead of vanishing. The
behaviour is still fail-open — a broken rule must not wedge the hook path —
but its absence is no longer silent.A failed import also no longer leaves a half-executed module behind in
sys.modules, which would be the worse failure: importable by name from
anywhere else in the process, and appearing to work.
Verification
- New census test: every shipped hardrule must import. A NEW rule that fails
fails this file rather than going quietly missing. - Mutation-verified: removing the registration reddens the census AND the
dataclass test. - The dataclass test's first draft used a plain
@dataclassand passed with
the fix reverted — measured that only the deferred-annotations form
reproduces (@dataclass(slots=True)does not either). Corrected, because a
test that cannot reproduce its own subject is the defect this repo is about.
v0.22.18
Added
-
jira-closure-evidencenow enforces via a receipt. The comment is judged
where it is written —addCommentToJiraIssue, whose payload carries the body
and whose comment demonstrably persists — and a small local receipt records
the verdict. A transition into a declared Done id then requires a fresh
passing receipt.This is the design that replaces the one v0.22.17 recorded as failed. Carrying
the comment inside the transition does not work: Jira accepts the ADF, returns
success, moves the issue to Done, and silently drops the comment.No credentials, no network, and it validates text that actually exists.
-
AIPLAYBOOK_CLOSURE_DONE_TRANSITIONS— the opt-in. Transition ids are
per-workflow, so without credentials the gate cannot tell a closure from a
move to In Progress. Unset means the rule does nothing: a consumer joins by
naming its own workflow, not by minting a token. Where credentials do exist,
the live status category still wins. -
New clause C4 — no blank halves. If a closure enumerates its work as an
ordered list, every item must carry its own artefact. Catches the error the
rule was built for (GPLO-1469, closed after verifying 1 requirement of 3)
using only the comment. Ordered lists only — bullets carry prose.
Changed
- Old C4 (path fidelity) → C5; old C5 (requirement count) → C6. Both
remote-only, skipped without credentials, and their absence is stated in the
refusal rather than left to be assumed. - Commenting is never blocked, only recorded. Refusing a comment that merely
says FIXED would be a false positive on the most common verdict word. statusback toenforced— accurate now.
Verification
- 45 tests. Mutation-verified twice, each killing only its own cluster: killing
the verdict matcher reddens the six recording tests; removing the receipt
check at transition time reddens the four enforcement tests. - The verdict matcher shipped in a draft with literal backspace bytes instead of
�word-boundary escapes, so it matched nothing and no receipt was ever
written — a silent no-op with a green suite.
test_a_verdict_token_is_what_makes_it_a_closureexists specifically to
redden on that.
v0.22.17
Fixed
-
The documented rework for
jira-closure-evidencedescribed a mechanism that
does not persist. The plan was to carry the closure comment inside the
transition (update.comment[].add.body). Measured against a real closure
before building on it:- A markdown string is rejected — "Operation value must be an Atlassian
Document". That error comes from Jira's own validator, proving the MCP
server does forwardupdate. - Proper ADF is accepted: the call returns success, the status moves to
Done, and the comment is silently dropped.comment.totalstays 0,
re-read twice minutes apart, while a sibling ticket returns its comments
through the identical call.
Almost certainly
hasScreen: falseon the transition — Jira drops field
operations for a screenless transition. That is a per-workflow property, so
even where it works it is not something a consumer could rely on.Shipping it would have been worse than shipping nothing: the gate would
demand the comment ride in the transition, the author would comply, Jira would
return success, and the ticket would land in Done with no comment at all.The lesson is this rule's own subject turned on its author — the API accepting
a field is not the field persisting. Acceptance is not persistence.The doc now records the failed experiment so nobody rebuilds it, and points at
a receipt-based design instead: validate the comment on
addCommentToJiraIssue(whose payload both carries the body and persists it),
write a local receipt, and require a recent valid receipt at transition time.
The credential-free opt-in idea survives intact. - A markdown string is rejected — "Operation value must be an Atlassian
v0.22.16
Fixed
-
jira-closure-evidenceis nowstatus: advisory. It was shipped as
enforcedand described as live. It was not blocking anything, and could not.The clauses need the ticket's description and its latest comment.
Neither is in the event: the MCP transition payload carries only
issueIdOrKeyandtransition.id. So the hardrule fetches the issue over
REST, which needsATLASSIAN_URL/ATLASSIAN_USERNAME/
ATLASSIAN_API_TOKENin the hook process's environment. Measured in the
consumer repo it was written for: those are in neither the SOPS dev secrets
nor OpenBao, and a hook subprocess cannot borrow the agent's MCP OAuth. The
rule ran, found no credentials, and failed open on every transition.A rule advertising
enforcedwhile verifying nothing is the exact defect this
rule exists to catch. It spent a day being one.advisoryhere means "cannot be relied on to fire", not "will never
fire" — the dispatcher routes ontriggers:alone and ignoresstatus:, so
a consumer who does export those variables still gets full C1-C5 blocking.
Both halves are documented, because either alone is the misleading half.
Known gap
-
The shape is wrong for an agnostic playbook, not merely unconfigured.
Every other hardrule here judges the event payload — the direct twin
jira-ticket-standardreadstool_input["description"], needing no
credentials and staying inert for anyone who does not use that tool. This is
the only rule that reaches out, and the dependency is not "Jira": it is
needing state the event does not carry, which would break identically
against GitHub Issues or Linear. A public, tracker-agnostic playbook must not
require every consumer to mint a long-lived API token for a capability most of
them will never use.The rework, tracked for a later release: carry the closure comment inside
the transition (update.comment[].add.body, which Jira's API and the MCP tool
both accept). C1-C3 then judge from the payload like the twin; C4-C5 change
from verify against the remote ticket to show your work — the comment must
enumerate the requirements it closes, each with its own artefact. Weaker in
principle, sufficient for the failure it was built for (forgetting a half, not
lying about one), and it makes the evidence atomic with the act it
justifies rather than whatever comment happened to land last.
v0.22.15
Fixed
-
shared-test-db-mutexcould not tell a dead holder from a live one on
Windows._pid_alivereturnedTrueunconditionally there, and the comment
justifying it called that "the safe direction" because the TTL would release a
genuinely dead holder. That reasoning was wrong for the platform this actually
runs on: with no liveness probe, the TTL was the only release mechanism, so
any interrupted run — Ctrl-C, a killed background task, a crashed worker —
locked the database for three hours.It was hit fifteen minutes after the rule was first wired to its event: a
probe process exited, its lock outlived it, and the next legitimate command was
refused by a holder that no longer existed.This is the failure mode the rule's own doc warns about in a different costume.
A gate that fires where it cannot judge honestly gets bypassed; a mutex whose
cost is a three-hour outage gets switched off, and then it protects nothing.Liveness is now probed on both platforms —
signal 0on POSIX,
OpenProcess+GetExitCodeProcesson Windows. Two asymmetries are
deliberate:ERROR_ACCESS_DENIEDcounts as alive (the process exists and
we may not inspect it — treating it as dead would let one user's run take
another's lock), and any unexpected condition counts as alive (holding a lock
too long is recoverable; releasing a held one corrupts a running suite
silently).Pinned by four tests against real processes, two of which fail against the
previous behaviour: a process that has exited is not alive, a running one is
(negative control), and a fresh lock naming a dead pid no longer blocks —
asserted with the TTL deliberately far from expiry, so only liveness can
satisfy it.
v0.22.14
Fixed
-
branch-name-validatorasked for a file the playbook gitignores. The gate
told authors to "create the proposal atopenspec/changes/<id>/and commit it
to this branch" — andopenspec/is ignored here on purpose ("the playbook
does not commit its own proposals/tasks/archive",.gitignore, #79). That
remedy could never be followed by anyone.Nobody was blocked, which is why it lasted. The third remedy — "use a
chore/*branch" — was the only one that worked, so every PR merged after
#158 usedchore/*, including ones titledfeat(...): #163, #162, #161,
#159, #157. The prefix stopped meaning "this is maintenance" and came to mean
"this is the only prefix that passes", while the check reported green the
whole time. A gate whose remedy is impossible does not stop the work; it
quietly retrains everyone to route around it.The requirement is still correct for a consumer repo that commits
openspec/,
so it is not deleted — it is made conditional on being satisfiable, tested
withgit check-ignoreon the exact path the error message names. The
branch-NAME pattern is untouched and still enforced.The first attempt at that test asked "does this repo track anything under
openspec/" and was wrong: gitignoring a directory does not untrack what was
already committed, and legacy proposals from before #79 are still in the tree.
It answered "satisfiable" while a new proposal still could not be added.
Caught by running the condition rather than reasoning about it —
test_the_repo_this_runs_in_actually_triggers_the_exemptionnow pins it.
Added
tests/test_branch_name_validator_workflow.py— 13 tests, the workflow's
first coverage. Negative controls carry the weight: an exemption that swallows
the gate would satisfy every "must not block" assertion while removing the
enforcement, so the branch-name pattern, the success path and the failing
verdict are each asserted to still exist.
v0.22.12
Added
-
English headings for the three sections that lacked them.
contexto,
alcanceandmetricasnow carryContext / Problem,Scope / Deliverables
andMetrics(plus short forms) alongside their Spanish aliases.This was never a decision anyone made. Four sections already had English
aliases —Test plan,Steps to reproduce,Expected vs actual,
Regression test— and three did not, which is simply what the first author
reached for. Measured 2026-08-02 while drafting a 15-ticket backlog in
English: a fully English ticket was rejected on exactly those three sections,
so the standard was not enforcing Spanish, it was forcing tickets to be
bilingual. Either language is a defensible house rule; half of each is not.Canonical stays Spanish so nothing already written moves, and the change is
purely additive: no description that validated before it stops validating
after.tests/fixtures/jira_ticket_gplo_1350.md— the real production
ticket the suite is anchored on — is asserted unchanged as the C regression.The negative control is the one worth reading:
## Backgroundis plausible
English prose for the same content and is still rejected. The matcher
resolves against the closed alias list, not against "English", so the contract
is still a contract.