Skip to content

Releases: AlexCiortan/setlist

Setlist 2.3.0 (edition v1.11, the attestation edition)

Choose a tag to compare

@AlexCiortan AlexCiortan released this 30 Aug 01:08

Edition v1.11 (the attestation edition). The headless build integrity chain: the one item on this project's Known-limitations list whose absence was a missing mechanism rather than a hole in one.

What it does

Declare an attestation block in .claude/sdd.json and /setlist:checkpoint writes and signs an approval when a spec goes ACTIVE, binding it to that spec's exact bytes. The git hooks then refuse a build commit, a push, or a close whose spec has drifted since that approval. Spec-hash and its session-start warning are layered underneath and unchanged: the two answer different questions, and a spec whose hash was quietly recomputed by hand passes the first and fails the second.

Nothing changes unless you turn it on. No block means off, and off is byte-for-byte the behaviour you have today, proven by a differential against the 2.2.0 hook bytes rather than asserted. An upgrade delivers the key set to false and migrates nothing.

What it is worth, which the mechanism prints rather than leaving you to work out. A signature proves a key was used, not that a person decided. If your signing key sits where the build can read it, a headless run can sign its own approval and the chain will verify, establishing that the run had the key, which is not the question. So Setlist makes you declare where your key lives and names that declaration in every verification it emits, including the ones that pass.

forge custody is designed and NOT BUILT. Verification would be a query your forge answers, and it arrives with the forge-side required check this list already names as a candidate rather than a promise. Declaring it today refuses and says so. signer custody is the model that works now.

The 2.2.0 commitment, answered by identifier

The 2.2.0 notes named five findings and said this release would carry them. Four shipped:

  • F3-2026 a broken or missing git can no longer make the commit gate report every staged line clean having read nothing.
  • F4-2026 a correctly wired hook is no longer certified NOT WIRED when its matcher is * or absent, so /setlist:upgrade on a working instance no longer exits INCOMPLETE forever.
  • F6-2026 the Architecture-diagram field is read as an answer rather than as a sentence containing one, and is decided by the first such field rather than the last.
  • F7-2026 the QA verdict block is decided by the first block, so an illustrative example after a real verdict no longer replaces it.

F5-2026 is deferred, and the measurement is published rather than the promise renewed. A compliant chore close spelled --ff or --squash is still accepted at commit and refused at push. Its scoped fix was measured during this release's own adversarial review to widen a more serious exemption in the same code, so taking it would have traded a disclosed gap for an undisclosed one. It is entangled with the exemption below and both are scheduled together, into the next cycle's design intake.

A limitation found by this release's own review, disclosed at real width

A single-parent trunk commit that closes a spec is content-exempt at the push-time audit. When a commit on the trunk has one parent and compliantly closes a spec, the audit accepts the whole commit without asking which role-path files it introduced, so such a commit can also carry feature code belonging to a spec that is still open. Reproduced with a negative control: the byte-identical commit without the row flip is refused.

What limits it, stated as precisely as the gap. The default close /setlist:checkpoint performs is a --no-ff merge, and merges take a different arm that does ask which files no parent supplied; a --no-ff close carrying injected code is refused. The exposed surface is single-parent closes specifically, meaning --squash and fast-forward. Like every hook it is a local check: a boundary that must hold against deliberate evasion belongs on your forge, in branch protection and required checks.

Three previous Known-limitations bullets are corrected in this release because the review showed them false in composition. The rebase, reset --hard and checkout -- <path> bullets each said "Measured: the push is refused with the same VIOLATION, remote unchanged". That holds for the bare form; composed with a compliant single-parent close in the same commit, the push succeeds. Each bullet now says so.

Why it is not fixed here. The obvious narrow fix was measured and refused: a squash has no parent carrying its legitimate content, so the merge arm's provenance question cannot separate an honest squash close from this one. Separating them needs a recorded fact this framework does not yet keep, which file belongs to which spec, and that is the subject of the next cycle's design intake.

Also in this release

A secret on an added line whose text begins ++ is no longer deleted from the scanners' own input at either layer. The advisory session gates carry a machine-readable code on every refusal, not only on some. The advisory commit gate now says that path exclusions are evaluated at the git-hook layer and that it does not read them, so you can tell from the message whether a commit will actually be blocked. The Known-limitations list gains the empty-remote trunk-audit boundary, which the framework document described and this list did not.

The rest of what the review found, with homes

  • A scaffolded value that is present and not a boolean still stands the session scope gate down silently instead of reporting itself. The one-line fix was written this cycle and cut: it needs a new refusal code, a new code is a new question, and a new question under this project's own release rules requires a full adversarial review that a repair round does not get. Scheduled for a cycle that owes one anyway. The layer affected is the advisory gate; the git hooks, which are what refuse, are unaffected.
  • Two defects in this project's private checking tools were found and are deferred under the rule that a defect in a checker never blocks a release. They do not affect anything you install.

A correction to an earlier draft of these notes

A draft of the changelog entry claimed four repairs as shipped that were not written. The claim was false when it was made. It was caught before release by this project's own adversarial review, which reads the changelog as ground truth and reported the code as contradicting it. The repairs above are the ones that then actually landed, and the one that did not is named as deferred rather than promised again. A release note is a claim about bytes, and this one was audited against the bytes before it shipped.

Attestation

One adversarial review read tree b7d57565, returned PASS-WITH-FINDINGS with ten surviving findings, and fix round 1 answered it. This release is attested by the mechanical evidence set under the project's A2 rule, verifying tree c1fdb7ea. That proves the known corpus still holds and that the fix diff survived a cold adversary; it proves nothing about classes nobody has enumerated, and the record says so out loud.

Setlist 2.2.0 (edition v1.10)

Choose a tag to compare

@AlexCiortan AlexCiortan released this 28 Aug 14:09

Setlist 2.2.0 (edition v1.10, the scoping edition)

Setlist turns Claude Code into a spec-driven engineering partner: you direct as architect
and reviewer, the agent writes the code, and a spec, not the keyboard, is the control
surface.

This is the enforcement release. The two before it changed documentation and left
every gate byte-identical. This one changes the bytes that do the enforcing: the git
hooks, the push-time trunk audit, the advisory session gates, and the upgrade
certification. Most of what moved is repair, and the repairs are mostly in the direction
people actually feel, which is refusals of honest work rather than holes.

The false denial disclosed in 2.1.0 is fixed, which is the promise this release was
made to keep

The 2.1.0 notes opened their disclosures with a defect rather than hiding it: a spec that
QUOTED the closing-report template inside a fenced code block, changed no lifecycle state,
and did not stage specs/STATUS.md, was refused an ordinary commit with
[SLH-STATUS-MISSING]. The detector read the raw staged diff and did not strip fences, so
quoted text was read as if it were live. The same regex meant an indented
## Closing report heading was not matched at all, while three other readers in the same
release accepted it.

Both halves are one fix, and it was made in the SHARED reader rather than in a fourth
private copy, so the four readers that had drifted now agree by construction. The exact
reproduction from that disclosure is now a test fixture that commits clean, and its mirror
(the indented heading, which must still be SEEN) is a fixture beside it, so the fix cannot
regress in either direction without the suite going red.

The one new feature: the staged-content scans can be path-scoped

By default the em-dash and secret scans read every added line. That is right for your own
writing and wrong for a vendored tree, a fixture carrying a dummy credential, or quoted
external text, and splitting the commit never helped: the scans follow the content, so
isolating the foreign file isolates it WITH the scanner.

Declare the paths, as repo-relative globs, in .claude/sdd.json:

"scan_exclusions": ["vendor/**", "test/fixtures/**"]

Honoured by both scans, at the commit layer and the push layer alike. Four properties are
part of the mechanism rather than incidental to it:

  • Every skip is printed, every time, naming the file and the glob that matched it,
    because an exclusion nobody is told about is a hole one directory over:

    setlist [SLH-SCAN-EXCLUDED]: staged content: vendor/three.min.js was NOT scanned
    (matched "vendor/**" in .claude/sdd.json scan_exclusions). Nothing in that file was
    read by the em-dash or secret scan.
    
  • The set reaches those two scans and nothing else. It cannot quiet the trunk audit,
    lifecycle detection, role-path judgment or any close check, so it cannot become a way
    to close a spec quietly. A glob pointed at specs/ changes nothing about closing one.

  • A set that cannot be read, or one made only of wildcards, is REFUSED with a named
    code
    rather than resolved in either direction. A config error that quietly scans
    everything ignores a declaration; one that quietly scans nothing is an exemption nobody
    wrote. There is deliberately no pattern that means "all".

  • Declaring nothing leaves the scans exactly as they were. The feature is opt-in and
    its absence changes no verdict.

Where a match cannot be decided, the scan RUNS: a case-variant spelling does not match and
is scanned, and a path git had to quote is scanned and says so. Failing to exclude costs a
refusal you can see; failing to scan costs a published secret. This is scoping, not an off
switch.

Other repairs you will feel

  • A compliant SPEC close spelled as a fast-forward or --squash is no longer
    permanently unpushable.
    It was refused by the push-time audit and could not be pushed
    at all. The audit now decides on parent COUNT, so one fix covers both spellings.
    The same is NOT yet true of a CHORE close: a compliant chore close spelled --ff or
    --squash is still accepted at commit time and then refused by the push-time audit, and
    that is disclosed as a known open issue below rather than left for you to hit. Preferring
    --no-ff, which /setlist:checkpoint does for you, avoids both.
  • SETLIST_SKIP_TRUNK_AUDIT=1 stops being all-or-nothing. It narrows to the audit arm:
    the secret and em-dash scans keep running, so skipping the history check is not also
    turning the content checks off.
  • The push-time scan reads every commit in the pushed range, rather than diffing the
    two endpoints, which missed content added and then removed inside a range. A first push
    of the trunk derives a range that cannot be empty, and a tag push whose target introduces
    commits is either scanned or refused by name, because a scan of nothing must never read
    as a scan that found nothing.
  • The advisory gates say so when they cannot run. With git unusable they produced
    zero bytes, which reads as approval; they now probe for a usable repository and refuse
    with an explicit code. Two pieces of advice that pointed at a variable with no effect on
    the refusal in question are corrected.
  • The upgrade certification stops being wrong in both directions. It read one tool's
    matchers where a project may configure four, so it could report a correctly protected
    instance as unprotected and an under-protected one as clean. It now reads every entry
    that runs the scope hook and takes the union.

Upgrading from 2.1.x, and this one is different from the last two

Run /setlist:upgrade in your instance. This is the first release since the hooks were
pinned by content that REPLACES hook bytes in an existing instance
, so unlike 2.1.0 and
2.1.1 the upgrade moves enforcement files and not only a document. Two things are worth
knowing before you run it:

  • The hooks and their shared library move as a SET, or nothing moves. If you have
    hand-edited a hook in .githooks/, the upgrade REFUSES, names the file, and changes
    nothing at all: not that hook, not its siblings, not the advisory layer. Your edit
    survives byte for byte. The refusal is deliberate rather than conservative, because a
    partial upgrade would leave a new hook calling a function its old library does not
    define, and the shell's answer to that is to keep going and let the commit land. A
    silently skipped check is the failure this layer exists to remove, so a refusal you can
    see is the better outcome.
  • That refusal's message currently explains itself with the wrong reason, and this is
    the release where you are most likely to meet it.
    It is written for a different case,
    a genuinely foreign hook layer being displaced, so it calls your own customized hook
    "foreign" and warns that arming Setlist "would switch off" a third-party layer such as
    gitleaks or detect-secrets. Nothing is being displaced: the hook directory is already
    Setlist's and what it found is one file you edited. The refusal is correct and it
    changes nothing; only its explanation is wrong.
    If you see it, read it as "one of your
    hooks differs from the shipped bytes", restore or set aside that file, and re-run. The
    wording is a known defect and is not fixed in this release.
  • Your existing hooks are recognised as yours across the seam. The known-good hook
    contents are append-only, so both the generation you are on and the generation you are
    moving to are recognised, before and after.

Full per-version deltas are in CHANGELOG.md; the framework document ships as
setlist.md (edition v1.10), and its own Changelog is the delta list /setlist:upgrade
reads.

Known open issues in this release

This release was reviewed by an adversarial pass against these exact bytes, and the
result is published rather than summarised.
The review returned PASS WITH FINDINGS.
Twenty-five distinct candidates were raised, thirteen were refuted by a pass whose
instruction was to disprove them, and twelve survived: one blocker, six major, five
minor.

The blocker is fixed in this release, and so is the one confirmed false denial beside
it.
The blocker was real and it was in the layer that carries the guarantee: the
push-time trunk audit read git's quoted path output as though it were a literal path, so a
file under a role path whose name contained a non-ASCII byte, a quote, a backslash or a
control character was invisible to the role test, and unreviewed code could reach the trunk
with the push allowed. Any project with a non-English filename got that for free. The same
misreading, on the reading side, refused to close a compliant spec whose filename carried
the same bytes. Both are one defect and both are fixed, with the four evasion classes now
pinned by tests that were watched failing before the fix existed. The fixes were then
re-verified against the review's full set of findings, the complete mechanical gate, and a
second independent adversarial pass over the change itself.

Five of the remaining findings are deferred to the next release under a recorded decision,
and they are listed below by the review's own identifiers.
Every one of them was measured
against the previously shipped release and behaves no worse there, which is the condition
under which they were allowed to wait: nothing below is a new defect introduced by this
release.

1. The path-scoped scans stop at the git hooks. The in-session advisory gate is not
scoped, and this half is not built.
With "scan_exclusions": ["vendor/**"] declared and
a secret-shaped string in vendor/, the git hooks accept the commit and the in-session
advisory commit gate still returns a denial for the same content. Measured, not inferred.
The practical effect: inside a Claude Code session the feature is blocked at the
convenience layer even though the layer carrying the guarantee honour...

Read more

Setlist 2.1.1 (edition v1.9)

Choose a tag to compare

@AlexCiortan AlexCiortan released this 26 Aug 19:17

Setlist 2.1.1 (edition v1.9, the reckoning edition, unchanged)

Setlist turns Claude Code into a spec-driven engineering partner: you direct as
architect and reviewer, the agent writes the code, and a spec, not the keyboard,
is the control surface.

2.1.1 is a documentation release. No command, no hook and no gate changes, and
the framework document is not edited.
Every enforcement file in this release is
byte-for-byte what 2.1.0 shipped. What changed is the honesty and the legibility
of what this project tells you about its own limits.

Known limitations is now three lists instead of one

The section had grown to 29 bullets arriving as a single undifferentiated wall.
That is a bad way to read a list where the items are not the same kind of thing:
sixteen of them were deliberate design decisions, and a decision that reads like
a defect overstates the problem exactly as much as a defect that reads like a
decision understates it. The section now splits into three, each carrying its own
count:

  • Design boundaries. Places the layer deliberately stops, each with the date
    the decision was taken and the reason it was taken. These are not bugs and are
    not scheduled to be fixed.
  • Open limitations. Real defects and gaps. Each one now carries a status line
    naming what will close it: the release cycle it is scheduled to, the gate it
    waits on, or an honest "candidate, nothing scheduled" where that is the truth.
  • Upstream conditions. Things this project does not control, each naming what
    would lift it and the check that re-measures it every release, so it stops
    being true by measurement rather than by someone remembering to look.

Every pre-existing bullet was MOVED, not rewritten. No claim, no measurement and
no workaround changed, and no bullet was deleted or softened. That is now the
stated policy for this section: the list shrinks by fixing, never by editing.

Four limitations that were true but undocumented are now listed

Three are routes the session gates do not read and the push-time trunk audit
refuses: git rebase onto a spec branch, git reset --hard onto one, and the
pathspec form of git checkout. All three were measured for this release, with
controls in both directions: each puts role-path code on your local trunk, and
each is refused at push with the remote untouched. What they cost you is the
early refusal, not the trunk.

The fourth discloses a false denial that is still present, because you are
entitled to meet it in the documentation rather than in a refused commit: a spec
that QUOTES the closing-report template inside a fenced block is refused an
ordinary commit. The detector reads the raw staged diff and does not strip
fences, so a quotation looks identical to a real close. The bullet names the
workaround that works, and names explicitly the plausible one that does not.
That bullet is scheduled to be deleted by the commit that fixes the defect,
which is the policy above working as intended.

An honest positioning note

The enforcement guarantee itself is harness-agnostic. It lives in git hooks that
git runs from its own state, so it holds under any agent, another vendor's, or
none at all, and it travels with your repository rather than with your tool. What
is Claude Code-specific is the ceremony that installs and tailors it, plus the
advisory session layer above it. Changing harness later costs you the ceremony
and the advisories, not the part that refuses work.

Upgrading from 2.1.x or 2.0.x: run /setlist:upgrade in your instance.
Because no enforcement file changes in this release, the upgrade moves the
framework document and leaves your hooks exactly as they are; the wiring check
still verifies the enforcement is live before it reports success. Full per-version
deltas are in CHANGELOG.md; the framework document ships as setlist.md
(edition v1.9).

Known open issues in this release

Everything below is in the Known limitations section of the README, with its
measurement and its workaround. This is the short list of what a new adopter
most wants to know before starting.

1. A spec that quotes the closing-report template inside a fence is refused an
ordinary commit.
This is a false denial: it refuses honest work rather than
permitting dishonest work, which makes it the entry most likely to meet you
first. It is pre-existing rather than new in this release. The workaround is to
keep the quotation out of specs/*.md, where the same fenced text commits
cleanly. Staging specs/STATUS.md alongside does NOT help, because adding an
unmodified file stages nothing. A fix is scheduled for the next release cycle.

2. A compliant fast-forward or --squash close is still permanently
unpushable.
Close with --no-ff, which is what /setlist:checkpoint does for
you. The commit-time and push-time layers read a fast-forward differently, and
rather than add a defence that would refuse ordinary work, it stays documented.

3. Three ordinary git commands reach the trunk without a session-level
warning.
git rebase, git reset --hard, and the pathspec form of
git checkout, newly documented in this release. All three are refused at push
and the remote is not reached.

4. Secret and style scanning is best-effort early warning, not a control.
Treat any secret that reached a commit as compromised and rotate it, whatever
these hooks reported. Several structural holes in the scan are enumerated in the
README, four of them scheduled for the next release cycle.

5. The gates are verified on the platforms that are actually tested. That is
Linux and macOS on every push, plus a local bash 3.2 run before each release. It
is not a claim about every shell, every awk or every BSD. The set of tested
platforms is a list, not a proof.

6. The session gates' warnings do not reach the agent on current harnesses.
The advisory layer computes its verdict and emits it on three channels, and the
harness delivers none of them to the model when the decision is allow, which is
every advisory verdict. The feedback you actually see is the git hooks' refusal
at commit or merge time. This lifts by itself when the harness renders them.

The standing framing has not changed and is worth repeating: these hooks are a
backstop against ordinary mistakes, not a sandbox. Anything that can run a shell
can reach the trunk, and the gates have never claimed otherwise. A merge
performed in a forge's web UI is not reached by them at all. The trunk audit
(scripts/trunk-audit.sh) reads history rather than commands and is the designed
catch for that whole family. If you need a boundary that holds against deliberate
evasion, it belongs on your forge, in branch protection and required checks.

Setlist 2.1.0 (edition v1.9)

Choose a tag to compare

@AlexCiortan AlexCiortan released this 25 Aug 23:49

Setlist 2.1.0 (edition v1.9, the reckoning edition)

Setlist turns Claude Code into a spec-driven engineering partner: you direct as architect
and reviewer, the agent writes the code, and a spec, not the keyboard, is the control
surface. 2.1.0 changes no command, no hook and no gate. It is a corrections release, and
the corrections are of the framework document's own claims about itself: five places where
the text said something the shipped bytes did not, or named a hole by the wrong name, or
pointed at a document no reader outside this project can open. It is called the reckoning
edition because that is what it does, and because a framework whose whole argument is that
claims should be checkable owes the same standard to its own prose before it asks for it
anywhere else.

What changed in the document

  • Part 8c now names the git-hook boundary explicitly. Upgrading a pre-v1.8 instance
    delivers the .githooks/ files and turns on two git settings, and the text says which
    ones instead of describing "a hook and a config key" in the abstract.
  • The "pathspec hole" gets its narrow meaning back. It means git commit <file> and
    nothing else. The nested-repository hole and the GIT_INDEX_FILE hole are now named as
    their own separate limitations, because the fix for one is not the fix for another.
  • Two citations of an unpublished internal design document are gone from the edition
    text.
    Those passages now name the mechanism directly, so every reference in the
    document resolves to something a reader can actually reach.
  • A stale README bullet is corrected. It told readers the shipped gate messages still
    promise a denial and to believe the bullet over the bytes. The bytes have been advisory
    for two editions; the bullet was the stale copy, and it taught exactly the wrong reflex.
  • The changelog's own 1.1.0 entry says what it was. An internal milestone, never
    tagged here, shipped as part of 2.0.0. An outside reader had to work that out.

New doctrine, in Part 6

Part 6's doctrine for anything that checks anything gains two rules, and both came from
defects found in this project's own checking tools rather than from theory.

  • A comparison asserts the size of what it compares, and refuses on zero. An empty
    list matches anything, so a coverage check whose extraction quietly fails compares
    nothing and reports every row covered. Assert the size of each side before comparing,
    treat zero as a refusal rather than a pass, and mutate the thing under test once to watch
    the check go red. A comparison that has never been seen to fail has not been shown to
    compare anything.
  • Label every green with what it is evidence OF. This is the more dangerous cousin,
    because it survives the negative test: the check is real, it does go red under mutation,
    and its result is filed under a claim it does not test. No mechanical guard catches it,
    since the defect sits one level above the check. The habit that does: say what each green
    is evidence of, in the report, and when every row passes, construct the input that should
    fail. If you cannot construct one, the name of the check is what is wrong.

Upgrading from 2.0.x: run /setlist:upgrade in your instance. Because no enforcement
file changes in this release, the upgrade moves the framework document and leaves your
hooks exactly as they are; the wiring check still verifies the enforcement is live before
it reports success. Full per-version deltas are in CHANGELOG.md; the framework document
ships as setlist.md (edition v1.9).

Known open issues in this release

This release was reviewed by an adversarial pass against these exact bytes, and the
result is published rather than summarised.
The review returned PASS WITH FINDINGS:
no blockers, six major findings, five minor. Twenty-three distinct candidates were
raised, twelve were refuted, and the eleven below survived a refuter whose instruction was
to disprove them. Every one is recorded, with a disposition and a trigger, and none of them
puts unreviewed code on a shared trunk. This is the first Setlist release whose evidence
gate was satisfied by rule rather than by a decision to proceed past it, which is worth
saying plainly given that the previous one was not.

1. One of the findings is a false denial, and it is not fixed in this release. In one
narrow shape, pre-commit refuses an honest commit: a spec that QUOTES the closing-report
template inside a fenced code block, changes no lifecycle state, and does not stage
specs/STATUS.md, is refused with [SLH-STATUS-MISSING]. The detector reads the raw
staged diff and does not strip fences, so quoted text is read as if it were live. The same
regex is why an indented ## Closing report heading is not matched at all, while three
other readers in this same release do accept it.

It is disclosed rather than fixed for two reasons, both of which are decisions rather than
oversights. It is pre-existing: the detector has not changed since before 2.0.0, so
anyone running 2.0.0 has this defect today, and holding this release back would leave them
with both the defect and the documentation errors this edition exists to correct. And
fixing it inside this release would change what a guarantee check ASKS, which under this
project's own rules requires a fresh adversarial review of the changed bytes before they
ship. That is the right thing to spend on the fix, and the wrong thing to spend on a
release whose only other change is documentation. It is scheduled for the next cycle's
guarantee-layer batch.
If you hit it, the workaround is to stage specs/STATUS.md in the
same commit, which is what the message asks for.

2. The remaining findings are major and minor, not blocking, and several are in this
project's own checking tools rather than in the gates you install.
Two are advisory-layer
gaps where an unusable git or an unusual command spelling makes a session-level check
allow rather than refuse. One is a per-merge-hook route where an illustrative inventory row
written as a blockquote or a list item satisfies a closed-spec check. Two are in the
upgrade certification, which can report a correctly protected instance as unprotected or an
under-protected one as clean. The rest are reporting and documentation defects. In every
case the push-time trunk audit is unaffected: it reads history rather than commands, and it
is the layer the guarantee rests on.

3. A compliant fast-forward or --squash close is still permanently unpushable.
--no-ff is required, which is what /setlist:checkpoint does for you. The commit-time
gate and the push-time audit read a fast-forward differently, and rather than add a defence
that would refuse ordinary work, it stays documented.

4. A stale remote-tracking ref can deny an ordinary merge. (Corrected 2026-08-26: item 4 as published described PRE-ADVISORY behaviour. Measured on the shipped 2.1.0 bytes, the close gate emits permissionDecision: "allow" and carries the verdict only in its machine-readable setlistAdvisory field, so it does not deny the merge. The cost is a misleading advisory, not a blocked merge. The rest of this item, including the remedy, stands. This note is added at the record rather than the item being rewritten, which is this project's convention for a published correction.) If you rename a spec branch
while a remote-tracking ref for the old name still exists, the close gate denies the merge
with CG-SPEC-MISSING. It is a false denial rather than a way past the gate. The remedy is
one command:

git update-ref -d refs/remotes/origin/spec/0004-thing

5. The gates are verified on the platforms that are actually tested. That is Linux and
macOS on every push, plus a local bash 3.2 and BWK awk run before each release. It is not a
claim about every shell, every awk or every BSD. A platform difference nobody had tested for
once turned two gates off for an entire release, and the honest statement is that the set of
tested platforms is a list, not a proof.

6. Three limitations the test suite pins are not yet in the Known limitations list.
git rebase, git reset --hard and a pathspec git checkout are all pinned as known holes
in the suite and named in none of the published bullets. All three are refused at push by
the guarantee layer, so the remote is not reached; what is missing is the documentation, and
it lands in the next documentation pass rather than being written into a release whose bytes
are already frozen.

The standing framing has not changed and is worth repeating: these hooks are a backstop
against ordinary mistakes, not a sandbox. Anything that can run a shell can reach the trunk,
and the gates have never claimed otherwise. A merge performed in a forge's web UI is not
reached by them at all. The trunk audit (scripts/trunk-audit.sh) reads history rather than
commands and is the designed catch for that whole family.

Setlist 2.0.0 (edition v1.8, the boundary edition)

Choose a tag to compare

@AlexCiortan AlexCiortan released this 20 Aug 14:35

Setlist 2.0.0 (edition v1.8, the boundary edition)

Setlist turns Claude Code into a spec-driven engineering partner: you direct as architect
and reviewer, the agent writes the code, and a spec, not the keyboard, is the control
surface. 2.0.0 is the release where enforcement moved to the strongest layer available:
git itself.

Highlights

  • The guarantee now lives in git's own hooks. The push-time trunk audit stands between
    unreviewed work and a shared trunk, and it decides by identity and ancestry: does this
    history descend from the commit that adopted the rules, and did this content arrive
    through a closed spec's lineage. Because git runs the audit after the shell has already
    parsed arguments and resolved refs, a merge is a merge however it was spelled.
  • Fast feedback stays where you work. pre-commit and pre-merge-commit refuse
    incomplete closes at commit and merge time, with precise, actionable messages, so the
    ordinary day feels immediate while the push-time audit holds the line.
  • /setlist:retrofit respects the hooks you already have. Retrofitting into a project
    that uses husky, lefthook or pre-commit now detects the existing hook layer by content
    and stops with a clear choice, instead of silently taking over. One environment variable
    (SETLIST_ADOPT_HOOKSPATH=1) expresses the deliberate handover.
  • Brand-new remotes are protected from the first push. On a repository's first push to
    an empty remote, every pushed branch is audited as a trunk candidate, so a project's
    default branch is born clean. Push the trunk first and it simply passes.
  • Close conditions read what a human sees. The mandatory Architecture-diagram field is
    now read from live text only: prose inside code fences, comments or collapsed examples
    no longer satisfies (or poisons) a Closing report, matching how the spec inventory and
    QA verdicts are already read.
  • Every decision is measured, not assumed. The severity model, the audit's
    identity-and-ancestry rules, and each close condition were exercised by generated
    adversarial inputs, mutation checks, and cold end-to-end dogfood runs on three stacks
    before this tag was cut.

Upgrading from 1.x: run /setlist:upgrade in your instance; the wiring check verifies
the new enforcement is live before it reports success. Full per-version deltas are in
CHANGELOG.md; the framework document ships as setlist.md (edition v1.8).

Setlist 1.0.9 (edition v1.6)

Choose a tag to compare

@AlexCiortan AlexCiortan released this 31 Jul 15:08

If you use Setlist on a Mac and you are on 1.0.8, update now. On macOS,
1.0.8's commit gate and close gate allowed everything. They did not crash and
they printed no error. They simply permitted every commit and every merge they
exist to check, silently, from the moment 1.0.8 was installed.

Linux and WSL were never affected. 1.0.7 and every release before it were never
affected on any platform. If you are not on a Mac, this release changes nothing
you will notice, and taking it is still the right move.

What broke, and why the platform decided it

Both gates normalise the command they are handed through a small awk program
before deciding whether the command is one they govern. In 1.0.8 that program
ended with }\' where it should have ended with }'.

GNU awk and mawk, which is what Linux ships, accept the trailing backslash
without a word. The BWK "one true awk" that macOS ships as /usr/bin/awk does
not: it reports a syntax error, exits 2, and writes nothing at all to standard
output. So the normalised command came back EMPTY. The test for "is this a
command I govern" then matched nothing, and each gate concluded, reasonably on
its own terms, that it had nothing to do and got out of the way.

The shape is worth naming because this project has now hit it twice in two
releases. An input failed to materialise, the empty result was indistinguishable
from "nothing here to govern", and absence read as permission. In 1.0.8 the
dependency that failed was jq, which was installed but could not run. Here it
was awk, which ran perfectly well and was simply STRICTER than the one the
code had been written against. Both gates now state the rule in the file, next
to the code it governs: the lexer's awk program must not end in a backslash, and
a gate whose lexer can fail must not treat lexer failure as a clean parse.

What changed

Two files, two bytes, plus the comments that explain them. No new command
spellings are recognised, no gate semantics moved, and no checks were added or
removed. Everything 1.0.8 closed stays closed, and it now closes on macOS too.

How this escaped, stated plainly

1.0.8's evidence was real. It was also platform-scoped, and the release notes
did not say so.

Every mechanical leg behind 1.0.8 ran under Linux with GNU awk, which is the one
environment where this defect cannot be observed. The macOS leg was known to be
missing at publish time. It was recorded as a debt to watch after the push
rather than a gate to pass before it, and it went red on the first run: 169 of
451 assertions failed on macos-latest while the Linux leg was green.

Two things change as a result. The macOS CI leg is now a BLOCKING pre-publish
gate, so no future release can be pushed to the default branch before a real Mac
has run the suite against the exact bytes being published. And the suite is run
locally under bash 3.2.57 with BWK awk, the same implementations macOS ships, as
part of the ordinary evidence set rather than as an optional extra.

What to do if you were running 1.0.8 on a Mac

Update, then treat any work that reached your trunk while 1.0.8 was installed as
unverified by the gates. The gates were not the only thing standing between you
and a bad merge, and in normal use /setlist:checkpoint runs the same close
checks before you ever reach the gate, so a normal session was still checked. It
is the backstop that was missing, not the whole process.

If you want to know for certain what reached the trunk unchecked, the trunk
audit reads history rather than commands and is the designed tool for exactly
this question:

bash scripts/trunk-audit.sh

Known open issues in this release

Five things about this release are disclosed here rather than left for a reader
to discover, because each is something a careful adopter would want to know. The
first four carry forward from 1.0.8 unchanged in substance.

1. This release was not reviewed by an adversarial pass against its exact
bytes.
That was true of 1.0.8 for a reason given at the time (each fix changed
the bytes the previous review had read, a loop with no exit), and it is true of
1.0.9 for a simpler one: this is a two-byte platform repair, and the argument
for a scoped gate is the same one used for 1.0.1 and 1.0.2. What stands behind
these bytes is the full mechanical evidence set, now run twice, once under Linux
and once under bash 3.2.57 with BWK awk.

2. A stale remote-tracking ref can deny an ordinary merge. If you rename a
spec branch (git branch -m spec/0004-thing feature/thing) while a
remote-tracking ref for the old name still exists, the close gate denies
git merge --no-ff feature/thing with CG-SPEC-MISSING. It is a false denial
rather than a way past the gate, so it is disclosed rather than fixed. The
remedy is one command:

git update-ref -d refs/remotes/origin/spec/0004-thing

3. Findings from the last two adversarial reviews remain open and are not
secret.
They are MAJOR and MINOR rather than blocking: none of them puts
unreviewed code on the trunk. Several are defects in the project's own checking
tools rather than in the gates you install.

4. The gates need a WORKING jq, not merely an installed one. A jq that
exists but fails to run (a broken dynamic link, a killed process, a wrong
architecture) used to make two gates allow silently. That was fixed in 1.0.8:
all four hooks run jq rather than just locating it, and each distinguishes
"missing", "present but not runnable" and "could not parse this input" so the
message names what to fix.

5. The gates are still only verified on the platforms that are actually
tested.
That is now Linux and macOS on every push, plus a local bash 3.2 and
BWK awk run before each release. It is not a claim about every shell, every awk
or every BSD. This release exists because a platform difference nobody had
tested for turned two gates off, and the honest statement is that the set of
tested platforms is a list, not a proof.

The standing framing has not changed and is worth repeating: these hooks are a
backstop against ordinary mistakes, not a sandbox. Anything that can run a shell
can reach the trunk, and the gates have never claimed otherwise. A merge
performed in a forge's web UI is not reached by them at all. The trunk audit
(scripts/trunk-audit.sh, opt-in) reads history rather than commands and is the
designed catch for that whole family.

If you already have a project running Setlist

Run /setlist:upgrade once after updating the plugin. This release changes hook
bytes but not the settings wiring, so the refresh is hook files only.

/plugin marketplace update setlist

If a Claude Code session was open when you ran that update, restart it before
upgrading.

Setlist 1.0.8 (edition v1.6)

Choose a tag to compare

@AlexCiortan AlexCiortan released this 30 Jul 13:37

If you are on any earlier version, take this one. It closes fifteen ways past
the gates, and every one of them was present in 1.0.6 and 1.0.7 as well, so none
of this is a regression you might have escaped by waiting.

They were a small number of causes rather than fifteen unrelated bugs, which is
why they could be fixed together.

A ref is identified by the commit it names, not by how it is spelled. The
close gate used to recognise a spec branch by stripping literal prefixes from
the merge argument, so every other spelling of the same commit read as an
ordinary sync merge and skipped every close check: heads/spec/0001-x,
remotes/origin/spec/0001-x, another remote's name, a TAG pointing at the
branch, an alias branch, a raw commit id. Worse, for origin/spec/0001-x the
strip produced the LOCAL branch name, so a compliant local branch could
green-light a non-compliant remote one. The gate now resolves the argument to a
commit and asks git which refs point at it.

Where several refs sit on one commit, a spec ref wins over a chore ref, and two
DIFFERENT specs on one commit are refused outright with [CG-AMBIGUOUS-SPEC]
rather than resolved by sort order: the gate cannot know whose Closing report it
would be judging. A branch, its remote-tracking copy and a tag are one spec
rather than three, so ordinary closes are unaffected.

Shell grammar no longer hides the command, and neither does punctuation.
Wrapping a merge in { ...; }, if ... then, for ... do, or prefixing it
with ! moved the git verb out of the position the gate inspects. So did a
leading redirection (>/dev/null git merge ...), and so did the absolute-path
spelling of a wrapper (/usr/bin/env git merge ..., which is what a shebang
produces). All of them really did land a merge on the trunk. Reserved words,
redirections and path-qualified wrappers are stripped at the head of a segment
now, in both gates.

Relatedly, the & inside 2>&1 was read as a command separator, cutting
>/tmp/out 2>&1 git merge ... into two fragments, neither of which began with
git. File-descriptor duplication is recognised before the line is split.

Quoted text is opaque. The close gate deleted quote characters and kept
their contents, so a commit message that merely mentioned a checkout could
retarget the branch the gate believed it was standing on. The commit gate did
the opposite, deleting whole quoted spans, so quoting the word git deleted the
word it matches on. A quoted span of one shell-safe word is now kept as that
word, and anything longer becomes a single inert token that cannot supply a
command or split a line. Escaped quotes are understood, so the ordinary way of
writing a contraction no longer confuses the scan, and a command whose quotes
never close is REFUSED rather than half-read.

A checkout is conditional. The gate modelled every git checkout as having
succeeded, but a checkout aborts whenever local changes would be overwritten,
and after it aborts you are still on the trunk. So git checkout other; git merge --no-ff spec/0001-x ran the merge on the trunk with every close check skipped.
Only && implies the previous command succeeded, so only && is trusted to
have moved the branch; after any other separator the branch is treated as
unknown and the merge is refused rather than waved through.

Every index-writing verb counts, and every commit in the line is judged. A
command that writes the index and commits in one step is denied, because the
hook decides before the command runs and only already-staged content is
scannable. That set is the full one now (stash pop, restore --staged,
reset, a pathspec checkout, the plumbing verbs), not just add, rm and
mv. And a compound carrying two commits is judged on both: appending a second
plain git commit used to disarm the check on the first.

A branch must write its own Closing report. A branch cut after a spec closed
inherits that spec's artifacts entire, so reusing a closed number carried
unreviewed work onto the trunk against somebody else's evidence. The gate now
requires the branch to have modified its own spec file. Writing the Closing
report into the spec is what closing a spec IS, so an honest close always
satisfies it.

Configuration is read for meaning, not shape. A .claude/sdd.json that is
valid JSON but not a single object (a top-level array, or two documents in one
file, which a half-merged config produces) silently disabled the trunk rule in
both the close gate and the scope hook. So did a "trunk" that is present but
empty, or an array, or an object: jq's // falls back only on null, so an empty
string passed straight through and every branch comparison failed. Both are
refused now. A trunk key that is simply absent still defaults, because not
declaring one is ordinary.

The upgrade path no longer certifies a disarmed instance. The check for "are
the gates wired" matched the hook filename anywhere in your settings, so gates
moved to the wrong hook event, given a matcher that never names the tool they
govern, replaced by a local fork, or merely MENTIONED inside an unrelated
command all certified as a complete refresh. It checks the event, the matcher,
and that the entry actually executes one of the four stamped files.

Every denial carries a code, like [CG-NO-CLOSING-REPORT] or
[CM-INDEX-COMPOUND]. Nothing about the messages changed for you; it means a
reworded explanation can never silently break a test that was matching its
prose.

Known open issues in this release

Four things about this release are disclosed here rather than left for a reader
to discover, because each is something a careful adopter would want to know.

1. This release was not reviewed by an adversarial pass against its exact
bytes.
Five adversarial reviews ran during 1.0.8's development, against five
earlier trees. The fifth found seven ways past the gates, all seven were fixed,
and those fixes changed the bytes the fifth review had read. Re-reviewing would
have changed the bytes again, which is a loop with no exit, so it was ended by an
explicit decision. What stands behind these bytes instead is the full mechanical
evidence set: the hook suite (451 assertions), an outcome oracle that runs
generated commands for real and compares the gate's verdict to what actually
happened in the repository (zero misses across all three gates), mutation testing
that reintroduces each previously fixed defect and confirms the suite still
catches it (8 of 8), a replay of every payload from all five reviews, the hook
smoke tests, and the upgrade-seam check. That proves every KNOWN way past the
gates is closed. It proves nothing about ways nobody has thought of yet.

2. A stale remote-tracking ref can deny an ordinary merge. If you rename a
spec branch (git branch -m spec/0004-thing feature/thing) while a
remote-tracking ref for the old name still exists, the close gate denies
git merge --no-ff feature/thing with CG-SPEC-MISSING. It is a false denial
rather than a way past the gate, so it is disclosed rather than fixed in this
release. The remedy is one command:

git update-ref -d refs/remotes/origin/spec/0004-thing

3. Findings from the last two reviews remain open and are not secret. They
are MAJOR and MINOR rather than blocking: none of them puts unreviewed code on
the trunk. Several are defects in the project's own checking tools rather than in
the gates you install.

4. The gates need a WORKING jq, not merely an installed one. A jq that
exists but fails to run (a broken dynamic link, a killed process, a wrong
architecture) used to make two gates allow silently. That is fixed in this
release: all four hooks now run jq rather than just locating it, and each
distinguishes "missing", "present but not runnable" and "could not parse this
input" so the message names what to fix.

The standing framing has not changed and is worth repeating: these hooks are a
backstop against ordinary mistakes, not a sandbox. Anything that can run a shell
can reach the trunk, and the gates have never claimed otherwise. A merge
performed in a forge's web UI is not reached by them at all. The trunk audit
(scripts/trunk-audit.sh, opt-in) reads history rather than commands and is the
designed catch for that whole family.

One honest caveat about the fixes above. The gates are clean against every
spelling a large generated corpus can produce, which is a statement about the
corpus rather than a proof about the gates. Shell is not a regular language, and
three of the four most recent releases found their bypass in a dimension the
corpus did not enumerate. This release added a new KIND of dimension to it, the
state of the repository rather than the shape of the command, which is where the
worst of this round was hiding.

If you already have a project running Setlist

Run /setlist:upgrade once after updating the plugin. This release changes hook
bytes but not the settings wiring, so the refresh is hook files only.

/plugin marketplace update setlist

If a Claude Code session was open when you ran that update, restart it before
upgrading.

Setlist 1.0.7 (edition v1.6)

Choose a tag to compare

@AlexCiortan AlexCiortan released this 28 Jul 10:25

If you are on 1.0.6, take this one. Four ways past the gates are closed, and
every one of them fires on something a person or an agent types without
meaning anything by it.

A wrapper flag that takes a separate value no longer strands it. 1.0.6
taught both gates to look past a wrapper like nice or env. It stripped the
wrapper word and then stripped flags one word at a time, which is right for a
flag that carries its own value (stdbuf -o0) and wrong for one that does
not. In nice -n 5 git merge --no-ff spec/0001-x, the 5 was left sitting at
the front of the command, the gate stopped recognising the line as starting
with git, and it was never judged at all. env -u VAR git merge ... went the
same way. A flag is now consumed together with the value that follows it, and
the command word itself is never swallowed, so env -i git merge ... keeps
its git.

Discarding working-tree changes no longer looks like switching branches.
git checkout is two commands sharing one name. With a branch it switches;
with a path it throws away local edits and switches nothing. The gate recorded
the argument as a branch either way, so git checkout -- . && git merge --no-ff spec/0001-x concluded the merge would run on a branch named ., decided that
was not your trunk, and skipped every check. Discarding changes right before a
merge is ordinary, so nobody had to be trying. The gate now tells the two
apart: a real branch is tracked, an existing path leaves the branch alone,
anything after -- is a path by definition, and an argument that is neither
refuses rather than guessing. git restore was never affected, and
git switch needs none of this because switch only ever takes a branch.

A bare commit name is refused whatever its shape. Merging a spec branch by
its commit id rather than its name leaves the gate nothing to check, because
the close conditions live in the branch's spec file. It knew that, and
recognised commit ids by their SHAPE: 7 to 40 lowercase hex characters. Every
spelling outside that shape went through instead, including a 6-character
abbreviation and the same id uppercased. Shape was the wrong question. The
gate now asks git whether the word names anything at all, which is free of
shape and also stops a branch that happens to be spelled in hex from being
mistaken for a commit id.

Two spellings of the same file now get the same answer. The scope hook,
which keeps feature code off your trunk, compared paths as text. So
src/app.js was denied while docs/../src/app.js and a path through a
symlinked source directory were allowed onto the trunk in silence. Neither
needs an attacker: .. is what a tool composing paths produces, and symlinked
source directories are ordinary. Paths are now normalised, and where the
filesystem can resolve them they are resolved, with the stricter of the two
readings deciding.

A skill that never loaded now loads. design-surface shipped from 1.0.0
through 1.0.6 with an unquoted colon in its description, which meant its
frontmatter did not parse and the skill loaded with all of its metadata
silently dropped. The release gate that should have caught it was pointed at
the marketplace manifest, which does not read skills at all. The gate now
checks both manifests, and the test suite independently checks that every
shipped skill's frontmatter parses, so this cannot depend on which argument
someone passed to a CLI.

Known limitations gains one entry, which is not a new gap but a newly
described one: the commit gate scans the index of the project it governs, so a
commit aimed at a nested repository or at a named index file is not scanned.
Your own staged content is checked every time. Following the target index
would mean re-deriving which repository each command line means, in every
spelling, and that kind of parser-chasing is what produced two of the holes
this release closes.

Staging is more than git add. The commit gate refuses a command that
writes the index and commits in one step, because it decides before the command
runs and would otherwise scan an index that does not hold your content yet. It
recognised add, rm and mv, so everything else that writes the index went
straight through: git stash pop && git commit -m x and
git restore --staged . && git commit -m x both committed content nothing had
scanned. git stage, a plain synonym for add, was missing for the same
reason. The full set is recognised now, and it is a generated corpus dimension
checked against the gate's own list, so the next missing verb fails the suite
instead of waiting for a reviewer.

A single & separates commands. The gates split a line on &&, ||, ;,
| and newlines, but not on a lone &, which backgrounds what came before it
and starts something new. echo hi & git merge --no-ff spec/0001-x was read as
one command beginning with echo, and the merge was never judged. && is
still one separator rather than two, and an & inside a redirection like
2>&1 still is not one.

A pathspec checkout is not a branch switch, even with a branch in front of
it.
This release already handled git checkout -- .. It did not handle
git checkout other-branch -- src/file, which also restores files and switches
nothing: the gate recorded a switch that never happens, then judged the merge
that followed against the wrong branch. That is the third spelling of one class
to be closed one spelling at a time, so the test is now the -- separator
itself, which is what git uses to tell its own two commands apart.

A closed spec's number cannot be reused to carry unreviewed work. Every
close check reads the spec file as it stands on the branch being merged. That
settles what the artifacts say and not who wrote them. A branch cut from the
trunk after spec 0001 closed inherits that spec, Closing report and all, so a
branch named spec/0001-anything could merge arbitrary changes with no
artifacts of its own and pass every check, and the trunk audit would then call
it compliant because it reads the same inherited artifacts. The gate now
requires the branch to have modified its own spec file. Writing the Closing
report into the spec is what closing a spec is, so an honest close is
unaffected, including the common case of planning a spec on the trunk and
closing it on the branch.

Upgrades check that the gates are actually wired. refresh-instance.sh
verified your hook FILES were current and, separately, that whatever entries
were present in .claude/settings.json were well formed. Nothing checked the
gates were present at all, so an instance with both gate entries deleted was
reported as a complete refresh at exit 0, with a note that the refreshed gates
would bind from the next session. They would never bind again. The same check
also claimed hooks it does not own: a hook of yours living in .claude/hooks/
with no timeout was reported as a Setlist entry, producing an upgrade that
could not be completed because the fix it demanded was editing your own file.
Ownership is by name now, and both directions are covered by the upgrade leg.

Both new bypass classes are now generated corpus dimensions in the test suite
and in the release-time strictness check rather than example cases, so they
stay closed. The suite is at 259 assertions.

Known open issues in this release

An adversarial review ran against the exact tree being published here and found
nine ways past the gates. They are listed rather than quietly carried, because a
backstop you trust more than it deserves is worse than one you know the shape of.

All nine are present in v1.0.6 as well. None is a regression, and every gap
this release closes is closed on top of them, so 1.0.7 is strictly better than
what it replaces. They are queued for 1.0.8; the fix for the largest group is
already designed.

  • Quoted text can hide or forge a git operation. The close gate keeps the
    contents of quoted strings and the commit gate deletes quoted spans, and each
    behaviour is wrong for the other gate. A commit message that merely mentions a
    branch can retarget the close gate; quoting the word git or commit can hide
    a commit from the commit gate; an odd number of quote characters in ordinary
    prose can swallow the rest of the line. Four of the nine are this one defect.
  • A ref spelled an unusual way may not be recognised. The gate normalises a
    handful of prefixes by hand instead of asking git to resolve the ref, so
    heads/spec/0001-x is not seen as the spec branch it is. Related: for a
    remote-tracking ref the gate checks your LOCAL branch of that name, which is
    not necessarily what git is about to merge.
  • Shell compound syntax hides the command. Wrapping a merge in { ...; },
    if ... then, for ... do, or prefixing it with ! moves the git verb out of
    the position the gate looks at.
  • A malformed .claude/sdd.json disables the trunk rule. The parse check
    accepts any valid JSON, including a top-level array or two documents in one
    file, and the hooks then read no trunk and allow everything. A file written by
    hand or half-merged can land in that state.
  • The upgrade refresh does not really verify your gates are wired. New in
    1.0.7 and it does not work: it matches the hook filename as a string anywhere
    in your settings, so a disarmed instance can still report a complete refresh.
    It is an inert check rather than a harmful one (v1.0.6 had no such check at
    all), but do not rely on it. Check .claude/settings.json by eye after an
    upgrade: the scope hook on Write|Edit|MultiEdit|NotebookEdit, and the commit
    and close gates on Bash, all under PreToolUse.

The standing framing has not changed and is worth repeating: these hooks are a
backstop against ordinary mistakes, not a sandbox. Anything that can run a shell
can reach the trunk, and the gates have never claimed otherwise.

If you already have a project running Setlist

Run `/setlis...

Read more

Setlist 1.0.6 (edition v1.6)

Choose a tag to compare

@AlexCiortan AlexCiortan released this 25 Jul 16:16

Two bypasses closed, both found by review of 1.0.5, one of which fired on an
everyday command rather than a clever one.

git checkout - no longer walks past the close gate. The gate tracks
which branch each part of a command line runs on, so that a compound like
git checkout main && git merge --no-ff spec/0001-x is recognised as a close.
It read the branch name as the first argument that was not a flag, and - is
a flag as far as that reading goes, so it was skipped: the gate concluded the
merge was still running on the spec branch, which is a case it deliberately
allows. git checkout - immediately after git checkout spec/0001-x is how a
person and an agent both return to the trunk, so this was reachable without
anyone trying to reach it.

The gate now resolves - and @{-1}. It runs before your command does, so
the previous branch is still there to resolve, and if it cannot be resolved
the gate refuses rather than assuming the merge is harmless.

Wrapper prefixes no longer escape either gate. command git merge ...,
env git merge ..., nice, nohup, exec, and a leading VAR=value
assignment all reached the trunk unchecked, and command git commit -am x
walked past the commit gate in the same way. Both gates now strip those
prefixes before deciding whether a command is one they govern.

That list is not a claim of completeness and cannot be one. The trunk audit is
the designed catch for the wider family, because it reads what ended up in
your history and does not care how the command was spelled.

The two layers are documented as one story. Known limitations described
the Bash escape hatch as a file-writing route without saying that running git
through another interpreter is the same boundary, and the sideways-routes list
did not mention merging a spec branch under a second name. Both are named now,
each is cross-referenced to the trunk audit as the thing that catches it, and
the suite pins the pairing: the interpreter forms pass the gate AND their
outcome is caught by the audit.

The audit's opt-in status is stated plainly in the same place, because until
you install it the four hooks are the only enforcement running, and the hooks
are the layer that can be spelled around.

The test suite is at 203 assertions. Its generated corpus gained two
dimensions it had been missing, wrappers and branch-switching shorthand, which
is why both of these bypasses got through a suite that had just found 144
spellings of their cousins.

If you already have a project running Setlist

Run /setlist:upgrade once after updating the plugin. Nothing in this release
changes the settings wiring, so the refresh is hook files only.

/plugin marketplace update setlist

If a Claude Code session was open when you ran that update, restart it before
upgrading.

Setlist 1.0.5 (edition v1.6)

Choose a tag to compare

@AlexCiortan AlexCiortan released this 25 Jul 15:47

A release about how releases are checked, and one substantial fix that the new
checking found at a scale no human review had.

A merge into your trunk can no longer hide behind the shape of the command
line.
The close gate read a merge's arguments from the text after the LAST
occurrence of the word merge. Two consequences, both live in 1.0.4: a
compound like git merge --no-ff spec/0001-x && git merge main discarded the
first merge's arguments entirely and passed with no close condition checked,
and any commit message containing the word, -m "improve merge of main",
displaced the real arguments and did the same. The second matters most,
because it fires on ordinary usage with no intent behind it: a natural merge
message silently skipped the gate.

The gate now splits a command line on its connectors, judges each segment on
its own while tracking which branch that segment runs on, and reads a merge's
arguments from the first merge token in its own segment. The same treatment
went to the commit gate.

A corrupted config no longer switches the gates off in silence. The hooks
read .claude/sdd.json for your trunk name and role paths. If that file was
unparseable, a truncated write or a bad merge being the ordinary ways it
happens, every value read as empty, and the gates exited without checking
anything: feature code went onto the trunk and merges went through with no
message at all. Having jq installed is not the same as the file being
readable, and the hooks were only checking the former. They now refuse, naming
the file and the command that shows the parse error.

The trunk audit can run at push time, if you want it to. A sample git
pre-push hook ships alongside the audit. Copying it into .git/hooks/ runs
the audit at the last moment your history is still private, which is also the
only place a local hook can notice work that arrived through your forge's
merge button. It refuses the push rather than passing if it cannot find its
own tool, because a check that could not run has not passed. Nothing installs
it for you.

Generated tests, not just written ones. Every previous fix here was pinned
by examples somebody thought of, and every bypass lived in what nobody thought
of. The suite now generates its adversarial inputs: 504 spellings of a merge
naming an unclosed spec, all of which must deny, and a matching inverse corpus
so a gate that denies ordinary work also fails. On its first run it found 144
bypasses where two had been reported, and an over-denial nobody had noticed
(echo git merge spec/... was denied though nothing merges).

A trunk audit, advisory. scripts/trunk-audit.sh reads your trunk's
history and reports any commit that put code there outside a closed spec. It
answers a question no command parser can answer correctly: a shell command can
compute its own arguments, while history simply is what it is. So it catches
by construction what a parser misses, including chained merges, a branch
renamed to hide it, cherry-picks, and your forge's merge button. Nothing calls
it automatically in this release and a finding blocks nothing; run it when you
want to know. It was validated against 148 commits of real project history
before shipping, which is how two false-positive classes were found and fixed
rather than shipped.

Every documented limitation is now pinned or declared. The Known
limitations list and the test suite are kept in step mechanically, in both
directions: a limitation with no test, or a test for a limitation nobody
documented, blocks the release. Limitations that cannot be exercised by a test
suite carry the manual procedure that checks them instead of quietly
disappearing.

If you already have a project running Setlist

Run /setlist:upgrade once after updating the plugin. Nothing in this release
changes the settings wiring, so the refresh is hook files only.

/plugin marketplace update setlist

If a Claude Code session was open when you ran that update, restart it before
upgrading.