Setlist 2.2.0 (edition v1.10)
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 atspecs/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
--squashis 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--ffor
--squashis 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:checkpointdoes for you, avoids both. SETLIST_SKIP_TRUNK_AUDIT=1stops 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
gitunusable 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 honours it. Running the
commit outside a session, or past the advisory verdict, works today.
It is disclosed rather than fixed because the fix is a design decision rather than a chore,
and the decision has an owner who has not made it. The advisory gates live in a different
tree that deliberately does not depend on the git hooks' shared library, so scoping them
means either creating that dependency or writing a second reader of the same declaration.
A second reader is exactly what this feature's own design forbids, since a rule with two
implementations is the failure mode that produced most of this project's worst defects. It
is recorded and pinned by a test in its current state, with a twin asserting that the git
hooks DO accept the same content, so the gap cannot close by accident and cannot be
misread as everything being broken.
2. 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.
3. The scans are early warning, not a guarantee, and the secret scan is a first cut.
Neither claim changed in this release, and path-scoping does not weaken either: it narrows
where the scans look, by declaration, in the open. What stands between unreviewed work and
a shared trunk is the push-time trunk audit, which reads history rather than commands, and
the exclusion set cannot reach it.
4. Five defects this release's adversarial review confirmed, all of them present in
2.1.1 as well, all scheduled for the next release. They are listed with the review's own
identifiers so the next release's notes can be checked against them. None is a regression:
each was measured against the shipped 2.1.1 bytes and behaves no worse there, which is the
condition under which they were allowed to wait.
- F4. An upgrade can refuse to certify a correctly protected instance, forever. If a
hook is wired with the match-all matcher"*", or with the matcher key absent, the
upgrade reports it as NOT WIRED and--applyexits with an INCOMPLETE status every time.
Your protection is real; the certification of it is wrong. Workaround: name the tools
explicitly in the matcher (for exampleWrite|Edit|MultiEdit|NotebookEdit). - F5. A compliant CHORE close spelled
--ffor--squashis accepted at commit and then
refused at push. This is the half of the fast-forward repair that did not land, and it
is why the bullet above is scoped to spec closes. Unchanged from earlier releases rather
than newly broken. Workaround:--no-ff, which/setlist:checkpointdoes for you. - F6. The Architecture-diagram field matches its answer as a substring. A line that
states the opposite of an answer can still satisfy the field at all three layers. The
label is anchored; the answer is not. - F7. The QA verdict reader takes the LAST verdict block in the Closing report. An
illustrative example block placed after the real one replaces it, in both directions.
Keep example verdict blocks out of the Closing report section. - F3. The in-session commit gate goes silent instead of refusing when
gitis unusable.
If git is missing, broken, or the directory is not a repository, that gate reads an empty
staged diff and reports the commit clean, having examined nothing. Its two sibling gates
refuse with an explicit code in this release; this one does not yet. The guarantee is
unaffected: the git hooks still refuse the content, and they are the layer that
enforces. The practical effect is a missing warning inside a session, not a way past the
checks. Workaround: if the in-session gate goes quiet unexpectedly, run
git rev-parse --git-dirto see whether git is usable at all.
All five are committed to the 2.3.0 cycle, which owes its own adversarial review
regardless. That is the same commitment 2.1.0 made about its own disclosure and this
release kept: the fenced-template false denial 2.1.0 disclosed as item 1 is fixed above.
The commitment is repeated deliberately, and it is checkable: if 2.3.0 ships without them,
this paragraph is the evidence.
5. Three spec readers disagree about HTML comments opened mid-line. A Closing report
and its readers can be made to disagree about what is commented out. It is documented
rather than fixed because closing it means changing what the readers parse, and this
project does not change parser behaviour to chase a crafted spelling. Write HTML comments
on their own lines.
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.