queue-kit — a git-native, agent-readable task tracker
One Markdown file is the tracker: sections are queues, bullets are tasks,
bold kebab-case slugs are the task handles, and square-bracket tags are the
state machine. The problem the kit solves: a coding agent selects work by
parsing, not by reading — so everything selection trusts (position, slugs,
tags) is grammar a gate can enforce, and everything a human writes freely
(task prose) is kept out of the parse path. Drift between what the prose says
and what a parser sees is the failure mode; all but two of the gates exist to
close one instance of it each. The two exceptions hold a different axis —
projection freshness (check-roadmap-fresh) and the deferred pool’s
per-entry budget (check-queue-entry-budget).
The kit carries the queue grammar and its gates only; a consumer’s section names, wrap budget, and protocol vocabulary are config, with this repo’s layout as the defaults. Requires gate-sdk (the gates follow its four contracts and resolve through its registry).
The queue format
The governed surface is one queue file (default TASK-QUEUE.md), structured
as ## sections over column-0 bullets:
- Active sections (default
New Features,Technical Debt) — the pickable queue, in work-order. Selection discipline: pick the first entry carrying no[blocked-by:]tag, in section order; do not invent work order. - The deferred section (default
Deferred) — parked tasks, excluded from selection;###subsections are presentation, not semantics. An entry’s prose may carry aSurfaced <date>mark — an ungated convention recording when the premise was observed. A deferred body is free prose, and four bold-lead-in fields recur, each answering a question a later scope asks:Deliverable(what landing looks like),Why [design-pending](what the open design actually is),Cost while deferred, and a closingFiled <date> by <stage>provenance line. Three are conventions no gate reads;Cost while deferredis required — the Gap-disposition rule’s costing, held bycheck-queue-entry-budget(§check-queue-entry-budget), which also caps the entry’s total length. The field’s bold lead-in is line-local like a tag: split across a reflow it is invisible to the scanner and the entry reads as uncosted. - The icebox section (optional,
QUEUE_KIT_ICEBOX_SECTION, default empty — no section, no tier) — dormant tasks compressed to one line each, sitting after the deferred section and before the done section. Live work, not history: §The icebox tier owns the tier, its grammar, its eligibility rule, and the position contract a cross-kit reader depends on. - The done section (default
Done) — the record that an entry has left the live pool: one line per exit, the bare slug only, with prose about what happened living in git history. An entry reaches it when a landed unit or a closed ruling has mooted it, or as a ruled wontfix. It is not a delivery claim, and never was — delivery is recorded by the landing commit’s type, which is where every reader that cares about it already looks (§The icebox tier enumerates the exits; drift-kit/SPEC.md §Bundled KPIs owns what the commit-type reading yields for an exit that shipped nothing). The grammar is unchanged — a bare slug line, no tag, no disposition token — so every existing reader parses exactly what it parsed before. What widened is which entries may lawfully arrive, not how they are written.
The defer date of an entry is its Surfaced <date> mark when present, else
the date on its Filed <date> provenance line. One definition, two readers —
drift-kit’s deferred-age KPI and the queue-index arm’s --icebox-candidates.
Widening, not replacing: Surfaced records when the premise was observed and
is the better premise-rot datum, Filed is the honest available fallback, so
no entry owes a migration. drift-kit re-implements the definition rather than
sourcing lib/queue.sh, because a kit dependency the other way would close a
cross-kit cycle; both implementations cite this section. That is the general
rule for this format, not a one-off for drift-kit: a kit that cannot depend on
queue-kit re-implements the predicate and both ends cite this section.
gate-sdk’s check-gate-exemption-tasks re-implements it for a reason stronger
than a cycle — gate-sdk is the substrate every kit vendors, so depending on
queue-kit would invert the layering and make a queue format a precondition for
running any gate. The holder roster and the residue accounting for the whole set
live at gate-sdk/SPEC.md §check-gate-exemption-tasks, and are deliberately not
restated here: two hand-maintained copies of one roster are worse than one,
because a holder added or dropped is then owed two edits in two SPECs and only
the one that prompted the change is likely to get it.
- Any other section (an iteration header, a lessons section) is outside the grammar and ignored by every gate except the file-wide hygiene axes.
Where this format has two holders, what is compared between them is classification over a corpus, never the derived literals. The shell library exposes ERE strings and an array; its compiled counterpart exposes a section value and free predicates over it, so there is no value either could hand the other — comparing them would compare nothing, or force one side to grow an accessor whose only reader is the test. The comparable question is the one this section defines: given the same queue file, do both agree on which lines are section boundaries, which are task bullets, which is the lessons line, which slugs are live? Two witnesses held to each other over a golden corpus, with no committed expected file — a maintained golden is a third copy to drift, and the failure the comparison exists to catch is one side edited without the other, which it catches directly (§lib/queue.sh names the holder).
An entry is a column-0 - **slug** — prose… bullet; continuation lines are
indented, never column 0. An indented bullet with a bold lead-in is a
sub-task (same grammar); with a plain or italic lead-in it is a prose note
and is left alone. Slugs match [a-z0-9][a-z0-9-]* in one global, unique
namespace across active + deferred + icebox + sub-tasks — a slug is the task’s
stable handle for its whole life: [blocked-by:] references it and the
done-section line carries it verbatim. The slug grammar is kit mechanism, not
config.
Section order sequences an iteration; it does not rank it. The default
("New Features" "Technical Debt") is read as selection order (§Layout and
configuration), and the argument for it is not that features outrank debt:
position starves nothing, because an active entry’s residency is one iteration
by the drain rule — the drain stage’s entry requires the active sections empty
(lifecycle-kit/SPEC.md §check-stage-entry) — so every promoted entry lands in
the same iteration whichever section holds it. What ranks is the promoting
stage’s decision to put an entry in an active section at all; order within them
is a sequencing default, the posture the roadmap projection already states
outright for queue order generally (§The roadmap arm).
Features lead on the feature/debt asymmetry canon-kit owns (canon-kit/SPEC.md §The amendment lifecycle): a feature merges an amendment into the canonical spec, debt converges an implementation on the spec as it already stands. Reversed, a debt entry sharing an owner doc with a sibling amendment converges on text the same iteration is about to rewrite. It stays a default rather than a rule — the array is consumer config, and a consumer whose iterations do not mix the two kinds reorders it freely.
The icebox tier
The deferred pool has an intake asymmetry the delivery doctrine itself creates: gap disposition and scope-gated intake make filing mandatory and cheap, while the only exit is building the entry. The icebox is the missing exit — a third live task section holding entries whose own cost field says they are dormant, each compressed to its lead line.
close-surface: TASK-QUEUE.md#Deferred advisory
advisory is the honest mode: no structural forcing function refuses on an
untriaged backlog, and inventing one would redden an unrelated commit over a
curation opinion. That is what makes eviction symmetric to promotion —
promotion is forced by the scoping stage’s exit condition, eviction gets roster
visibility and an auditable skip.
It joins the shared task adapters rather than standing outside them, and that is the whole design:
- Eviction is a conserved move.
check-task-conservationdiffs the live slug set at HEAD against the worktree, so deferred → icebox keeps the slug live and the gate stays silent. No sanctioned disappearance needs inventing, and none exists — every exit from the design-pending pool is conserved: deferred → icebox, icebox → deferred on real recurrence, and either → done for a ruled wontfix, or for an entry a landed unit or a closed ruling has mooted. The added route is conserved like the rest — the slug stays visible in the file, on a bare done line — which is why the claim above is strengthened by it rather than disturbed. - One namespace, one parse.
check-task-nameskeeps slug uniqueness and[blocked-by:]resolution across the whole pool; an iceboxed slug is a legal blocker target, because it is unbuilt. - The living-prose contract keeps reaching it, so a page citing an iceboxed task stays green — correct, because the task is dormant, not landed.
- Tags stay lead-line-governed, with no gate edit.
Every gate keyed on the shared task regex therefore widens with no change to
any gate file — the widening is entirely in lib/queue.sh, which is what the
one-adapter rule was built to buy. check-queue-prose-precondition deliberately does not
reach the tier: forward-looking phrasing is normal vocabulary in a parked
entry, the same exemption the deferred section already carries.
A mooted entry takes the done exit, not this one, and the two routes into it are worth naming because neither reads like the criterion that covers it. A slug merged away as a duplicate — two entries found to own one gap, one of them kept — is an entry a landed unit has mooted: the first criterion exactly, not a new class. A slug mooted by supersession, where the work became unnecessary rather than being folded into a sibling, is covered by whichever criterion did the mooting — a landed unit that made it unnecessary, or a closed ruling that did. Both routes are instances and neither is a class needing its own state. They are enumerated because the criteria are phrased as supersession, so a session holding a duplicate merge will not recognise itself in them.
Distinct from this tier, which is the reason a merged duplicate does not land here. The icebox holds unbuilt work that stays promotable and dormant. An absorbed duplicate is not dormant, it is redundant: there is nothing to promote it back to, and returning it to any live section would re-open the namespace collision the merge closed. The done exit is what closes that collision, because a slug reaching Done leaves the live namespace (§check-task-names).
No disposition token marks which route was taken, and that is a refusal
rather than an omission. It would have no reader:
check-task-conservation reads membership, drift-kit’s split KPI reads the
landing commit, and a human reads git history. A field with no named reader is
removed rather than added.
The grammar is the lead line and nothing else:
- **<slug>** [design-pending] — <one sentence: what it is, and why it is dormant>
- It carries the same
[design-pending]tag every deferred entry carries. No[icebox]tag is minted: section membership is the state, and a tag restating its own section is the two-sources defect. What generalizes across the amendment lifecycle is the design-pending section set, not its tag set. The “why it is dormant” clause is written where it fits and otherwise left to the tier — membership already declares it. - No
###subsections and no sub-tasks. Grouping is presentation on a surface whose entire purpose is minimum residency, and a sub-task is a continuation line the shape rule rejects. - A
[roadmap:]-tagged entry is not icebox-eligible, and the rule needs no new code: the roadmap parse walks the live task sections, so the tier enters its walk, andcheck-roadmap-freshthen reds any[roadmap:]entry carrying noroadmap-summary:declaration — which a one-line entry has nowhere to put. The enforcement is conditional: that gate exits clean at its top whenQUEUE_KIT_ROADMAP_FILEis empty, so the guarantee holds for a consumer publishing a roadmap page and degrades to convention for one that is not. The eligibility rule binds either way. Keeping the tier inside the roadmap walk rather than excluding it is deliberate: excluded, a[roadmap:]tag that drifted into the icebox would silently drop a public commitment; included, it reds. - The narrative is recoverable, not discarded. The commit that iceboxes an
entry necessarily contains the removed body in its own diff, so recovery is
git log -p -S'<slug>' -- <queue-file>— a pickaxe that depends on no commit-message convention. Nothing is copied into the queue to point at it. Recovery is mandatory before any ruling on the entry, not merely available: the lead line is a one-clause summary that carries neither the deliverable nor the blocking design question, so promoting a dormant entry, or ruling a build has subsumed one, is judged against the recovered body. Ruling off the line alone is ruling off a summary written to be dropped.
Eligibility, judged at the closing stage: the entry’s cost field opens in
the low class; it carries no [roadmap:] tag; and it has no live promotion
trigger. What counts as live was narrowed 2026-08-23 by operator ruling,
on a measured pool where the prior reading let almost nothing qualify: a live
trigger is a live, unbuilt queue slug the entry names, a dated recurrence:
line, or a [roadmap:] tag. A trigger that is itself gated on launch — a first
external adopter, a preview cohort, an install count — is dormant: the entry
waits in the icebox until the event occurs and returns on it, which is the round
trip the tier already conserves. The cost field’s opening token is the class word the
--icebox-candidates arm reads (low, zero, bounded, cosmetic), so a
cost field that opens in prose has declared no class and is read as not-low;
authoring the class word first is the contract, and the gate that holds it is
owed — filed 2026-08-23 into the gap inbox, drained by the next close.
Direct filing into the icebox is permitted on one condition — ruled 2026-08-23, reversing the prior rule. The prior rule held that nothing files directly into the icebox because a newly filed finding has never been triaged. What changed is who triages: the closing stage’s drain is the triage, and a drained bullet whose cost opens in the low class and names no live trigger may land as a one-line icebox entry without first being written as a deferred body that the next close compresses. The net-delta KPI stays honest because the arm counts the tier on the way in. A mid-iteration filing still goes through the gap inbox; what this permits is the drain landing it one tier lower.
Position is a contract, not a preference. The read order is pickable →
parked → dormant → history, and a cross-kit reader depends on the tier sitting
between the deferred and done sections (gate-sdk/SPEC.md
§check-gate-exemption-tasks). Accepted residual, and it is quiet: no gate
enforces section order. check-queue-sections counts occurrences per
required section with no positional state, and no other gate carries one. A
misplaced icebox surfaces only as an exemption naming an iceboxed slug
reddening with “no live task” — which requires such an exemption to exist.
Otherwise the misplacement is silent. The contract is carried by this SPEC and
by review rather than by a gate, and saying so is the honest form of it.
The tag algebra
Tags are square-bracket literals with fixed spelling (mechanism, not config
— the one exception is the consumer-named harvest vocabulary below); every tag
sits on its bullet’s lead line — the only line the parsing tools scan
(enforced by check-tag-lead-line).
Outside the queue file, living prose claims a task’s queue membership with one
grammar: a bold-code token — **`<slug>`** whose token matches the
slug grammar — which check-queue-slug-liveness resolves against the live
slug set on the configured prose surfaces (§check-queue-slug-liveness).
Inside the queue file, that same token in single backticks — matching the
slug grammar, appearing in an entry’s body rather than on its lead line —
is a citation of the named entry: the relation an author already drew while
writing about something else. The two forms are deliberately different claims.
The bold-code form is a membership claim, and a dead one is a false statement
check-queue-slug-liveness reds on; the in-body citation is a reference,
aggregated by bin/queue-edges.sh (§bin/queue-edges.sh) and audited by
nothing. The rules below make citations parseable, each covering a shape a live
corpus contains:
- Resolution is against the live slug set —
queue_live_slugs: active, deferred, and a configured icebox, the existing source of truth — and against the retired one, the slugs the file’s own history shows were once live (§bin/queue-edges.sh). A citation resolving to a retired slug is an edge, marked retired; the two sets are disjoint by construction. - An unresolved token is not an error; it is simply not an edge. This is the load-bearing rule. Entries legitimately name landed work — a closed defect class, a shipped contract, a settled ruling — and that citation is valuable prose no gate may punish, which is why no liveness assertion reaches inward from the configured prose surfaces (§check-queue-slug-liveness). What the remainder actually contains was measured, and the measurement is why no report over it and no gate on it can exist. After the retired slugs are taken out, what is left is overwhelmingly not citations at all: commit SHAs, content digests, iteration names, KPI and gate names, runner labels, CI job names, doctrine rule names, and ordinary backticked words a slug-shaped grammar cannot tell from a slug. On a mature corpus that remainder is the large majority of unresolved tokens. A listing of it is unusable and a red on it would fire on prose that is correct.
- Whether a citation of landed work should be distinguishable in prose at all — no, and the answer follows from the two refusals below rather than adding a third. A prose marker separating landed from live is the maintained roster under a new spelling: it would need writing on every citation a corpus has already written, and re-writing on every disposition. The distinction is derived instead (§bin/queue-edges.sh’s retired block), which is the form both refusals leave open, and no author writes anything.
- Self-citation is not an edge — an entry naming its own slug in its own body is narration, not a relation.
[blocked-by: <slug>]is an edge too, and the one already-structured class: including it makes an inbound set complete rather than merely prose-derived.
No relational vocabulary is declared, and that refusal is the design. Prose
phrases a relation many ways and keeps inventing more, so an enumerated verb set
would be brittle the day it landed — and, decisively, a kit literal spelling one
project’s relational verbs would ship that project’s vocabulary as everyone’s.
That is the provenance seam, the same reason [roadmap:]’s horizons and tracks
are consumer-configured arrays rather than kit literals. Declaring no vocabulary
means there is no vocabulary to leak and no consumer config to invent: a
relation’s kind rides the citing line itself, quoted verbatim
(§bin/queue-edges.sh).
A relates: <kind> <slug> declaration on the roadmap-summary: pattern was
weighed and refused. It is precise and it carries a kind — and it is wrong
here on three counts. Every citation a corpus has already written would need
hand-authoring, the maintained-roster anti-pattern derivation-first forbids;
those lines would land inside entries measured against
check-queue-entry-budget’s raw-line cap, where sub-tasks do not relieve a
parent’s budget, so precision would be paid for in evictions; and a
hand-declared edge can be forgotten in exactly the moment it matters, whereas a
citation written in prose is written because the author was already thinking
about the relation. The derived grammar costs zero lines in every entry and is
satisfied by any corpus that already cross-references — no migration, and no
adoption step between landing it and reading it.
[blocked-by: <slug>]— the entry is unpickable until<slug>completes. Repeat per blocker. Must resolve to a live task (active or deferred — a deferred blocker stands; it is unbuilt); a blocker in the done section is a stale tag that must be removed, because the tag alone marks a task unpickable.[design-pending]— design-pending marker, spanning both design-pending sections (deferred and, where configured, the icebox). queue-kit parses and displays it; the placement semantics (section-wide enforcement, promotion rules) are canon-kit’s amendment lifecycle and land with that kit.[spec: <file>]— spec-ready pointer. Same split: syntax here, amendment semantics in canon-kit.[drain-exempt: <reason>]— drain-stage residue marker: the entry legitimately stays active into the drain stage (a drain-spanning feature whose remaining half is drain-stage work). Same split: syntax here; placement semantics — the drain-entry exemption, the non-empty-reason grammar, the successor-entry backstop — are lifecycle-kit’scheck-stage-entryassertion B. The Done move takes the tag with it (done entries drop their lifecycle tags; every reader scans live task sections only). The lead-line reason is a terse marker, not the audit trail:check-tag-lead-linepins the tag to the lead line whilecheck-queue-wrapcaps its width, so a long slug can squeeze the reason to a few columns — keep it a keyword and carry the full exemption rationale in the entry body.[precondition-ok: <reason>]— per-entry opt-out valve forcheck-queue-prose-precondition.[roadmap: <horizon>/<track>]— public-projection marker: the entry is curated onto the generated roadmap page (§The roadmap arm), under<horizon>and labelled<track>. The tag’s spelling is fixed mechanism; its two field values are drawn from the consumer-configuredQUEUE_KIT_HORIZONSandQUEUE_KIT_TRACKSarrays — the same fixed-spelling/configured-values split[spec:]already carries, and for the same reason: a kit literal spelling one project’s horizons would ship its roadmap posture as everyone’s. One tag with two slash-joined fields rather than two tags, because the fields are never independently meaningful — a track with no horizon has no slot on the page and a horizon with no track has no label in it, so splitting the pair would invent two governed names where the domain has one. An entry carries at most one[roadmap:]tag; an untagged entry is simply not projected, the normal case. Placement is unconstrained by section — an active entry and a deferred entry may both be projected, because the projection is about public direction, not selection order. The icebox is the one exclusion, and it falls out of the declaration rule rather than needing one: a one-line entry has nowhere to put aroadmap-summary:, so a[roadmap:]tag that drifted into the tier reds on assertion C whereverQUEUE_KIT_ROADMAP_FILEis set (§The icebox tier states the knob-conditional limit on that backstop). Field validity ischeck-roadmap-fresh’s assertion B.
A tagged entry also carries a roadmap-summary: <text> declaration — one
indented line in the entry body holding the single sentence the public page
prints. It is a declaration, not a tag, and the difference is the point: a tag is
bracketed and lead-line-scoped because its readers scan lead lines alone, while
this is read off a line of its own, the shape the close-surface: declaration
above already uses (and QUEUE_KIT_PROSE_LEADS’ Protocol: token at column 0).
Spelling is fixed mechanism; check-tag-lead-line does not govern it, and it
cannot collide with the [roadmap: scan, which keys on the bracket.
The declaration is a whitelist, and that is a privacy boundary before it is a
design one. Only text an author explicitly marked is ever projected, so the
mechanism’s failure mode is a thin page rather than published internal prose —
the direction a consumer whose queue is private and whose roadmap is not can
afford to fail in. The emitter reads the text verbatim, so no sentence heuristic
decides what a public page inherits. One line, not a block: an indented
declaration has roughly 81 columns behind it under the default wrap budget —
comfortably a sentence, and needing no block-end rule a reflow could silently
truncate. check-roadmap-fresh’s assertion C makes the tag and the declaration
mandatory for each other in both directions.
An entry that has been re-filed carries a second declaration on the same
pattern, recurrence: <slug> <YYYY-MM-DD> [<YYYY-MM-DD>…] — one indented body
line naming the entry’s own slug, then one date per re-filing, appended in order
and never rewritten. The initial filing is not a recurrence and is not listed, so
the count is the number of dates. It is a declaration rather than a tag for the
reasons the paragraph above gives: its readers scan a line of its own, and it
marks no move across a pending/ready boundary, so it fails canon-kit’s
further-tag test. check-tag-lead-line does not govern it, and it cannot collide
with the bracket scans.
The self-naming slug field is mechanism, not decoration, and it is the rule
for every body-line declaration this format defines rather than for
recurrence: alone, so a declaration added later inherits it instead of
re-arguing it. check-queue-hygiene
rejects any exact-duplicate non-blank line across the whole file, unnormalized, so
a slug-free recurrence: <date> on two entries stamped the same day would red the
gate — and same-day recurrence on two entries is exactly the case the declaration
exists to record. Naming the slug makes the line unique by construction. It also
makes the declaration resolvable by one anchored grep with no entry-boundary
parsing, which is what lets its two readers — lifecycle-kit’s scope pre-emption
rule (lifecycle-kit/SPEC.md §Layout and configuration,
LIFECYCLE_KIT_RECURRENCE_THRESHOLD) and drift-kit’s kpi-incident-recurrence
(drift-kit/SPEC.md §Bundled KPIs) — each read it without depending on the other’s
kit. Self-citation is already narration rather than an edge (above), so the field
costs no new rule. That one-grep property is also why no queue-index
recurrence mode exists — the obvious home, refused: drift-kit could not call it
without the cycle its own KPI already records, which would leave lifecycle-kit’s
rule as the only caller, and §The queue-index arm already resists a further mode on
the grounds that folding jobs together gives one tool two output grammars.
One line with appended dates, not one line per recurrence. The per-recurrence
form is byte-unique on the (slug, date) pair and is refused anyway: it grows an
entry linearly against check-queue-entry-budget’s cap, so a machine writer
could push a deferred entry past that cap and red some later, unrelated session’s
commit — and the cap’s remedy is only half self-served (§check-queue-entry-budget:
relocating grounds into an entry that already owns their subject is self-served
for a mandated write, while minting a new entry to hold them stays
authorization-gated). That ground survives the discount §check-queue-entry-budget
grants this declaration, because the discount is one line per grammar: the
second and later lines of a per-recurrence variant match the grammar already
discounted and are counted like any other, so the variant still grows an entry
linearly against the cap. The single-line form costs no
counted line at all, which strengthens the case for it rather than weakening it.
Its own ceiling is
check-queue-wrap’s budget, reached after a handful of dates on a long slug, and
reaching it is the correct complaint: a slug recorded as recurring that many
times without anyone promoting it is a governance failure, surfaced loudly rather
than absorbed.
Unlike the relates: declaration refused above, no corpus needs migrating. The
line is session-written under judgment — the closing stage’s gap-inbox drain
is its only mechanized producer, while any session that judges a recurrence is
obliged to stamp one directly, and each date records that session’s ruling that
the finding re-occurred, read off the filed prose rather than derived from it
(lifecycle-kit/SPEC.md §The committed gap inbox owns the rule and the standard) —
and hand-read; an entry with no declaration is simply an entry that has not
recurred.
A ruling recorded onto the entry it rules carries a further declaration on
the same pattern, ruled: <slug> <authority> <YYYY-MM-DD> <channel> — one
indented body line naming the entry’s own slug, then the authority that ruled,
the date it was taken, and a keyword naming how the authorization reached the
recording session. One line per ruling, appended and never rewritten; rulings
sharing an authority and a channel share a line on the recurrence: model, and
a ruling that shares neither takes a counted second one. It is a declaration
rather than a tag on the same further-tag test recurrence: clears — its
readers scan a line of its own and it marks no move across a pending/ready
boundary — so check-tag-lead-line does not govern it and it cannot collide
with the bracket scans. The date form is recurrence:’s, so one date grammar
serves the file.
<authority> is required and has no default, and that requirement is the
whole of the grammar. Every other way a queue entry records an authorization
states only how one arrived, so a ruling a party took on its own authority and
one it relayed from elsewhere are written identically. The expensive direction is
the second: a ruling recorded as coming from the authority a later session may
not overrule alone freezes a decision that should have stayed re-rulable, and it
freezes it silently. A party’s own ruling is therefore written naming that party,
never left unmarked — an absence reads as nobody claims this, and
gate-sdk/SPEC.md §check-gate-fail-closed carries a worked instance of what
repairing a misattribution after the fact costs. <channel> is a keyword and
not a sentence, for the reason [gate-exempt:]’s reason keyword is one:
check-queue-wrap’s budget is the line’s ceiling and free prose reaches it
immediately, while the full account of how a ruling arrived has its reader in the
entry body already.
The authority vocabulary is the consumer’s and no kit enumerates it.
Governance roles are a project’s own vocabulary, and a kit literal spelling them
would ship one project’s posture as everyone’s — the provenance seam. The kit
ships the slot: position, requiredness, one line, self-naming. Which
authorities exist is named on the consumer’s own always-loaded surface, where a
session already learns its governance roles. No knob is minted, because no
kit mechanism reads the values, and a knob whose only reader is
check-knob-citation is a knob that should not exist.
Presence is deliberately ungated, and the residual bound is stated here rather
than left to be inferred. No gate asserts that an entry claiming a ruling
carries the declaration. Its one machine reader is §check-queue-entry-budget’s
discount, so a malformed line is simply not discounted — a soft, self-correcting
consequence rather than a red. A presence gate would demand a declaration on
every entry already carrying ruling vocabulary, and for the older ones the
provenance is unrecoverable: the recording session’s transcript is gone, so
the only way to make such a gate green is to invent the fact it audits, which
is the precise failure the declaration exists to prevent. Grandfathering by date
would instead leave a permanent unexplained boundary in the file. recurrence:
has no presence gate either, on the compatible ground that a declaration written
under judgment is not one a scanner can demand — which is also why the grammar
lands with no retrofit: existing entries are not back-filled.
What the line does and does not buy, stated because a claim without its bound is the defect this declaration repairs. It is still the relaying party’s own word about a channel the tracked record cannot reach, so it raises the cost of inventing a ruling and does not make an invented one detectable. No governed surface may state it as verification. What it closes is the direction the unstructured practice missed: an unattributed ruling stops being indistinguishable from one taken at the highest authority, and a party ruling on its own authority gains a first-class way to record that rather than an absence. Its reader is the auditing one — a security review, a later session, or an operator reading the tracked record at the moment a recorded ruling is relied on or re-ruled.
Two tags ride Lessons Learned entries — a lesson is a top-level bullet
under the fixed-spelling ## Lessons Learned heading, and the queue-index arm
plus check-tag-lead-line read that section’s lead lines too. That section is
one of close’s inbound triage surfaces, and its forcing function is the sibling
of the gap inbox’s — the first-stage entry refuses while it holds entries — so it
declares itself on the close-surface roster here:
close-surface: TASK-QUEUE.md#Lessons-Learned forced=lifecycle-kit/SPEC.md §bin/enter-stage.sh
[attend]— fixed spelling, kit mechanism (the inbound channel): the filing session marks a lesson as a live attention point for later sessions of the same iteration. Thequeue-indexarm emits an attention block of[attend]lead lines (§The queue-index arm); the injection dies at the iteration boundary because lifecycle-kit’s first-stage entry refuses a non-empty Lessons section.- Harvest tags —
QUEUE_KIT_LESSON_TAGS(array of bare tag names, default empty) is the one configured exception to fixed spelling (the outbound channel): the tag names, their sinks, and their handling are consumer rule content — a kit literal carrying them would publish a private vocabulary (the provenance seam,check-graph/graph-vocabpattern). The close ritual streams a tagged entry’s body throughbin/lesson-sink.sh <tag>(§bin/lesson-sink.sh), which resolves the sink fromQUEUE_KIT_LESSON_SINKS; the tracked close skill names that mechanism, never a sink value. queue-kit only parses the tag’s placement.
Layout and configuration
The kit is vendored beside gate-sdk (conventionally at queue-kit/); its
gates are registered in the consumer’s gates.list by name and resolve
through gate-sdk’s multi-kit path. The queue-index arm is not among them: it
is a non-gate arm of the binary, registered in no gates.list and carrying no
.gate descriptor (§The queue-index arm).
Config follows lifecycle-kit’s pattern: copy templates/queue-config.sh
into the gates dir as queue-config.sh (or point QUEUE_KIT_CONFIG_FILE
elsewhere) and override any knob; defaults fill what the consumer left unset,
and the loader exits 2 on a malformed config — a broken grammar must not gate
anything. A gitignored queue-config.local.sh beside that file sources last
(§lib/queue.sh) — the home for a private value a tracked config cannot carry.
Knobs:
QUEUE_KIT_QUEUE_FILE— default${GATE_SDK_QUEUE_FILE:-TASK-QUEUE.md}; every gate also takes the file as$1(fixture capability).QUEUE_KIT_ACTIVE_SECTIONS— array, default("New Features" "Technical Debt"), order = selection order (§The queue format argues the default).QUEUE_KIT_DEFERRED_SECTION/QUEUE_KIT_DONE_SECTION— defaultsDeferred/Done.QUEUE_KIT_ICEBOX_SECTION— default empty: no icebox tier at all, and no kit-shaped empty section on the consumer’s queue. A twelve-entry backlog has no carry problem, and shipping the section anyway would leak the same postureQUEUE_KIT_HORIZONSandQUEUE_KIT_PROSE_SURFACE_GLOBSalready ship empty to avoid. Naming the deferred section is malformed config.QUEUE_KIT_ENTRY_LINE_CAP— positive integer, default50; the per-entry line capcheck-queue-entry-budgetassertion A holds over the deferred section.QUEUE_KIT_ICEBOX_AGE_DAYS— positive integer, default30; the defer-date age filter for thequeue-indexarm’s--icebox-candidates, and nothing else.QUEUE_KIT_WRAP_BUDGET— default100(check-queue-wrapgate floor).QUEUE_KIT_PROSE_LEADS— array of column-0 lead tokens exempt from the hygiene gate’s no-prose axis, default("Protocol:").QUEUE_KIT_PROSE_SURFACE_GLOBS— array of repo-root-relative globs naming the living-prose surfacescheck-queue-slug-livenessscans, default empty (no surface makes queue claims until the consumer opts one in). Which pages claim queue membership is the consumer’s editorial posture — a kit literal would presume a docs layout — so the kit ships none; a bold-code token on a named surface is then a checked membership claim (§check-queue-slug-liveness).QUEUE_KIT_PRECONDITION_REGEX— the forward-precondition trigger set forcheck-queue-prose-precondition, default = the shipped phrase set.QUEUE_KIT_REQUIRED_SECTIONS— array of##headings that must each appear exactly once (check-queue-sections), default = the iteration header (Iteration:, prefix-matched for its dynamic suffix) plusNew Features/Technical Debt/Deferred/Done/Lessons Learned. A trailing:marks a prefix-matched heading; every other entry is matched exactly. A configuredQUEUE_KIT_ICEBOX_SECTIONis appended by derivation, never by a second consumer list: an icebox configured but absent from the file would otherwise let every icebox assertion pass open on a section that is not there, the exact fail-open classcheck-queue-sectionsexists to stop.QUEUE_KIT_LESSON_TAGS— array of bare harvest-tag names, default empty; the consumer-named outbound lesson vocabulary (§The tag algebra), read bycheck-tag-lead-linefor placement. Names, sinks, and handling are consumer rule content — the kit ships none.QUEUE_KIT_LESSON_SINKS— associative array, harvest tag → sink command, default empty; read bybin/lesson-sink.sh, which owns resolution and the fail-open default (§bin/lesson-sink.sh). A private sink value belongs in the local overlay, not this tracked file.QUEUE_KIT_ATTEND_CAP— positive integer, default3; the maximum[attend]lead lines thequeue-indexarm emits in its attention block before folding the rest into an overflow note.QUEUE_KIT_HORIZONS— ordered array of horizon names for the[roadmap:]tag’s first field, default empty. The order is the projection’s emitted section order.QUEUE_KIT_TRACKS— array of track labels for the tag’s second field, default empty.QUEUE_KIT_ROADMAP_FILE— repo-relative path of the projection page, default empty: no page, andcheck-roadmap-freshskips clean.QUEUE_KIT_ROADMAP_MARKER— the marker-block token, defaultroadmap, delimiting the generated span as<!-- roadmap:begin -->/<!-- roadmap:end -->. One knob with two readers — the--writesplice and the gate’s block locator — which must agree, so it resolves through this loader rather than being re-defaulted in each.
The roadmap vocabulary is configured as a pair: QUEUE_KIT_HORIZONS or
QUEUE_KIT_TRACKS set while the other is empty is malformed config and
lib/queue.sh exits 2, per the loader’s broken-grammar contract. A
half-configured vocabulary would silently accept every value of the
unconfigured field. Both empty is the unconfigured default — a consumer that
publishes no roadmap gets a clean skip rather than a kit-shaped one.
Cross-kit note: lifecycle-kit’s LIFECYCLE_KIT_ACTIVE_SECTIONS carries the same
default. The knobs are independent (either kit runs without the other); a
consumer renaming its active sections sets both. The icebox section name spans
every kit that reads the tier, on the same independent-knob shape —
QUEUE_KIT_ICEBOX_SECTION here, CANON_KIT_ICEBOX_SECTION for the
amendment lifecycle, DRIFT_KIT_ICEBOX_SECTION for the queue KPIs — so a
consumer enabling the tier sets each of them, and one left unset degrades that
kit to “no icebox” rather than to a wrong section.
Per-component contracts
lib/queue.sh
The sourced config loader plus shared adapters: the section-regex builders
the gates pass to awk (both sides of every section boundary must parse
identically — a shared adapter removes that drift axis), and the slug/tag
extraction helpers. Values and adapters only, never gate structure
(gate-sdk’s lib/gate.sh rule).
The loader validates what it loads: an empty or non-numeric knob is a broken grammar, and the roadmap vocabulary is validated as a pair (§Layout and configuration) rather than per-array, because the failure the check exists to stop is one array set alone. Validation failures are collected and reported together, then exit 2.
The icebox is derived here rather than configured twice: a non-empty
QUEUE_KIT_ICEBOX_SECTION joins both the shared task regex and the effective
required-sections set, and when it is empty every icebox regex is the empty
string so each reader degrades to “no icebox” rather than to “every section”.
That degradation is the reader’s to complete, not the library’s. The empty
regex only carries the “no icebox” answer; an awk consumer must test it
explicitly (iceboxre != "" && $0 ~ iceboxre), because awk’s $0 ~ "" matches
every line and an unguarded match therefore degrades to exactly the “every
section” reading the empty string was chosen to avoid. Any reader of an
optional section regex owes that guard.
QUEUE_TASK_SECTIONS is that composition itself — the active sections, the
deferred section, and a configured icebox, in configured order — exposed rather
than consumed and discarded. QUEUE_TASK_RE is built from it, and a reader that
needs the sections individually rather than as one alternation
(§bin/queue-counts.sh) reads the array. One composition, two shapes of the same
answer: a second reader recomposing the set from the three knobs is the drift
axis a shared adapter exists to remove.
roadmap_entries — native/src/queue.rs’s, this library holding no counterpart
— is the single [roadmap:] and roadmap-summary: parse, shared by the roadmap
arm (§The roadmap arm) and check-roadmap-fresh so the emitter and the gate can
never disagree about what an entry claims — the same one-adapter rule the section
regexes follow. It walks the live task sections in queue order and returns one
record per entry carrying either marking: the entry’s [roadmap:] tag count,
the raw field text, the slug, the roadmap-summary: declaration count, and the
declaration’s text. Emitting on either marking rather than on the tag alone is
what lets assertion C see a dead declaration — an entry the tag has fallen off is
exactly the case a tag-triggered walk cannot report.
The record is a typed value rather than a TSV line, and that difference is a
deletion rather than a translation. A tab-separated form exists only because a
shell function’s only return channel is stdout, and such a form must carry a
defensive - in an untagged entry’s field column: a tab counts as IFS
whitespace, so a reader splitting on it coalesces an empty column and silently
shifts every field after it. In-crate the five fields travel as fields, so the
field-count and embedded-tab hazards have no spelling and an absent field is
simply empty.
The in-body citation scan (§The tag algebra) is deliberately not here: it
has one reader, and this library holds adapters two or more readers share. It
lives in bin/queue-edges.sh, which sources this library for the section
regexes and queue_live_slugs it does share. A second reader of body-position
slug tokens is what would promote the scan into this roster.
The entry lead-line grammar is here, and it is here by that same rule rather
than by convention. QUEUE_LEAD_RE (a bullet opening an entry) and
QUEUE_SLUG_BOLD_RE (the bold slug token inside it) are exported globals like
the section regexes, because three readers now share them: queue_live_slugs
above, bin/queue-edges.sh’s bullet scan, and that tool’s history walk
(§bin/queue-edges.sh), which asks the same question of an older revision. A
grammar answering one question in three places is the shape this library exists
to hold, and a history walk carrying its own spelling would let the retired set
and the live set disagree about what an entry is.
Both are written with bracketed literals ([*][*]) rather than backslash
escapes. They reach awk through -v, where awk’s string-escape pass runs
before the regex engine sees the value and eats a \* with a warning; the
bracket form has no escape to lose. This is mechanism, not style — the same
value spelled with backslashes is a different regex by the time it is applied.
The one-adapter guarantee is split for queue_live_slugs and the section
regexes, and the split is machine-held rather than filed as debt. The gates
that read the queue ported to the binary substrate (gate-sdk/SPEC.md §Porting a
gate to the binary substrate), so check-queue-slug-liveness and
check-task-conservation call a Rust reimplementation of that helper while
bin/queue-edges.sh keeps calling this one; the same is true of the section
regexes, every one of which a bin/ script still reads directly. The split is
permanent — every bin/ script in the kit still sources this library
(grep -rl lib/queue.sh queue-kit/bin queue-kit/checks is the roster, and the
checks/ half of it is empty now that every gate here dispatches to the binary)
— so the
shell form cannot be deleted the way a ported primitive’s is, and a port-time
byte-identity proof would expire at the next edit to either side. What holds the
two equal from here is gate-tests/queue-lib-parity.test.sh: it feeds one canned
corpus to both and compares their classification of it byte for byte
(§The queue format owns why classification is the comparable thing), which is
criterion 6’s machine-held disposition rather than its duplication-absent one
(gate-sdk/SPEC.md §The port-candidate criteria, criterion 6).
The obligation covers one helper — queue_live_slugs — and every derived
global this library exports (grep -n '^QUEUE_[A-Z_]*=' queue-kit/lib/queue.sh),
and nothing else is owed one. The crate’s done_slugs has no counterpart here and needs none: a shell
twin with no caller is a duplication removal disposes of, which enforcement-first
ranks above gating it, and the bound on that disposition is undocumented surface
— no section named it, which is what separates it from the section regexes, every
one of which is documented and load-bearing.
roadmap_entries is not in the split
either, for the opposite reason: its only two consumers are the roadmap arm and
check-roadmap-fresh, and both now sit on the crate function. The exclusion
is therefore satisfied on the other substrate, not repealed — the two still
cannot disagree, by the identical argument, and what would have spent it is
porting exactly one of the pair, which is why the arm and the gate landed in one
commit.
The loader sources the consumer config, then a <config>.local.sh overlay
beside it when present — last write wins. This is the tracked-name /
gitignored-value split (the msg-patterns.local.list precedent): a private
sink value tracked in queue-config.sh would itself be the leak, so it lands
in the gitignored overlay instead. The overlay is optional — its absence is
fail-open, not an error.
The queue-index arm
A non-gate arm of the binary (gate-sdk/SPEC.md §The non-gate arm), reached
through the battery runner’s --emit front-end:
run-gates.sh --emit queue-index [<flags>] [<queue-file>]. It registers in the
emitter table under the derived spelling --emit-queue-index, stays outside
--list, and owes no .gate descriptor, no gates.list registration and no
good/+bad/ fixture pair. The shell tool it replaces is deleted rather than
retained as a dispatching shim: a shim keeps the interpreted lines the port
exists to retire and adds a second entry point into the emission path, which the
class forbids.
The front-end is not optional dressing. A caller that invoked the binary
directly would resolve platform defaults and silently ignore every consumer
override; the front-end sources the shell library and supplies the bridged
environment in front of the arm. The arm is configured — its bridged reads are
declared beside it in the emitter table — which is why it is a table member
rather than a hardcoded top-level flag: a hardcoded flag receives no
configuration at all, and an arm that cannot see QUEUE_KIT_ICEBOX_SECTION
silently drops the tally in every consumer that configures a tier. The derived
regexes the shell library built (QUEUE_ACTIVE_RE and its siblings) were never a
configuration surface, only that library’s internal spelling of these knobs, so
none of them crosses into the arm and nothing is lost by their not crossing.
The three modes stay three modes on one arm, selected from the arm’s own argv tail — the emitter type is defined over an argv slice precisely so a mode rides as a flag rather than needing a second arm. The refusal to grow a fourth mode is unchanged, and so are the three grammars: they are the contract this port preserves.
One observable moved, and it is stated rather than absorbed. A --extent
slug that resolves to nothing reported queue-index: slug not found: <slug> at
exit 1 and now reports the arm’s error at exit 2, because the emitter type
returns a Result and the dispatcher maps every error arm to 2. Preserving the
old code would mean widening the class’s return contract for one mode’s error
path; no caller reads it — extent’s caller is a session reading two integers —
so the widening is not taken. Every mode’s stdout grammar is byte-preserved.
Compact surface of the queue for task selection — the iteration header line
if present, then every top-level active entry as a one-line title plus tags,
ready (•) vs blocked (✗) marked from the [blocked-by:] tag alone. Tags
are stripped from the one-line title leaving no residue of the separator they
sat beside, so an entry whose lead line is nothing but tags has no title and
renders as the bare slug rather than a dangling —; [blocked-by:] and
[drain-exempt: <reason>] are re-echoed after it — the two tags a picking
session acts on without opening the entry body.
--extent <slug> prints the inclusive line range of one entry’s body (parent
slug → whole subtree; boundary = next sibling-or-shallower bullet, heading,
---, or EOF, including the trailing blank so a range deletion leaves no
double blank). --collapse-deferred replaces the deferred listing with a
per-###-subsection tally — generic over whatever subsection names the file
has (a hardcoded tally table would be consumer rule content), with
entries under no subsection tallied as (top).
A configured icebox contributes exactly one line to the index —
<section>: N entries — and never an entry listing, on both index-mode
renderings (--collapse-deferred is a flag within that mode, not a mode of its
own; the arm’s modes are index, extent, and icebox-candidates). The
tally must not reach extent, whose contract is two integers and nothing else.
The reasoning is the surface’s: this output is embedded in the always-loaded
session-context brief, so listing the tier would re-import the very tokens the
tier exists to remove, while a bare count keeps it visible. An unconfigured
icebox emits no line at all.
--icebox-candidates prints the closing stage’s eviction worklist over the
deferred section: one line per entry whose defer date (§The queue format) is
older than QUEUE_KIT_ICEBOX_AGE_DAYS and whose cost field opens in the low
class, carrying the entry’s line count and that opener. An entry with no defer
date is listed as (undated) and an uncosted one as (uncosted) rather than
filtered out — an absent input appears rather than vanishing.
Each row is led by the •/✗ mark the index rendering already uses: • for an
entry §The icebox tier’s eligibility rule admits, ✗ for one it excludes. An
excluded row prints its cause in place of the cost opener, because the opener
is inclusion evidence and decides nothing once a categorical exclusion has
settled the row. The cause names the trigger class — the [roadmap:] tag, a
dated recurrence: declaration, or the first live slug named in file order,
which stands for the class rather than enumerating it. A slug counts as named
only where it stands as a whole token, the neighbouring bytes falling outside the
slug alphabet: a substring hit inside a longer slug is a different entry, and
holding a row against it would invent a trigger nobody wrote.
Both filters are deliberately non-load-bearing: this is a worklist filter in a projection tool, not a threshold in a gate, so miscalibration costs a longer or shorter review list and never a wrong disposition. That is what dissolves “an eviction age threshold needs a policy owner” — the judgment is the closing stage’s, held to §The icebox tier’s eligibility rule, and the worklist only bounds how much it must look at. Matching a cost-class opener on prose would be an unacceptable heuristic in a gate; in an advisory worklist it is exactly the right ceiling. A pool younger than the age threshold yields an empty worklist, which is correct and is not a reason to seed a tier by hand against the tool: eligibility is the rule, the worklist is the convenience.
The worklist applies the tier’s categorical exclusions, reopening a ruling that
held them off. Operator-ruled 2026-08-18 that it deliberately would not, on
five consecutive measurements at zero precision: filtering the machine-readable
half would have moved the eligible count from zero to zero, and the other half
read as a prose-parsing fork nothing justified. Reopened and taken by operator
ruling 2026-08-24, on measurement rather than on preference. Both halves of the
ground fell: all three triggers are mechanically decidable — the [roadmap:] tag
is a lead-line tag, the dated recurrence: declaration is one indented body line
with a fixed lead token (§The queue format), and a named live slug is the
slug-liveness test the shared adapter already answers for §check-task-names — and
zero precision is what makes the exclusions worth reporting, since a worklist
whose rows are all re-adjudicated by hand pays that read at every close.
It prints them rather than filtering, which is this arm’s own rule and not a
new one. (undated) and (uncosted) above already list an entry that fails an
input test instead of dropping it, and a categorical failure is the same case
under the same sentence. Two further grounds hold in the same direction: dropping
the row would turn an advisory projection into a silent disposition, which is
what the non-load-bearing ruling above exists to prevent; and it would deny the
closing stage a row §The icebox tier requires it to have recovered before
ruling on the entry. So the division of labour is unchanged — the arm reports,
the closing stage rules, and a row the rule excludes is still a line read rather
than a defect. What changed is that the line now carries its reason instead of
leaving every close to re-derive one.
No queue-mutating tool is added — --extent <slug> already yields the line
range an eviction deletes.
Both index renderings append an attention block when the ## Lessons Learned section
carries [attend] entries (§The tag algebra): each such entry’s lead line,
capped at QUEUE_KIT_ATTEND_CAP (default 3) with overflow noted as
(+N more [attend]). Lead lines only, never bodies — the block is
always-loaded through the session-context hook that embeds this output, so the
cap is its token budget. This is the inbound lesson channel reaching every
later session of the iteration with zero consumer-hook edits.
The age cutoff keeps its date -d derivation rather than moving to an in-crate
civil-date computation, and the reason is behaviour preservation: date resolves
the operator’s local zone where an in-crate computation would resolve UTC, and a
port that shifts the cutoff by a day is not the behaviour-preserving port this
was. Its refusal on a date without -d survives with it.
This is a task-selection surface, walking bullet lead lines. Its sibling
bin/queue-edges.sh walks entry bodies to aggregate citations — a
different question over the same file (§bin/queue-edges.sh). They stay two
tools rather than one with a fourth mode, because folding them would give
one tool two jobs and two output grammars.
bin/queue-counts.sh
One job: the size of each task section, for a caller that wants the shape of
the queue rather than its contents. It emits one <section-name><TAB><count>
line per task section, in configured order, and nothing else — no flags, no
modes, one output grammar.
The section set is derived, never listed: it is QUEUE_TASK_SECTIONS, the
same composition QUEUE_TASK_RE is built from (§lib/queue.sh). So
QUEUE_KIT_DONE_SECTION is out because Done is not a task section, a configured
QUEUE_KIT_ICEBOX_SECTION is in because the icebox is a live one, and a consumer
who renamed their sections gets their own names back. Nothing here enumerates a
section name, which is what keeps the vocabulary inside this kit when a caller
outside it renders the numbers.
The counted unit is the top-level entry bullet — the same unit §The queue-index arm lists, so the index and the counters cannot report different sizes for one queue. Not lines, and not bullets: an indented bullet inside an entry body is body.
Why a second tool rather than a fourth mode on the queue-index arm. That
arm’s modes are fixed at index, extent and icebox-candidates
(§The queue-index arm), on the stated grounds that folding jobs together gives one
tool two output grammars — the same refusal that keeps bin/queue-edges.sh
separate. A tally keyed by section name is a different job with a different
grammar, so it lands beside the index rather than inside it. Recorded here
because the next reader meeting two queue tools would otherwise read the split as
an oversight and merge them.
It is invoked as a subprocess, never sourced. lib/queue.sh exits 2 at
source time on a missing QUEUE_KIT_CONFIG_FILE and on any malformed-config
assertion — correct for a gate, and fatal for a long-lived caller that sourced
it. A subprocess turns that contract into a non-zero exit and empty stdout, which
a caller degrades on. The caller this was built for is delegation-kit’s
statusline template, whose own contract records the degradation
(delegation-kit/SPEC.md §The statusline template).
bin/queue-edges.sh
The inbound-citation aggregator: a tool, not a gate (no # graph: manifest),
following the queue-index precedent. It reads the queue, writes stdout only, and
mutates nothing.
usage: queue-edges.sh [--inbound <slug>] [queue-file]
default: live slugs with inbound edges in queue order, then retired targets
--inbound <slug>: the inbound set for one live or retired slug
Inbound only. An outbound view is refused for want of a reader: an entry’s outbound edges are its own body, which a session asking the question is already reading. Inbound is the direction invisible without a scan of the whole file — which is the entire finding this tool answers. Nothing anywhere sums an entry’s inbound edges into that entry’s own cost and benefit, so a survey that reads every sibling entry individually can still misrank a unit that several of them separately depend on. The tool is that missing sum, which is also what makes splitting an entry safe: a split scatters an entry’s weight across siblings, and this is what adds it back up.
Targets print in queue order — the order every other reader of this file walks
— each with its inbound count, then one line per citing edge. A slug with no
inbound edges is absent from the default listing and yields empty output under
--inbound; that is the normal case, not a finding. A --inbound slug that
resolves to neither a live nor a retired target exits 1 with a message on stderr
rather than printing nothing, because silence from this tool has to mean “no
inbound edges” and nothing else. Widening the addressable domain to retired
slugs grew that domain and left the meaning of silence exactly where it was.
The resolution set is the live slugs plus the file’s retired ones, and a
citation resolving to a retired slug is an edge like any other, marked as such.
Retired targets print after the live block, in a trailing block, each as
<slug> (<N> inbound, retired) above its edges in the same two-column shape.
They sort alphabetically: a retired slug has no queue position to order by,
and inventing one — last-seen revision, say — would be a datum with no reader.
A retired slug is one that held a top-level entry lead line in some earlier
revision of the queue file and holds none now. That is the whole discriminator,
and it is what makes the report readable rather than merely longer. The naive
alternative — listing every backticked token that resolves to no live entry — is
dominated by commit SHAs, content digests and ordinary backticked words, because
the token grammar [a-z0-9][a-z0-9-]* is not a slug detector. Splitting the
unresolved remainder against the ever-live set leaves a small retired minority,
and it is the half a reader wants: on this repo’s own corpus, both attested
instances of a citation instructing a session to sequence against work that no
longer exists fall in it. Run the tool for the current numbers on your own
corpus — they are one adopter’s queue statistics, not a property of the kit.
One tool, one job, and this is not a second one. §bin/queue-counts.sh’s refusal — folding jobs together gives one tool two output grammars — is honoured rather than worked around. The job is aggregate in-body citations by target, and a retired target is a target whose entry has been disposed of, not a different question over the same file.
The retired set is derived from the file’s own history, so nothing is
maintained. One git log -p --format= -- <queue-file> pass at start-up, its
added, removed and context lines matched against lib/queue.sh’s exported
lead-line grammar (§lib/queue.sh) — the same grammar the live reader applies,
never a second spelling. git joins awk as a dependency of this tool alone;
the section’s “reads the queue, writes stdout only, and mutates nothing”
contract is untouched, and now has a second input. The cost is a tool’s budget
rather than a gate’s: on a queue file with roughly fifteen hundred revisions the
whole pass measures well under a second, and it scales with history depth rather
than with queue size.
Two degradations are declared rather than discovered. A queue file not in
a git work tree, or a git that is absent, yields an empty retired set and
the tool prints its live block alone — byte for byte the output it printed before
retired targets existed, which is why the degradation is silent-safe rather than
misleading. And the derivation sees only the history this clone has: a
shallow clone, or a queue file whose history was rewritten under it, reports
fewer retired slugs and never more, so the report under-claims and never
invents. Both are stated because the natural reading of “derived from history”
is that history is total, and here it is whatever the clone holds.
Each edge carries the citing entry’s slug and its citing line verbatim (surrounding whitespace stripped, since the output re-indents). This is what buys a relation’s kind without declaring a vocabulary for one: the nature of the relation is already written, in the citing author’s own words, by the person who understood it. Quoting it beats a one-token classification from a fixed set, and it is free. It also keeps the tool honest about precision — a citation that is a passing mention rather than a relation is visibly a passing mention once its line is on screen, so a reader discards it in the time it takes to read one line. Recall is what the surface needs, and a high-recall list whose noise self-identifies is the right trade.
A citation is attributed to the nearest preceding slug bullet, so a
sub-task cites in its own name rather than its parent’s, and a lead line
contributes its [blocked-by:] tag alone — never its prose, which is title and
tags rather than relation.
The body-citation scan lives in this tool, not in lib/queue.sh: it has
exactly one reader, and the library’s rule is shared adapters (§lib/queue.sh).
It reuses that library’s section regexes and queue_live_slugs; a second
reader of body-position slugs is what would move it.
No tracked projection, and no freshness gate. A tracked artifact needs a
reader who cannot run the tool, and there is none — the one consumer is a
session with a shell. A generated public page like the roadmap is tracked
because its audience is outside the repo; this has no such audience. Against
that, a committed copy of derived edges over what is typically a repo’s
highest-churn file would restale on essentially every queue edit, buying a
per-commit regeneration tax for zero benefit. Derivation-first is satisfied by
deriving on demand: that rule’s “generate and freshness-gate” clause governs a
copy that is needed, and this one is not. The queue-index arm writes nothing
and is gated by nothing, for the same reason.
Not a gate, either. There is nothing to red: an entry with no inbound edges
is normal, and an entry with many is not a defect. The adjacent temptation —
redding on a backticked token that resolves to no live slug — is refused
outright, because those are overwhelmingly legitimate citations of landed work
(§The tag algebra), and check-queue-slug-liveness already owns the liveness
invariant on the surfaces where a dead reference is a false claim. Extending
it inward would red on good prose.
The refusal and the retired block are the same ruling, not a softening of it. Reporting is what a no-red posture always left available, and the retired block is only the listing half: an entry citing landed work is legitimate prose, so a retired edge is a finding to read and never a violation. Nothing about it is stricter than the day the refusal was written — the tool gained an output section, not a verdict.
Two named readers, at two named transitions, because a report nobody reads is the failure this listing was built to end:
- scope, at its ranking survey — the primary, and the attested harm’s own moment: a citation pointing at disposed work is a false premise in a survey input at exactly the point a session decides what to promote. Scope already runs this tool there, so it gains the retired block as one more thing that call’s output carries and no new step.
- close, at the gap-inbox drain — the corrective transition, where an instance found is fixed inline on its owning entry. This is that stage’s first invocation of this tool: its drain dispositions bullets from a different file entirely, so unlike scope’s it is a new command in the step rather than a wider reading of an existing one. Still no new mechanism — one more command a session already running shell tools invokes.
Neither stage gains a gate and neither gains a refusal.
The roadmap arm
The public-roadmap emitter: a non-gate arm of the gate binary
(gate-sdk/SPEC.md §The non-gate arm), reached as
bash gate-sdk/bin/run-gates.sh --emit roadmap, following the queue-index
precedent. The bare arm prints the generated block to stdout; --write splices
it between the markers in QUEUE_KIT_ROADMAP_FILE through the crate’s shared
marker writer, leaving every byte outside them untouched.
Table membership is forced rather than chosen, and the alternative is the
failure mode that looks like success. An arm receives no configuration and only
an emitter-table member is bridged, so this tool ported as a hardcoded top-level
flag would resolve platform defaults and silently ignore every consumer override
of the horizons, the tracks, the queue path or the marker name. The table’s knob
column is what keeps QUEUE_KIT_HORIZONS and QUEUE_KIT_TRACKS on the caller’s
side of the bridge, which is the provenance seam holding: the crate ships the
projection grammar — the headings, the bullet shape, the placeholder, the
trailing blank — and not one lane name (§The tag algebra).
The section trio (QUEUE_KIT_ACTIVE_SECTIONS, QUEUE_KIT_DEFERRED_SECTION,
QUEUE_KIT_ICEBOX_SECTION) rides the same roster, because the shared adapter is
what scopes the scan and a knob the bridge does not carry is a knob the arm
cannot read. It reads the live task sections only — active, deferred, and a configured icebox,
never the done section, because a shipped item is history, not direction. The
icebox enters that walk on purpose even though no icebox entry can be
projectable: excluded, a [roadmap:] tag that drifted into the tier would
silently drop a public commitment; included, it reds (§The icebox tier).
The emit grammar is a contract on both sides: check-roadmap-fresh byte-compares
it and a reader consumes it as the page’s body.
- One
### <horizon>heading per configured horizon, in the knob array’s own order — never a sort the crate imposes — each emitted even when the queue puts nothing there — an empty horizon is information, and a section that vanishes when it empties reads as a page that forgot it. An empty horizon carries a one-line placeholder instead of bullets. - Under each, one bullet per tagged entry, in queue order:
- **`<slug>`** *(<track>)* — <summary>. - The summary is the entry’s
roadmap-summary:declaration (§The tag algebra), printed verbatim. Nothing else in the entry is projected — not the lead line, not the body, which carries internal design rationale and cost accounting, precisely the content a public page must not inherit. The emitter neither summarizes nor truncates: an author decided what the page says, and an entry without a single non-empty declaration contributes no bullet at all, so unmarked prose cannot reach the page even with the gate bypassed.
The block ends on a blank line. That blank is load-bearing rather than cosmetic:
the Pages parser closes a list only on one, so without it the :end marker abuts
the last bullet and renders inside that list item. Both sides of the freshness
compare strip their trailing newlines, so the byte-compare is unaffected — the same arrangement the value-rollup emitter uses to keep a
table from swallowing its marker.
Honest limit — the lead line is a shared budget, and [roadmap:] competes for it
with every other tag the entry carries. Against check-queue-wrap’s floor a
[spec: <file>] tag is far wider than [design-pending], so an entry carrying
a spec pointer and a long slug may have room for the [roadmap:] tag but none
for prose after it. That costs nothing now the summary lives on its own line:
the lead line needs room for the tag alone. It does bound the tag itself — a
sufficiently long slug plus spec pointer leaves no room even for that, and such an
entry is unprojectable until the pointer drops at the amendment’s merge. A true
state of the queue rather than a defect to gate around: the wrap floor exists so a
runaway never reflows to column 0, and widening it to make room for a public page
would trade a parse guarantee for a presentation one.
Ordering inside a horizon is queue order, not a rank; the page states so in its framing rather than implying a priority the queue does not carry. The three-to-five items per horizon band is editorial posture, stated in the page’s framing prose, and deliberately not gated: a horizon legitimately holding two items is a true state of the queue, and reddening a commit over it would buy a curation opinion at the cost of the low-false-positive contract every gate here is held to.
check-roadmap-fresh
Invariant, in two assertions over QUEUE_KIT_ROADMAP_FILE:
- (A) Freshness. The committed marker block byte-matches the roadmap arm’s
emission, called in-process rather than spawned — the arm ported in the same
commit, so there is no shell left to reach — on the
check-footprint-fresh/check-value-rollup-freshmodel — diff the emission against the block and name the regen command in the red output. A missing file, missing markers, or an unbalanced marker pair is fail-closed (exit 2): an absent projection under a configured path is a broken install, not a clean skip. - (B) Tag-field validity. Every
[roadmap:]tag in the live task sections parses as<horizon>/<track>with both fields members of their configured arrays. An unknown horizon or track, a missing slash, or a second[roadmap:]tag on one entry is a violation naming the entry and the offending field. - (C) Marking parity, in both directions. A
[roadmap:]-tagged entry carries exactly oneroadmap-summary:declaration — none, or two, is a violation, because a curated entry with no marked text would otherwise reach a reader as a bullet with no prose. And a declaration on an entry whose lead line carries no[roadmap:]tag is a violation too: a dead marking is what a dropped or reflowed tag looks like from the page’s side, and the tag is what decides projection.
Assertion B runs before assertion A, and the ordering is carried deliberately. A tag naming an unconfigured horizon is silently dropped from the emission, so a freshness verdict taken before the fields are validated would pass a page that quietly lost an item. That is a correctness property of the pair of assertions rather than an implementation accident, and it is written here because a reader taking the three assertions as an unordered set would lose it with nothing reddening.
Assertions B and C live here rather than in check-tag-lead-line because this is
the gate that already loads the horizon and track vocabularies and the shared
[roadmap:] parse; putting them in the lead-line gate would make a placement gate
load a value roster and a body-scoped declaration it has no other use for.
check-tag-lead-line still governs the tag on its placement axis, and governs
the declaration not at all — it is body-scoped by design.
Calibration: QUEUE_KIT_ROADMAP_FILE empty is a clean skip for both assertions
— the correct no-op for a consumer that publishes no roadmap, matching
check-queue-slug-liveness’s empty-globs behavior. The two-argument form
([projection-file] [emit-file]) steers assertion A off the live emitter onto
pre-baked fixture files, the interface both cited models carry: a freshness gate
offering only its bare form has no fixture that does not read the live queue, so
the pair’s verdict would turn on tree state outside the fixture directory.
What the fixture pair proves is bounded, and it is explicitly not the arm’s parity proof. Both cases steer assertion A off the live emitter, so the pair would have gone green over an arm with no implementation at all. The pair proves the comparator and the two-argument plumbing; what proved the emission was the transition-scoped parity run against the shell emitter, held while both existed (gate-sdk/SPEC.md §The first cohort, and the rule that selects the next).
The declaration is checks/check-roadmap-fresh.gate — hermetic, precommit,
binary-dispatched. Its couples= names every crate module the gate reaches
transitively, the emit module and the two shared with the emit side of the
compare included: the generated hook’s staged_matches trigger is derived from
that field, so an omitted module leaves the gate registered and green while the
page it holds goes stale at commit time.
bin/lesson-sink.sh
The outbound channel’s router: reads a lesson body on stdin and resolves the
sink for its <tag> argument from QUEUE_KIT_LESSON_SINKS. A configured entry
runs as a command with the body streamed to its stdin — a command, not a
path, so the sink may reformat the body into a downstream backlog’s own
grammar; the command’s exit status becomes the tool’s, so a failing sink is a
red close step and harvest material is never half-routed to a silent fallback.
An unconfigured tag falls open to appending the body to
${GATE_SDK_WORKFLOW_DIR:-.workflow}/<tag>-harvest.md — the honest
manual-drain default that keeps a fresh clone (no overlay) closing cleanly and
preserves the staging file’s documented reclaim path. A tool, not a gate (no
# graph: manifest); the consumer close skill invokes it so the tracked skill
names the mechanism, never a sink value, and a private sink command lives in
the queue-config.local.sh overlay (§lib/queue.sh).
check-queue-hygiene
Invariant: the queue contains only tasks, tags, and section structure — no
HTML comments (provenance belongs in git history), no exact-duplicate
non-blank non---- lines (copy-paste artifacts), no column-0 prose (every
column-0 line is a heading, a bullet, ---, or a configured
QUEUE_KIT_PROSE_LEADS token — the shape that carries protocol duplication
is banned, not semantic duplication, which is not mechanizable).
Calibration: indented lines are never flagged on the prose axis; the
lead-token allowance is a whole-line lead match, not a substring. Division of
labour with check-queue-sections: hygiene owns line shape (what a column-0
line may be), the sections gate owns heading presence (that each required
## heading exists exactly once) — neither subsumes the other.
check-queue-sections
Invariant: the queue file carries each QUEUE_KIT_REQUIRED_SECTIONS heading
exactly once — zero occurrences (missing/typo’d) and two-or-more (accidental
paste) are both red. This is the fail-closed floor under every section-scoped
scanner: check-amendment-queue, check-task-names, check-task-conservation,
and the session-context index all locate work by ## <section> boundaries and
silently find nothing — passing open — when a heading is dropped or
misspelled. A trailing : on a required entry marks a dynamic-suffix heading
(the iteration header, whose suffix is its iteration name) and is
prefix-matched; every other entry is matched exactly. A grep error (not a
no-match) is fail-closed (exit 2). The required set is the configured array
plus the derived icebox heading (§Layout and configuration), which is what
closes the half-configured hole by construction. The # graph: couples the
queue file at tier=precommit.
check-queue-entry-budget
Invariant, one statement from three sides: a deferred entry is a costed filing — bounded above so it is not an inlined amendment, bounded below so it is not a flag-and-skip, and bounded in what it may displace, so a bound on filing never becomes a bound on the record. The first two sides are about the entry’s size; the third is about what the cap spends to stay inside it. Three assertions:
- (A) Size. No deferred entry exceeds
QUEUE_KIT_ENTRY_LINE_CAPcounted lines. An entry’s extent is the lead line through the line before the next bullet at the same or shallower indent — the same extent thequeue-indexarm’s--extentyields, so the range the gate measures is the range an eviction deletes. Its count is that extent less at most one line of each declaration grammar the queue format defines (§The tag algebra) — todayrecurrence:andruled:, and any later grammar by construction rather than by a further edit here. Extent and count differ by those discounted lines and by nothing else, at most one per grammar, which is what keeps the equality above a statement about the range while the cap binds the count. A sub-task nests inside its parent’s extent and is measured as its own entry too — and claims its own at-most-one-per-grammar discount, so a parent whose extent holds tworecurrence:lines still discounts one of them. - (B) Icebox shape. Every icebox entry is exactly one line; a continuation
line under an icebox bullet is a violation. Skips clean when
QUEUE_KIT_ICEBOX_SECTIONis empty, the empty-knob behaviorcheck-queue-slug-livenessoriginates. - (C) Cost present. Every top-level deferred entry carries a
Cost while deferredbold lead-in. Deliberately not applied to the icebox: a one-line entry cannot carry the field and does not need to — membership in the tier is itself the cost declaration (low, non-rotting, no live trigger). The requirement binds exactly where the section does not already imply the answer.
Calibration: QUEUE_KIT_ENTRY_LINE_CAP defaults to 50. The cap’s job is to
keep compression from regrowing rather than to force the initial cut, so it is
calibrated at the tail rather than the body — above the 75th percentile of a
real pool, with no natural break in the distribution to read a value off. It is
a stated policy with a stated purpose rather than a derived one, and it is a
knob for exactly that reason. The scan is line-local: the cost field’s bold
lead-in must sit on one line, the same reason a tag must sit on its lead line
(§check-tag-lead-line), and a lead-in split by a reflow reads as absent. (C)
binds top-level entries only — a sub-task is covered by its parent’s costing.
The active sections are uncapped, and the reason is structural: an active entry’s residency is one iteration by the drain rule, so it has no carry to cap. The carry problem is the deferred pool’s alone.
Clean-path headroom. When all three assertions hold, the clean line is
followed by one line per Deferred entry — a sub-task included, since it is
measured as its own entry under (A) — naming that entry’s headroom to the cap:
the same count assertion A already derives, one subtraction away, so exposing
it costs no new computation and mints no new name. This is exposure, not a new
capability: the enforcing member already computes each entry’s count in order
to enforce the cap, so a session sizing an edit reads the measurement instead
of hand-rolling it — a hand-rolled probe carries the risk of a second spelling
of the cap, a second implementation of the parse, and a mismeasured entry
boundary that reads as a false cap overrun. The rejected
alternative was a --headroom <slug> mode on the queue-index arm: it mints a
name against that arm’s standing no-fourth-mode refusal, where this
exposure spends nothing beyond a print at the compiled assertion’s existing
computation.
No collision counter is minted either, and this line is where that decision is recorded rather than left as the absence of one. A count of how often the cap displaced a mandated write would be a second measurement of a quantity this headroom line already prints, maintained by hand, against a criterion (the split test below) that reads an entry’s composition rather than its collision history. What a firing count would have been evidence for, the split criterion decides directly.
Why a declaration line is discounted, and why exactly one of each grammar. Such
a line is
fixed-shape and width-bounded — §The tag algebra rules its ceiling to be
check-queue-wrap’s budget and rules reaching that ceiling the correct
complaint, so the discount cannot let an entry grow without bound. It is exactly
the generated-shaped content that argued content was otherwise spent to seat,
so making the count blind to it removes that trade rather than arbitrating it.
And it is one line per grammar, not a grammar-wide exemption: each
declaration’s own rule is one line per entry with its repeats appended, so one
line is the whole of what each form can claim — §The tag algebra refuses the
one-line-per-recurrence variant partly because it grows an entry linearly
against this cap, and a grammar-wide exemption would retire that refusal’s ground
as a side effect. Widening from one line to one of each is the invariant’s own
third side doing its job rather than a relaxation of the cap: a provenance
declaration is record, not filing, and an entry forced to drop who ruled it in
order to stay under the cap is the cap spending the record to stay inside
itself — precisely the failure that third side names. The match is each
declaration’s own grammar — its lead token, a slug, then at least
one ISO date past the slug, the shape drift-kit’s kpi-incident-recurrence
already reads for recurrence: —
with no entry-boundary or self-slug condition added, since the at-most-one bound
is what does the scoping. The discount narrows the count only and removes
nothing from the scan, so (B), which counts icebox continuation lines, and (C),
which asserts a lead-in is present, cannot flip; (A) itself reds on
exceeding the cap and is monotone in the count, so a smaller count reds
strictly less.
A mandated write is the class the cap has no self-served answer for. A
mandated write is a write on a deferred entry that another governed contract
obliges the writing session to make in the commit it is already making, and
whose obligation is citable to that contract by name. Two instances exist, both
already in the tree rather than invented here: a judged recurrence: date and
the grounds it is read from, which must land in the commit the judging session is
already making because that commit is the audit artifact (lifecycle-kit/SPEC.md
§The committed gap inbox); and a ruling recorded onto the entry it rules — its
content under the recording-in-the-moment rule below, and its ruled:
declaration in that same commit under lifecycle-kit/SPEC.md §The state machine. Its producers are the sessions those
two contracts already bind — any session that judges a recurrence, the gap-inbox
drain being its mechanized instance, and any session recording a
ruling — both running today with no new trigger, no new field and nothing to
configure; its consumers are that same session reading this section when
assertion A blocks it, a path the gate’s failure text already routes it down, and
lifecycle-kit’s judgment contract, which cites the class by name for the write it
mandates. What is not a mandated write, because a class that excludes nothing
is not a class: the session’s own evidence for a claim, a further ground for a
claim already made, a cross-reference it would like to add, a correction it could
equally make in a later commit. All of those keep the reliefs they have. The test
is not how important is this — every session believes its write matters — but
does a named contract oblige this write in this commit.
Compression is lossless; relocation is self-served for a mandated write, and the split stays authorization-gated. An entry that will not fit is compressed by answering grounds, never by dropping or summarizing them away; a ground that survives unanswered is relocated into a distinct linked entry. The gate cannot hold this: it sees an entry’s current extent, and judging whether a removed line was answered or discarded is semantic. So the rule is a stated authoring contract — and that third relief is two acts wearing one name.
- Relocating grounds into an entry that already exists and already owns the
ground’s subject is self-served when, and only when, what the cap blocks is a
mandated write. The relocating session cites the mandating contract in the
commit that makes the relocation, and names the target with a
single-backticked slug in the entry body — already a citation under §The tag
algebra and already aggregated by
bin/queue-edges.sh, so the link needs no declaration of its own. Arelocated:declaration is refused on the ground §The tag algebra refusesrelates:on: it would cost a counted line against this very cap, on the entry least able to pay it. -
Splitting the unit — minting a new entry to hold what would not fit — carries its own authorization, unchanged: a parent session (the iteration lead), or the operator in the absence of one, grants permission to split and issues the recipe with the ruling. A session blocked by the cap does not self-serve that, the same lineage as a
check-stage-entryassertion C waiver (lifecycle-kit/SPEC.md §check-stage-entry), and the gate’s failure text therefore cites this section rather than inlining the recipe.An entry is a split candidate when it carries two or more deliverables that can take different dispositions — where different dispositions means one could be promoted, deferred, declined or closed while the other stays open. The test is executed by reading the entry’s deliverable statements and asking whether ruling one leaves the other unruled. If it does, the entry is two units wearing one slug, and its grounds are two ground-sets sharing one cap.
The counter-class is what makes it a class, and it is stated with the criterion rather than left to inference: an entry accumulating further grounds for one deliverable — recurrence evidence, a repeated measurement, an answered objection, a corroborating firing — is not a split candidate however hard it collides. Those are one unit’s history, and splitting them mints exactly the ranking peer competing with its own parent that the two-acts paragraph below names as the sharpest displacement shape. Compression by answering is the correct relief there and stays the default.
Why dispositionability, and not size, age or ruling-count. Size is the cap itself, so it would make the criterion circular. Age is refuted by the measurements a colliding pool actually produces — the collider is the most recently ruled-on entry rather than the longest-lived, which is the opposite of what a first reading of a collision log suggests. Ruling-count is the near miss and the one worth stating, because collisions genuinely do concentrate where rulings accumulate; it is refused because it is a symptom shared by both classes. An entry accumulating rulings on one deliverable collides exactly as hard as one accumulating rulings on two, so a criterion that cannot separate them licenses the split that mints a competing peer. Dispositionability separates them and is decidable by inspection, which is what an authoring contract needs.
The criterion is read at the collision, and it is an input to the authorization rather than a licence. The blocked session states which side of the test its entry falls on when it asks; the authorizing session rules with that statement in front of it. What changes is the shape of the ask, never who answers it. Without a test the authorizing session has only the blocked session’s summary of its own entry, so the answer is a judgment call re-derived per firing — which is the mechanism behind the pattern a collision log shows: relief was always available, so the question was always answered by the cheapest relief rather than by composition. A denied split is not a dead end, stated so the criterion does not read as gating relief: an entry that fails the test still has compression by answering and the self-served relocation, which are the reliefs it should have been using, and an entry that passes and is denied anyway gets the denial’s ground recorded on it as any other ruling would be.
An authorized split records itself through the citation grammar already here — the parent names the child and the child names the parent, each with the single-backticked slug §The tag algebra already rules a citation. A
split-of:declaration is refused on exactly the groundrelocated:is refused on above, so the two refusals read as one rule: it would cost a counted line against this very cap, on the two entries least able to pay it — the parent that just overflowed and the child carrying what would not fit. The backticked slug costs no line of its own, because it rides prose the split is writing anyway. Bidirectional rather than child-to-parent alone, because the failure a one-way citation leaves is the one this whole class is about: a reader meeting the parent sees an entry whose deliverable set silently shrank, with no surviving statement of what left or why, and re-derives the missing half as a gap — the same information loss the compression rule forbids, arriving through a split instead of through a deletion.The pair is an aggregated edge only while both slugs are live, on §The tag algebra’s resolution rule, and the bound is stated because the parent is the end that reaches it first: a
## Doneentry is a bare slug, so it leaves the live set and takes the parent’s citation with the rest of its prose, and the edge silently stops being an edge. That is the correct outcome rather than a loss — the parent-side citation exists to stop a live parent reading as an entry whose deliverable set shrank, and a completed parent has no open deliverable set to misread. What it means is that the split’s durable record is the child’s citation, and the parent’s is owed for exactly as long as the parent is open.
Why the line falls between the two acts. Grounds are not rankable work, so moving them into an entry that already owns their subject mints no ranking peer; a new entry is a new unit, competing for the scope attention that ranks both, which is a scope-class judgment and keeps the authorization. That is what stops the sharpest displacement shape — an entry too full to hold its own design half, so the half is filed as a separate entry competing with its own parent — from becoming routine under a blanket relief. And the authorization is safe to drop for the narrow act because a mandated write is by construction not discretionary: its obligation belongs to another surface and is citable, so a reader can check the trigger even though no gate reads it, and the act is net-negative on the parent by construction — a relocation that does not reduce the parent’s extent is not a relocation, which assertion A enforces from the other side. The honest limit, stated because it is real: where no existing entry owns the ground’s subject, the self-served relief is unavailable and the session is back to compressing by answering, or to asking. That is the correct outcome rather than a hole — needing to mint an entry is the signal that a unit, not a ground, is what would not fit.
Why the cap is not widened for exceptional content. Assertion A’s bound is the amendment-inlining line above, not a length preference, so raising the number moves the number without moving the line — and the entries that would claim an exception are the likeliest ungoverned amendments. A conditional cap collapses back into authorization anyway, or it is the self-issued exemption the delegation doctrine already names as the standard failure mode. The declaration discount is not that widening and must not be read as one: it changes what the count includes, never the number, and it is unconditional — no entry claims it by being exceptional. Widening it to one line of each grammar keeps both properties, since a grammar either is one the format defines or is not, which is a property of the format rather than of the entry claiming it.
A ruling the operator restates from memory is filed in the moment. The compression rule above is an authoring contract, so a break in it is silent, and it surfaces in one recognisable form: the operator supplies a closed decision from memory because no surface carries it any more. That restatement is the moment the content is in hand and the cheapest it will ever be, so it is filed then, as a recording entry — never noted for a later pass. Deferring the filing is not a cheaper version of it: the ruling stays exactly where it was lost from, and the next session pays the same restatement over again. A recording entry is the queue’s holding pen for a decision in flight, never its home; its deliverable is the ruling landed in the surface that owns it, one owner per fact as everywhere else, and the entry is done when that surface carries it. The gate cannot hold this either, for the same reason it cannot hold compression: nothing distinguishes a sentence that was recalled from one that was read.
check-queue-wrap
Invariant: no line exceeds the QUEUE_KIT_WRAP_BUDGET gate floor (default
100 columns; the authoring target is ~80). The tools key on the column-0
- lead, so an unwrapped runaway that reflows to column 0 corrupts the
parse; the tripwire fires before that lands.
Calibration: three exemptions mirror the wrapping convention — table rows, fenced-code blocks, and lines whose own longest token itself exceeds the budget (URL, path). That third one is narrower than “over budget because of one long token”: a 101-column line whose longest token is 55 is not exempt, because the token still wraps. Width is Unicode code points, not bytes.
check-tag-lead-line
Invariant: every lead-line-scoped tag sits on its bullet’s lead line — the
only line its readers scan; such a tag pushed to a continuation line by a
reflow silently unblocks a task, masks a design-pending state, voids a drain
exemption, or drops a lesson out of the attention block. Membership tracks reader semantics, not §The tag
algebra: a tag is governed here when its readers scan lead lines alone, so the
set is narrower than the algebra’s and [precondition-ok:] is deliberately
outside it — check-queue-prose-precondition honors that tag anywhere in the
entry, leaving it no lead-line requirement to enforce. The governed set and
scanned surface both widen with the lesson channels: [blocked-by:] /
[spec:] / [design-pending] / [drain-exempt:] / [roadmap:] in the task
sections (active + deferred), plus
[attend] and every QUEUE_KIT_LESSON_TAGS name in the ## Lessons Learned
section — the section the queue-index arm now reads, which retires the old “parsed
by no reader” exemption for it. Couples
the width-only wrap gate to the tag-parsing tools over the same surface: gate
the coupling, not just each side.
The gate states that set once, as a class table of <name><terminator>
tokens (] for a bare tag, : for a field tag) from which both the match
literal and the class key are derived. That single statement is the whole
point: a table entry naming the tag once cannot desync its matcher from its
key, and the desync had one silent direction — a key renamed without its regex
leaves every downstream reader agreeing while the matcher hunts a token no
entry carries, and the lead-line guard for that class dies with nothing
reddening. The table is also the derivation surface a consumer’s enum
emitter reads for the tag vocabulary (this repo’s scripts/enum-sets.sh →
check-prose-enum), so it is the single source for the spelling on both the
enforcement and the prose side.
Calibration: lead-class rule — a tag of class C on a continuation line is a
violation only when the lead line lacks class C (prose that mentions a tag
the lead already carries is tolerated); tags outside the scanned sections are
parsed by no reader and ignored. Fenced-code and table lines exempt.
Single backticks are not an exemption, and the consequence is worth stating
where an author meets it: an entry body discussing a tag it does not carry —
`[roadmap:]` inside a sentence about the roadmap arm — reds exactly as a
reflowed tag would, because the class table matches the bracketed token and the
lead-class rule only tolerates what the lead line already carries. So prose
about a tag spells its name without the brackets. Widening the exemption to
inline code is refused: the entry-body citation grammar above already puts
single backticks to work as a live claim (§The tag algebra), so a scan that
skipped them would have to skip the citation surface too.
Residual accepted gap: a same-class duplicate sliding off while one stays on
the lead — negligible severity, not reflow-realistic.
check-task-names
Invariant: every entry in a live task section — active, deferred, and a
configured icebox — and every sub-task leads with a valid kebab-case slug,
unique across the file; every done-section entry is a bare slug only; every
[blocked-by: X] resolves to a live task, and one pointing at a done slug is
flagged stale.
Calibration: the live task sections share one namespace so resolution works regardless of where the target lives — an iceboxed slug is a legal blocker target, because it is unbuilt. The non-bold indented bullet is the documented prose-note escape; done slugs are validated as tokens but not cross-checked against the live namespace. Help texts cite this SPEC.
check-task-conservation
Invariant: every live slug (active + deferred + icebox, sub-tasks included) present at HEAD is still present in the working tree — live or done. A slug vanishing from both is a lost task: the absence class diff-review reliably misses (you notice wrong things present, not right things missing). Half-applied moves, botched renames, and undocumented withdrawals all fire.
Eviction to the icebox is conserved by construction — the slug stays in the
live set, so the gate stays silent and no sanctioned-disappearance escape had
to be invented. The done side is where care is owed: a done entry is matched as
a bare - <slug> line and nothing else — no bold, no tag, no trailing
prose. An entry carried into the done section with its
- **<slug>** [design-pending] — … shape intact therefore matches neither the
done grammar nor the live one, lands in no set, and reds here as a lost task.
Dispositioning an entry to done is a rewrite to its slug, not a relocation
of the entry; the tag is dropped by that reduction rather than swapped.
Calibration: diffs git show HEAD:<queue> against the worktree, hence
no-fixture: (a committed fixture has HEAD == worktree; the bad case needs
an uncommitted deletion — infeasible, not a stopgap). No git baseline ⇒ clean
exit (nothing to compare is not a violation). Clearing the done section is
safe: only HEAD’s live slugs are conserved. A rename intentionally fires —
move the old slug to done, sweep refs.
The gate is a binary subcommand, and the no-fixture: reason survived the
port intact — what it does not excuse is parity. The reason is structural
rather than a stopgap, so it stays true on either substrate; but a member with
no pair still owes the proof that both implementations agree, and it is paid by
the constructed scenario gate-sdk/SPEC.md §The port-candidate criteria specifies
for exactly this shape: throwaway repositories, the worktree mutated without
committing, both implementations run over the same case and compared byte for
byte. git is the one external program the gate invokes — the sanctioned floor
exception — and the branch that reports a clean exit when there is no repository
at all is part of what the scenario covers. The same absence of a fixture case
is why gate-sdk/SPEC.md §check-gate-output reads this member’s output contract
off its implementation module rather than off its descriptor.
check-queue-prose-precondition
Invariant: no active-section entry states a forward precondition in prose
(“revisit when …”, “once X lands”, “gated on …”) without a [blocked-by:]
tag — such an entry is latently blocked yet mechanically pickable as “first
unblocked”, because selection trusts tags, not prose. Resolution: tag the
real blocker, move the entry to the deferred section, rephrase past-tense if
the precondition is met, or the [precondition-ok: <reason>] opt-out (a
queue tag, not an HTML comment, so it survives the hygiene gate).
Calibration: the trigger set (QUEUE_KIT_PRECONDITION_REGEX) is deliberately
narrow — forward-looking phrasing only, past-tense narration stripped before
matching — and scoped to the active sections (the deferred section uses
“revisit when” as normal vocabulary and is exempt, and the icebox inherits that
exemption for the same reason: forward-looking phrasing is what a parked entry
is for). FP-bearing by
construction (parsing prose intent); the blocking grade is justified by a
silent pick attested in production use and the bounded scope.
check-queue-slug-liveness
Invariant: on every prose surface named by QUEUE_KIT_PROSE_SURFACE_GLOBS,
every slug-shaped bold-code token — **`<token>`** whose token matches the
slug grammar ([a-z0-9][a-z0-9-]*, so a --flag mention falls outside) —
resolves against the queue’s live slug set (lib/queue.sh’s queue_live_slugs,
active + deferred + icebox — a page citing an iceboxed task stays green,
correctly: the task is dormant, not landed). A token naming no live task is a
dead membership claim,
reported by file, line, and slug. The slug set excludes done tasks by
construction: prose about landed work must drop the bold-code form and cite the
owning SPEC instead, so a living page can never keep calling a landed task
queued.
The bold-code token is prose’s only sanctioned queue-membership claim
(§The tag algebra) — a grammar narrow enough for a gate to read, so the
living-page current-state rule stops being a standing duty and becomes a
checked contract. Default-empty knob: a consumer with no configured surface
no-ops clean; the surfaces are consumer editorial config, never a kit literal
(the provenance seam). Resolution direction is one-way (dir=one): the queue
is ground truth, the prose the audited follower.
templates/
queue-config.sh — the consumer config template: a two-line # spec: pointer
to the §Layout knob table (which now includes QUEUE_KIT_LESSON_SINKS and its
local-overlay reminder), so the table stays the one owner of the knob roster
rather than a parallel copy in the template drifting against it.
TASK-QUEUE.md — a starter queue skeleton: the sections in default order,
one example entry per grammar shape shown under Technical Debt (the
[spec:]-gated New Features carries teaching prose only, since a spec-ready
example would dangle a ref). It ships battery-clean when copied verbatim — the
starter-template conformance contract owned by gate-sdk/SPEC.md §Consumer smoke
— and carries lifecycle-kit’s iteration header as inert scaffold so a
combined-tree copy clears the stage gates too.
Shipping this file is also what makes this kit the queue format’s owner as far
as an adopter’s install is concerned: checkwright init seeds their queue from
the template of the first kit in their profile that ships one, and falls back to
an inline skeleton only when none does (installer/README.md §What init seeds).
The declaration is the artifact rather than a roster naming this kit. Until that
selection was resolved over the whole profile at once, no profile reached this
kit first and no adopter ever received the file.
Out of scope
The amendment lifecycle around [design-pending]/[spec:] (section-wide
enforcement, promotion procedure, check-amendment-queue) is canon-kit’s
scope. Code-comment TODO scanning (TODO(task:<slug>) resolution against
the queue) couples to source-file conventions and is canon-kit’s, on the
governed comment surface (check-todo-task-liveness). Task-output readers and delegation tooling are delegation-kit’s
scope. A consumer’s protocol prose, deferred-subsection taxonomy, and task
bodies are rule content and never ship.